33const fs = require ( 'fs' ) ;
44const path = require ( 'path' ) ;
55const http = require ( 'http' ) ;
6- const { execSync, spawnSync } = require ( 'child_process' ) ;
6+ const { execSync } = require ( 'child_process' ) ;
77const WebSocket = require ( 'ws' ) ;
88
99// ── Credential check ─────────────────────────────────────────────────────────
@@ -27,28 +27,77 @@ const { createApp } = require('../src/index.js');
2727const PORT = 3099 ;
2828const AUDIO_URL = 'https://dpgr.am/spacewalk.wav' ;
2929const 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.
3330const 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+
3794function 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