From a4995606e5b79dc0eb3ae3af1a6ef7a32b0e1d0c Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 16 Apr 2026 17:47:45 -0400 Subject: [PATCH] chore(voice): remove amplitude-based silence detector (replaced by VAD) Co-Authored-By: Claude Opus 4.6 --- .../src/composables/useSilenceDetector.ts | 121 ------------------ 1 file changed, 121 deletions(-) delete mode 100644 frontend/src/composables/useSilenceDetector.ts diff --git a/frontend/src/composables/useSilenceDetector.ts b/frontend/src/composables/useSilenceDetector.ts deleted file mode 100644 index db5c29a..0000000 --- a/frontend/src/composables/useSilenceDetector.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { ref, readonly } from 'vue' - -export interface SilenceDetectorOptions { - /** - * Absolute fallback silence threshold in dBFS, used during the first - * moments before any real speech has been observed. Once the session peak - * clears `dynamicArmDb` we switch to the dynamic threshold instead. - */ - fallbackThresholdDb?: number // default -45 - /** How many dB below the session peak counts as "silent" once armed. */ - dropFromPeakDb?: number // default 15 - /** - * Session peak must reach this level (dBFS) before dynamic thresholding - * kicks in. Until then the fallback threshold is used so we don't lock - * onto a noise-floor peak. - */ - dynamicArmDb?: number // default -25 - /** How long to wait at the start of recording before running silence checks. */ - graceMs?: number // default 1500 - silenceDurationMs?: number // default 2000 - minRecordingMs?: number // default 500 -} - -/** - * Mic silence detector + live amplitude signal. - * - * Uses `getFloatTimeDomainData()` for honest linear RMS in [-1, 1] space, - * then derives dBFS for the silence threshold. The previous implementation - * ran RMS over `getByteFrequencyData` bytes — those bytes are already a - * dB-scaled quantity, so taking their RMS and re-log'ing it produced - * numbers that didn't line up with real dBFS and made any static threshold - * unpredictable. - * - * Silence threshold is dynamic: we track the session peak dBFS and treat - * "silent" as "current level is at least [dropFromPeakDb] below peak." - * This auto-calibrates to whatever mic and room the user is on. Until the - * peak climbs above [dynamicArmDb] we fall back to a conservative static - * threshold so a quiet room doesn't spin forever. A grace period at the - * start of the recording gives the user time to begin speaking before - * silence checks arm. - * - * `amplitude` is the raw linear RMS scaled ×5 and clamped to 1 so quiet - * speech still visibly moves UI that binds to it. - */ -export function useSilenceDetector(options: SilenceDetectorOptions = {}) { - const { - fallbackThresholdDb = -45, - dropFromPeakDb = 15, - dynamicArmDb = -25, - graceMs = 1500, - silenceDurationMs = 2000, - minRecordingMs = 500, - } = options - - const amplitude = ref(0) - let audioCtx: AudioContext | null = null - let intervalId: ReturnType | null = null - let silenceMs = 0 - let startedAt = 0 - let peakDb = -100 - - function start(stream: MediaStream, onSilence: () => void): void { - stop() - audioCtx = new AudioContext() - // Some browsers start AudioContext in "suspended" state — resume so - // the analyser returns real values instead of all zeros. - audioCtx.resume().catch(() => {}) - const source = audioCtx.createMediaStreamSource(stream) - const analyser = audioCtx.createAnalyser() - analyser.fftSize = 1024 - source.connect(analyser) - - const samples = new Float32Array(analyser.fftSize) - silenceMs = 0 - startedAt = Date.now() - peakDb = -100 - - intervalId = setInterval(() => { - analyser.getFloatTimeDomainData(samples) - let sumSq = 0 - for (let i = 0; i < samples.length; i++) sumSq += samples[i] * samples[i] - const rms = Math.sqrt(sumSq / samples.length) - amplitude.value = Math.min(rms * 5, 1) - - const db = rms > 1e-7 ? 20 * Math.log10(rms) : -100 - if (db > peakDb) peakDb = db - - const elapsed = Date.now() - startedAt - if (elapsed < graceMs) return - - const threshold = - peakDb > dynamicArmDb ? peakDb - dropFromPeakDb : fallbackThresholdDb - - if (db < threshold) { - silenceMs += 100 - if (silenceMs >= silenceDurationMs && elapsed >= minRecordingMs) { - stop() - onSilence() - } - } else { - silenceMs = 0 - } - }, 100) - } - - function stop(): void { - if (intervalId !== null) { - clearInterval(intervalId) - intervalId = null - } - if (audioCtx) { - audioCtx.close().catch(() => {}) - audioCtx = null - } - amplitude.value = 0 - silenceMs = 0 - peakDb = -100 - } - - return { amplitude: readonly(amplitude), start, stop } -}