#!/usr/bin/env node // Import a WAV, assign a face to a pattern row, hear it in the loop. import { readFileSync } from 'node:fs'; const WIRE = process.env.ZHILA_WS ?? 'ws://127.0.0.1:4317'; const TOKEN = process.env.ZHILA_TOKEN ?? 'dev'; const fixture = new URL('../fixtures/clicks-01.wav', import.meta.url); const bytes = readFileSync(fixture); const ws = new WebSocket(WIRE); ws.binaryType = 'arraybuffer'; const pending = new Map(); let snapshot = null; let imported = null; let frames = 0; let maxMaster = 0; const events = []; let failure = null; const timeout = setTimeout(() => fail(new Error('material smoke timed out')), 30_000); function fail(error) { if (failure) return; failure = error; clearTimeout(timeout); try { ws.close(); } catch {} console.error('FAIL', error.message); process.exit(1); } function send(id, command) { return new Promise((resolve, reject) => { pending.set(id, { resolve, reject }); ws.send(JSON.stringify({ type: 'command', id, command })); }); } function finish() { clearTimeout(timeout); try { ws.close(); } catch {} console.log(JSON.stringify({ core: imported?.core?.name, id: imported?.core?.id, faces: imported?.core?.faces?.length, peaks: imported?.core?.peaks?.length, duration: imported?.core?.duration_sec, topFace: imported?.core?.faces?.find(f => f.rank === 0)?.index, patternFace: snapshot?.pattern?.rows?.[0]?.face, frames, maxMaster: +maxMaster.toFixed(3), events: events.map(e => e.kind) }, null, 2)); process.exit(0); } ws.onopen = () => { ws.send(JSON.stringify({ type: 'hello', protocol: 1, token: TOKEN, client: 'material-smoke' })); }; ws.onerror = () => fail(new Error('websocket error')); ws.onmessage = async (message) => { if (failure) return; if (message.data instanceof ArrayBuffer) { const view = new DataView(message.data); if (view.byteLength === 31 && view.getUint8(0) === 1) { frames++; maxMaster = Math.max(maxMaster, view.getFloat32(7, true)); } return; } let parsed; try { parsed = JSON.parse(message.data); } catch { return; } if (parsed.type === 'hello') return; if (parsed.type === 'snapshot') { snapshot = parsed.state; if (!imported) { // first snapshot: begin the import ws.send(JSON.stringify({ type: 'import_core', id: 100, name: 'clicks-01.wav', seal: 'found' })); ws.send(bytes); } return; } if (parsed.type === 'imported') { imported = parsed; try { const core = imported.core; if (!core || core.faces.length !== 16) throw new Error('the core did not get 16 faces'); if (!core.peaks.length) throw new Error('the core has no waveform peaks'); const top = core.faces.find(f => f.rank === 0); if (!top) throw new Error('no ranked face'); await send(101, { type: 'set_pattern_face', row: 0, core: core.id, face: top.index }); await send(102, { type: 'play' }); await new Promise(r => setTimeout(r, 2200)); await send(103, { type: 'strike_face', core: core.id, face: top.index, velocity: 0.9 }); await new Promise(r => setTimeout(r, 500)); if (!snapshot) throw new Error('no snapshot after import'); if (!snapshot.cores?.length) throw new Error('the snapshot has no cores'); if (!snapshot.pattern?.rows?.[0]?.face) throw new Error('the pattern row has no face'); if (!(maxMaster > 0.001)) throw new Error('the engine never made a level'); if (!events.some(e => e.kind === 'face_struck')) throw new Error('no face_struck event'); finish(); } catch (error) { fail(error); } return; } if (parsed.type === 'result') { const entry = pending.get(parsed.id); if (!entry) return; pending.delete(parsed.id); if (parsed.result?.state) snapshot = parsed.result.state; entry.resolve(parsed.result); return; } if (parsed.type === 'event') { events.push(parsed.event); return; } if (parsed.type === 'error') { const entry = parsed.id != null ? pending.get(parsed.id) : null; if (entry) { pending.delete(parsed.id); entry.reject(new Error(parsed.message)); } else fail(new Error(parsed.message)); } };