diff --git a/src/audio/pcm.ts b/src/audio/pcm.ts new file mode 100644 index 0000000..fcf4320 --- /dev/null +++ b/src/audio/pcm.ts @@ -0,0 +1,28 @@ +export function upsample24kMonoTo48kStereo(mono24k: Buffer): Buffer { + const numSamples = mono24k.length / 2; + const out = Buffer.alloc(numSamples * 8); + + for (let i = 0; i < numSamples; i++) { + const sample = mono24k.readInt16LE(i * 2); + const base = i * 8; + out.writeInt16LE(sample, base); + out.writeInt16LE(sample, base + 2); + out.writeInt16LE(sample, base + 4); + out.writeInt16LE(sample, base + 6); + } + + return out; +} + +export function rmsDb(pcm: Buffer): number { + let sum = 0; + const samples = pcm.length / 2; + + for (let i = 0; i < samples; i++) { + const sample = pcm.readInt16LE(i * 2) / 32768; + sum += sample * sample; + } + + const rms = Math.sqrt(sum / samples); + return 20 * Math.log10(rms); +} diff --git a/src/webserver.ts b/src/webserver.ts index 8614e46..e657325 100644 --- a/src/webserver.ts +++ b/src/webserver.ts @@ -12,6 +12,7 @@ import express, { import helmet from "helmet"; import * as prism from "prism-media"; import { WebSocketServer } from "ws"; +import { rmsDb, upsample24kMonoTo48kStereo } from "./audio/pcm"; import { config } from "./config"; import { AppError } from "./errors"; import { createChildLogger, logger } from "./logger"; @@ -165,32 +166,6 @@ function patchSharedUIState(patch: SharedUIStatePatch) { return getSharedUIState(); } -// Upsample 24kHz mono s16le → 48kHz stereo s16le (pure JS) -function upsample(mono24k: Buffer): Buffer { - const numSamples = mono24k.length / 2; - const out = Buffer.alloc(numSamples * 8); - for (let i = 0; i < numSamples; i++) { - const s = mono24k.readInt16LE(i * 2); - const base = i * 8; - out.writeInt16LE(s, base); - out.writeInt16LE(s, base + 2); - out.writeInt16LE(s, base + 4); - out.writeInt16LE(s, base + 6); - } - return out; -} - -// Calculate RMS dB level of a PCM s16le buffer -function rmsDb(pcm: Buffer): number { - let sum = 0; - const samples = pcm.length / 2; - for (let i = 0; i < samples; i++) { - const s = pcm.readInt16LE(i * 2) / 32768; - sum += s * s; - } - const rms = Math.sqrt(sum / samples); - return 20 * Math.log10(rms); -} export async function startWebserver( port: number = 3000, @@ -517,7 +492,7 @@ export async function startWebserver( lastBrowserAudioTime = Date.now(); // Upsample 24kHz mono → 48kHz stereo and add to buffer - const upsampled = upsample(data); + const upsampled = upsample24kMonoTo48kStereo(data); // Cap buffer to avoid runaway growth during stall if (pcmBuffer.length < MAX_BUF_BYTES) { diff --git a/tests/audio/pcm.test.ts b/tests/audio/pcm.test.ts new file mode 100644 index 0000000..2b31b23 --- /dev/null +++ b/tests/audio/pcm.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { rmsDb, upsample24kMonoTo48kStereo } from "../../src/audio/pcm"; + +describe("PCM helpers", () => { + it("upsamples 24kHz mono s16le to 48kHz stereo s16le", () => { + const input = Buffer.alloc(4); + input.writeInt16LE(1000, 0); + input.writeInt16LE(-1000, 2); + + const output = upsample24kMonoTo48kStereo(input); + + expect(output.length).toBe(16); + expect(output.readInt16LE(0)).toBe(1000); + expect(output.readInt16LE(2)).toBe(1000); + expect(output.readInt16LE(4)).toBe(1000); + expect(output.readInt16LE(6)).toBe(1000); + expect(output.readInt16LE(8)).toBe(-1000); + expect(output.readInt16LE(10)).toBe(-1000); + expect(output.readInt16LE(12)).toBe(-1000); + expect(output.readInt16LE(14)).toBe(-1000); + }); + + it("calculates RMS dB for non-silent PCM", () => { + const input = Buffer.alloc(4); + input.writeInt16LE(32767, 0); + input.writeInt16LE(32767, 2); + + expect(rmsDb(input)).toBeCloseTo(0, 1); + }); +});