@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.
258 lines • 9.56 kB
JavaScript
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._init(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._invalidFormats = new Set();
this._isUsingOfflineAudioContext = false;
this._listener = null;
this._pauseCalled = false;
this._resumeOnInteraction = true;
this._resumeOnPause = true;
this._resumeOnPauseRetryInterval = 1000;
this._resumeOnPauseTimerId = null;
this._resumePromise = null;
this._listenerAutoUpdate = true;
this._listenerMinUpdateTime = 0;
this._unmuteUI = null;
this._validFormats = new Set();
this._volume = 1;
/** @internal */
this.isReadyPromise = new Promise((resolve) => {
this._resolveIsReadyPromise = resolve;
});
/** @internal */
this.stateChangedObservable = new Observable();
/** @internal */
this.userGestureObservable = new Observable();
this._initAudioContext = 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(() => {
this.resumeAsync();
}, this._resumeOnPauseRetryInterval);
}
}
this.stateChangedObservable.notifyObservers(this.state);
};
this._onUserGesture = async () => {
if (this._resumeOnInteraction) {
await this._audioContext.resume();
}
this.userGestureObservable.notifyObservers();
};
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 _init(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._onUserGesture);
await this._initAudioContext();
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;
}
}
/** @internal */
async createBusAsync(name, options = {}) {
const module = await import("./webAudioBus.js");
const bus = new module._WebAudioBus(name, this, options);
await bus._init(options);
return bus;
}
/** @internal */
async createMainBusAsync(name, options = {}) {
const module = await import("./webAudioMainBus.js");
const bus = new module._WebAudioMainBus(name, this);
await bus._init(options);
return bus;
}
/** @internal */
async createSoundAsync(name, source, options = {}) {
const module = await import("./webAudioStaticSound.js");
const sound = new module._WebAudioStaticSound(name, this, options);
await sound._init(source, options);
return sound;
}
/** @internal */
async createSoundBufferAsync(source, options = {}) {
const module = await import("./webAudioStaticSound.js");
const soundBuffer = new module._WebAudioStaticSoundBuffer(this);
await soundBuffer._init(source, options);
return soundBuffer;
}
/** @internal */
async createStreamingSoundAsync(name, source, options = {}) {
const module = await import("./webAudioStreamingSound.js");
const sound = new module._WebAudioStreamingSound(name, this, options);
await sound._init(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) {
this._audioContext.close();
}
document.removeEventListener("click", this._onUserGesture);
this._audioContext.removeEventListener("statechange", this._onAudioContextStateChange);
this._unmuteUI?.dispose();
this._unmuteUI = null;
}
/** @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 */
resumeAsync() {
this._pauseCalled = false;
if (this._resumePromise) {
return this._resumePromise;
}
this._resumePromise = this._audioContext.resume();
return this._resumePromise;
}
/** @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 */
_setAudioParam(audioParam, value) {
audioParam.linearRampToValueAtTime(value, this.currentTime + this.parameterRampDuration);
}
}
//# sourceMappingURL=webAudioEngine.js.map