Commit the WAI standard source: a content + capability-dispatch standard
whose thesis is "carries whatever it can reconstruct, verifies whatever
it carries." Source only — large vendored/model assets and build output
are gitignored.
Reconstruct classes (each: spec + conformance corpus + in-tab browser
sink that re-derives the corpus hash, one wai.det.fixed64 fixed-point
floor, one *-equivalence apiece):
- interactive worlds (replay-equivalence) + 3 sim registrants,
multiplayer over a relay, session receipts, glTF + real SPZ assets
- spatial audio (mixdown-equivalence)
- haptics (sample-equivalence)
- state-feeds (replay-equivalence, non-physics sim)
- avatars / digital humans (pose-equivalence) + telepresence over the relay
- volumetric / 4D splats (frame-state-equivalence) + WebGPU EWA render,
real-SPZ-derived clip
Verify half (lever 4):
- asset provenance: a signed, energy-accounted lineage for opaque
captured content WAI does not reconstruct; tamper-evident, in-tab.
Cross-cutting: JWP receipt profiles per class; a native energy meter
(IOReport via macmon, no sudo) emitting receipts with REAL measured
joules; the learned-image/neural codec family and JPEG AI entropy port
from earlier work.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
99 lines
3.9 KiB
JavaScript
99 lines
3.9 KiB
JavaScript
// WAI spatial-audio sink — load a wai.audio.scene, mix it with the same
|
|
// deterministic Rust engine that made the corpus, verify the mixdown
|
|
// hash in-browser (mixdown-equivalence), play the exact PCM, and show a
|
|
// top-down radar of the moving objects.
|
|
|
|
import init, { AudioSink } from './pkg/wai_web.js';
|
|
|
|
const $ = (id) => document.getElementById(id);
|
|
const ui = {
|
|
scene: $('scene'), play: $('play'), badge: $('badge'),
|
|
objs: $('objs'), rate: $('rate'), dur: $('dur'),
|
|
scenehash: $('scenehash'), mixhash: $('mixhash'),
|
|
};
|
|
const canvas = $('c'), g = canvas.getContext('2d');
|
|
|
|
await init();
|
|
let expected = await (await fetch('audio/expected.json')).json();
|
|
|
|
let sink = null, audioCtx = null, srcNode = null, startedAt = 0, playing = false;
|
|
|
|
async function load(name) {
|
|
stop();
|
|
const wscn = new Uint8Array(await (await fetch(`audio/${name}.wscn`)).arrayBuffer());
|
|
const bin = new Uint8Array(await (await fetch(`audio/${name}.sources.bin`)).arrayBuffer());
|
|
sink = new AudioSink(wscn);
|
|
sink.load_sources_bin(bin);
|
|
const layout = sink.floor_layout();
|
|
const mh = sink.mixdown_hash_hex(layout);
|
|
const exp = expected[`${name}.wscn`];
|
|
const ok = exp && mh === exp.mixdown_hash && sink.scene_hash_hex() === exp.scene_hash;
|
|
ui.badge.className = ok ? 'ok' : 'bad';
|
|
ui.badge.textContent = ok
|
|
? '✓ mixdown verified — hash matches the conformance corpus'
|
|
: '✗ mixdown hash mismatch';
|
|
ui.objs.textContent = sink.num_objects();
|
|
ui.rate.textContent = sink.sample_rate();
|
|
ui.dur.textContent = `${sink.duration()} samp`;
|
|
ui.scenehash.textContent = sink.scene_hash_hex().slice(0, 16) + '…';
|
|
ui.mixhash.textContent = mh.slice(0, 16) + '…';
|
|
}
|
|
|
|
function stop() {
|
|
if (srcNode) { try { srcNode.stop(); } catch {} srcNode = null; }
|
|
playing = false;
|
|
ui.play.textContent = '▶ play';
|
|
}
|
|
|
|
function play() {
|
|
if (!sink) return;
|
|
audioCtx = audioCtx || new (window.AudioContext || window.webkitAudioContext)();
|
|
const layout = sink.floor_layout(); // "stereo"
|
|
const pcm = sink.mix(layout); // exact i16 interleaved
|
|
const n = Number(sink.duration());
|
|
const buf = audioCtx.createBuffer(2, n, sink.sample_rate());
|
|
const L = buf.getChannelData(0), R = buf.getChannelData(1);
|
|
for (let t = 0; t < n; t++) { L[t] = pcm[t * 2] / 32768; R[t] = pcm[t * 2 + 1] / 32768; }
|
|
srcNode = audioCtx.createBufferSource();
|
|
srcNode.buffer = buf;
|
|
srcNode.loop = true;
|
|
srcNode.connect(audioCtx.destination);
|
|
srcNode.start();
|
|
startedAt = audioCtx.currentTime;
|
|
playing = true;
|
|
ui.play.textContent = '⏸ stop';
|
|
}
|
|
|
|
ui.scene.addEventListener('change', () => load(ui.scene.value));
|
|
ui.play.addEventListener('click', () => (playing ? stop() : play()));
|
|
|
|
// ---- radar: listener at center, objects at their (x,z), looping with playback
|
|
const W = canvas.width, H = canvas.height, CX = W / 2, CY = H / 2, SCALE = 26;
|
|
function draw() {
|
|
g.fillStyle = '#04060a'; g.fillRect(0, 0, W, H);
|
|
// range rings + axes
|
|
g.strokeStyle = 'rgba(74,85,104,0.5)';
|
|
for (let r = SCALE; r < W; r += SCALE * 2) { g.beginPath(); g.arc(CX, CY, r, 0, 7); g.stroke(); }
|
|
g.beginPath(); g.moveTo(0, CY); g.lineTo(W, CY); g.moveTo(CX, 0); g.lineTo(CX, H); g.stroke();
|
|
// listener
|
|
g.fillStyle = '#f0b429'; g.beginPath(); g.arc(CX, CY, 5, 0, 7); g.fill();
|
|
g.fillStyle = '#4a5568'; g.font = '10px monospace'; g.fillText('listener', CX + 8, CY - 8);
|
|
|
|
if (sink) {
|
|
const dur = Number(sink.duration());
|
|
const t = (playing && audioCtx)
|
|
? (Math.floor((audioCtx.currentTime - startedAt) * sink.sample_rate()) % dur)
|
|
: 0;
|
|
const xz = sink.object_xz(BigInt(t));
|
|
g.fillStyle = '#00e5a0';
|
|
for (let i = 0; i * 2 < xz.length; i++) {
|
|
const x = CX + xz[i * 2] * SCALE; // +x → right (matches pan)
|
|
const z = CY - xz[i * 2 + 1] * SCALE; // +z (front) → up
|
|
g.beginPath(); g.arc(x, z, 7, 0, 7); g.fill();
|
|
}
|
|
}
|
|
requestAnimationFrame(draw);
|
|
}
|
|
|
|
await load(ui.scene.value);
|
|
requestAnimationFrame(draw);
|