UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

62 lines (61 loc) 2.4 kB
import { r as mulawToPcm } from "./audio-codec-DFkaYkvm.js"; //#region src/talk/audio-energy.ts const PCM16_MAX_AMPLITUDE = 32768; const MULAW_LINEAR_SAMPLES = (() => { const encoded = Buffer.from([...Array(256).keys()]); const decoded = mulawToPcm(encoded); return Int16Array.from(encoded, (_, index) => decoded.readInt16LE(index * 2)); })(); /** Read RMS and absolute peak from complete little-endian signed PCM16 samples. */ function readPcm16AudioStats(audio) { let sumSquares = 0; let peak = 0; const samples = Math.floor(audio.byteLength / 2); for (let index = 0; index < samples; index += 1) { const sample = audio.readInt16LE(index * 2); peak = Math.max(peak, Math.abs(sample)); sumSquares += sample * sample; } return { rms: samples > 0 ? Math.sqrt(sumSquares / samples) : 0, peak }; } /** Calculate normalized RMS from G.711 mu-law bytes. */ function calculateMulawRms(muLaw) { if (muLaw.length === 0) return 0; let sumSquares = 0; for (const encoded of muLaw) { const normalized = (MULAW_LINEAR_SAMPLES[encoded] ?? 0) / PCM16_MAX_AMPLITUDE; sumSquares += normalized * normalized; } return Math.sqrt(sumSquares / muLaw.length); } /** Build an OR-threshold gate with optional sustained onset, silence hold, and cooldown. */ function createSpeechThresholdGate(options) { const speechFrames = Math.max(1, Math.floor(options.speechFrames ?? 1)); const silenceFrames = Math.max(0, Math.floor(options.silenceFrames ?? 0)); const cooldownMs = Math.max(0, options.cooldownMs ?? 0); let loudFrames = 0; let quietFrames = 0; let speaking = false; let lastTriggerAt = Number.NEGATIVE_INFINITY; return { accept(stats, acceptOptions = {}) { if (!(options.rmsThreshold !== void 0 && stats.rms >= options.rmsThreshold || options.peakThreshold !== void 0 && stats.peak >= options.peakThreshold)) { loudFrames = 0; if (speaking && ++quietFrames >= silenceFrames) speaking = false; return false; } quietFrames = 0; loudFrames += 1; if (speaking || loudFrames < speechFrames) return false; const nowMs = acceptOptions.nowMs ?? Date.now(); if (nowMs - lastTriggerAt < cooldownMs || acceptOptions.onTrigger?.() === false) return false; lastTriggerAt = nowMs; speaking = silenceFrames > 0; if (!speaking) loudFrames = 0; return true; } }; } //#endregion export { createSpeechThresholdGate as n, readPcm16AudioStats as r, calculateMulawRms as t };