babylon.sound.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. var BABYLON;
  2. (function (BABYLON) {
  3. var Sound = (function () {
  4. /**
  5. * Create a sound and attach it to a scene
  6. * @param name Name of your sound
  7. * @param urlOrArrayBuffer Url to the sound to load async or ArrayBuffer
  8. * @param readyToPlayCallback Provide a callback function if you'd like to load your code once the sound is ready to be played
  9. * @param options Objects to provide with the current available options: autoplay, loop, volume, spatialSound, maxDistance, rolloffFactor, refDistance, distanceModel, panningModel
  10. */
  11. function Sound(name, urlOrArrayBuffer, scene, readyToPlayCallback, options) {
  12. var _this = this;
  13. this.autoplay = false;
  14. this.loop = false;
  15. this.useCustomAttenuation = false;
  16. this.spatialSound = false;
  17. this.refDistance = 1;
  18. this.rolloffFactor = 1;
  19. this.maxDistance = 100;
  20. this.distanceModel = "linear";
  21. this.panningModel = "HRTF";
  22. this._playbackRate = 1;
  23. this._startTime = 0;
  24. this._startOffset = 0;
  25. this._position = BABYLON.Vector3.Zero();
  26. this._localDirection = new BABYLON.Vector3(1, 0, 0);
  27. this._volume = 1;
  28. this._isLoaded = false;
  29. this._isReadyToPlay = false;
  30. this._isPlaying = false;
  31. this._isDirectional = false;
  32. // Used if you'd like to create a directional sound.
  33. // If not set, the sound will be omnidirectional
  34. this._coneInnerAngle = 360;
  35. this._coneOuterAngle = 360;
  36. this._coneOuterGain = 0;
  37. this.name = name;
  38. this._scene = scene;
  39. this._readyToPlayCallback = readyToPlayCallback;
  40. // Default custom attenuation function is a linear attenuation
  41. this._customAttenuationFunction = function (currentVolume, currentDistance, maxDistance, refDistance, rolloffFactor) {
  42. if (currentDistance < maxDistance) {
  43. return currentVolume * (1 - currentDistance / maxDistance);
  44. }
  45. else {
  46. return 0;
  47. }
  48. };
  49. if (options) {
  50. this.autoplay = options.autoplay || false;
  51. this.loop = options.loop || false;
  52. // if volume === 0, we need another way to check this option
  53. if (options.volume !== undefined) {
  54. this._volume = options.volume;
  55. }
  56. this.spatialSound = options.spatialSound || false;
  57. this.maxDistance = options.maxDistance || 100;
  58. this.useCustomAttenuation = options.useCustomAttenuation || false;
  59. this.rolloffFactor = options.rolloffFactor || 1;
  60. this.refDistance = options.refDistance || 1;
  61. this.distanceModel = options.distanceModel || "linear";
  62. this.panningModel = options.panningModel || "HRTF";
  63. this._playbackRate = options.playbackRate || 1;
  64. }
  65. if (BABYLON.Engine.audioEngine.canUseWebAudio) {
  66. this._soundGain = BABYLON.Engine.audioEngine.audioContext.createGain();
  67. this._soundGain.gain.value = this._volume;
  68. this._inputAudioNode = this._soundGain;
  69. this._ouputAudioNode = this._soundGain;
  70. if (this.spatialSound) {
  71. this._createSpatialParameters();
  72. }
  73. this._scene.mainSoundTrack.AddSound(this);
  74. // if no parameter is passed, you need to call setAudioBuffer yourself to prepare the sound
  75. if (urlOrArrayBuffer) {
  76. // If it's an URL
  77. if (typeof (urlOrArrayBuffer) === "string") {
  78. BABYLON.Tools.LoadFile(urlOrArrayBuffer, function (data) {
  79. _this._soundLoaded(data);
  80. }, null, null, true);
  81. }
  82. else {
  83. if (urlOrArrayBuffer instanceof ArrayBuffer) {
  84. this._soundLoaded(urlOrArrayBuffer);
  85. }
  86. else {
  87. BABYLON.Tools.Error("Parameter must be a URL to the sound or an ArrayBuffer of the sound.");
  88. }
  89. }
  90. }
  91. }
  92. else {
  93. // Adding an empty sound to avoid breaking audio calls for non Web Audio browsers
  94. this._scene.mainSoundTrack.AddSound(this);
  95. if (!BABYLON.Engine.audioEngine.WarnedWebAudioUnsupported) {
  96. BABYLON.Tools.Error("Web Audio is not supported by your browser.");
  97. BABYLON.Engine.audioEngine.WarnedWebAudioUnsupported = true;
  98. }
  99. }
  100. }
  101. Sound.prototype.dispose = function () {
  102. if (BABYLON.Engine.audioEngine.canUseWebAudio && this._isReadyToPlay) {
  103. if (this._isPlaying) {
  104. this.stop();
  105. }
  106. this._isReadyToPlay = false;
  107. if (this.soundTrackId === -1) {
  108. this._scene.mainSoundTrack.RemoveSound(this);
  109. }
  110. else {
  111. this._scene.soundTracks[this.soundTrackId].RemoveSound(this);
  112. }
  113. this._soundGain.disconnect();
  114. this._soundSource.disconnect();
  115. if (this._soundPanner) {
  116. this._soundPanner.disconnect();
  117. this._soundPanner = null;
  118. }
  119. this._audioBuffer = null;
  120. this._soundGain = null;
  121. this._soundSource = null;
  122. if (this._connectedMesh) {
  123. this._connectedMesh.unregisterAfterWorldMatrixUpdate(this._registerFunc);
  124. this._connectedMesh = null;
  125. }
  126. }
  127. };
  128. Sound.prototype._soundLoaded = function (audioData) {
  129. var _this = this;
  130. this._isLoaded = true;
  131. BABYLON.Engine.audioEngine.audioContext.decodeAudioData(audioData, function (buffer) {
  132. _this._audioBuffer = buffer;
  133. _this._isReadyToPlay = true;
  134. if (_this.autoplay) {
  135. _this.play();
  136. }
  137. if (_this._readyToPlayCallback) {
  138. _this._readyToPlayCallback();
  139. }
  140. }, function (error) {
  141. BABYLON.Tools.Error("Error while decoding audio data: " + error.err);
  142. });
  143. };
  144. Sound.prototype.setAudioBuffer = function (audioBuffer) {
  145. if (BABYLON.Engine.audioEngine.canUseWebAudio) {
  146. this._audioBuffer = audioBuffer;
  147. this._isReadyToPlay = true;
  148. }
  149. };
  150. Sound.prototype.updateOptions = function (options) {
  151. if (options) {
  152. this.loop = options.loop || this.loop;
  153. this.maxDistance = options.maxDistance || this.maxDistance;
  154. this.useCustomAttenuation = options.useCustomAttenuation || this.useCustomAttenuation;
  155. this.rolloffFactor = options.rolloffFactor || this.rolloffFactor;
  156. this.refDistance = options.refDistance || this.refDistance;
  157. this.distanceModel = options.distanceModel || this.distanceModel;
  158. this.panningModel = options.panningModel || this.panningModel;
  159. this._playbackRate = options.playbackRate || this._playbackRate;
  160. }
  161. };
  162. Sound.prototype._createSpatialParameters = function () {
  163. if (BABYLON.Engine.audioEngine.canUseWebAudio) {
  164. this._soundPanner = BABYLON.Engine.audioEngine.audioContext.createPanner();
  165. if (this.useCustomAttenuation) {
  166. // Tricks to disable in a way embedded Web Audio attenuation
  167. this._soundPanner.distanceModel = "linear";
  168. this._soundPanner.maxDistance = Number.MAX_VALUE;
  169. this._soundPanner.refDistance = 1;
  170. this._soundPanner.rolloffFactor = 1;
  171. this._soundPanner.panningModel = "HRTF";
  172. }
  173. else {
  174. this._soundPanner.distanceModel = this.distanceModel;
  175. this._soundPanner.maxDistance = this.maxDistance;
  176. this._soundPanner.refDistance = this.refDistance;
  177. this._soundPanner.rolloffFactor = this.rolloffFactor;
  178. this._soundPanner.panningModel = this.panningModel;
  179. }
  180. this._soundPanner.connect(this._ouputAudioNode);
  181. this._inputAudioNode = this._soundPanner;
  182. }
  183. };
  184. Sound.prototype.connectToSoundTrackAudioNode = function (soundTrackAudioNode) {
  185. if (BABYLON.Engine.audioEngine.canUseWebAudio) {
  186. this._ouputAudioNode.disconnect();
  187. this._ouputAudioNode.connect(soundTrackAudioNode);
  188. }
  189. };
  190. /**
  191. * Transform this sound into a directional source
  192. * @param coneInnerAngle Size of the inner cone in degree
  193. * @param coneOuterAngle Size of the outer cone in degree
  194. * @param coneOuterGain Volume of the sound outside the outer cone (between 0.0 and 1.0)
  195. */
  196. Sound.prototype.setDirectionalCone = function (coneInnerAngle, coneOuterAngle, coneOuterGain) {
  197. if (coneOuterAngle < coneInnerAngle) {
  198. BABYLON.Tools.Error("setDirectionalCone(): outer angle of the cone must be superior or equal to the inner angle.");
  199. return;
  200. }
  201. this._coneInnerAngle = coneInnerAngle;
  202. this._coneOuterAngle = coneOuterAngle;
  203. this._coneOuterGain = coneOuterGain;
  204. this._isDirectional = true;
  205. if (this._isPlaying && this.loop) {
  206. this.stop();
  207. this.play();
  208. }
  209. };
  210. Sound.prototype.setPosition = function (newPosition) {
  211. this._position = newPosition;
  212. if (this._isPlaying && this.spatialSound) {
  213. this._soundPanner.setPosition(this._position.x, this._position.y, this._position.z);
  214. }
  215. };
  216. Sound.prototype.setLocalDirectionToMesh = function (newLocalDirection) {
  217. this._localDirection = newLocalDirection;
  218. if (this._connectedMesh && this._isPlaying) {
  219. this._updateDirection();
  220. }
  221. };
  222. Sound.prototype._updateDirection = function () {
  223. var mat = this._connectedMesh.getWorldMatrix();
  224. var direction = BABYLON.Vector3.TransformNormal(this._localDirection, mat);
  225. direction.normalize();
  226. this._soundPanner.setOrientation(direction.x, direction.y, direction.z);
  227. };
  228. Sound.prototype.updateDistanceFromListener = function () {
  229. if (this._connectedMesh && this.useCustomAttenuation) {
  230. var distance = this._connectedMesh.getDistanceToCamera(this._scene.activeCamera);
  231. this._soundGain.gain.value = this._customAttenuationFunction(this._volume, distance, this.maxDistance, this.refDistance, this.rolloffFactor);
  232. }
  233. };
  234. Sound.prototype.setAttenuationFunction = function (callback) {
  235. this._customAttenuationFunction = callback;
  236. };
  237. /**
  238. * Play the sound
  239. * @param time (optional) Start the sound after X seconds. Start immediately (0) by default.
  240. */
  241. Sound.prototype.play = function (time) {
  242. if (this._isReadyToPlay) {
  243. try {
  244. var startTime = time ? BABYLON.Engine.audioEngine.audioContext.currentTime + time : 0;
  245. if (!this._soundSource) {
  246. if (this.spatialSound) {
  247. this._soundPanner.setPosition(this._position.x, this._position.y, this._position.z);
  248. if (this._isDirectional) {
  249. this._soundPanner.coneInnerAngle = this._coneInnerAngle;
  250. this._soundPanner.coneOuterAngle = this._coneOuterAngle;
  251. this._soundPanner.coneOuterGain = this._coneOuterGain;
  252. if (this._connectedMesh) {
  253. this._updateDirection();
  254. }
  255. else {
  256. this._soundPanner.setOrientation(this._localDirection.x, this._localDirection.y, this._localDirection.z);
  257. }
  258. }
  259. }
  260. }
  261. this._soundSource = BABYLON.Engine.audioEngine.audioContext.createBufferSource();
  262. this._soundSource.buffer = this._audioBuffer;
  263. this._soundSource.connect(this._inputAudioNode);
  264. this._soundSource.loop = this.loop;
  265. this._soundSource.playbackRate.value = this._playbackRate;
  266. this._startTime = startTime;
  267. if (this.onended) {
  268. this._soundSource.onended = this.onended;
  269. }
  270. this._soundSource.start(startTime, this._startOffset % this._soundSource.buffer.duration);
  271. this._isPlaying = true;
  272. }
  273. catch (ex) {
  274. BABYLON.Tools.Error("Error while trying to play audio: " + this.name + ", " + ex.message);
  275. }
  276. }
  277. };
  278. /**
  279. * Stop the sound
  280. * @param time (optional) Stop the sound after X seconds. Stop immediately (0) by default.
  281. */
  282. Sound.prototype.stop = function (time) {
  283. if (this._isPlaying) {
  284. var stopTime = time ? BABYLON.Engine.audioEngine.audioContext.currentTime + time : 0;
  285. this._soundSource.stop(stopTime);
  286. this._isPlaying = false;
  287. }
  288. };
  289. Sound.prototype.pause = function () {
  290. if (this._isPlaying) {
  291. this._soundSource.stop(0);
  292. this._startOffset += BABYLON.Engine.audioEngine.audioContext.currentTime - this._startTime;
  293. }
  294. };
  295. Sound.prototype.setVolume = function (newVolume, time) {
  296. if (BABYLON.Engine.audioEngine.canUseWebAudio) {
  297. if (time) {
  298. this._soundGain.gain.linearRampToValueAtTime(this._volume, BABYLON.Engine.audioEngine.audioContext.currentTime);
  299. this._soundGain.gain.linearRampToValueAtTime(newVolume, time);
  300. }
  301. else {
  302. this._soundGain.gain.value = newVolume;
  303. }
  304. }
  305. this._volume = newVolume;
  306. };
  307. Sound.prototype.setPlaybackRate = function (newPlaybackRate) {
  308. this._playbackRate = newPlaybackRate;
  309. if (this._isPlaying) {
  310. this._soundSource.playbackRate.value = this._playbackRate;
  311. }
  312. };
  313. Sound.prototype.getVolume = function () {
  314. return this._volume;
  315. };
  316. Sound.prototype.attachToMesh = function (meshToConnectTo) {
  317. var _this = this;
  318. this._connectedMesh = meshToConnectTo;
  319. if (!this.spatialSound) {
  320. this._createSpatialParameters();
  321. this.spatialSound = true;
  322. if (this._isPlaying && this.loop) {
  323. this.stop();
  324. this.play();
  325. }
  326. }
  327. this._onRegisterAfterWorldMatrixUpdate(this._connectedMesh);
  328. this._registerFunc = function (connectedMesh) { return _this._onRegisterAfterWorldMatrixUpdate(connectedMesh); };
  329. meshToConnectTo.registerAfterWorldMatrixUpdate(this._registerFunc);
  330. };
  331. Sound.prototype._onRegisterAfterWorldMatrixUpdate = function (connectedMesh) {
  332. this.setPosition(connectedMesh.getBoundingInfo().boundingSphere.centerWorld);
  333. if (this._isDirectional && this._isPlaying) {
  334. this._updateDirection();
  335. }
  336. };
  337. return Sound;
  338. })();
  339. BABYLON.Sound = Sound;
  340. })(BABYLON || (BABYLON = {}));
  341. //# sourceMappingURL=babylon.sound.js.map