NourNasr9's picture
fix errors
3a0b0b9
Raw
History Blame Contribute Delete
6.07 kB
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 (
<div style={styles.wrapper}>
{/* Waveform */}
<div style={styles.waveform}>
{bars.map((h, i) => (
<div
key={i}
style={{
...styles.bar,
height: `${h}px`,
backgroundColor: recording ? "#6366f1" : "#334155",
transition: recording ? "height 0.05s" : "height 0.3s",
}}
/>
))}
</div>
{/* Mic button */}
<button
onClick={recording ? stopRecording : startRecording}
disabled={status === "analyzing"}
style={{
...styles.btn,
backgroundColor: recording ? "#ef4444" : "#6366f1",
transform: recording ? "scale(1.08)" : "scale(1)",
}}
>
{recording ? "⏹" : "🎤"}
</button>
<p style={styles.status}>{statusText}</p>
</div>
);
}
const styles = {
wrapper: {
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "16px",
width: "100%",
},
waveform: {
display: "flex",
alignItems: "center",
gap: "3px",
height: "64px",
width: "100%",
justifyContent: "center",
},
bar: {
width: "6px",
borderRadius: "3px",
minHeight: "3px",
},
btn: {
width: "72px",
height: "72px",
borderRadius: "50%",
border: "none",
fontSize: "28px",
cursor: "pointer",
transition: "background-color 0.2s, transform 0.2s",
boxShadow: "0 4px 20px rgba(99,102,241,0.4)",
},
status: {
color: "#94a3b8",
fontSize: "14px",
margin: 0,
},
};