Files
dc-recorder/docs/superpowers/plans/2026-05-13-web-mic-noise-suppression.md

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, and onaudioprocess pipeline.