#!/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');