audioEngine.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. import { IDisposable } from "../scene";
  2. import { Analyser } from "./analyser";
  3. import { Nullable } from "../types";
  4. import { Observable } from "../Misc/observable";
  5. import { Logger } from "../Misc/logger";
  6. import { Engine } from "../Engines/engine";
  7. /**
  8. * This represents an audio engine and it is responsible
  9. * to play, synchronize and analyse sounds throughout the application.
  10. * @see http://doc.babylonjs.com/how_to/playing_sounds_and_music
  11. */
  12. export interface IAudioEngine extends IDisposable {
  13. /**
  14. * Gets whether the current host supports Web Audio and thus could create AudioContexts.
  15. */
  16. readonly canUseWebAudio: boolean;
  17. /**
  18. * Gets the current AudioContext if available.
  19. */
  20. readonly audioContext: Nullable<AudioContext>;
  21. /**
  22. * The master gain node defines the global audio volume of your audio engine.
  23. */
  24. readonly masterGain: GainNode;
  25. /**
  26. * Gets whether or not mp3 are supported by your browser.
  27. */
  28. readonly isMP3supported: boolean;
  29. /**
  30. * Gets whether or not ogg are supported by your browser.
  31. */
  32. readonly isOGGsupported: boolean;
  33. /**
  34. * Defines if Babylon should emit a warning if WebAudio is not supported.
  35. * @ignoreNaming
  36. */
  37. WarnedWebAudioUnsupported: boolean;
  38. /**
  39. * Defines if the audio engine relies on a custom unlocked button.
  40. * In this case, the embedded button will not be displayed.
  41. */
  42. useCustomUnlockedButton: boolean;
  43. /**
  44. * Gets whether or not the audio engine is unlocked (require first a user gesture on some browser).
  45. */
  46. readonly unlocked: boolean;
  47. /**
  48. * Event raised when audio has been unlocked on the browser.
  49. */
  50. onAudioUnlockedObservable: Observable<AudioEngine>;
  51. /**
  52. * Event raised when audio has been locked on the browser.
  53. */
  54. onAudioLockedObservable: Observable<AudioEngine>;
  55. /**
  56. * Flags the audio engine in Locked state.
  57. * This happens due to new browser policies preventing audio to autoplay.
  58. */
  59. lock(): void;
  60. /**
  61. * Unlocks the audio engine once a user action has been done on the dom.
  62. * This is helpful to resume play once browser policies have been satisfied.
  63. */
  64. unlock(): void;
  65. }
  66. // Sets the default audio engine to Babylon.js
  67. Engine.AudioEngineFactory = (hostElement: Nullable<HTMLElement>) => { return new AudioEngine(hostElement); };
  68. /**
  69. * This represents the default audio engine used in babylon.
  70. * It is responsible to play, synchronize and analyse sounds throughout the application.
  71. * @see http://doc.babylonjs.com/how_to/playing_sounds_and_music
  72. */
  73. export class AudioEngine implements IAudioEngine {
  74. private _audioContext: Nullable<AudioContext> = null;
  75. private _audioContextInitialized = false;
  76. private _muteButton: Nullable<HTMLButtonElement> = null;
  77. private _hostElement: Nullable<HTMLElement>;
  78. /**
  79. * Gets whether the current host supports Web Audio and thus could create AudioContexts.
  80. */
  81. public canUseWebAudio: boolean = false;
  82. /**
  83. * The master gain node defines the global audio volume of your audio engine.
  84. */
  85. public masterGain: GainNode;
  86. /**
  87. * Defines if Babylon should emit a warning if WebAudio is not supported.
  88. * @ignoreNaming
  89. */
  90. public WarnedWebAudioUnsupported: boolean = false;
  91. /**
  92. * Gets whether or not mp3 are supported by your browser.
  93. */
  94. public isMP3supported: boolean = false;
  95. /**
  96. * Gets whether or not ogg are supported by your browser.
  97. */
  98. public isOGGsupported: boolean = false;
  99. /**
  100. * Gets whether audio has been unlocked on the device.
  101. * Some Browsers have strong restrictions about Audio and won t autoplay unless
  102. * a user interaction has happened.
  103. */
  104. public unlocked: boolean = true;
  105. /**
  106. * Defines if the audio engine relies on a custom unlocked button.
  107. * In this case, the embedded button will not be displayed.
  108. */
  109. public useCustomUnlockedButton: boolean = false;
  110. /**
  111. * Event raised when audio has been unlocked on the browser.
  112. */
  113. public onAudioUnlockedObservable = new Observable<AudioEngine>();
  114. /**
  115. * Event raised when audio has been locked on the browser.
  116. */
  117. public onAudioLockedObservable = new Observable<AudioEngine>();
  118. /**
  119. * Gets the current AudioContext if available.
  120. */
  121. public get audioContext(): Nullable<AudioContext> {
  122. if (!this._audioContextInitialized) {
  123. this._initializeAudioContext();
  124. }
  125. else {
  126. if (!this.unlocked && !this._muteButton) {
  127. this._displayMuteButton();
  128. }
  129. }
  130. return this._audioContext;
  131. }
  132. private _connectedAnalyser: Nullable<Analyser>;
  133. /**
  134. * Instantiates a new audio engine.
  135. *
  136. * There should be only one per page as some browsers restrict the number
  137. * of audio contexts you can create.
  138. * @param hostElement defines the host element where to display the mute icon if necessary
  139. */
  140. constructor(hostElement: Nullable<HTMLElement> = null) {
  141. if (typeof window.AudioContext !== 'undefined' || typeof window.webkitAudioContext !== 'undefined') {
  142. window.AudioContext = window.AudioContext || window.webkitAudioContext;
  143. this.canUseWebAudio = true;
  144. }
  145. var audioElem = document.createElement('audio');
  146. this._hostElement = hostElement;
  147. try {
  148. if (audioElem && !!audioElem.canPlayType && audioElem.canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/, '')) {
  149. this.isMP3supported = true;
  150. }
  151. }
  152. catch (e) {
  153. // protect error during capability check.
  154. }
  155. try {
  156. if (audioElem && !!audioElem.canPlayType && audioElem.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '')) {
  157. this.isOGGsupported = true;
  158. }
  159. }
  160. catch (e) {
  161. // protect error during capability check.
  162. }
  163. }
  164. /**
  165. * Flags the audio engine in Locked state.
  166. * This happens due to new browser policies preventing audio to autoplay.
  167. */
  168. public lock() {
  169. this._triggerSuspendedState();
  170. }
  171. /**
  172. * Unlocks the audio engine once a user action has been done on the dom.
  173. * This is helpful to resume play once browser policies have been satisfied.
  174. */
  175. public unlock() {
  176. this._triggerRunningState();
  177. }
  178. private _resumeAudioContext(): Promise<void> {
  179. let result: Promise<void>;
  180. if (this._audioContext!.resume) {
  181. result = this._audioContext!.resume();
  182. }
  183. return result! || Promise.resolve();
  184. }
  185. private _initializeAudioContext() {
  186. try {
  187. if (this.canUseWebAudio) {
  188. this._audioContext = new AudioContext();
  189. // create a global volume gain node
  190. this.masterGain = this._audioContext.createGain();
  191. this.masterGain.gain.value = 1;
  192. this.masterGain.connect(this._audioContext.destination);
  193. this._audioContextInitialized = true;
  194. if (this._audioContext.state === "running") {
  195. // Do not wait for the promise to unlock.
  196. this._triggerRunningState();
  197. }
  198. }
  199. }
  200. catch (e) {
  201. this.canUseWebAudio = false;
  202. Logger.Error("Web Audio: " + e.message);
  203. }
  204. }
  205. private _tryToRun = false;
  206. private _triggerRunningState() {
  207. if (this._tryToRun) {
  208. return;
  209. }
  210. this._tryToRun = true;
  211. this._resumeAudioContext()
  212. .then(() => {
  213. this._tryToRun = false;
  214. if (this._muteButton) {
  215. this._hideMuteButton();
  216. }
  217. }).catch(() => {
  218. this._tryToRun = false;
  219. this.unlocked = false;
  220. });
  221. // Notify users that the audio stack is unlocked/unmuted
  222. this.unlocked = true;
  223. this.onAudioUnlockedObservable.notifyObservers(this);
  224. }
  225. private _triggerSuspendedState() {
  226. this.unlocked = false;
  227. this.onAudioLockedObservable.notifyObservers(this);
  228. this._displayMuteButton();
  229. }
  230. private _displayMuteButton() {
  231. if (this.useCustomUnlockedButton || this._muteButton) {
  232. return;
  233. }
  234. this._muteButton = <HTMLButtonElement>document.createElement("BUTTON");
  235. this._muteButton.className = "babylonUnmuteIcon";
  236. this._muteButton.id = "babylonUnmuteIconBtn";
  237. this._muteButton.title = "Unmute";
  238. const imageUrl = !window.SVGSVGElement ? "https://cdn.babylonjs.com/Assets/audio.png" : "data:image/svg+xml;charset=UTF-8,%3Csvg%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2239%22%20height%3D%2232%22%20viewBox%3D%220%200%2039%2032%22%3E%3Cpath%20fill%3D%22white%22%20d%3D%22M9.625%2018.938l-0.031%200.016h-4.953q-0.016%200-0.031-0.016v-12.453q0-0.016%200.031-0.016h4.953q0.031%200%200.031%200.016v12.453zM12.125%207.688l8.719-8.703v27.453l-8.719-8.719-0.016-0.047v-9.938zM23.359%207.875l1.406-1.406%204.219%204.203%204.203-4.203%201.422%201.406-4.219%204.219%204.219%204.203-1.484%201.359-4.141-4.156-4.219%204.219-1.406-1.422%204.219-4.203z%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E";
  239. var css = ".babylonUnmuteIcon { position: absolute; left: 20px; top: 20px; height: 40px; width: 60px; background-color: rgba(51,51,51,0.7); background-image: url(" + imageUrl + "); background-size: 80%; background-repeat:no-repeat; background-position: center; background-position-y: 4px; border: none; outline: none; transition: transform 0.125s ease-out; cursor: pointer; z-index: 9999; } .babylonUnmuteIcon:hover { transform: scale(1.05) } .babylonUnmuteIcon:active { background-color: rgba(51,51,51,1) }";
  240. var style = document.createElement('style');
  241. style.appendChild(document.createTextNode(css));
  242. document.getElementsByTagName('head')[0].appendChild(style);
  243. document.body.appendChild(this._muteButton);
  244. this._moveButtonToTopLeft();
  245. this._muteButton.addEventListener('touchend', () => {
  246. this._triggerRunningState();
  247. }, true);
  248. this._muteButton.addEventListener('click', () => {
  249. this._triggerRunningState();
  250. }, true);
  251. window.addEventListener("resize", this._onResize);
  252. }
  253. private _moveButtonToTopLeft() {
  254. if (this._hostElement && this._muteButton) {
  255. this._muteButton.style.top = this._hostElement.offsetTop + 20 + "px";
  256. this._muteButton.style.left = this._hostElement.offsetLeft + 20 + "px";
  257. }
  258. }
  259. private _onResize = () => {
  260. this._moveButtonToTopLeft();
  261. }
  262. private _hideMuteButton() {
  263. if (this._muteButton) {
  264. document.body.removeChild(this._muteButton);
  265. this._muteButton = null;
  266. }
  267. }
  268. /**
  269. * Destroy and release the resources associated with the audio ccontext.
  270. */
  271. public dispose(): void {
  272. if (this.canUseWebAudio && this._audioContextInitialized) {
  273. if (this._connectedAnalyser && this._audioContext) {
  274. this._connectedAnalyser.stopDebugCanvas();
  275. this._connectedAnalyser.dispose();
  276. this.masterGain.disconnect();
  277. this.masterGain.connect(this._audioContext.destination);
  278. this._connectedAnalyser = null;
  279. }
  280. this.masterGain.gain.value = 1;
  281. }
  282. this.WarnedWebAudioUnsupported = false;
  283. this._hideMuteButton();
  284. window.removeEventListener("resize", this._onResize);
  285. this.onAudioUnlockedObservable.clear();
  286. this.onAudioLockedObservable.clear();
  287. }
  288. /**
  289. * Gets the global volume sets on the master gain.
  290. * @returns the global volume if set or -1 otherwise
  291. */
  292. public getGlobalVolume(): number {
  293. if (this.canUseWebAudio && this._audioContextInitialized) {
  294. return this.masterGain.gain.value;
  295. }
  296. else {
  297. return -1;
  298. }
  299. }
  300. /**
  301. * Sets the global volume of your experience (sets on the master gain).
  302. * @param newVolume Defines the new global volume of the application
  303. */
  304. public setGlobalVolume(newVolume: number): void {
  305. if (this.canUseWebAudio && this._audioContextInitialized) {
  306. this.masterGain.gain.value = newVolume;
  307. }
  308. }
  309. /**
  310. * Connect the audio engine to an audio analyser allowing some amazing
  311. * synchornization between the sounds/music and your visualization (VuMeter for instance).
  312. * @see http://doc.babylonjs.com/how_to/playing_sounds_and_music#using-the-analyser
  313. * @param analyser The analyser to connect to the engine
  314. */
  315. public connectToAnalyser(analyser: Analyser): void {
  316. if (this._connectedAnalyser) {
  317. this._connectedAnalyser.stopDebugCanvas();
  318. }
  319. if (this.canUseWebAudio && this._audioContextInitialized && this._audioContext) {
  320. this._connectedAnalyser = analyser;
  321. this.masterGain.disconnect();
  322. this._connectedAnalyser.connectAudioNodes(this.masterGain, this._audioContext.destination);
  323. }
  324. }
  325. }