MSFT_audio_emitter.ts 12 KB

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