89 lines
2.3 KiB
TypeScript
89 lines
2.3 KiB
TypeScript
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<Buffer, void, unknown> {
|
|
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;
|
|
}
|