audioSceneComponent.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. import { Sound } from "./sound";
  2. import { SoundTrack } from "./soundTrack";
  3. import { Engine } from "../Engines/engine";
  4. import { Camera } from "../Cameras/camera";
  5. import { Nullable } from "../types";
  6. import { Matrix, Vector3 } from "../Maths/math.vector";
  7. import { SceneComponentConstants, ISceneSerializableComponent } from "../sceneComponent";
  8. import { Scene } from "../scene";
  9. import { AbstractScene } from "../abstractScene";
  10. import { AssetContainer } from "../assetContainer";
  11. import "./audioEngine";
  12. import { PrecisionDate } from '../Misc/precisionDate';
  13. // Adds the parser to the scene parsers.
  14. AbstractScene.AddParser(SceneComponentConstants.NAME_AUDIO, (parsedData: any, scene: Scene, container: AssetContainer, rootUrl: string) => {
  15. // TODO: add sound
  16. var loadedSounds: Sound[] = [];
  17. var loadedSound: Sound;
  18. container.sounds = container.sounds || [];
  19. if (parsedData.sounds !== undefined && parsedData.sounds !== null) {
  20. for (let index = 0, cache = parsedData.sounds.length; index < cache; index++) {
  21. var parsedSound = parsedData.sounds[index];
  22. if (Engine.audioEngine.canUseWebAudio) {
  23. if (!parsedSound.url) { parsedSound.url = parsedSound.name; }
  24. if (!loadedSounds[parsedSound.url]) {
  25. loadedSound = Sound.Parse(parsedSound, scene, rootUrl);
  26. loadedSounds[parsedSound.url] = loadedSound;
  27. container.sounds.push(loadedSound);
  28. }
  29. else {
  30. container.sounds.push(Sound.Parse(parsedSound, scene, rootUrl, loadedSounds[parsedSound.url]));
  31. }
  32. } else {
  33. container.sounds.push(new Sound(parsedSound.name, null, scene));
  34. }
  35. }
  36. }
  37. loadedSounds = [];
  38. });
  39. declare module "../abstractScene" {
  40. export interface AbstractScene {
  41. /**
  42. * The list of sounds used in the scene.
  43. */
  44. sounds: Nullable<Array<Sound>>;
  45. }
  46. }
  47. declare module "../scene" {
  48. export interface Scene {
  49. /**
  50. * @hidden
  51. * Backing field
  52. */
  53. _mainSoundTrack: SoundTrack;
  54. /**
  55. * The main sound track played by the scene.
  56. * It cotains your primary collection of sounds.
  57. */
  58. mainSoundTrack: SoundTrack;
  59. /**
  60. * The list of sound tracks added to the scene
  61. * @see http://doc.babylonjs.com/how_to/playing_sounds_and_music
  62. */
  63. soundTracks: Nullable<Array<SoundTrack>>;
  64. /**
  65. * Gets a sound using a given name
  66. * @param name defines the name to search for
  67. * @return the found sound or null if not found at all.
  68. */
  69. getSoundByName(name: string): Nullable<Sound>;
  70. /**
  71. * Gets or sets if audio support is enabled
  72. * @see http://doc.babylonjs.com/how_to/playing_sounds_and_music
  73. */
  74. audioEnabled: boolean;
  75. /**
  76. * Gets or sets if audio will be output to headphones
  77. * @see http://doc.babylonjs.com/how_to/playing_sounds_and_music
  78. */
  79. headphone: boolean;
  80. /**
  81. * Gets or sets custom audio listener position provider
  82. * @see http://doc.babylonjs.com/how_to/playing_sounds_and_music
  83. */
  84. audioListenerPositionProvider: Nullable<() => Vector3>;
  85. /**
  86. * Gets or sets a refresh rate when using 3D audio positioning
  87. */
  88. audioPositioningRefreshRate: number;
  89. }
  90. }
  91. Object.defineProperty(Scene.prototype, "mainSoundTrack", {
  92. get: function(this: Scene) {
  93. let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO) as AudioSceneComponent;
  94. if (!compo) {
  95. compo = new AudioSceneComponent(this);
  96. this._addComponent(compo);
  97. }
  98. if (!this._mainSoundTrack) {
  99. this._mainSoundTrack = new SoundTrack(this, { mainTrack: true });
  100. }
  101. return this._mainSoundTrack;
  102. },
  103. enumerable: true,
  104. configurable: true
  105. });
  106. Scene.prototype.getSoundByName = function(name: string): Nullable<Sound> {
  107. var index: number;
  108. for (index = 0; index < this.mainSoundTrack.soundCollection.length; index++) {
  109. if (this.mainSoundTrack.soundCollection[index].name === name) {
  110. return this.mainSoundTrack.soundCollection[index];
  111. }
  112. }
  113. if (this.soundTracks) {
  114. for (var sdIndex = 0; sdIndex < this.soundTracks.length; sdIndex++) {
  115. for (index = 0; index < this.soundTracks[sdIndex].soundCollection.length; index++) {
  116. if (this.soundTracks[sdIndex].soundCollection[index].name === name) {
  117. return this.soundTracks[sdIndex].soundCollection[index];
  118. }
  119. }
  120. }
  121. }
  122. return null;
  123. };
  124. Object.defineProperty(Scene.prototype, "audioEnabled", {
  125. get: function(this: Scene) {
  126. let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO) as AudioSceneComponent;
  127. if (!compo) {
  128. compo = new AudioSceneComponent(this);
  129. this._addComponent(compo);
  130. }
  131. return compo.audioEnabled;
  132. },
  133. set: function(this: Scene, value: boolean) {
  134. let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO) as AudioSceneComponent;
  135. if (!compo) {
  136. compo = new AudioSceneComponent(this);
  137. this._addComponent(compo);
  138. }
  139. if (value) {
  140. compo.enableAudio();
  141. }
  142. else {
  143. compo.disableAudio();
  144. }
  145. },
  146. enumerable: true,
  147. configurable: true
  148. });
  149. Object.defineProperty(Scene.prototype, "headphone", {
  150. get: function(this: Scene) {
  151. let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO) as AudioSceneComponent;
  152. if (!compo) {
  153. compo = new AudioSceneComponent(this);
  154. this._addComponent(compo);
  155. }
  156. return compo.headphone;
  157. },
  158. set: function(this: Scene, value: boolean) {
  159. let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO) as AudioSceneComponent;
  160. if (!compo) {
  161. compo = new AudioSceneComponent(this);
  162. this._addComponent(compo);
  163. }
  164. if (value) {
  165. compo.switchAudioModeForHeadphones();
  166. }
  167. else {
  168. compo.switchAudioModeForNormalSpeakers();
  169. }
  170. },
  171. enumerable: true,
  172. configurable: true
  173. });
  174. Object.defineProperty(Scene.prototype, "audioListenerPositionProvider", {
  175. get: function(this: Scene) {
  176. let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO) as AudioSceneComponent;
  177. if (!compo) {
  178. compo = new AudioSceneComponent(this);
  179. this._addComponent(compo);
  180. }
  181. return compo.audioListenerPositionProvider;
  182. },
  183. set: function(this: Scene, value: () => Vector3) {
  184. let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO) as AudioSceneComponent;
  185. if (!compo) {
  186. compo = new AudioSceneComponent(this);
  187. this._addComponent(compo);
  188. }
  189. if (typeof value !== 'function') {
  190. throw new Error('The value passed to [Scene.audioListenerPositionProvider] must be a function that returns a Vector3');
  191. } else {
  192. compo.audioListenerPositionProvider = value;
  193. }
  194. },
  195. enumerable: true,
  196. configurable: true
  197. });
  198. Object.defineProperty(Scene.prototype, "audioPositioningRefreshRate", {
  199. get: function(this: Scene) {
  200. let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO) as AudioSceneComponent;
  201. if (!compo) {
  202. compo = new AudioSceneComponent(this);
  203. this._addComponent(compo);
  204. }
  205. return compo.audioPositioningRefreshRate;
  206. },
  207. set: function(this: Scene, value: number) {
  208. let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO) as AudioSceneComponent;
  209. if (!compo) {
  210. compo = new AudioSceneComponent(this);
  211. this._addComponent(compo);
  212. }
  213. compo.audioPositioningRefreshRate = value;
  214. },
  215. enumerable: true,
  216. configurable: true
  217. });
  218. /**
  219. * Defines the sound scene component responsible to manage any sounds
  220. * in a given scene.
  221. */
  222. export class AudioSceneComponent implements ISceneSerializableComponent {
  223. /**
  224. * The component name helpfull to identify the component in the list of scene components.
  225. */
  226. public readonly name = SceneComponentConstants.NAME_AUDIO;
  227. /**
  228. * The scene the component belongs to.
  229. */
  230. public scene: Scene;
  231. private _audioEnabled = true;
  232. /**
  233. * Gets whether audio is enabled or not.
  234. * Please use related enable/disable method to switch state.
  235. */
  236. public get audioEnabled(): boolean {
  237. return this._audioEnabled;
  238. }
  239. private _headphone = false;
  240. /**
  241. * Gets whether audio is outputing to headphone or not.
  242. * Please use the according Switch methods to change output.
  243. */
  244. public get headphone(): boolean {
  245. return this._headphone;
  246. }
  247. /**
  248. * Gets or sets a refresh rate when using 3D audio positioning
  249. */
  250. public audioPositioningRefreshRate = 500;
  251. private _audioListenerPositionProvider: Nullable<() => Vector3> = null;
  252. /**
  253. * Gets the current audio listener position provider
  254. */
  255. public get audioListenerPositionProvider(): Nullable<() => Vector3> {
  256. return this._audioListenerPositionProvider;
  257. }
  258. /**
  259. * Sets a custom listener position for all sounds in the scene
  260. * By default, this is the position of the first active camera
  261. */
  262. public set audioListenerPositionProvider(value: Nullable<() => Vector3>) {
  263. this._audioListenerPositionProvider = value;
  264. }
  265. /**
  266. * Creates a new instance of the component for the given scene
  267. * @param scene Defines the scene to register the component in
  268. */
  269. constructor(scene: Scene) {
  270. this.scene = scene;
  271. scene.soundTracks = new Array<SoundTrack>();
  272. scene.sounds = new Array<Sound>();
  273. }
  274. /**
  275. * Registers the component in a given scene
  276. */
  277. public register(): void {
  278. this.scene._afterRenderStage.registerStep(SceneComponentConstants.STEP_AFTERRENDER_AUDIO, this, this._afterRender);
  279. }
  280. /**
  281. * Rebuilds the elements related to this component in case of
  282. * context lost for instance.
  283. */
  284. public rebuild(): void {
  285. // Nothing to do here. (Not rendering related)
  286. }
  287. /**
  288. * Serializes the component data to the specified json object
  289. * @param serializationObject The object to serialize to
  290. */
  291. public serialize(serializationObject: any): void {
  292. serializationObject.sounds = [];
  293. if (this.scene.soundTracks) {
  294. for (var index = 0; index < this.scene.soundTracks.length; index++) {
  295. var soundtrack = this.scene.soundTracks[index];
  296. for (var soundId = 0; soundId < soundtrack.soundCollection.length; soundId++) {
  297. serializationObject.sounds.push(soundtrack.soundCollection[soundId].serialize());
  298. }
  299. }
  300. }
  301. }
  302. /**
  303. * Adds all the elements from the container to the scene
  304. * @param container the container holding the elements
  305. */
  306. public addFromContainer(container: AbstractScene): void {
  307. if (!container.sounds) {
  308. return;
  309. }
  310. container.sounds.forEach((sound) => {
  311. sound.play();
  312. sound.autoplay = true;
  313. this.scene.mainSoundTrack.AddSound(sound);
  314. });
  315. }
  316. /**
  317. * Removes all the elements in the container from the scene
  318. * @param container contains the elements to remove
  319. * @param dispose if the removed element should be disposed (default: false)
  320. */
  321. public removeFromContainer(container: AbstractScene, dispose = false): void {
  322. if (!container.sounds) {
  323. return;
  324. }
  325. container.sounds.forEach((sound) => {
  326. sound.stop();
  327. sound.autoplay = false;
  328. this.scene.mainSoundTrack.RemoveSound(sound);
  329. if (dispose) {
  330. sound.dispose();
  331. }
  332. });
  333. }
  334. /**
  335. * Disposes the component and the associated ressources.
  336. */
  337. public dispose(): void {
  338. const scene = this.scene;
  339. if (scene._mainSoundTrack) {
  340. scene.mainSoundTrack.dispose();
  341. }
  342. if (scene.soundTracks) {
  343. for (var scIndex = 0; scIndex < scene.soundTracks.length; scIndex++) {
  344. scene.soundTracks[scIndex].dispose();
  345. }
  346. }
  347. }
  348. /**
  349. * Disables audio in the associated scene.
  350. */
  351. public disableAudio() {
  352. const scene = this.scene;
  353. this._audioEnabled = false;
  354. if (Engine.audioEngine && Engine.audioEngine.audioContext) {
  355. Engine.audioEngine.audioContext.suspend();
  356. }
  357. let i: number;
  358. for (i = 0; i < scene.mainSoundTrack.soundCollection.length; i++) {
  359. scene.mainSoundTrack.soundCollection[i].pause();
  360. }
  361. if (scene.soundTracks) {
  362. for (i = 0; i < scene.soundTracks.length; i++) {
  363. for (var j = 0; j < scene.soundTracks[i].soundCollection.length; j++) {
  364. scene.soundTracks[i].soundCollection[j].pause();
  365. }
  366. }
  367. }
  368. }
  369. /**
  370. * Enables audio in the associated scene.
  371. */
  372. public enableAudio() {
  373. const scene = this.scene;
  374. this._audioEnabled = true;
  375. if (Engine.audioEngine && Engine.audioEngine.audioContext) {
  376. Engine.audioEngine.audioContext.resume();
  377. }
  378. let i: number;
  379. for (i = 0; i < scene.mainSoundTrack.soundCollection.length; i++) {
  380. if (scene.mainSoundTrack.soundCollection[i].isPaused) {
  381. scene.mainSoundTrack.soundCollection[i].play();
  382. }
  383. }
  384. if (scene.soundTracks) {
  385. for (i = 0; i < scene.soundTracks.length; i++) {
  386. for (var j = 0; j < scene.soundTracks[i].soundCollection.length; j++) {
  387. if (scene.soundTracks[i].soundCollection[j].isPaused) {
  388. scene.soundTracks[i].soundCollection[j].play();
  389. }
  390. }
  391. }
  392. }
  393. }
  394. /**
  395. * Switch audio to headphone output.
  396. */
  397. public switchAudioModeForHeadphones() {
  398. const scene = this.scene;
  399. this._headphone = true;
  400. scene.mainSoundTrack.switchPanningModelToHRTF();
  401. if (scene.soundTracks) {
  402. for (var i = 0; i < scene.soundTracks.length; i++) {
  403. scene.soundTracks[i].switchPanningModelToHRTF();
  404. }
  405. }
  406. }
  407. /**
  408. * Switch audio to normal speakers.
  409. */
  410. public switchAudioModeForNormalSpeakers() {
  411. const scene = this.scene;
  412. this._headphone = false;
  413. scene.mainSoundTrack.switchPanningModelToEqualPower();
  414. if (scene.soundTracks) {
  415. for (var i = 0; i < scene.soundTracks.length; i++) {
  416. scene.soundTracks[i].switchPanningModelToEqualPower();
  417. }
  418. }
  419. }
  420. private _cachedCameraDirection = new Vector3();
  421. private _cachedCameraPosition = new Vector3();
  422. private _lastCheck = 0;
  423. private _afterRender() {
  424. var now = PrecisionDate.Now;
  425. if (this._lastCheck && now - this._lastCheck < this.audioPositioningRefreshRate) {
  426. return;
  427. }
  428. this._lastCheck = now;
  429. const scene = this.scene;
  430. if (!this._audioEnabled || !scene._mainSoundTrack || !scene.soundTracks || (scene._mainSoundTrack.soundCollection.length === 0 && scene.soundTracks.length === 1)) {
  431. return;
  432. }
  433. var audioEngine = Engine.audioEngine;
  434. if (audioEngine.audioContext) {
  435. // A custom listener position provider was set
  436. // Use the users provided position instead of camera's
  437. if (this._audioListenerPositionProvider) {
  438. var position: Vector3 = this._audioListenerPositionProvider();
  439. // Make sure all coordinates were provided
  440. position.x = position.x || 0;
  441. position.y = position.y || 0;
  442. position.z = position.z || 0;
  443. // Set the listener position
  444. audioEngine.audioContext.listener.setPosition(position.x, position.y, position.z);
  445. } else {
  446. var listeningCamera: Nullable<Camera>;
  447. if (scene.activeCameras.length > 0) {
  448. listeningCamera = scene.activeCameras[0];
  449. } else {
  450. listeningCamera = scene.activeCamera;
  451. }
  452. // Check if there is a listening camera
  453. if (listeningCamera) {
  454. // Set the listener position to the listening camera global position
  455. if (!this._cachedCameraPosition.equals(listeningCamera.globalPosition)) {
  456. this._cachedCameraPosition.copyFrom(listeningCamera.globalPosition);
  457. audioEngine.audioContext.listener.setPosition(listeningCamera.globalPosition.x, listeningCamera.globalPosition.y, listeningCamera.globalPosition.z);
  458. }
  459. // for VR cameras
  460. if (listeningCamera.rigCameras && listeningCamera.rigCameras.length > 0) {
  461. listeningCamera = listeningCamera.rigCameras[0];
  462. }
  463. var mat = Matrix.Invert(listeningCamera.getViewMatrix());
  464. var cameraDirection = Vector3.TransformNormal(new Vector3(0, 0, -1), mat);
  465. cameraDirection.normalize();
  466. // To avoid some errors on GearVR
  467. if (!isNaN(cameraDirection.x) && !isNaN(cameraDirection.y) && !isNaN(cameraDirection.z)) {
  468. if (!this._cachedCameraDirection.equals(cameraDirection)) {
  469. this._cachedCameraDirection.copyFrom(cameraDirection);
  470. audioEngine.audioContext.listener.setOrientation(cameraDirection.x, cameraDirection.y, cameraDirection.z, 0, 1, 0);
  471. }
  472. }
  473. }
  474. // Otherwise set the listener position to 0, 0 ,0
  475. else {
  476. // Set the listener position
  477. audioEngine.audioContext.listener.setPosition(0, 0, 0);
  478. }
  479. }
  480. var i: number;
  481. for (i = 0; i < scene.mainSoundTrack.soundCollection.length; i++) {
  482. var sound = scene.mainSoundTrack.soundCollection[i];
  483. if (sound.useCustomAttenuation) {
  484. sound.updateDistanceFromListener();
  485. }
  486. }
  487. if (scene.soundTracks) {
  488. for (i = 0; i < scene.soundTracks.length; i++) {
  489. for (var j = 0; j < scene.soundTracks[i].soundCollection.length; j++) {
  490. sound = scene.soundTracks[i].soundCollection[j];
  491. if (sound.useCustomAttenuation) {
  492. sound.updateDistanceFromListener();
  493. }
  494. }
  495. }
  496. }
  497. }
  498. }
  499. }
  500. Sound._SceneComponentInitialization = (scene: Scene) => {
  501. let compo = scene._getComponent(SceneComponentConstants.NAME_AUDIO);
  502. if (!compo) {
  503. compo = new AudioSceneComponent(scene);
  504. scene._addComponent(compo);
  505. }
  506. };