70 lines
1.9 KiB
TypeScript
70 lines
1.9 KiB
TypeScript
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<typeof setInterval> | 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 }
|
|
}
|