3.5 KiB
Web Mic Noise Suppression Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Reduce background noise from the browser microphone before audio is sent to Discord.
Architecture: Use native browser audio constraints for echo cancellation, noise suppression, and auto gain control at getUserMedia capture time. Add a lightweight RMS noise gate inside the existing onaudioprocess transmit loop so quiet background noise becomes silence before PCM is sent over WebSocket.
Tech Stack: Browser MediaDevices API, Web Audio API, plain JavaScript in public/index.html, existing Bun/TypeScript verification scripts.
File Structure
- Modify
public/index.html: update mic capture constraints and add local RMS noise gate constants/helpers inside the existing script. - No new dependencies.
- No server changes required.
Task 1: Enable Browser-Level Audio Processing
Files:
-
Modify:
public/index.html -
Step 1: Update microphone constraints
Replace:
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
With:
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
channelCount: 1,
sampleRate: SAMPLE_RATE,
},
});
- Step 2: Run lint
Run:
bun run lint
Expected: exits 0.
Task 2: Add Lightweight RMS Noise Gate
Files:
-
Modify:
public/index.html -
Step 1: Add threshold constants near audio constants
Add after:
const CHANNELS = 1;
This code:
const NOISE_GATE_THRESHOLD = 0.01;
const NOISE_GATE_HOLD_FRAMES = 3;
let noiseGateHold = 0;
- Step 2: Add RMS helper function before
startStreaming()
Add before:
async function startStreaming() {
This function:
function calculateRms(samples) {
let sum = 0;
for (let i = 0; i < samples.length; i++) {
sum += samples[i] * samples[i];
}
return Math.sqrt(sum / samples.length);
}
- Step 3: Apply gate before PCM conversion
Replace:
const inputData = e.inputBuffer.getChannelData(0);
const pcmData = new Int16Array(inputData.length);
for (let i = 0; i < inputData.length; i++) {
pcmData[i] = Math.max(-1, Math.min(1, inputData[i])) * 32767;
}
socket.send(pcmData.buffer);
With:
const inputData = e.inputBuffer.getChannelData(0);
const rms = calculateRms(inputData);
if (rms >= NOISE_GATE_THRESHOLD) {
noiseGateHold = NOISE_GATE_HOLD_FRAMES;
} else if (noiseGateHold > 0) {
noiseGateHold--;
}
const pcmData = new Int16Array(inputData.length);
for (let i = 0; i < inputData.length; i++) {
const sample = noiseGateHold > 0 ? inputData[i] : 0;
pcmData[i] = Math.max(-1, Math.min(1, sample)) * 32767;
}
socket.send(pcmData.buffer);
- Step 4: Reset gate on stop
Add inside stopStreaming() after:
isStreaming = false;
This line:
noiseGateHold = 0;
- Step 5: Run verification
Run:
bun run test && bun run typecheck && bun run lint && bun run build
Expected: all commands exit 0.
Self-Review
- Spec coverage: Browser native noise suppression and JS noise gate are both covered.
- Placeholder scan: No placeholders or TODOs.
- Type consistency: Uses existing
SAMPLE_RATE,CHANNELS, andonaudioprocesspipeline.