chore(voice): remove amplitude-based silence detector (replaced by VAD)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<typeof setInterval> | 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 }
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user