/* Drive the REAL live-audio module the way the mic feed does, and see whether the stream it produces is continuous. */ import { execFileSync } from 'child_process'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); const { LiveAudio } = require('./server/live-audio.js'); const RATE = 16000, CHUNK = 11200; // MIC_CHUNK const d = fs.mkdtempSync(path.join(os.tmpdir(), 'micfeed-')); function encode(samples, hz) { const pcm = Buffer.alloc(samples * 2); for (let i = 0; i < samples; i++) pcm.writeInt16LE(Math.round(9000 * Math.sin(i / hz)), i * 2); const wav = Buffer.alloc(44 + pcm.length); wav.write('RIFF', 0); wav.writeUInt32LE(36 + pcm.length, 4); wav.write('WAVE', 8); wav.write('fmt ', 12); wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22); wav.writeUInt32LE(RATE, 24); wav.writeUInt32LE(RATE * 2, 28); wav.writeUInt16LE(2, 32); wav.writeUInt16LE(16, 34); wav.write('data', 36); wav.writeUInt32LE(pcm.length, 40); pcm.copy(wav, 44); const w = path.join(d, 'c.wav'), a = path.join(d, 'c.aac'); fs.writeFileSync(w, wav); if (fs.existsSync(a)) fs.unlinkSync(a); execFileSync('/usr/bin/afconvert', ['-f', 'adts', '-d', 'aac@' + RATE, '-b', '48000', w, a]); return fs.readFileSync(a); } const live = new LiveAudio((m) => console.log(' [live] ' + m)); const st = live.ensure('eng_Latn', RATE); st.start(); /* Count silence frames the pump invents because the queue ran dry. */ const SIL = require('./server/live-audio.js').silenceFrame(RATE); let underrun = 0, real = 0, counting = false; const origClose = st.closeSegment.bind(st); st.closeSegment = function () { if (counting) for (const f of this.building) { if (f === SIL) underrun++; else real++; } return origClose(); }; console.log('pushing 700ms chunks every 700ms for 14s, exactly as the mic feed does…\n'); const clip = encode(CHUNK, 9); let pushed = 0; const t0 = Date.now(); let stalls = 0; function schedule(delay) { setTimeout(tickPush, delay); } function tickPush() { st.pushContinuous(clip); pushed++; if (pushed === 2) counting = true; // skip the startup seed const backlogMs = (st.pendingFrames * 1024) / RATE * 1000; const wall = Date.now() - t0; const audioIn = pushed * 700; console.log(' chunk ' + String(pushed).padStart(2) + ' wall ' + String(wall).padStart(5) + 'ms' + ' audio pushed ' + String(audioIn).padStart(5) + 'ms' + ' backlog ' + backlogMs.toFixed(0).padStart(4) + 'ms' + ' segments ' + st.segments.length); /* Two deliberate stalls: 500ms and 900ms late, modelling a blocked event loop or two chunks bunched by the socket. */ let nextIn = 700; if (pushed === 8) { nextIn = 1200; stalls++; } if (pushed === 14) { nextIn = 1600; stalls++; } if (pushed < 20) { schedule(nextIn); return; } if (pushed >= 20) { const held = st.segments.length * (st.framesPerSegment * 1024 / RATE); console.log('\n produced ' + st.segments.length + ' segments (' + held.toFixed(1) + 's held)'); console.log(' real audio frames : ' + real); console.log(' SILENCE frames the pump invented on underrun : ' + underrun + ' (' + (underrun * 64) + ' ms of gaps)'); console.log(' audio in: ' + (pushed * 0.7).toFixed(1) + 's wall: ' + ((Date.now()-t0)/1000).toFixed(1) + 's'); console.log(' deliberate stalls injected: ' + stalls + ' (one 500ms late, one 900ms late)'); console.log(' UNDERRUNS reported by the stream: ' + (st.underruns || 0)); fs.rmSync(d, { recursive: true, force: true }); process.exit(0); } } schedule(700);