import { ref, readonly } from 'vue' export interface SilenceDetectorOptions { thresholdDb?: number // default -40 silenceDurationMs?: number // default 1500 minRecordingMs?: number // default 500 } export function useSilenceDetector(options: SilenceDetectorOptions = {}) { const { thresholdDb = -40, silenceDurationMs = 1500, minRecordingMs = 500, } = options const amplitude = ref(0) let audioCtx: AudioContext | null = null let intervalId: ReturnType | null = null let silenceMs = 0 let startedAt = 0 function start(stream: MediaStream, onSilence: () => void): void { stop() audioCtx = new AudioContext() // Some browsers start AudioContext in "suspended" state — resume so // getByteFrequencyData returns real values instead of all zeros. audioCtx.resume().catch(() => {}) const source = audioCtx.createMediaStreamSource(stream) const analyser = audioCtx.createAnalyser() analyser.fftSize = 256 source.connect(analyser) const data = new Uint8Array(analyser.frequencyBinCount) silenceMs = 0 startedAt = Date.now() intervalId = setInterval(() => { analyser.getByteFrequencyData(data) const rms = Math.sqrt(data.reduce((s, v) => s + v * v, 0) / data.length) / 255 amplitude.value = rms const db = rms > 0 ? 20 * Math.log10(rms) : -100 if (db < thresholdDb) { silenceMs += 100 if (silenceMs >= silenceDurationMs && Date.now() - startedAt >= 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 } return { amplitude: readonly(amplitude), start, stop } }