UNPKG

babylon-mmd

Version:
530 lines (529 loc) 16.6 kB
import { Observable } from "@babylonjs/core/Misc/observable"; /** * This class is used to pooling the audio element */ export class AudioElementPool { _audioElements = []; /** * Rent the audio element * @returns The audio element */ rent() { if (this._audioElements.length === 0) { return new Audio(); } else { const audio = this._audioElements.pop(); audio.loop = false; audio.autoplay = false; audio.playbackRate = 1; audio.volume = 1; audio.muted = false; audio.preservesPitch = true; audio.ondurationchange = null; audio.onerror = null; audio.onplaying = null; audio.onpause = null; audio.onseeked = null; return audio; } } /** * Return the audio element * * Returned audio element should not be used anymore * @param audioElement The audio element */ return(audioElement) { audioElement.pause(); audioElement.src = ""; audioElement.load(); this._audioElements.push(audioElement); } } /** * Stream audio player * * This class is used to play the audio from the stream * * It is suitable for playing long sounds because it plays even if all of the audio is not loaded * * Wrapper of `HTMLAudioElement` which handles audio playback permission issues gracefully */ export class StreamAudioPlayer { /** * Default global audio element pool instance */ static DefaultAudioElementPool = new AudioElementPool(); /** * On load error observable * * This observable is notified when the audio load is failed */ onLoadErrorObservable; /** * On duration changed observable * * This observable is notified when the audio duration is changed */ onDurationChangedObservable; /** * On mute state changed observable * * This observable is notified when the mute state is changed */ onPlaybackRateChangedObservable; /** * On mute state changed observable * * This observable is notified when the mute state is changed */ onMuteStateChangedObservable; /** * On play observable * * This observable is notified when the player is played */ onPlayObservable; /** * On pause observable * * This observable is notified when the player is paused */ onPauseObservable; /** * On seek observable * * This observable is notified when the player is seeked */ onSeekObservable; _pool; _audio; _duration; _playbackRate; _isVirtualPlay; _virtualStartTime; _virtualPaused; _virtualPauseCurrentTime; _metadataLoaded; _bindedDispose; _disposeObservableObject; /** * Create a stream audio player * * In general disposeObservable should be `Scene` of Babylon.js * * @param disposeObservable Objects that limit the lifetime of this instance * @param options Options */ constructor(disposeObservable, options = {}) { const poolOption = options.pool ?? false; this.onLoadErrorObservable = new Observable(); this.onDurationChangedObservable = new Observable(); this.onPlaybackRateChangedObservable = new Observable(); this.onMuteStateChangedObservable = new Observable(); this.onPlayObservable = new Observable(); this.onPauseObservable = new Observable(); this.onSeekObservable = new Observable(); const pool = this._pool = typeof poolOption === "boolean" ? poolOption ? StreamAudioPlayer.DefaultAudioElementPool : null : poolOption; const audio = this._audio = pool !== null ? pool.rent() : new Audio(); audio.loop = false; audio.autoplay = false; this._duration = 0; this._playbackRate = 1; this._isVirtualPlay = false; this._virtualStartTime = 0; this._virtualPaused = true; this._virtualPauseCurrentTime = 0; this._metadataLoaded = false; audio.ondurationchange = this._onDurationChanged; audio.onerror = this._onLoadError; audio.onplaying = this._onPlay; audio.onpause = this._onPause; audio.onseeked = this._onSeek; this._bindedDispose = this.dispose.bind(this); this._disposeObservableObject = disposeObservable; if (this._disposeObservableObject !== null) { this._disposeObservableObject.onDisposeObservable.add(this._bindedDispose); } } _onDurationChanged = () => { this._duration = this._audio.duration; if (this._isVirtualPlay) { this._isVirtualPlay = false; this.onMuteStateChangedObservable.notifyObservers(); } this._virtualPaused = true; this._virtualPauseCurrentTime = 0; this._metadataLoaded = true; this.onDurationChangedObservable.notifyObservers(); }; _onLoadError = () => { this._duration = 0; if (this._isVirtualPlay) { this._isVirtualPlay = false; this.onMuteStateChangedObservable.notifyObservers(); } this._virtualPaused = true; this._virtualPauseCurrentTime = 0; this._metadataLoaded = false; this.onLoadErrorObservable.notifyObservers(); this.onDurationChangedObservable.notifyObservers(); }; _onPlay = () => { if (!this._isVirtualPlay) { this._audio.playbackRate = this._playbackRate; } this.onPlayObservable.notifyObservers(); }; _onPause = () => { if (!this._isVirtualPlay) { this.onPauseObservable.notifyObservers(); } else { if (this._virtualPaused) { this.onPauseObservable.notifyObservers(); } } }; _ignoreSeekedEventOnce = false; _onSeek = () => { if (this._ignoreSeekedEventOnce) { this._ignoreSeekedEventOnce = false; return; } this.onSeekObservable.notifyObservers(); }; /** * Audio duration (in seconds) */ get duration() { return this._duration; } /** * Current time (in seconds) * * This property may be slow to update */ get currentTime() { if (this._isVirtualPlay) { if (this._virtualPaused) { return this._virtualPauseCurrentTime; } else { const computedTime = (performance.now() / 1000 - this._virtualStartTime) * this._playbackRate; if (computedTime > this._duration) { this._virtualPaused = true; this._virtualPauseCurrentTime = this._duration; this._onPause(); return this._virtualPauseCurrentTime; } else { return computedTime; } } } else { return this._audio?.currentTime ?? 0; } } set currentTime(value) { if (this._isVirtualPlay) { if (this._virtualPaused) { this._virtualPauseCurrentTime = value; } else { this._virtualStartTime = performance.now() / 1000 - value / this._playbackRate; } this._onSeek(); } else { if (this._audio !== null) { this._audio.currentTime = value; } } } /** @internal */ _setCurrentTimeWithoutNotify(value) { if (this._isVirtualPlay) { if (this._virtualPaused) { this._virtualPauseCurrentTime = value; } else { this._virtualStartTime = performance.now() / 1000 - value / this._playbackRate; } } else { this._ignoreSeekedEventOnce = true; if (this._audio !== null) { this._audio.currentTime = value; } } } /** * Volume (0.0 to 1.0) */ get volume() { return this._audio?.volume ?? 0; } set volume(value) { if (this._audio !== null) { this._audio.volume = value; } } /** * Whether the audio is muted */ get muted() { return this._isVirtualPlay; } /** * Mute the audio */ mute() { if (this._audio === null) return; if (this._isVirtualPlay) return; this._isVirtualPlay = true; this._virtualStartTime = performance.now() / 1000 - this._audio.currentTime / this._playbackRate; this._virtualPaused = this._audio.paused; this._virtualPauseCurrentTime = this._audio.currentTime; this._audio.pause(); this.onMuteStateChangedObservable.notifyObservers(); } /** * Unmute the audio * * Unmute is possible failed if user interaction is not performed * @returns Whether the audio is unmuted */ // eslint-disable-next-line @typescript-eslint/naming-convention async unmute() { if (this._audio === null) return false; if (!this._isVirtualPlay) return true; let notAllowedError = false; this._ignoreSeekedEventOnce = true; if (this._virtualPaused) { this._audio.currentTime = this._virtualPauseCurrentTime; } else { this._audio.currentTime = (performance.now() / 1000 - this._virtualStartTime) * this._playbackRate; try { await this._audio.play(); this._audio.playbackRate = this._playbackRate; } catch (e) { if (!(e instanceof DOMException && e.name === "NotAllowedError")) throw e; notAllowedError = true; } } if (!notAllowedError) { this._isVirtualPlay = false; this._virtualPaused = true; this._virtualPauseCurrentTime = 0; this.onMuteStateChangedObservable.notifyObservers(); return true; } return false; } /** * Playback rate (0.07 to 16.0) */ get playbackRate() { return this._playbackRate; } set playbackRate(value) { this._setPlaybackRateWithoutNotify(value); this.onPlaybackRateChangedObservable.notifyObservers(); } /** @internal */ _setPlaybackRateWithoutNotify(value) { if (this._isVirtualPlay && !this._virtualPaused) { const nowInSec = performance.now() / 1000; const currentTime = (nowInSec - this._virtualStartTime) * this._playbackRate; this._virtualStartTime = nowInSec - currentTime / value; } this._playbackRate = value; if (this._audio !== null) { this._audio.playbackRate = value; } } /** * Determines whether or not the browser should adjust the pitch of the audio to compensate for changes to the playback rate made by setting */ get preservesPitch() { return this._audio?.preservesPitch ?? true; } set preservesPitch(value) { if (this._audio !== null) { this._audio.preservesPitch = value; } } /** * Whether the player is paused */ get paused() { if (this._isVirtualPlay) { return this._virtualPaused; } else { return this._audio?.paused ?? true; } } /** * Audio source URL */ get source() { return this._audio?.src ?? ""; } set source(value) { if (this._audio === null) { return; } if (value === this._audio.src) return; this._audio.src = value; this._metadataLoaded = false; if (this._isVirtualPlay) { this._isVirtualPlay = false; this.onMuteStateChangedObservable.notifyObservers(); } this._virtualPaused = true; this._virtualPauseCurrentTime = 0; this._audio.load(); } /** * Whether the audio metadata(durations) is loaded */ get metadataLoaded() { return this._metadataLoaded; } async _virtualPlayAsync() { if (this._metadataLoaded) { if (this._virtualPaused) { this._virtualStartTime = performance.now() / 1000 - this._virtualPauseCurrentTime / this._playbackRate; this._virtualPaused = false; } if (!this._isVirtualPlay) { this._isVirtualPlay = true; this.onMuteStateChangedObservable.notifyObservers(); } this._onPlay(); } else { await new Promise((resolve, reject) => { const onDurationChanged = () => { if (this._virtualPaused) { this._virtualStartTime = performance.now() / 1000 - this._virtualPauseCurrentTime / this._playbackRate; this._virtualPaused = false; } if (!this._isVirtualPlay) { this._isVirtualPlay = true; this.onMuteStateChangedObservable.notifyObservers(); } this._onPlay(); this.onLoadErrorObservable.removeCallback(onLoadError); resolve(); }; const onLoadError = () => { this.onDurationChangedObservable.removeCallback(onDurationChanged); reject(new DOMException("The media resource indicated by the src attribute or assigned media provider object was not suitable.", "NotSupportedError")); }; this.onDurationChangedObservable.addOnce(onDurationChanged); this.onLoadErrorObservable.addOnce(onLoadError); }); } } _playRequestBlocking = false; /** * Play the audio from the current position * * If context don't have permission to play the audio, play audio in a mute state */ // eslint-disable-next-line @typescript-eslint/naming-convention async play() { if (this._isVirtualPlay && !this._virtualPaused) return; if (this._isVirtualPlay) { await this._virtualPlayAsync(); return; } if (this._playRequestBlocking) return; this._playRequestBlocking = true; try { await this._audio?.play(); } catch (e) { if (e instanceof DOMException && e.name === "NotAllowedError") { await this._virtualPlayAsync(); } else { throw e; } } finally { this._playRequestBlocking = false; } } /** * Pause the audio */ pause() { if (this._isVirtualPlay) { if (this._virtualPaused) return; this._virtualPaused = true; this._virtualPauseCurrentTime = (performance.now() / 1000 - this._virtualStartTime) * this._playbackRate; this._onPause(); } else { this._audio?.pause(); } } /** * Dispose the player */ dispose() { if (this._audio === null) { return; } const audio = this._audio; audio.pause(); audio.ondurationchange = null; audio.onerror = null; audio.onplaying = null; audio.onpause = null; audio.onseeked = null; audio.src = ""; audio.load(); if (this._pool !== null) { this._pool.return(audio); } else { this._audio.remove(); } this.onLoadErrorObservable.clear(); this.onDurationChangedObservable.clear(); this.onPlaybackRateChangedObservable.clear(); this.onMuteStateChangedObservable.clear(); this.onPlayObservable.clear(); this.onPauseObservable.clear(); this.onSeekObservable.clear(); if (this._disposeObservableObject !== null) { this._disposeObservableObject.onDisposeObservable.removeCallback(this._bindedDispose); } this._audio = null; this._pool = null; } }