P2b: offline WAV render of the arrangement
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
// Render the current arrangement through the daemon and verify the WAV.
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
|
||||
const PORT = 4317;
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${PORT}`);
|
||||
const pending = new Map();
|
||||
let rendered = null;
|
||||
let resolveRendered;
|
||||
const renderedReady = new Promise((resolve) => { resolveRendered = resolve; });
|
||||
|
||||
ws.onopen = () => ws.send(JSON.stringify({ type: 'hello', protocol: 1, token: 'dev', client: 'render-smoke' }));
|
||||
ws.onmessage = (message) => {
|
||||
if (typeof message.data !== 'string') return;
|
||||
const parsed = JSON.parse(message.data);
|
||||
if (parsed.type === 'snapshot') {
|
||||
ws.send(JSON.stringify({ type: 'command', id: 1, command: { type: 'render' } }));
|
||||
} else if (parsed.type === 'result' && parsed.id === 1) {
|
||||
pending.set(1, parsed.result);
|
||||
} else if (parsed.type === 'event' && parsed.event?.kind === 'rendered') {
|
||||
rendered = parsed.event;
|
||||
resolveRendered();
|
||||
}
|
||||
};
|
||||
|
||||
await renderedReady;
|
||||
if (!rendered) throw new Error('no rendered event');
|
||||
const path = rendered.path;
|
||||
if (!existsSync(path)) throw new Error(`render file missing: ${path}`);
|
||||
const file = readFileSync(path);
|
||||
if (file.toString('ascii', 0, 4) !== 'RIFF' || file.toString('ascii', 8, 12) !== 'WAVE') throw new Error('not a RIFF/WAVE file');
|
||||
let offset = 12;
|
||||
let fmt = null;
|
||||
let data = null;
|
||||
while (offset + 8 <= file.length) {
|
||||
const id = file.toString('ascii', offset, offset + 4);
|
||||
const size = file.readUInt32LE(offset + 4);
|
||||
if (id === 'fmt ') fmt = { offset: offset + 8, size };
|
||||
if (id === 'data') {
|
||||
data = { offset: offset + 8, size: Math.min(size, file.length - offset - 8) };
|
||||
break;
|
||||
}
|
||||
offset += 8 + size + (size % 2);
|
||||
}
|
||||
if (!fmt || !data) throw new Error('missing fmt or data chunk');
|
||||
let format = file.readUInt16LE(fmt.offset);
|
||||
const channels = file.readUInt16LE(fmt.offset + 2);
|
||||
const rate = file.readUInt32LE(fmt.offset + 4);
|
||||
const bits = file.readUInt16LE(fmt.offset + 14);
|
||||
if (format === 0xfffe) format = file.readUInt16LE(fmt.offset + 24);
|
||||
if (format !== 3) throw new Error(`expected float WAV, got format ${format}`);
|
||||
if (channels !== 2 || rate !== 48000 || bits !== 32) throw new Error(`unexpected spec ${channels}/${rate}/${bits}`);
|
||||
const bytes = file.subarray(data.offset, data.offset + data.size);
|
||||
let peak = 0;
|
||||
for (let cursor = 0; cursor + 4 <= bytes.length; cursor += 4) {
|
||||
const sample = bytes.readFloatLE(cursor);
|
||||
peak = Math.max(peak, Math.abs(sample));
|
||||
}
|
||||
const duration = data.size / (channels * 4) / rate;
|
||||
console.log(JSON.stringify({
|
||||
path,
|
||||
bytes: file.length,
|
||||
format, channels, rate, bits,
|
||||
durationSec: Number(duration.toFixed(3)),
|
||||
eventDurationSec: rendered.duration_sec,
|
||||
peak: Number(peak.toFixed(3)),
|
||||
renderedState: pending.get(1)?.state?.revision
|
||||
}, null, 2));
|
||||
if (peak < 0.01) throw new Error('render is silent');
|
||||
if (duration < 15) throw new Error('render is shorter than the arrangement');
|
||||
if (Math.abs(duration - rendered.duration_sec) > 0.05) throw new Error('duration mismatch');
|
||||
ws.close();
|
||||
console.log('render smoke passed');
|
||||
Reference in New Issue
Block a user