Skip to content

Commit 5ad54a8

Browse files
examples-botclaude
andcommitted
fix(020-twilio-media-streams-node): replace ffmpeg with pure Node.js μ-law conversion
ffmpeg was removed from the GitHub Actions ubuntu-24.04 runner image (version 20260323.65), causing the test to crash with "Cannot read properties of null (reading 'toString')" when spawnSync returns {status: null, stderr: null} for a missing binary. Replace ffmpeg-based WAV→μ-law conversion with a pure Node.js implementation (ITU-T G.711 lookup table + WAV chunk parser). Also fix SDK v5 boolean options to use strings per migration guide. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 23a11fa commit 5ad54a8

2 files changed

Lines changed: 68 additions & 19 deletions

File tree

examples/020-twilio-media-streams-node/src/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ const DEEPGRAM_LIVE_OPTIONS = {
1414
encoding: 'mulaw',
1515
sample_rate: 8000,
1616
channels: 1,
17-
smart_format: true,
18-
interim_results: true,
17+
smart_format: 'true',
18+
interim_results: 'true',
1919
utterance_end_ms: 1000,
2020
};
2121

examples/020-twilio-media-streams-node/tests/test.js

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const fs = require('fs');
44
const path = require('path');
55
const http = require('http');
6-
const { execSync, spawnSync } = require('child_process');
6+
const { execSync } = require('child_process');
77
const WebSocket = require('ws');
88

99
// ── Credential check ─────────────────────────────────────────────────────────
@@ -27,28 +27,77 @@ const { createApp } = require('../src/index.js');
2727
const PORT = 3099;
2828
const AUDIO_URL = 'https://dpgr.am/spacewalk.wav';
2929
const TMP_WAV = '/tmp/twilio_test.wav';
30-
const TMP_MULAW = '/tmp/twilio_test.mulaw';
31-
// Twilio sends ~20 ms frames; 8 000 Hz × 1 byte/sample × 0.02 s = 160 bytes.
32-
// We use 320 to match Twilio's actual observed frame size.
3330
const CHUNK_SIZE = 320;
3431

35-
// Convert a known audio file to μ-law 8 kHz using ffmpeg.
36-
// ffmpeg is pre-installed on all GitHub Actions ubuntu runners.
32+
const LINEAR_TO_ULAW = (() => {
33+
const BIAS = 0x84;
34+
const CLIP = 32635;
35+
const table = new Int8Array(65536);
36+
for (let i = -32768; i < 32768; i++) {
37+
let sample = i < 0 ? ~i : i;
38+
if (sample > CLIP) sample = CLIP;
39+
sample += BIAS;
40+
let exponent = 7;
41+
for (let expMask = 0x4000; (sample & expMask) === 0 && exponent > 0; exponent--, expMask >>= 1);
42+
const mantissa = (sample >> (exponent + 3)) & 0x0F;
43+
let ulawByte = ~(((i < 0 ? 0x80 : 0) | (exponent << 4) | mantissa)) & 0xFF;
44+
table[i & 0xFFFF] = ulawByte;
45+
}
46+
return table;
47+
})();
48+
49+
function wavToMulaw8k(wavBuffer) {
50+
let offset = 12;
51+
let sampleRate = 0, bitsPerSample = 0, numChannels = 0, dataStart = 0, dataSize = 0;
52+
while (offset < wavBuffer.length - 8) {
53+
const chunkId = wavBuffer.toString('ascii', offset, offset + 4);
54+
const chunkSize = wavBuffer.readUInt32LE(offset + 4);
55+
if (chunkId === 'fmt ') {
56+
numChannels = wavBuffer.readUInt16LE(offset + 10);
57+
sampleRate = wavBuffer.readUInt32LE(offset + 12);
58+
bitsPerSample = wavBuffer.readUInt16LE(offset + 22);
59+
} else if (chunkId === 'data') {
60+
dataStart = offset + 8;
61+
dataSize = chunkSize;
62+
break;
63+
}
64+
offset += 8 + chunkSize;
65+
}
66+
if (!dataStart) throw new Error('Invalid WAV: no data chunk');
67+
68+
const bytesPerSample = bitsPerSample / 8;
69+
const totalSamples = Math.floor(dataSize / (bytesPerSample * numChannels));
70+
const ratio = sampleRate / 8000;
71+
const outLen = Math.floor(totalSamples / ratio);
72+
const out = Buffer.alloc(outLen);
73+
74+
for (let i = 0; i < outLen; i++) {
75+
const srcIdx = Math.floor(i * ratio);
76+
const byteOff = dataStart + srcIdx * bytesPerSample * numChannels;
77+
let sample;
78+
if (bitsPerSample === 16) {
79+
sample = wavBuffer.readInt16LE(byteOff);
80+
} else if (bitsPerSample === 24) {
81+
sample = (wavBuffer[byteOff] | (wavBuffer[byteOff + 1] << 8) | (wavBuffer[byteOff + 2] << 16));
82+
if (sample & 0x800000) sample |= ~0xFFFFFF;
83+
sample = sample >> 8;
84+
} else if (bitsPerSample === 32) {
85+
sample = wavBuffer.readInt32LE(byteOff) >> 16;
86+
} else {
87+
sample = (wavBuffer[byteOff] - 128) << 8;
88+
}
89+
out[i] = LINEAR_TO_ULAW[sample & 0xFFFF];
90+
}
91+
return out;
92+
}
93+
3794
function prepareMulawAudio() {
3895
console.log('Downloading test audio...');
3996
execSync(`curl -s -L -o "${TMP_WAV}" "${AUDIO_URL}"`, { stdio: 'pipe' });
4097

41-
console.log('Converting to μ-law 8 kHz mono (ffmpeg)...');
42-
const result = spawnSync('ffmpeg', [
43-
'-y', '-i', TMP_WAV,
44-
'-ar', '8000', '-ac', '1', '-f', 'mulaw', TMP_MULAW,
45-
], { stdio: 'pipe' });
46-
47-
if (result.status !== 0) {
48-
throw new Error(`ffmpeg failed: ${result.stderr.toString().slice(0, 300)}`);
49-
}
50-
51-
const audio = fs.readFileSync(TMP_MULAW);
98+
console.log('Converting to μ-law 8 kHz mono...');
99+
const wavData = fs.readFileSync(TMP_WAV);
100+
const audio = wavToMulaw8k(wavData);
52101
console.log(`✓ Audio ready: ${audio.length} bytes of μ-law 8 kHz`);
53102
return audio;
54103
}

0 commit comments

Comments
 (0)