81 lines
3.6 KiB
JavaScript
81 lines
3.6 KiB
JavaScript
/* 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);
|
||
|
|
|