audioSceneComponent.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  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 https://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 https://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 https://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 https://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. private static _CameraDirectionLH = new Vector3(0, 0, -1);
  224. private static _CameraDirectionRH = new Vector3(0, 0, 1);
  225. /**
  226. * The component name helpfull to identify the component in the list of scene components.
  227. */
  228. public readonly name = SceneComponentConstants.NAME_AUDIO;
  229. /**
  230. * The scene the component belongs to.
  231. */
  232. public scene: Scene;
  233. private _audioEnabled = true;
  234. /**
  235. * Gets whether audio is enabled or not.
  236. * Please use related enable/disable method to switch state.
  237. */
  238. public get audioEnabled(): boolean {
  239. return this._audioEnabled;
  240. }
  241. private _headphone = false;
  242. /**
  243. * Gets whether audio is outputing to headphone or not.
  244. * Please use the according Switch methods to change output.
  245. */
  246. public get headphone(): boolean {
  247. return this._headphone;
  248. }
  249. /**
  250. * Gets or sets a refresh rate when using 3D audio positioning
  251. */
  252. public audioPositioningRefreshRate = 500;
  253. private _audioListenerPositionProvider: Nullable<() => Vector3> = null;
  254. /**
  255. * Gets the current audio listener position provider
  256. */
  257. public get audioListenerPositionProvider(): Nullable<() => Vector3> {
  258. return this._audioListenerPositionProvider;
  259. }
  260. /**
  261. * Sets a custom listener position for all sounds in the scene
  262. * By default, this is the position of the first active camera
  263. */
  264. public set audioListenerPositionProvider(value: Nullable<() => Vector3>) {
  265. this._audioListenerPositionProvider = value;
  266. }
  267. /**
  268. * Creates a new instance of the component for the given scene
  269. * @param scene Defines the scene to register the component in
  270. */
  271. constructor(scene: Scene) {
  272. this.scene = scene;
  273. scene.soundTracks = new Array<SoundTrack>();
  274. scene.sounds = new Array<Sound>();
  275. }
  276. /**
  277. * Registers the component in a given scene
  278. */
  279. public register(): void {
  280. this.scene._afterRenderStage.registerStep(SceneComponentConstants.STEP_AFTERRENDER_AUDIO, this, this._afterRender);
  281. }
  282. /**
  283. * Rebuilds the elements related to this component in case of
  284. * context lost for instance.
  285. */
  286. public rebuild(): void {
  287. // Nothing to do here. (Not rendering related)
  288. }
  289. /**
  290. * Serializes the component data to the specified json object
  291. * @param serializationObject The object to serialize to
  292. */
  293. public serialize(serializationObject: any): void {
  294. serializationObject.sounds = [];
  295. if (this.scene.soundTracks) {
  296. for (var index = 0; index < this.scene.soundTracks.length; index++) {
  297. var soundtrack = this.scene.soundTracks[index];
  298. for (var soundId = 0; soundId < soundtrack.soundCollection.length; soundId++) {
  299. serializationObject.sounds.push(soundtrack.soundCollection[soundId].serialize());
  300. }
  301. }
  302. }
  303. }
  304. /**
  305. * Adds all the elements from the container to the scene
  306. * @param container the container holding the elements
  307. */
  308. public addFromContainer(container: AbstractScene): void {
  309. if (!container.sounds) {
  310. return;
  311. }
  312. container.sounds.forEach((sound) => {
  313. sound.play();
  314. sound.autoplay = true;
  315. this.scene.mainSoundTrack.AddSound(sound);
  316. });
  317. }
  318. /**
  319. * Removes all the elements in the container from the scene
  320. * @param container contains the elements to remove
  321. * @param dispose if the removed element should be disposed (default: false)
  322. */
  323. public removeFromContainer(container: AbstractScene, dispose = false): void {
  324. if (!container.sounds) {
  325. return;
  326. }
  327. container.sounds.forEach((sound) => {
  328. sound.stop();
  329. sound.autoplay = false;
  330. this.scene.mainSoundTrack.RemoveSound(sound);
  331. if (dispose) {
  332. sound.dispose();
  333. }
  334. });
  335. }
  336. /**
  337. * Disposes the component and the associated ressources.
  338. */
  339. public dispose(): void {
  340. const scene = this.scene;
  341. if (scene._mainSoundTrack) {
  342. scene.mainSoundTrack.dispose();
  343. }
  344. if (scene.soundTracks) {
  345. for (var scIndex = 0; scIndex < scene.soundTracks.length; scIndex++) {
  346. scene.soundTracks[scIndex].dispose();
  347. }
  348. }
  349. }
  350. /**
  351. * Disables audio in the associated scene.
  352. */
  353. public disableAudio() {
  354. const scene = this.scene;
  355. this._audioEnabled = false;
  356. if (Engine.audioEngine && Engine.audioEngine.audioContext) {
  357. Engine.audioEngine.audioContext.suspend();
  358. }
  359. let i: number;
  360. for (i = 0; i < scene.mainSoundTrack.soundCollection.length; i++) {
  361. scene.mainSoundTrack.soundCollection[i].pause();
  362. }
  363. if (scene.soundTracks) {
  364. for (i = 0; i < scene.soundTracks.length; i++) {
  365. for (var j = 0; j < scene.soundTracks[i].soundCollection.length; j++) {
  366. scene.soundTracks[i].soundCollection[j].pause();
  367. }
  368. }
  369. }
  370. }
  371. /**
  372. * Enables audio in the associated scene.
  373. */
  374. public enableAudio() {
  375. const scene = this.scene;
  376. this._audioEnabled = true;
  377. if (Engine.audioEngine && Engine.audioEngine.audioContext) {
  378. Engine.audioEngine.audioContext.resume();
  379. }
  380. let i: number;
  381. for (i = 0; i < scene.mainSoundTrack.soundCollection.length; i++) {
  382. if (scene.mainSoundTrack.soundCollection[i].isPaused) {
  383. scene.mainSoundTrack.soundCollection[i].play();
  384. }
  385. }
  386. if (scene.soundTracks) {
  387. for (i = 0; i < scene.soundTracks.length; i++) {
  388. for (var j = 0; j < scene.soundTracks[i].soundCollection.length; j++) {
  389. if (scene.soundTracks[i].soundCollection[j].isPaused) {
  390. scene.soundTracks[i].soundCollection[j].play();
  391. }
  392. }
  393. }
  394. }
  395. }
  396. /**
  397. * Switch audio to headphone output.
  398. */
  399. public switchAudioModeForHeadphones() {
  400. const scene = this.scene;
  401. this._headphone = true;
  402. scene.mainSoundTrack.switchPanningModelToHRTF();
  403. if (scene.soundTracks) {
  404. for (var i = 0; i < scene.soundTracks.length; i++) {
  405. scene.soundTracks[i].switchPanningModelToHRTF();
  406. }
  407. }
  408. }
  409. /**
  410. * Switch audio to normal speakers.
  411. */
  412. public switchAudioModeForNormalSpeakers() {
  413. const scene = this.scene;
  414. this._headphone = false;
  415. scene.mainSoundTrack.switchPanningModelToEqualPower();
  416. if (scene.soundTracks) {
  417. for (var i = 0; i < scene.soundTracks.length; i++) {
  418. scene.soundTracks[i].switchPanningModelToEqualPower();
  419. }
  420. }
  421. }
  422. private _cachedCameraDirection = new Vector3();
  423. private _cachedCameraPosition = new Vector3();
  424. private _lastCheck = 0;
  425. private _afterRender() {
  426. var now = PrecisionDate.Now;
  427. if (this._lastCheck && now - this._lastCheck < this.audioPositioningRefreshRate) {
  428. return;
  429. }
  430. this._lastCheck = now;
  431. const scene = this.scene;
  432. if (!this._audioEnabled || !scene._mainSoundTrack || !scene.soundTracks || (scene._mainSoundTrack.soundCollection.length === 0 && scene.soundTracks.length === 1)) {
  433. return;
  434. }
  435. var audioEngine = Engine.audioEngine;
  436. if (!audioEngine) {
  437. return;
  438. }
  439. if (audioEngine.audioContext) {
  440. // A custom listener position provider was set
  441. // Use the users provided position instead of camera's
  442. if (this._audioListenerPositionProvider) {
  443. var position: Vector3 = this._audioListenerPositionProvider();
  444. // Make sure all coordinates were provided
  445. position.x = position.x || 0;
  446. position.y = position.y || 0;
  447. position.z = position.z || 0;
  448. // Set the listener position
  449. audioEngine.audioContext.listener.setPosition(position.x, position.y, position.z);
  450. } else {
  451. var listeningCamera: Nullable<Camera>;
  452. if (scene.activeCameras.length > 0) {
  453. listeningCamera = scene.activeCameras[0];
  454. } else {
  455. listeningCamera = scene.activeCamera;
  456. }
  457. // Check if there is a listening camera
  458. if (listeningCamera) {
  459. // Set the listener position to the listening camera global position
  460. if (!this._cachedCameraPosition.equals(listeningCamera.globalPosition)) {
  461. this._cachedCameraPosition.copyFrom(listeningCamera.globalPosition);
  462. audioEngine.audioContext.listener.setPosition(listeningCamera.globalPosition.x, listeningCamera.globalPosition.y, listeningCamera.globalPosition.z);
  463. }
  464. // for VR cameras
  465. if (listeningCamera.rigCameras && listeningCamera.rigCameras.length > 0) {
  466. listeningCamera = listeningCamera.rigCameras[0];
  467. }
  468. var mat = Matrix.Invert(listeningCamera.getViewMatrix());
  469. var cameraDirection = Vector3.TransformNormal(scene.useRightHandedSystem ? AudioSceneComponent._CameraDirectionRH : AudioSceneComponent._CameraDirectionLH, mat);
  470. cameraDirection.normalize();
  471. // To avoid some errors on GearVR
  472. if (!isNaN(cameraDirection.x) && !isNaN(cameraDirection.y) && !isNaN(cameraDirection.z)) {
  473. if (!this._cachedCameraDirection.equals(cameraDirection)) {
  474. this._cachedCameraDirection.copyFrom(cameraDirection);
  475. audioEngine.audioContext.listener.setOrientation(cameraDirection.x, cameraDirection.y, cameraDirection.z, 0, 1, 0);
  476. }
  477. }
  478. }
  479. // Otherwise set the listener position to 0, 0 ,0
  480. else {
  481. // Set the listener position
  482. audioEngine.audioContext.listener.setPosition(0, 0, 0);
  483. }
  484. }
  485. var i: number;
  486. for (i = 0; i < scene.mainSoundTrack.soundCollection.length; i++) {
  487. var sound = scene.mainSoundTrack.soundCollection[i];
  488. if (sound.useCustomAttenuation) {
  489. sound.updateDistanceFromListener();
  490. }
  491. }
  492. if (scene.soundTracks) {
  493. for (i = 0; i < scene.soundTracks.length; i++) {
  494. for (var j = 0; j < scene.soundTracks[i].soundCollection.length; j++) {
  495. sound = scene.soundTracks[i].soundCollection[j];
  496. if (sound.useCustomAttenuation) {
  497. sound.updateDistanceFromListener();
  498. }
  499. }
  500. }
  501. }
  502. }
  503. }
  504. }
  505. Sound._SceneComponentInitialization = (scene: Scene) => {
  506. let compo = scene._getComponent(SceneComponentConstants.NAME_AUDIO);
  507. if (!compo) {
  508. compo = new AudioSceneComponent(scene);
  509. scene._addComponent(compo);
  510. }
  511. };