MSFT_audio_emitter.ts 12 KB

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