UNPKG

@oiij/use

Version:

Som Composable Functions for Vue 3

288 lines (286 loc) 8.48 kB
import { computed, onUnmounted, readonly, ref, watch } from "vue"; import { createEventHook } from "@vueuse/core"; //#region src/composables/use-audio-context.ts function formatTime(seconds) { const minutes = Math.floor(seconds / 60); seconds = Math.floor(seconds % 60); return `${minutes}:${seconds.toString().padStart(2, "0")}`; } function useAudioContext(options) { const { volume: defaultVolume = 1, playbackRate: defaultPlaybackRate = 1, fade } = options ?? {}; const eqFrequencies = [ 32, 64, 125, 250, 500, 1e3, 2e3, 4e3, 8e3, 16e3 ]; const defaultFadeOptions = typeof fade === "boolean" ? { fade: true, duration: 1 } : fade ?? {}; const controller = new AbortController(); const audioContext = new AudioContext(); const audioElement = new Audio(); audioElement.crossOrigin = "anonymous"; const sourceNode = audioContext.createMediaElementSource(audioElement); const gainNode = audioContext.createGain(); const analyserNode = audioContext.createAnalyser(); analyserNode.fftSize = 2048; const filters = eqFrequencies.map((freq) => { const filter = audioContext.createBiquadFilter(); filter.type = "peaking"; filter.frequency.value = freq; filter.Q.value = 1; filter.gain.value = 0; return filter; }); function createFilterNode() { let filterNode$1 = sourceNode; filters.forEach((filter) => { filterNode$1.connect(filter); filterNode$1 = filter; }); return filterNode$1; } const filterNode = createFilterNode(); filterNode.connect(analyserNode); analyserNode.connect(gainNode); gainNode.connect(audioContext.destination); const onVolumeUpdateEv = createEventHook(); const onMutedEv = createEventHook(); const onRateUpdateEv = createEventHook(); const onPlayingEv = createEventHook(); const onPausedEv = createEventHook(); const onEndedEv = createEventHook(); const onTimeUpdateEv = createEventHook(); const onDurationUpdateEv = createEventHook(); const volumeRef = ref(defaultVolume); const mutedRef = ref(false); gainNode.gain.value = volumeRef.value; function setVolume(volume) { gainNode.gain.cancelScheduledValues(audioContext.currentTime); gainNode.gain.setValueAtTime(Math.max(0, Math.min(1, volume)), audioContext.currentTime); volumeRef.value = volume; if (volume === 0) { mutedRef.value = true; onMutedEv.trigger(audioElement); } onVolumeUpdateEv.trigger(audioElement); } watch(volumeRef, (volume) => { setVolume(volume); }); let volumeCache = defaultVolume; function mute(mute$1 = true) { if (mute$1) { volumeCache = volumeRef.value; setVolume(0); } else setVolume(volumeCache); } watch(mutedRef, (muted) => { mute(muted); }); const playbackRateRef = ref(defaultPlaybackRate); function setPlaybackRate(playbackRate) { audioElement.playbackRate = playbackRate; } watch(playbackRateRef, (playbackRate) => { setPlaybackRate(playbackRate); }); audioElement.addEventListener("ratechange", () => { playbackRateRef.value = audioElement.playbackRate; onRateUpdateEv.trigger(audioElement); }, { signal: controller.signal }); const playingRef = ref(false); const urlRef = ref(); async function play(url) { urlRef.value = url; audioElement.src = url; audioElement.load(); if (audioContext.state === "suspended") await audioContext.resume(); try { await audioElement.play(); } catch (error) { console.error("useAudioContext:play error:", error); throw error; } } watch(urlRef, (url) => { if (url) play(url); }); function stop() { audioElement.pause(); audioElement.currentTime = 0; } audioElement.addEventListener("playing", () => { playingRef.value = true; onPlayingEv.trigger(audioElement); }, { signal: controller.signal }); const pausedRef = ref(false); function pause(options$1) { const { fade: fade$1 = true, duration = 1 } = options$1 ?? defaultFadeOptions; if (fade$1) { const currentTime = audioContext.currentTime; gainNode.gain.cancelScheduledValues(currentTime); gainNode.gain.setValueAtTime(gainNode.gain.value, currentTime); gainNode.gain.linearRampToValueAtTime(0, currentTime + duration); setTimeout(() => { audioElement.pause(); }, duration * 1e3); return; } audioElement.pause(); } function resume(options$1) { const { fade: fade$1 = true, duration = 1 } = options$1 ?? defaultFadeOptions; if (fade$1) { const currentTime = audioContext.currentTime; gainNode.gain.cancelScheduledValues(currentTime); gainNode.gain.setValueAtTime(0, currentTime); gainNode.gain.linearRampToValueAtTime(gainNode.gain.value, currentTime + duration); setTimeout(() => { audioElement.play(); }, duration * 1e3); return; } audioElement.play(); } function toggle() { audioElement.paused ? resume() : pause({ fade: true }); } watch(playingRef, (playing) => { if (playing) resume(); else pause(); }); watch(pausedRef, (paused) => { if (paused) pause(); else resume(); }); audioElement.addEventListener("pause", () => { pausedRef.value = true; onPausedEv.trigger(audioElement); }, { signal: controller.signal }); const endedRef = ref(false); audioElement.addEventListener("ended", () => { endedRef.value = true; onEndedEv.trigger(audioElement); }, { signal: controller.signal }); const currentTimeRef = ref(0); const currentTimeText = computed(() => formatTime(currentTimeRef.value)); function setCurrentTime(time) { audioElement.currentTime = time; } watch(currentTimeRef, (time) => { setCurrentTime(time); }); const progressRef = ref(0); function setProgress(progress) { audioElement.currentTime = Number((progress / 100 * audioElement.duration).toFixed(2)); } watch(progressRef, (progress) => { setProgress(progress); }); audioElement.addEventListener("timeupdate", () => { currentTimeRef.value = audioElement.currentTime; progressRef.value = Number((audioElement.currentTime / audioElement.duration * 100).toFixed(2)); onTimeUpdateEv.trigger(audioElement); }, { signal: controller.signal }); const durationRef = ref(0); const durationText = computed(() => formatTime(durationRef.value)); audioElement.addEventListener("durationchange", () => { durationRef.value = audioElement.duration; onDurationUpdateEv.trigger(audioElement); }, { signal: controller.signal }); const cachedDurationRef = ref(0); const cachedDurationText = computed(() => formatTime(cachedDurationRef.value)); const cachedProgressRef = ref(0); audioElement.addEventListener("canplay", () => { const duration = audioElement.buffered.end(Math.max(0, audioElement.buffered.length - 1)); cachedDurationRef.value = Number(duration.toFixed(2)); cachedProgressRef.value = Number((duration / audioElement.duration * 100).toFixed(2)); }, { signal: controller.signal }); function destroy() { controller.abort(); sourceNode.disconnect(); gainNode.disconnect(); analyserNode.disconnect(); filterNode.disconnect(); audioContext.close(); audioElement.remove(); } function getFrequencyData() { const frequencyData = new Uint8Array(analyserNode.frequencyBinCount); analyserNode.getByteFrequencyData(frequencyData); return frequencyData; } function setEQFrequency(index, value) { filters[index].gain.value = value; } function getEQFrequency(index) { return filters[index].gain.value; } function getEQFrequencies() { return eqFrequencies.map((freq, index) => ({ frequency: freq, gain: getEQFrequency(index) })); } onUnmounted(() => { destroy(); }); return { eqFrequencies, audioContext, audioElement, sourceNode, gainNode, analyserNode, filters, filterNode, volume: volumeRef, setVolume, muted: mutedRef, mute, playbackRate: playbackRateRef, setPlaybackRate, playing: readonly(playingRef), paused: pausedRef, ended: readonly(endedRef), currentTime: currentTimeRef, currentTimeText, setCurrentTime, duration: durationRef, durationText, progress: progressRef, setProgress, cachedDuration: cachedDurationRef, cachedDurationText, cachedProgress: cachedProgressRef, url: urlRef, play, pause, resume, stop, toggle, getFrequencyData, setEQFrequency, getEQFrequency, getEQFrequencies, onVolumeUpdate: onVolumeUpdateEv.on, onMuted: onMutedEv.on, onRateUpdate: onRateUpdateEv.on, onTimeUpdate: onTimeUpdateEv.on, onDurationUpdate: onDurationUpdateEv.on, onPlaying: onPlayingEv.on, onPaused: onPausedEv.on, onEnded: onEndedEv.on }; } //#endregion export { useAudioContext };