@threlte/extras
Version:
Utilities, abstractions and plugins for your Threlte apps
139 lines (138 loc) • 4.03 kB
JavaScript
import { useLoader } from '@threlte/core';
import { AudioLoader } from 'three';
/**
* This hook handles basic audio functionality.
* It’s used by the <Audio> and <PositionalAudio> components.
*/
export const useAudio = (audio, src, autoplay, loop, volume, playbackRate, detune, props) => {
let loaded = $state(false);
let shouldPlay = $state(false);
let audioDestroyed = false;
// @Todo: replace with an AbortController
let audioEpoch = 0;
const isCurrentAudio = (currentAudio, epoch) => {
return !audioDestroyed && epoch === audioEpoch && currentAudio === audio();
};
const stopAudio = (currentAudio) => {
if (!currentAudio)
return;
if (!currentAudio.source) {
return currentAudio;
}
return currentAudio.stop();
};
$effect(() => {
const currentAudio = audio();
audioEpoch += 1;
return () => {
audioEpoch += 1;
try {
stopAudio(currentAudio);
}
catch (error) {
console.warn('Error while destroying audio', error);
}
};
});
$effect(() => {
audio()?.setVolume(volume());
});
$effect(() => {
audio()?.setPlaybackRate(playbackRate());
});
$effect(() => {
const currentAudio = audio();
if (currentAudio?.source && currentAudio.detune) {
currentAudio.setDetune(detune());
}
});
$effect(() => {
audio()?.setLoop(loop());
});
$effect(() => {
if (!loaded) {
if (audio()?.isPlaying)
stop();
return;
}
if (autoplay() || shouldPlay) {
play();
}
});
$effect(() => {
audioEpoch += 1;
setSrc(src());
});
const loader = useLoader(AudioLoader);
const setSrc = async (source) => {
loaded = false;
const currentAudio = audio();
const epoch = audioEpoch;
if (!currentAudio)
return;
const { onload, onprogress, onerror } = props();
if (!isCurrentAudio(currentAudio, epoch))
return;
try {
if (typeof source === 'string') {
const audioBuffer = await loader.load(source, {
onProgress(event) {
onprogress?.(event);
}
});
currentAudio.setBuffer(audioBuffer);
}
else if (source instanceof AudioBuffer) {
currentAudio.setBuffer(source);
}
else if (source instanceof HTMLMediaElement) {
currentAudio.setMediaElementSource(source);
}
else if (source instanceof AudioBufferSourceNode) {
currentAudio.setNodeSource(source);
}
else if (source instanceof MediaStream) {
currentAudio.setMediaStreamSource(source);
}
loaded = true;
onload?.(currentAudio.buffer);
}
catch (error) {
onerror?.(error);
}
};
const play = async (delay) => {
// source is not loaded yet, so we should play it after it's loaded
if (!loaded) {
shouldPlay = true;
return;
}
const currentAudio = audio();
const epoch = audioEpoch;
if (!currentAudio)
return;
if (currentAudio.context.state !== 'running') {
await currentAudio.context.resume();
if (!isCurrentAudio(currentAudio, epoch)) {
return;
}
}
return currentAudio.play(delay);
};
const pause = () => {
return audio()?.pause();
};
const stop = () => {
return stopAudio(audio());
};
$effect(() => {
return () => {
audioDestroyed = true;
};
});
return {
play,
pause,
stop
};
};