@oiij/use
Version:
Som Composable Functions for Vue 3
288 lines (286 loc) • 8.48 kB
JavaScript
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 };