import { useState, useRef, useEffect } from "react"; import { createSocket } from "../api"; export default function Recorder({ onResult }) { const [recording, setRecording] = useState(false); const [status, setStatus] = useState("idle"); // idle | recording | analyzing | error const [bars, setBars] = useState(new Array(30).fill(3)); const mediaRecorderRef = useRef(null); const socketRef = useRef(null); const analyserRef = useRef(null); const animFrameRef = useRef(null); const chunksRef = useRef([]); const audioChunksRef = useRef([]); const processorRef = useRef(null); const streamRef = useRef(null); // Clean up on unmount useEffect(() => { return () => { stopVisualization(); if (socketRef.current) socketRef.current.close(); }; }, []); const startVisualization = (stream) => { const ctx = new AudioContext(); const source = ctx.createMediaStreamSource(stream); const analyser = ctx.createAnalyser(); analyser.fftSize = 64; source.connect(analyser); analyserRef.current = analyser; const data = new Uint8Array(analyser.frequencyBinCount); const tick = () => { analyser.getByteFrequencyData(data); const slice = Array.from(data).slice(0, 30); setBars(slice.map((v) => Math.max(3, (v / 255) * 60))); animFrameRef.current = requestAnimationFrame(tick); }; tick(); }; const stopVisualization = () => { if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current); setBars(new Array(30).fill(3)); }; const startRecording = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // Connect WebSocket socketRef.current = createSocket( (data) => { setStatus("idle"); onResult(data); }, () => setStatus("error") ); startVisualization(stream); //chunksRef.current = []; //const mr = new MediaRecorder(stream, { mimeType: "audio/webm" }); //mr.ondataavailable = (e) => { //if (e.data.size > 0) chunksRef.current.push(e.data); //}; //mr.onstop = () => { // const blob = new Blob(chunksRef.current, { type: "audio/webm" }); // setStatus("analyzing"); // socketRef.current.sendAudio(blob); // stream.getTracks().forEach((t) => t.stop()); // stopVisualization(); //}; //mediaRecorderRef.current = mr; //mr.start(); const audioContext = new AudioContext({ sampleRate: 16000 }); const source = audioContext.createMediaStreamSource(stream); const processor = audioContext.createScriptProcessor(4096, 1, 1); processorRef.current = processor; streamRef.current = stream; audioChunksRef.current = []; // reset buffer source.connect(processor); processor.connect(audioContext.destination); processor.onaudioprocess = (event) => { const input = event.inputBuffer.getChannelData(0); const buffer = new ArrayBuffer(input.length * 2); const view = new DataView(buffer); for (let i = 0; i < input.length; i++) { let s = Math.max(-1, Math.min(1, input[i])); view.setInt16(i * 2, s * 0x7fff, true); } //STORE instead of sending audioChunksRef.current.push(buffer); }; setRecording(true); setStatus("recording"); } catch (err) { console.error("Microphone access denied", err); setStatus("error"); } }; const stopRecording = () => { setRecording(false); setStatus("analyzing"); stopVisualization(); //Stop audio processing if (processorRef.current) { processorRef.current.disconnect(); } if (streamRef.current) { streamRef.current.getTracks().forEach((t) => t.stop()); } //Merge all chunks into ONE buffer const totalLength = audioChunksRef.current.reduce( (sum, buf) => sum + buf.byteLength, 0 ); const combined = new Uint8Array(totalLength); let offset = 0; audioChunksRef.current.forEach((buf) => { combined.set(new Uint8Array(buf), offset); offset += buf.byteLength; }); //Send once socketRef.current.sendAudio(combined.buffer); }; const statusText = { idle: "Press to start recording", recording: "Recording... press to stop", analyzing: "Analyzing emotion...", error: "Error — check mic & backend", }[status]; return (
{statusText}