audioEngine.ts 14 KB

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