babylon.sound.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. module BABYLON {
  2. export class Sound {
  3. public autoplay: boolean = false;
  4. public loop: boolean = false;
  5. public useCustomAttenuation: boolean = false;
  6. public soundTrackId: number;
  7. public spatialSound: boolean = false;
  8. public refDistance: number = 1;
  9. public rolloffFactor: number = 1;
  10. public maxDistance: number = 100;
  11. public distanceModel: string = "linear";
  12. public panningModel: string = "HRTF";
  13. private startTime: number = 0;
  14. private startOffset: number = 0;
  15. private _position: Vector3 = Vector3.Zero();
  16. private _localDirection: Vector3 = new Vector3(1,0,0);
  17. private _volume: number = 1;
  18. private _isLoaded: boolean = false;
  19. private _isReadyToPlay: boolean = false;
  20. private _isPlaying: boolean = false;
  21. private _isDirectional: boolean = false;
  22. private _audioEngine: BABYLON.AudioEngine;
  23. private _readyToPlayCallback;
  24. private _audioBuffer;
  25. private _soundSource: AudioBufferSourceNode;
  26. private _soundPanner: PannerNode;
  27. private _soundGain: GainNode;
  28. private _audioNode: AudioNode;
  29. // Used if you'd like to create a directional sound.
  30. // If not set, the sound will be omnidirectional
  31. private _coneInnerAngle: number = 360;
  32. private _coneOuterAngle: number = 360;
  33. private _coneOuterGain: number = 0;
  34. private _scene: BABYLON.Scene;
  35. private _name: string;
  36. private _connectedMesh: BABYLON.AbstractMesh;
  37. private _customAttenuationFunction: (currentVolume: number, currentDistance: number, maxDistance: number, refDistance: number, rolloffFactor: number) => number;
  38. /**
  39. * Create a sound and attach it to a scene
  40. * @param name Name of your sound
  41. * @param urlOrArrayBuffer Url to the sound to load async or ArrayBuffer
  42. * @param readyToPlayCallback Provide a callback function if you'd like to load your code once the sound is ready to be played
  43. * @param options Objects to provide with the current available options: autoplay, loop, volume, spatialSound, maxDistance, rolloffFactor, refDistance, distanceModel, panningModel
  44. */
  45. constructor(name: string, urlOrArrayBuffer: any, scene: BABYLON.Scene, readyToPlayCallback?: () => void, options?) {
  46. this._name = name;
  47. this._scene = scene;
  48. this._audioEngine = this._scene.getEngine().getAudioEngine();
  49. this._readyToPlayCallback = readyToPlayCallback;
  50. // Default custom attenuation function is a linear attenuation
  51. this._customAttenuationFunction = (currentVolume: number, currentDistance: number, maxDistance: number, refDistance: number, rolloffFactor: number) => {
  52. if (currentDistance < maxDistance) {
  53. return currentVolume * (1 - currentDistance / maxDistance);
  54. }
  55. else {
  56. return 0;
  57. }
  58. };
  59. if (options) {
  60. this.autoplay = options.autoplay || false;
  61. this.loop = options.loop || false;
  62. this._volume = options.volume || 1;
  63. this.spatialSound = options.spatialSound || false;
  64. this.maxDistance = options.maxDistance || 100;
  65. this.useCustomAttenuation = options.useCustomAttenation || false;
  66. this.rolloffFactor = options.rolloffFactor || 1;
  67. this.refDistance = options.refDistance || 1;
  68. this.distanceModel = options.distanceModel || "linear";
  69. this.panningModel = options.panningModel || "HRTF";
  70. }
  71. if (this._audioEngine.canUseWebAudio) {
  72. this._soundGain = this._audioEngine.audioContext.createGain();
  73. this._soundGain.gain.value = this._volume;
  74. if (this.spatialSound) {
  75. this._createSpatialParameters();
  76. }
  77. else {
  78. this._audioNode = this._soundGain;
  79. }
  80. this._scene.mainSoundTrack.AddSound(this);
  81. if (typeof (urlOrArrayBuffer) === "string") {
  82. BABYLON.Tools.LoadFile(urlOrArrayBuffer, (data) => { this._soundLoaded(data); }, null, null, true);
  83. }
  84. else {
  85. if (urlOrArrayBuffer instanceof ArrayBuffer) {
  86. this._soundLoaded(urlOrArrayBuffer);
  87. }
  88. else {
  89. BABYLON.Tools.Error("Parameter must be a URL to the sound or an ArrayBuffer of the sound.");
  90. }
  91. }
  92. }
  93. }
  94. public updateOptions(options) {
  95. if (options) {
  96. this.loop = options.loop || this.loop;
  97. this.maxDistance = options.maxDistance || this.maxDistance;
  98. this.useCustomAttenuation = options.useCustomAttenation || this.useCustomAttenuation;
  99. this.rolloffFactor = options.rolloffFactor || this.rolloffFactor;
  100. this.refDistance = options.refDistance || this.refDistance;
  101. this.distanceModel = options.distanceModel || this.distanceModel;
  102. this.panningModel = options.panningModel || this.panningModel;
  103. }
  104. }
  105. private _createSpatialParameters() {
  106. this._soundPanner = this._audioEngine.audioContext.createPanner();
  107. if (this.useCustomAttenuation) {
  108. // Tricks to disable in a way embedded Web Audio attenuation
  109. this._soundPanner.distanceModel = "linear";
  110. this._soundPanner.maxDistance = Number.MAX_VALUE;
  111. this._soundPanner.refDistance = 1;
  112. this._soundPanner.rolloffFactor = 1;
  113. this._soundPanner.panningModel = "HRTF";
  114. }
  115. else {
  116. this._soundPanner.distanceModel = this.distanceModel;
  117. this._soundPanner.maxDistance = this.maxDistance;
  118. this._soundPanner.refDistance = this.refDistance;
  119. this._soundPanner.rolloffFactor = this.rolloffFactor;
  120. this._soundPanner.panningModel = this.panningModel;
  121. }
  122. this._soundPanner.connect(this._soundGain);
  123. this._audioNode = this._soundPanner;
  124. }
  125. public connectToSoundTrackAudioNode(soundTrackAudioNode: AudioNode) {
  126. if (this._audioEngine.canUseWebAudio) {
  127. this._audioNode.disconnect();
  128. this._audioNode.connect(soundTrackAudioNode);
  129. }
  130. }
  131. /**
  132. * Transform this sound into a directional source
  133. * @param coneInnerAngle Size of the inner cone in degree
  134. * @param coneOuterAngle Size of the outer cone in degree
  135. * @param coneOuterGain Volume of the sound outside the outer cone (between 0.0 and 1.0)
  136. */
  137. public setDirectionalCone(coneInnerAngle: number, coneOuterAngle: number, coneOuterGain: number) {
  138. if (coneOuterAngle < coneInnerAngle) {
  139. BABYLON.Tools.Error("setDirectionalCone(): outer angle of the cone must be superior or equal to the inner angle.");
  140. return;
  141. }
  142. this._coneInnerAngle = coneInnerAngle;
  143. this._coneOuterAngle = coneOuterAngle;
  144. this._coneOuterGain = coneOuterGain;
  145. this._isDirectional = true;
  146. if (this._isPlaying && this.loop) {
  147. this.stop();
  148. this.play();
  149. }
  150. }
  151. public setPosition(newPosition: Vector3) {
  152. this._position = newPosition;
  153. if (this._isPlaying && this.spatialSound) {
  154. this._soundPanner.setPosition(this._position.x, this._position.y, this._position.z);
  155. }
  156. }
  157. public setLocalDirectionToMesh(newLocalDirection: Vector3) {
  158. this._localDirection = newLocalDirection;
  159. if (this._connectedMesh && this._isPlaying) {
  160. this._updateDirection();
  161. }
  162. }
  163. private _updateDirection() {
  164. var mat = this._connectedMesh.getWorldMatrix();
  165. var direction = BABYLON.Vector3.TransformNormal(this._localDirection, mat);
  166. direction.normalize();
  167. this._soundPanner.setOrientation(direction.x, direction.y, direction.z);
  168. }
  169. public updateDistanceFromListener() {
  170. if (this._connectedMesh && this.useCustomAttenuation) {
  171. var distance = this._connectedMesh.getDistanceToCamera(this._scene.activeCamera);
  172. this._soundGain.gain.value = this._customAttenuationFunction(this._volume, distance, this.maxDistance, this.refDistance, this.rolloffFactor);
  173. }
  174. }
  175. public setAttenuationFunction(callback: (currentVolume: number, currentDistance: number, maxDistance: number, refDistance: number, rolloffFactor: number) => number) {
  176. this._customAttenuationFunction = callback;
  177. }
  178. /**
  179. * Play the sound
  180. * @param time (optional) Start the sound after X seconds. Start immediately (0) by default.
  181. */
  182. public play(time?: number) {
  183. if (this._isReadyToPlay) {
  184. try {
  185. var startTime = time ? this._audioEngine.audioContext.currentTime + time : 0;
  186. this._soundSource = this._audioEngine.audioContext.createBufferSource();
  187. this._soundSource.buffer = this._audioBuffer;
  188. if (this.spatialSound) {
  189. this._soundPanner.setPosition(this._position.x, this._position.y, this._position.z);
  190. if (this._isDirectional) {
  191. this._soundPanner.coneInnerAngle = this._coneInnerAngle;
  192. this._soundPanner.coneOuterAngle = this._coneOuterAngle;
  193. this._soundPanner.coneOuterGain = this._coneOuterGain;
  194. if (this._connectedMesh) {
  195. this._updateDirection();
  196. }
  197. else {
  198. this._soundPanner.setOrientation(this._localDirection.x, this._localDirection.y, this._localDirection.z);
  199. }
  200. }
  201. }
  202. this._soundSource.connect(this._audioNode);
  203. this._soundSource.loop = this.loop;
  204. this.startTime = startTime;
  205. this._soundSource.start(startTime, this.startOffset % this._soundSource.buffer.duration);
  206. this._isPlaying = true;
  207. }
  208. catch (ex) {
  209. BABYLON.Tools.Error("Error while trying to play audio: " + this._name + ", " + ex.message);
  210. }
  211. }
  212. }
  213. /**
  214. * Stop the sound
  215. * @param time (optional) Stop the sound after X seconds. Stop immediately (0) by default.
  216. */
  217. public stop(time?: number) {
  218. var stopTime = time ? this._audioEngine.audioContext.currentTime + time : 0;
  219. this._soundSource.stop(stopTime);
  220. this._isPlaying = false;
  221. }
  222. public pause() {
  223. this._soundSource.stop(0);
  224. this.startOffset += this._audioEngine.audioContext.currentTime - this.startTime;
  225. }
  226. public setVolume(newVolume: number) {
  227. this._volume = newVolume;
  228. this._soundGain.gain.value = newVolume;
  229. }
  230. public getVolume(): number {
  231. return this._volume;
  232. }
  233. public attachToMesh(meshToConnectTo: BABYLON.AbstractMesh) {
  234. this._connectedMesh = meshToConnectTo;
  235. if (!this.spatialSound) {
  236. this._createSpatialParameters();
  237. this.spatialSound = true;
  238. if (this._isPlaying && this.loop) {
  239. this.stop();
  240. this.play();
  241. }
  242. }
  243. meshToConnectTo.registerAfterWorldMatrixUpdate((connectedMesh: BABYLON.AbstractMesh) => this._onRegisterAfterWorldMatrixUpdate(connectedMesh));
  244. }
  245. private _onRegisterAfterWorldMatrixUpdate(connectedMesh: BABYLON.AbstractMesh) {
  246. this.setPosition(connectedMesh.position);
  247. if (this._isDirectional && this._isPlaying) {
  248. this._updateDirection();
  249. }
  250. }
  251. private _soundLoaded(audioData: ArrayBuffer) {
  252. this._isLoaded = true;
  253. this._audioEngine.audioContext.decodeAudioData(audioData, (buffer) => {
  254. this._audioBuffer = buffer;
  255. this._isReadyToPlay = true;
  256. if (this.autoplay) { this.play(); }
  257. if (this._readyToPlayCallback) { this._readyToPlayCallback(); }
  258. }, function (error) {
  259. BABYLON.Tools.Error("Error while decoding audio data: " + error.err);
  260. });
  261. }
  262. }
  263. }