import type { IAudioContext, IAudioNode } from 'standardized-audio-context' import { createEventHook, refDefault, type EventHookOn, useEventListener } from '@vueuse/core' import { createAudioSource } from '~/composables/audio/audio-api' import { reactive, ref, type Ref } from 'vue' export interface SoundSource { uuid: string mimetype: string url: string } export interface Sound { preload(): void | Promise dispose(): void readonly audioNode: IAudioNode readonly isErrored: Ref readonly isLoaded: Ref readonly currentTime: number readonly duration: number readonly buffered: number looping: boolean pause(): void | Promise play(): void | Promise seekTo(seconds: number): void | Promise seekBy(seconds: number): void | Promise onSoundLoop: EventHookOn onSoundEnd: EventHookOn } export const soundImplementations = reactive(new Set>()) export const registerSoundImplementation = >(implementation: T) => { soundImplementations.add(implementation) return implementation } // Default Sound implementation @registerSoundImplementation export class HTMLSound implements Sound { #audio = new Audio() #soundLoopEventHook = createEventHook() #soundEndEventHook = createEventHook() readonly isErrored = ref(false) readonly isLoaded = ref(false) audioNode = createAudioSource(this.#audio) onSoundLoop: EventHookOn onSoundEnd: EventHookOn constructor (sources: SoundSource[]) { // TODO: Quality picker this.#audio.src = sources[0].url this.#audio.preload = 'auto' useEventListener(this.#audio, 'ended', () => this.#soundEndEventHook.trigger(this)) useEventListener(this.#audio, 'timeupdate', () => { if (this.#audio.currentTime === 0) { this.#soundLoopEventHook.trigger(this) } }) useEventListener(this.#audio, 'loadeddata', () => { // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState this.isLoaded.value = this.#audio.readyState >= 2 }) useEventListener(this.#audio, 'error', () => { this.isErrored.value = true this.isLoaded.value = true }) this.onSoundLoop = this.#soundLoopEventHook.on this.onSoundEnd = this.#soundEndEventHook.on } preload () { this.#audio.load() } dispose () { this.audioNode.disconnect() } async play () { this.#audio.play() } async pause () { this.#audio.pause() } async seekTo (seconds: number) { this.#audio.currentTime = seconds } async seekBy (seconds: number) { this.#audio.currentTime += seconds } get duration () { const { duration } = this.#audio return isNaN(duration) ? 0 : duration } get buffered () { // https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_delivery/buffering_seeking_time_ranges#creating_our_own_buffering_feedback if (this.duration > 0) { const { length } = this.#audio.buffered for (let i = 0; i < length; i++) { if (this.#audio.buffered.start(length - 1 - i) < this.#audio.currentTime) { return this.#audio.buffered.end(length - 1 - i) } } } return 0 } get currentTime () { return this.#audio.currentTime } get looping () { return this.#audio.loop } set looping (value: boolean) { this.#audio.loop = value } } export const soundImplementation = refDefault(ref>(), HTMLSound)