// ─── Audio playback hook — receives PCM from WebSocket and plays through Web Audio API ── import { useCallback, useRef, useState } from "react"; const SAMPLE_RATE = 24000; const CHANNELS = 1; export function useAudioPlayback() { const [isListening, setIsListening] = useState(false); const [levels, setLevels] = useState(Array.from({ length: 32 }, () => 0.04)); const audioContextRef = useRef(null); const userTimelinesRef = useRef(new Map()); const handleIncomingPcm = useCallback((data: ArrayBuffer) => { const headerView = new DataView(data, 0, 4); const userIdHash = headerView.getInt32(0, true); const audioData = data.slice(4); const int16Array = new Int16Array(audioData); let sum = 0; for (const sample of int16Array) sum += Math.abs(sample / 32768); const average = int16Array.length ? sum / int16Array.length : 0; setLevels((prev) => prev.map((_, index) => Math.max(0.04, average * (0.5 + Math.sin(index * 0.6 + Date.now() / 140) * 0.35 + 0.65) * 5), ), ); const audioContext = audioContextRef.current; if (!isListening || !audioContext) return; const float32Array = new Float32Array(int16Array.length); for (let i = 0; i < int16Array.length; i++) float32Array[i] = int16Array[i] / 32768; const audioBuffer = audioContext.createBuffer(CHANNELS, float32Array.length / SAMPLE_RATE, SAMPLE_RATE); audioBuffer.getChannelData(0).set(float32Array); const source = audioContext.createBufferSource(); source.buffer = audioBuffer; source.connect(audioContext.destination); const currentTime = audioContext.currentTime; let nextStart = userTimelinesRef.current.get(userIdHash) || 0; if (nextStart < currentTime) nextStart = currentTime + 0.05; source.start(nextStart); userTimelinesRef.current.set(userIdHash, nextStart + audioBuffer.duration); }, [isListening]); const toggleListening = useCallback(async () => { if (isListening) { await audioContextRef.current?.suspend(); userTimelinesRef.current.clear(); setIsListening(false); return; } const AudioContextCtor = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext; audioContextRef.current ??= new AudioContextCtor({ sampleRate: SAMPLE_RATE }); await audioContextRef.current.resume(); setIsListening(true); }, [isListening]); return { isListening, levels, handleIncomingPcm, toggleListening, audioContextRef }; }