UNPKG

@babylonjs/core

Version:

Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.

371 lines 13.9 kB
import { Observable } from "../../Misc/observable.js"; import { AudioEngineV2 } from "../abstractAudio/audioEngineV2.js"; import { _HasSpatialAudioListenerOptions } from "../abstractAudio/subProperties/abstractSpatialAudioListener.js"; import { _CreateSpatialAudioListener } from "./subProperties/spatialWebAudioListener.js"; import { _WebAudioMainOut } from "./webAudioMainOut.js"; import { _WebAudioUnmuteUI } from "./webAudioUnmuteUI.js"; /** * Creates a new v2 audio engine that uses the WebAudio API. * @param options - The options for creating the audio engine. * @returns A promise that resolves with the created audio engine. */ export async function CreateAudioEngineAsync(options = {}) { const engine = new _WebAudioEngine(options); await engine._initAsync(options); return engine; } const FormatMimeTypes = { aac: "audio/aac", ac3: "audio/ac3", flac: "audio/flac", m4a: "audio/mp4", mp3: 'audio/mpeg; codecs="mp3"', mp4: "audio/mp4", ogg: 'audio/ogg; codecs="vorbis"', wav: "audio/wav", webm: 'audio/webm; codecs="vorbis"', }; /** @internal */ export class _WebAudioEngine extends AudioEngineV2 { /** @internal */ constructor(options = {}) { super(options); this._audioContextStarted = false; this._destinationNode = null; this._invalidFormats = new Set(); this._isUpdating = false; this._listener = null; this._listenerAutoUpdate = true; this._listenerMinUpdateTime = 0; this._pauseCalled = false; this._resumeOnInteraction = true; this._resumeOnPause = true; this._resumeOnPauseRetryInterval = 1000; this._resumeOnPauseTimerId = null; this._resumePromise = null; this._silentHtmlAudio = null; this._unmuteUI = null; this._updateObservable = null; this._validFormats = new Set(); this._volume = 1; /** @internal */ this._isUsingOfflineAudioContext = false; /** @internal */ this.isReadyPromise = new Promise((resolve) => { this._resolveIsReadyPromise = resolve; }); /** @internal */ this.stateChangedObservable = new Observable(); /** @internal */ this.userGestureObservable = new Observable(); this._initAudioContextAsync = async () => { this._audioContext.addEventListener("statechange", this._onAudioContextStateChange); this._mainOut = new _WebAudioMainOut(this); this._mainOut.volume = this._volume; await this.createMainBusAsync("default"); }; this._onAudioContextStateChange = () => { if (this.state === "running") { clearInterval(this._resumeOnPauseTimerId); this._audioContextStarted = true; this._resumePromise = null; } if (this.state === "suspended" || this.state === "interrupted") { if (this._audioContextStarted && this._resumeOnPause && !this._pauseCalled) { clearInterval(this._resumeOnPauseTimerId); this._resumeOnPauseTimerId = setInterval(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.resumeAsync(); }, this._resumeOnPauseRetryInterval); } } this.stateChangedObservable.notifyObservers(this.state); }; this._onUserGestureAsync = async () => { if (this._resumeOnInteraction) { await this._audioContext.resume(); } // On iOS the ringer switch must be turned on for WebAudio to play. // This gets WebAudio to play with the ringer switch turned off by playing an HTMLAudioElement. if (!this._silentHtmlAudio) { this._silentHtmlAudio = document.createElement("audio"); const audio = this._silentHtmlAudio; audio.controls = false; audio.preload = "auto"; audio.loop = true; // Wave data for 0.0001 seconds of silence. audio.src = "data:audio/wav;base64,UklGRjAAAABXQVZFZm10IBAAAAABAAEAgLsAAAB3AQACABAAZGF0YQwAAAAAAAEA/v8CAP//AQA="; // eslint-disable-next-line @typescript-eslint/no-floating-promises audio.play(); } this.userGestureObservable.notifyObservers(); }; this._startUpdating = () => { if (this._isUpdating) { return; } this._isUpdating = true; if (this.state === "running") { this._update(); } else { const callback = () => { if (this.state === "running") { this._update(); this.stateChangedObservable.removeCallback(callback); } }; this.stateChangedObservable.add(callback); } }; this._update = () => { if (this._updateObservable?.hasObservers()) { this._updateObservable.notifyObservers(); requestAnimationFrame(this._update); } else { this._isUpdating = false; } }; if (typeof options.listenerAutoUpdate === "boolean") { this._listenerAutoUpdate = options.listenerAutoUpdate; } if (typeof options.listenerMinUpdateTime === "number") { this._listenerMinUpdateTime = options.listenerMinUpdateTime; } this._volume = options.volume ?? 1; if (options.audioContext) { this._isUsingOfflineAudioContext = options.audioContext instanceof OfflineAudioContext; this._audioContext = options.audioContext; } else { this._audioContext = new AudioContext(); } if (!options.disableDefaultUI) { this._unmuteUI = new _WebAudioUnmuteUI(this, options.defaultUIParentElement); } } /** @internal */ async _initAsync(options) { this._resumeOnInteraction = typeof options.resumeOnInteraction === "boolean" ? options.resumeOnInteraction : true; this._resumeOnPause = typeof options.resumeOnPause === "boolean" ? options.resumeOnPause : true; this._resumeOnPauseRetryInterval = options.resumeOnPauseRetryInterval ?? 1000; document.addEventListener("click", this._onUserGestureAsync); await this._initAudioContextAsync(); if (_HasSpatialAudioListenerOptions(options)) { this._listener = _CreateSpatialAudioListener(this, this._listenerAutoUpdate, this._listenerMinUpdateTime); this._listener.setOptions(options); } this._resolveIsReadyPromise(); } /** @internal */ get currentTime() { return this._audioContext.currentTime ?? 0; } /** @internal */ get _inNode() { return this._audioContext.destination; } /** @internal */ get mainOut() { return this._mainOut; } /** @internal */ get listener() { return this._listener ?? (this._listener = _CreateSpatialAudioListener(this, this._listenerAutoUpdate, this._listenerMinUpdateTime)); } /** @internal */ get state() { // Always return "running" for OfflineAudioContext so sound `play` calls work while the context is suspended. return this._isUsingOfflineAudioContext ? "running" : this._audioContext.state; } /** @internal */ get volume() { return this._volume; } /** @internal */ set volume(value) { if (this._volume === value) { return; } this._volume = value; if (this._mainOut) { this._mainOut.volume = value; } } /** * This property should only be used by the legacy audio engine. * @internal * */ get _audioDestination() { return this._destinationNode ? this._destinationNode : (this._destinationNode = this._audioContext.destination); } set _audioDestination(value) { this._destinationNode = value; } /** * This property should only be used by the legacy audio engine. * @internal */ get _unmuteUIEnabled() { return this._unmuteUI ? this._unmuteUI.enabled : false; } set _unmuteUIEnabled(value) { if (this._unmuteUI) { this._unmuteUI.enabled = value; } } /** @internal */ async createBusAsync(name, options = {}) { const module = await import("./webAudioBus.js"); const bus = new module._WebAudioBus(name, this, options); await bus._initAsync(options); return bus; } /** @internal */ async createMainBusAsync(name, options = {}) { const module = await import("./webAudioMainBus.js"); const bus = new module._WebAudioMainBus(name, this); await bus._initAsync(options); return bus; } /** @internal */ async createMicrophoneSoundSourceAsync(name, options) { let mediaStream; try { mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true }); } catch (e) { throw new Error("Unable to access microphone: " + e); } return await this.createSoundSourceAsync(name, new MediaStreamAudioSourceNode(this._audioContext, { mediaStream }), { outBusAutoDefault: false, ...options, }); } /** @internal */ async createSoundAsync(name, source, options = {}) { const module = await import("./webAudioStaticSound.js"); const sound = new module._WebAudioStaticSound(name, this, options); await sound._initAsync(source, options); return sound; } /** @internal */ async createSoundBufferAsync(source, options = {}) { const module = await import("./webAudioStaticSound.js"); const soundBuffer = new module._WebAudioStaticSoundBuffer(this); await soundBuffer._initAsync(source, options); return soundBuffer; } /** @internal */ async createSoundSourceAsync(name, source, options = {}) { const module = await import("./webAudioSoundSource.js"); const soundSource = new module._WebAudioSoundSource(name, source, this, options); await soundSource._initAsync(options); return soundSource; } /** @internal */ async createStreamingSoundAsync(name, source, options = {}) { const module = await import("./webAudioStreamingSound.js"); const sound = new module._WebAudioStreamingSound(name, this, options); await sound._initAsync(source, options); return sound; } /** @internal */ dispose() { super.dispose(); this._listener?.dispose(); this._listener = null; // Note that OfflineAudioContext does not have a `close` method. if (this._audioContext.state !== "closed" && !this._isUsingOfflineAudioContext) { // eslint-disable-next-line @typescript-eslint/no-floating-promises this._audioContext.close(); } document.removeEventListener("click", this._onUserGestureAsync); this._audioContext.removeEventListener("statechange", this._onAudioContextStateChange); this._silentHtmlAudio?.remove(); this._updateObservable?.clear(); this._updateObservable = null; this._unmuteUI?.dispose(); this._unmuteUI = null; this.stateChangedObservable.clear(); } /** @internal */ flagInvalidFormat(format) { this._invalidFormats.add(format); } /** @internal */ isFormatValid(format) { if (this._validFormats.has(format)) { return true; } if (this._invalidFormats.has(format)) { return false; } const mimeType = FormatMimeTypes[format]; if (mimeType === undefined) { return false; } const audio = new Audio(); if (audio.canPlayType(mimeType) === "") { this._invalidFormats.add(format); return false; } this._validFormats.add(format); return true; } /** @internal */ async pauseAsync() { await this._audioContext.suspend(); this._pauseCalled = true; } /** @internal */ // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax resumeAsync() { this._pauseCalled = false; if (this._resumePromise) { return this._resumePromise; } this._resumePromise = this._audioContext.resume(); return this._resumePromise; } /** @internal */ setVolume(value, options = null) { if (this._mainOut) { this._mainOut.setVolume(value, options); } else { throw new Error("Main output not initialized yet."); } } /** @internal */ _addMainBus(mainBus) { super._addMainBus(mainBus); } /** @internal */ _removeMainBus(mainBus) { super._removeMainBus(mainBus); } /** @internal */ _addNode(node) { super._addNode(node); } /** @internal */ _removeNode(node) { super._removeNode(node); } /** @internal */ _addUpdateObserver(callback) { if (!this._updateObservable) { this._updateObservable = new Observable(); } this._updateObservable.add(callback); this._startUpdating(); } _removeUpdateObserver(callback) { if (this._updateObservable) { this._updateObservable.removeCallback(callback); } } } //# sourceMappingURL=webAudioEngine.js.map