const TELEPHONY_SAMPLE_RATE = 8000; function clamp16(value: number): number { return Math.max(-32768, Math.min(32767, value)); } /** * Resample 16-bit PCM (little-endian mono) to 8kHz using linear interpolation. */ export function resamplePcmTo8k(input: Buffer, inputSampleRate: number): Buffer { if (inputSampleRate === TELEPHONY_SAMPLE_RATE) return input; const inputSamples = Math.floor(input.length / 2); if (inputSamples === 0) return Buffer.alloc(0); const ratio = inputSampleRate / TELEPHONY_SAMPLE_RATE; const outputSamples = Math.floor(inputSamples / ratio); const output = Buffer.alloc(outputSamples * 2); for (let i = 0; i < outputSamples; i++) { const srcPos = i * ratio; const srcIndex = Math.floor(srcPos); const frac = srcPos - srcIndex; const s0 = input.readInt16LE(srcIndex * 2); const s1Index = Math.min(srcIndex + 1, inputSamples - 1); const s1 = input.readInt16LE(s1Index * 2); const sample = Math.round(s0 + frac * (s1 - s0)); output.writeInt16LE(clamp16(sample), i * 2); } return output; } /** * Convert 16-bit PCM to 8-bit mu-law (G.711). */ export function pcmToMulaw(pcm: Buffer): Buffer { const samples = Math.floor(pcm.length / 2); const mulaw = Buffer.alloc(samples); for (let i = 0; i < samples; i++) { const sample = pcm.readInt16LE(i * 2); mulaw[i] = linearToMulaw(sample); } return mulaw; } export function convertPcmToMulaw8k( pcm: Buffer, inputSampleRate: number, ): Buffer { const pcm8k = resamplePcmTo8k(pcm, inputSampleRate); return pcmToMulaw(pcm8k); } /** * Chunk audio buffer into 20ms frames for streaming (8kHz mono mu-law). */ export function chunkAudio( audio: Buffer, chunkSize = 160, ): Generator { return (function* () { for (let i = 0; i < audio.length; i += chunkSize) { yield audio.subarray(i, Math.min(i + chunkSize, audio.length)); } })(); } function linearToMulaw(sample: number): number { const BIAS = 132; const CLIP = 32635; const sign = sample < 0 ? 0x80 : 0; if (sample < 0) sample = -sample; if (sample > CLIP) sample = CLIP; sample += BIAS; let exponent = 7; for (let expMask = 0x4000; (sample & expMask) === 0 && exponent > 0; exponent--) { expMask >>= 1; } const mantissa = (sample >> (exponent + 3)) & 0x0f; return ~(sign | (exponent << 4) | mantissa) & 0xff; }