openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
231 lines (230 loc) • 10.2 kB
JavaScript
import "./src-vebZIeLe.js";
import { t as expectDefined } from "./expect-CyE8FADM.js";
//#region src/talk/audio-codec.ts
const TELEPHONY_SAMPLE_RATE = 8e3;
const RESAMPLE_FILTER_TAPS = 31;
const RESAMPLE_CUTOFF_GUARD = .94;
const RESAMPLE_MAX_PRECOMPUTED_PHASES = 4096;
const RESAMPLE_HALF_TAPS = Math.floor(RESAMPLE_FILTER_TAPS / 2);
const RESAMPLE_WINDOW = Array.from({ length: RESAMPLE_FILTER_TAPS }, (_, tapIndex) => .5 - .5 * Math.cos(2 * Math.PI * tapIndex / 30));
const HOST_IS_LITTLE_ENDIAN = new Uint16Array(new Uint8Array([1, 0]).buffer)[0] === 1;
/** Clamp an intermediate sample to signed 16-bit PCM range. */
function clamp16(value) {
return Math.max(-32768, Math.min(32767, value));
}
function canUseInt16View(buffer) {
return HOST_IS_LITTLE_ENDIAN && buffer.byteOffset % Int16Array.BYTES_PER_ELEMENT === 0;
}
function int16View(buffer) {
return new Int16Array(buffer.buffer, buffer.byteOffset, Math.floor(buffer.byteLength / Int16Array.BYTES_PER_ELEMENT));
}
function readInt16Samples(buffer) {
if (canUseInt16View(buffer)) return int16View(buffer);
const samples = new Int16Array(Math.floor(buffer.byteLength / Int16Array.BYTES_PER_ELEMENT));
for (let i = 0; i < samples.length; i += 1) samples[i] = buffer.readInt16LE(i * Int16Array.BYTES_PER_ELEMENT);
return samples;
}
function sinc(x) {
if (x === 0) return 1;
return Math.sin(Math.PI * x) / (Math.PI * x);
}
function gcd(left, right) {
let a = Math.abs(Math.trunc(left));
let b = Math.abs(Math.trunc(right));
while (b !== 0) {
const next = a % b;
a = b;
b = next;
}
return a || 1;
}
function buildResampleKernel(inputSampleRate, outputSampleRate, cutoffCyclesPerSample) {
if (!Number.isInteger(inputSampleRate) || !Number.isInteger(outputSampleRate)) return;
const divisor = gcd(inputSampleRate, outputSampleRate);
const inputStep = inputSampleRate / divisor;
const phaseCount = outputSampleRate / divisor;
if (phaseCount > RESAMPLE_MAX_PRECOMPUTED_PHASES) return;
return {
coefficients: Array.from({ length: phaseCount }, (_, phaseIndex) => {
const phase = phaseIndex / phaseCount;
const phaseCoefficients = new Float64Array(RESAMPLE_FILTER_TAPS);
for (let tap = -15; tap <= RESAMPLE_HALF_TAPS; tap += 1) {
const distance = tap - phase;
const lowPass = 2 * cutoffCyclesPerSample * sinc(2 * cutoffCyclesPerSample * distance);
const tapIndex = tap + RESAMPLE_HALF_TAPS;
phaseCoefficients[tapIndex] = lowPass * (RESAMPLE_WINDOW[tapIndex] ?? 0);
}
return phaseCoefficients;
}),
inputStep,
phaseCount
};
}
function sampleBandlimitedWithCoefficients(input, center, coefficients) {
let weighted = 0;
let weightSum = 0;
for (let tap = -15; tap <= RESAMPLE_HALF_TAPS; tap += 1) {
const sampleIndex = center + tap;
if (sampleIndex < 0 || sampleIndex >= input.length) continue;
const coeff = coefficients[tap + RESAMPLE_HALF_TAPS] ?? 0;
weighted += (input[sampleIndex] ?? 0) * coeff;
weightSum += coeff;
}
if (weightSum === 0) return input[Math.max(0, Math.min(input.length - 1, center))] ?? 0;
return weighted / weightSum;
}
function sampleBandlimited(input, srcPos, cutoffCyclesPerSample) {
const center = Math.floor(srcPos);
let weighted = 0;
let weightSum = 0;
for (let tap = -15; tap <= RESAMPLE_HALF_TAPS; tap += 1) {
const sampleIndex = center + tap;
if (sampleIndex < 0 || sampleIndex >= input.length) continue;
const distance = sampleIndex - srcPos;
const coeff = 2 * cutoffCyclesPerSample * sinc(2 * cutoffCyclesPerSample * distance) * (RESAMPLE_WINDOW[tap + RESAMPLE_HALF_TAPS] ?? 0);
weighted += (input[sampleIndex] ?? 0) * coeff;
weightSum += coeff;
}
if (weightSum === 0) return input[Math.max(0, Math.min(input.length - 1, Math.round(srcPos)))] ?? 0;
return weighted / weightSum;
}
function createResamplePlan(inputSampleRate, outputSampleRate) {
const ratio = inputSampleRate / outputSampleRate;
const maxCutoff = .5;
const downsampleCutoff = ratio > 1 ? maxCutoff / ratio : maxCutoff;
const cutoffCyclesPerSample = Math.max(.01, downsampleCutoff * RESAMPLE_CUTOFF_GUARD);
return {
cutoffCyclesPerSample,
inputSampleRate,
kernel: buildResampleKernel(inputSampleRate, outputSampleRate, cutoffCyclesPerSample),
outputSampleRate,
ratio
};
}
function sampleResampledPcm(input, inputStartSample, outputIndex, plan) {
const sourcePosition = outputIndex * plan.inputSampleRate / plan.outputSampleRate;
return Math.round(plan.kernel ? sampleBandlimitedWithCoefficients(input, Math.floor(sourcePosition) - inputStartSample, expectDefined(plan.kernel.coefficients[outputIndex * plan.kernel.inputStep % plan.kernel.phaseCount], "coefficients entry at (output index * kernel input step) % kernel phase count") ?? plan.kernel.coefficients[0]) : sampleBandlimited(input, outputIndex * plan.ratio - inputStartSample, plan.cutoffCyclesPerSample));
}
function renderResampledPcm(input, inputStartSample, firstOutputIndex, outputSamples, plan) {
const output = Buffer.alloc(outputSamples * 2);
const inputView = readInt16Samples(input);
const outputView = canUseInt16View(output) ? int16View(output) : void 0;
for (let offset = 0; offset < outputSamples; offset += 1) {
const sample = clamp16(sampleResampledPcm(inputView, inputStartSample, firstOutputIndex + offset, plan));
if (outputView) outputView[offset] = sample;
else output.writeInt16LE(sample, offset * 2);
}
return output;
}
/** Resample little-endian signed 16-bit PCM to another integer sample rate. */
function resamplePcm(input, inputSampleRate, outputSampleRate) {
if (inputSampleRate === outputSampleRate) return input;
const inputSamples = Math.floor(input.length / 2);
if (inputSamples === 0) return Buffer.alloc(0);
const plan = createResamplePlan(inputSampleRate, outputSampleRate);
return renderResampledPcm(input, 0, 0, Math.floor(inputSamples / plan.ratio), plan);
}
/** Create a chunk-safe PCM resampler that preserves filter and fractional phase state. */
function createStreamingPcmResampler(inputSampleRate, outputSampleRate) {
if (inputSampleRate === outputSampleRate) return {
process: (chunk) => Buffer.from(chunk),
flush: () => Buffer.alloc(0)
};
const plan = createResamplePlan(inputSampleRate, outputSampleRate);
let bufferedInput = Buffer.alloc(0);
let inputStartSample = 0;
let totalInputSamples = 0;
let nextOutputIndex = 0;
let trailingByte = Buffer.alloc(0);
let flushed = false;
const renderAvailable = (includeRightEdge) => {
const targetOutputCount = Math.floor(totalInputSamples / plan.ratio);
let endOutputIndex = nextOutputIndex;
while (endOutputIndex < targetOutputCount) {
const center = Math.floor(endOutputIndex * plan.inputSampleRate / plan.outputSampleRate);
if (!includeRightEdge && center + RESAMPLE_HALF_TAPS >= totalInputSamples) break;
endOutputIndex += 1;
}
const output = renderResampledPcm(bufferedInput, inputStartSample, nextOutputIndex, endOutputIndex - nextOutputIndex, plan);
nextOutputIndex = endOutputIndex;
const nextCenter = Math.floor(nextOutputIndex * plan.inputSampleRate / plan.outputSampleRate);
const retainFromSample = Math.max(0, nextCenter - RESAMPLE_HALF_TAPS);
const dropSamples = retainFromSample - inputStartSample;
if (dropSamples > 0) {
bufferedInput = Buffer.from(bufferedInput.subarray(dropSamples * 2));
inputStartSample = retainFromSample;
}
return output;
};
return {
process(chunk) {
if (flushed) throw new Error("Cannot process PCM after the streaming resampler was flushed");
const combined = trailingByte.length > 0 ? Buffer.concat([trailingByte, chunk]) : chunk;
const completeBytes = combined.length - combined.length % 2;
trailingByte = Buffer.from(combined.subarray(completeBytes));
if (completeBytes > 0) {
const completePcm = combined.subarray(0, completeBytes);
bufferedInput = bufferedInput.length > 0 ? Buffer.concat([bufferedInput, completePcm]) : Buffer.from(completePcm);
totalInputSamples += completeBytes / 2;
}
return renderAvailable(false);
},
flush() {
if (flushed) return Buffer.alloc(0);
flushed = true;
trailingByte = Buffer.alloc(0);
const output = renderAvailable(true);
bufferedInput = Buffer.alloc(0);
return output;
}
};
}
/** Resample little-endian signed 16-bit PCM to the telephony 8 kHz rate. */
function resamplePcmTo8k(input, inputSampleRate) {
return resamplePcm(input, inputSampleRate, TELEPHONY_SAMPLE_RATE);
}
/** Convert little-endian signed 16-bit PCM samples to G.711 mu-law bytes. */
function pcmToMulaw(pcm) {
const pcmView = readInt16Samples(pcm);
const mulaw = Buffer.alloc(pcmView.length);
for (let i = 0; i < pcmView.length; i += 1) mulaw[i] = linearToMulaw(pcmView[i] ?? 0);
return mulaw;
}
/** Expand G.711 mu-law bytes into little-endian signed 16-bit PCM samples. */
function mulawToPcm(mulaw) {
const pcm = Buffer.alloc(mulaw.length * 2);
const pcmView = canUseInt16View(pcm) ? int16View(pcm) : void 0;
if (pcmView) {
for (let i = 0; i < mulaw.length; i += 1) pcmView[i] = clamp16(mulawToLinear(mulaw[i] ?? 0));
return pcm;
}
for (let i = 0; i < mulaw.length; i += 1) pcm.writeInt16LE(clamp16(mulawToLinear(mulaw[i] ?? 0)), i * 2);
return pcm;
}
/** Resample signed 16-bit PCM to 8 kHz and encode it as G.711 mu-law. */
function convertPcmToMulaw8k(pcm, inputSampleRate) {
return pcmToMulaw(resamplePcmTo8k(pcm, inputSampleRate));
}
function linearToMulaw(sampleInput) {
let sample = sampleInput;
const BIAS = 132;
const CLIP = 32635;
const sign = sample < 0 ? 128 : 0;
if (sample < 0) sample = -sample;
if (sample > CLIP) sample = CLIP;
sample += BIAS;
let exponent = 7;
for (let expMask = 16384; (sample & expMask) === 0 && exponent > 0; exponent -= 1) expMask >>= 1;
const mantissa = sample >> exponent + 3 & 15;
return ~(sign | exponent << 4 | mantissa) & 255;
}
function mulawToLinear(value) {
const muLaw = ~value & 255;
const sign = muLaw & 128;
const exponent = muLaw >> 4 & 7;
let sample = ((muLaw & 15) << 3) + 132 << exponent;
sample -= 132;
return sign ? -sample : sample;
}
//#endregion
export { resamplePcm as a, pcmToMulaw as i, createStreamingPcmResampler as n, resamplePcmTo8k as o, mulawToPcm as r, convertPcmToMulaw8k as t };