|
import { useCallback, useEffect, useRef, useState } from 'react'; |
|
|
|
|
|
type UseAudioResponse = { |
|
playback: (base64Data?: string, isLastTrackOfPlaylist?: boolean) => Promise<boolean>; |
|
progress: number; |
|
isLoaded: boolean; |
|
isPlaying: boolean; |
|
isSwitchingTracks: boolean; |
|
togglePause: () => void; |
|
}; |
|
|
|
export function useAudio(): UseAudioResponse { |
|
const audioContextRef = useRef<AudioContext | null>(null); |
|
const sourceNodeRef = useRef<AudioBufferSourceNode | null>(null); |
|
const [progress, setProgress] = useState(0.0); |
|
const [isPlaying, setIsPlaying] = useState(false); |
|
const [isLoaded, setIsLoaded] = useState(false); |
|
const [isSwitchingTracks, setSwitchingTracks] = useState(false); |
|
const startTimeRef = useRef(0); |
|
const pauseTimeRef = useRef(0); |
|
|
|
const stopAudio = useCallback(() => { |
|
try { |
|
audioContextRef.current?.close(); |
|
} catch (err) { |
|
|
|
} |
|
setSwitchingTracks(false); |
|
|
|
sourceNodeRef.current = null; |
|
sourceNodeRef.current = null; |
|
|
|
|
|
}, []); |
|
|
|
|
|
async function base64ToArrayBuffer(base64: string): Promise<ArrayBuffer> { |
|
const response = await fetch(base64); |
|
return response.arrayBuffer(); |
|
} |
|
|
|
const playback = useCallback( |
|
async (base64Data?: string, isLastTrackOfPlaylist?: boolean): Promise<boolean> => { |
|
stopAudio(); |
|
|
|
|
|
if (!base64Data) { |
|
return false; |
|
} |
|
|
|
|
|
const audioContext = new AudioContext(); |
|
audioContextRef.current = audioContext; |
|
|
|
|
|
const formattedBase64 = |
|
base64Data.startsWith('data:audio/wav') || base64Data.startsWith('data:audio/wav;base64,') |
|
? base64Data |
|
: `data:audio/wav;base64,${base64Data}`; |
|
|
|
console.log(`formattedBase64: ${formattedBase64.slice(0, 50)} (len: ${formattedBase64.length})`); |
|
|
|
const arrayBuffer = await base64ToArrayBuffer(formattedBase64); |
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
audioContext.decodeAudioData(arrayBuffer, (audioBuffer) => { |
|
|
|
const source = audioContext.createBufferSource(); |
|
const gainNode = audioContext.createGain(); |
|
|
|
|
|
source.buffer = audioBuffer; |
|
gainNode.gain.value = 1.0; |
|
|
|
|
|
source.connect(gainNode); |
|
gainNode.connect(audioContext.destination); |
|
|
|
|
|
sourceNodeRef.current = source; |
|
source.start(0, pauseTimeRef.current % audioBuffer.duration); |
|
startTimeRef.current = audioContextRef.current!.currentTime - pauseTimeRef.current; |
|
|
|
setSwitchingTracks(false); |
|
setProgress(0); |
|
setIsLoaded(true); |
|
setIsPlaying(true); |
|
|
|
|
|
const totalDuration = audioBuffer.duration; |
|
const updateProgressInterval = setInterval(() => { |
|
if (sourceNodeRef.current && audioContextRef.current) { |
|
const currentTime = audioContextRef.current.currentTime; |
|
const currentProgress = currentTime / totalDuration; |
|
setProgress(currentProgress); |
|
if (currentProgress >= 1.0) { |
|
clearInterval(updateProgressInterval); |
|
} |
|
} |
|
}, 50); |
|
|
|
if (source) { |
|
source.onended = () => { |
|
|
|
if (!isLastTrackOfPlaylist) { |
|
setSwitchingTracks(true); |
|
} |
|
setIsPlaying(false); |
|
clearInterval(updateProgressInterval); |
|
stopAudio(); |
|
resolve(true); |
|
}; |
|
} |
|
}, (error) => { |
|
console.error('Error decoding audio data:', error); |
|
reject(error); |
|
}); |
|
}) |
|
}, |
|
[stopAudio] |
|
); |
|
|
|
const togglePause = useCallback(() => { |
|
if (!audioContextRef.current || !sourceNodeRef.current) { |
|
return; |
|
} |
|
|
|
if (isPlaying) { |
|
|
|
pauseTimeRef.current += audioContextRef.current.currentTime - startTimeRef.current; |
|
sourceNodeRef.current.stop(); |
|
sourceNodeRef.current = null; |
|
setIsPlaying(false); |
|
} else { |
|
|
|
audioContextRef.current.resume().then(() => { |
|
playback(); |
|
}); |
|
} |
|
}, [audioContextRef, sourceNodeRef, isPlaying, playback]); |
|
|
|
|
|
useEffect(() => { |
|
return () => { |
|
stopAudio(); |
|
}; |
|
}, [stopAudio]); |
|
|
|
return { playback, isPlaying, isSwitchingTracks, isLoaded, progress, togglePause }; |
|
} |