MSFT_audio_emitter.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import { Vector3 } from "@babylonjs/core/Maths/math.vector.js";
  2. import { Tools } from "@babylonjs/core/Misc/tools.js";
  3. import { AnimationEvent } from "@babylonjs/core/Animations/animationEvent.js";
  4. import { Sound } from "@babylonjs/core/Audio/sound.js";
  5. import { WeightedSound } from "@babylonjs/core/Audio/weightedsound.js";
  6. import { GLTFLoader, ArrayItem } from "../glTFLoader.js";
  7. const NAME = "MSFT_audio_emitter";
  8. /**
  9. * [Specification](https://github.com/najadojo/glTF/blob/MSFT_audio_emitter/extensions/2.0/Vendor/MSFT_audio_emitter/README.md)
  10. * !!! Experimental Extension Subject to Changes !!!
  11. */
  12. // eslint-disable-next-line @typescript-eslint/naming-convention
  13. export class MSFT_audio_emitter {
  14. /**
  15. * @internal
  16. */
  17. constructor(loader) {
  18. /**
  19. * The name of this extension.
  20. */
  21. this.name = NAME;
  22. this._loader = loader;
  23. this.enabled = this._loader.isExtensionUsed(NAME);
  24. }
  25. /** @internal */
  26. dispose() {
  27. this._loader = null;
  28. this._clips = null;
  29. this._emitters = null;
  30. }
  31. /** @internal */
  32. onLoading() {
  33. const extensions = this._loader.gltf.extensions;
  34. if (extensions && extensions[this.name]) {
  35. const extension = extensions[this.name];
  36. this._clips = extension.clips;
  37. this._emitters = extension.emitters;
  38. ArrayItem.Assign(this._clips);
  39. ArrayItem.Assign(this._emitters);
  40. }
  41. }
  42. /**
  43. * @internal
  44. */
  45. loadSceneAsync(context, scene) {
  46. return GLTFLoader.LoadExtensionAsync(context, scene, this.name, (extensionContext, extension) => {
  47. const promises = new Array();
  48. promises.push(this._loader.loadSceneAsync(context, scene));
  49. for (const emitterIndex of extension.emitters) {
  50. const emitter = ArrayItem.Get(`${extensionContext}/emitters`, this._emitters, emitterIndex);
  51. if (emitter.refDistance != undefined ||
  52. emitter.maxDistance != undefined ||
  53. emitter.rolloffFactor != undefined ||
  54. emitter.distanceModel != undefined ||
  55. emitter.innerAngle != undefined ||
  56. emitter.outerAngle != undefined) {
  57. throw new Error(`${extensionContext}: Direction or Distance properties are not allowed on emitters attached to a scene`);
  58. }
  59. promises.push(this._loadEmitterAsync(`${extensionContext}/emitters/${emitter.index}`, emitter));
  60. }
  61. return Promise.all(promises).then(() => { });
  62. });
  63. }
  64. /**
  65. * @internal
  66. */
  67. loadNodeAsync(context, node, assign) {
  68. return GLTFLoader.LoadExtensionAsync(context, node, this.name, (extensionContext, extension) => {
  69. const promises = new Array();
  70. return this._loader
  71. .loadNodeAsync(extensionContext, node, (babylonMesh) => {
  72. for (const emitterIndex of extension.emitters) {
  73. const emitter = ArrayItem.Get(`${extensionContext}/emitters`, this._emitters, emitterIndex);
  74. promises.push(this._loadEmitterAsync(`${extensionContext}/emitters/${emitter.index}`, emitter).then(() => {
  75. for (const sound of emitter._babylonSounds) {
  76. sound.attachToMesh(babylonMesh);
  77. if (emitter.innerAngle != undefined || emitter.outerAngle != undefined) {
  78. sound.setLocalDirectionToMesh(Vector3.Forward());
  79. sound.setDirectionalCone(2 * Tools.ToDegrees(emitter.innerAngle == undefined ? Math.PI : emitter.innerAngle), 2 * Tools.ToDegrees(emitter.outerAngle == undefined ? Math.PI : emitter.outerAngle), 0);
  80. }
  81. }
  82. }));
  83. }
  84. assign(babylonMesh);
  85. })
  86. .then((babylonMesh) => {
  87. return Promise.all(promises).then(() => {
  88. return babylonMesh;
  89. });
  90. });
  91. });
  92. }
  93. /**
  94. * @internal
  95. */
  96. loadAnimationAsync(context, animation) {
  97. return GLTFLoader.LoadExtensionAsync(context, animation, this.name, (extensionContext, extension) => {
  98. return this._loader.loadAnimationAsync(context, animation).then((babylonAnimationGroup) => {
  99. const promises = new Array();
  100. ArrayItem.Assign(extension.events);
  101. for (const event of extension.events) {
  102. promises.push(this._loadAnimationEventAsync(`${extensionContext}/events/${event.index}`, context, animation, event, babylonAnimationGroup));
  103. }
  104. return Promise.all(promises).then(() => {
  105. return babylonAnimationGroup;
  106. });
  107. });
  108. });
  109. }
  110. _loadClipAsync(context, clip) {
  111. if (clip._objectURL) {
  112. return clip._objectURL;
  113. }
  114. let promise;
  115. if (clip.uri) {
  116. promise = this._loader.loadUriAsync(context, clip, clip.uri);
  117. }
  118. else {
  119. const bufferView = ArrayItem.Get(`${context}/bufferView`, this._loader.gltf.bufferViews, clip.bufferView);
  120. promise = this._loader.loadBufferViewAsync(`/bufferViews/${bufferView.index}`, bufferView);
  121. }
  122. clip._objectURL = promise.then((data) => {
  123. return URL.createObjectURL(new Blob([data], { type: clip.mimeType }));
  124. });
  125. return clip._objectURL;
  126. }
  127. _loadEmitterAsync(context, emitter) {
  128. emitter._babylonSounds = emitter._babylonSounds || [];
  129. if (!emitter._babylonData) {
  130. const clipPromises = new Array();
  131. const name = emitter.name || `emitter${emitter.index}`;
  132. const options = {
  133. loop: false,
  134. autoplay: false,
  135. volume: emitter.volume == undefined ? 1 : emitter.volume,
  136. };
  137. for (let i = 0; i < emitter.clips.length; i++) {
  138. const clipContext = `/extensions/${this.name}/clips`;
  139. const clip = ArrayItem.Get(clipContext, this._clips, emitter.clips[i].clip);
  140. clipPromises.push(this._loadClipAsync(`${clipContext}/${emitter.clips[i].clip}`, clip).then((objectURL) => {
  141. const sound = (emitter._babylonSounds[i] = new Sound(name, objectURL, this._loader.babylonScene, null, options));
  142. sound.refDistance = emitter.refDistance || 1;
  143. sound.maxDistance = emitter.maxDistance || 256;
  144. sound.rolloffFactor = emitter.rolloffFactor || 1;
  145. sound.distanceModel = emitter.distanceModel || "exponential";
  146. }));
  147. }
  148. const promise = Promise.all(clipPromises).then(() => {
  149. const weights = emitter.clips.map((clip) => {
  150. return clip.weight || 1;
  151. });
  152. const weightedSound = new WeightedSound(emitter.loop || false, emitter._babylonSounds, weights);
  153. if (emitter.innerAngle) {
  154. weightedSound.directionalConeInnerAngle = 2 * Tools.ToDegrees(emitter.innerAngle);
  155. }
  156. if (emitter.outerAngle) {
  157. weightedSound.directionalConeOuterAngle = 2 * Tools.ToDegrees(emitter.outerAngle);
  158. }
  159. if (emitter.volume) {
  160. weightedSound.volume = emitter.volume;
  161. }
  162. emitter._babylonData.sound = weightedSound;
  163. });
  164. emitter._babylonData = {
  165. loaded: promise,
  166. };
  167. }
  168. return emitter._babylonData.loaded;
  169. }
  170. _getEventAction(context, sound, action, time, startOffset) {
  171. switch (action) {
  172. case "play" /* IMSFTAudioEmitter_AnimationEventAction.play */: {
  173. return (currentFrame) => {
  174. const frameOffset = (startOffset || 0) + (currentFrame - time);
  175. sound.play(frameOffset);
  176. };
  177. }
  178. case "stop" /* IMSFTAudioEmitter_AnimationEventAction.stop */: {
  179. return () => {
  180. sound.stop();
  181. };
  182. }
  183. case "pause" /* IMSFTAudioEmitter_AnimationEventAction.pause */: {
  184. return () => {
  185. sound.pause();
  186. };
  187. }
  188. default: {
  189. throw new Error(`${context}: Unsupported action ${action}`);
  190. }
  191. }
  192. }
  193. _loadAnimationEventAsync(context, animationContext, animation, event, babylonAnimationGroup) {
  194. if (babylonAnimationGroup.targetedAnimations.length == 0) {
  195. return Promise.resolve();
  196. }
  197. const babylonAnimation = babylonAnimationGroup.targetedAnimations[0];
  198. const emitterIndex = event.emitter;
  199. const emitter = ArrayItem.Get(`/extensions/${this.name}/emitters`, this._emitters, emitterIndex);
  200. return this._loadEmitterAsync(context, emitter).then(() => {
  201. const sound = emitter._babylonData.sound;
  202. if (sound) {
  203. const babylonAnimationEvent = new AnimationEvent(event.time, this._getEventAction(context, sound, event.action, event.time, event.startOffset));
  204. babylonAnimation.animation.addEvent(babylonAnimationEvent);
  205. // Make sure all started audio stops when this animation is terminated.
  206. babylonAnimationGroup.onAnimationGroupEndObservable.add(() => {
  207. sound.stop();
  208. });
  209. babylonAnimationGroup.onAnimationGroupPauseObservable.add(() => {
  210. sound.pause();
  211. });
  212. }
  213. });
  214. }
  215. }
  216. GLTFLoader.RegisterExtension(NAME, (loader) => new MSFT_audio_emitter(loader));
  217. //# sourceMappingURL=MSFT_audio_emitter.js.map