UNPKG

@threlte/extras

Version:

Utilities, abstractions and plugins for your Threlte apps

139 lines (138 loc) 4.03 kB
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 }; };