P1a: material import, sixteen faces, sample engine and quarry
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
#!/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));
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user