voice: audit + noise suppression toggle + stability fixes

Gateway:
- transmitter.ts: cap backpressure queue at 500 chunks (prevents memory leak)
- transmitter.ts: fix Redis race — assign redisSub AFTER subscribe completes
- voice.handler.ts: static import Redis instead of dynamic (cleaner, no eval)

Frontend:
- mic-transmit.ts: MicAccessError with specific reasons (permission-denied, no-mic, timeout)
- mic-transmit.ts: noiseSuppression option in getUserMedia constraints
- mic-transmit.ts: proper DOMException handling for all getUserMedia failure modes
- use-voice.ts: noiseSuppression state + toggleNoiseSuppression exposed
- use-voice.ts: cleanup on unmount (stops transmitter, clears refs)
- voice/view.tsx: noise suppression toggle button (ShieldCheck/ShieldOff icons)
- voice/view.tsx: improved mic error toasts (permission denied / no mic specific)
- voice/view.tsx: NS status in codec footer (NS_ACTIVE when enabled)
This commit is contained in:
asepharyana
2026-08-26 23:32:37 +07:00
parent 4f4f92706c
commit 7e0d0d5123
5 changed files with 214 additions and 31 deletions
+27 -2
View File
@@ -144,10 +144,13 @@ export function useMicTransmit(ws: {
}) {
const transmitterRef = useRef<MicTransmitter | null>(null);
const [micLevel, setMicLevel] = useState(0);
const [noiseSuppression, setNoiseSuppressionState] = useState(true);
const action = useAction(async (active: boolean) => {
if (active) {
const transmitter = new MicTransmitter((frame) => ws.sendBinary(frame));
const transmitter = new MicTransmitter((frame) => ws.sendBinary(frame), {
noiseSuppression,
});
transmitterRef.current = transmitter;
await transmitter.start();
await voiceApi.sendCommand("voice:transmit:start");
@@ -163,6 +166,14 @@ export function useMicTransmit(ws: {
transmitterRef.current?.setVolume(volume / 100);
}, []);
const toggleNoiseSuppression = useCallback((enabled: boolean) => {
setNoiseSuppressionState(enabled);
// If mic is already active, toggling NS requires restart
if (transmitterRef.current?.isActive) {
transmitterRef.current.setNoiseSuppression(enabled);
}
}, []);
// Poll the analyser RMS so the UI can render a live input meter.
useEffect(() => {
const timer = setInterval(() => {
@@ -171,7 +182,21 @@ export function useMicTransmit(ws: {
return () => clearInterval(timer);
}, []);
return { ...action, setVolume, micLevel };
// Cleanup on unmount
useEffect(() => {
return () => {
transmitterRef.current?.stop();
transmitterRef.current = null;
};
}, []);
return {
...action,
setVolume,
micLevel,
noiseSuppression,
toggleNoiseSuppression,
};
}
/**