MSFT_audio_emitter.ts 13 KB

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