import TimedEvent from './timedEvent'; /** * TimedAudio wraps an HTMLAudioElement and maps playback to the TimedEvent * lifecycle. It synchronizes elapsedTime with the element's playback position * and forwards start/pause/resume/cancel calls to the audio element. * * constructor(duration=0, url='', gain=1.0) * - duration: number in ms (0 unknown, -1 loop) * - url: source URL for the audio element * - gain: initial volume (0.0 - 1.0) */ export default class TimedAudio extends TimedEvent { url: string; gain: number; muted: boolean; element: HTMLAudioElement | null; constructor(duration: number = 0, url: string = '', gain: number = 1.0) { // create element before super because we want event listeners to be set super(duration); this.url = url; this.element = new Audio(url); this.gain = gain; this.muted = false; this.element.volume = gain; if (duration === -1) { this.element.loop = true; } this.initElListeners(); } initElListeners() { if (!this.element) return; this.element.addEventListener('loadedmetadata', (e) => { if (!this.duration) { // duration is in seconds on the element; convert to ms this.duration = (this.element!.duration || 0) * 1000; } }); this.element.addEventListener('ended', (e) => { if (this.duration === -1) return; if (this.elapsedTime < (this.duration || 0) || !this.duration) { this.duration = this.elapsedTime; this.dispatchEvent(this.makeEvent('PROGRESS')); this.cancelNextTick(); } this.dispatchEvent(this.makeEvent('COMPLETE')); }); } public start() { if (this.started) return; this.element?.play(); super.start(); } public pause() { if (this.paused) return; this.element?.pause(); super.pause(); } public resume() { if (!this.paused) return; this.element?.play(); super.resume(); } public cancel() { this.cancelled = true; this.element?.pause(); this.element = null; super.cancel(); } public setVolume(gain: number = 1.0) { this.gain = gain; if (this.element) this.element.volume = gain; } protected tick() { if (!this.started || this.paused || this.cancelled) return; if (this.duration === -1) return; if (!this.duration && this.element) { this.duration = (this.element.duration || 0) * 1000; } const now = (this.element?.currentTime || 0) * 1000; this.elapsedTime += now - this.lastTick; this.lastTick = now; this.dispatchEvent(this.makeEvent('PROGRESS')); if (typeof window !== 'undefined' && typeof window.requestAnimationFrame !== 'undefined') { this.nextTick = requestAnimationFrame(() => { this.tick(); }); } else { this.nextTick = setTimeout(() => { this.tick(); }, 1000 / 60); } } }