// Renders the Rust sound stage off the main thread. The page knows where every
// duck is in the listener's frame; it sends those places and the sounds the
// flock has already decided on. This worker keeps a few blocks ahead of the
// worklet, exactly as duck-pond's own player does.
//
// The stage is fed live: a call that arrives while the queue is full is picked
// up by the next block rendered, so it is heard a queue-length after the
// visual gesture. Eight blocks is about 85 ms at 48 kHz.
let pond = null;
let memory = null;
let block = 512;
let feed = null;
let queue = 8;
let bootGrace = 2.5;
let queued = 0;
let started = false;
const pending = [];
onmessage = async ({ data }) => {
switch (data.type) {
case 'start':
await start(data);
break;
case 'sync':
if (pond) sync(data.positions);
else remember(data);
break;
case 'call':
case 'dabble':
case 'peck':
case 'fixed':
if (pond) apply(data);
else remember(data);
break;
case 'headphones':
if (pond) pond.quack_audio_set_headphones(data.on ? 1 : 0);
break;
default:
break;
}
};
async function start(data) {
if (started) return;
started = true;
try {
const url = new URL('quack_audio.wasm', self.location.href);
const response = await fetch(url);
if (!response.ok) throw new Error(`couldn't load quack_audio.wasm (${response.status})`);
const { instance } = await WebAssembly.instantiate(await response.arrayBuffer(), {});
pond = instance.exports;
memory = pond.memory;
block = pond.quack_audio_block();
queue = Math.max(2, data.queue || queue);
bootGrace = data.bootGrace || bootGrace;
pond.quack_audio_new(data.sampleRate, data.seed[0], data.seed[1], data.headphones ? 1 : 0);
const slots = Math.min(pond.quack_audio_ducks(), data.roster?.length ?? 0);
for (let i = 0; i < slots; i++) {
const voice = data.roster[i];
pond.quack_audio_set_voice(i, voice[0], voice[1]);
}
sync(data.positions);
feed = data.feed;
feed.onmessage = () => {
queued--;
pump();
};
postMessage({ type: 'ready', block, queue });
// sounds made while the module was loading are still worth hearing,
// unless they have already gone stale
const now = performance.now();
for (const event of pending) {
if (now - event.at <= bootGrace * 1000) apply(event);
}
pending.length = 0;
pump();
} catch (error) {
postMessage({ type: 'error', message: String(error?.message ?? error) });
}
}
function remember(data) {
pending.push({ ...data, at: performance.now() });
}
function apply(data) {
switch (data.type) {
case 'sync':
sync(data.positions);
break;
case 'call':
pond.quack_audio_set_pos(data.duck, data.x, data.y);
pond.quack_audio_call(data.duck, data.kind, data.notes, data.duration);
break;
case 'dabble':
pond.quack_audio_set_pos(data.duck, data.x, data.y);
pond.quack_audio_dabble(data.duck, data.seconds);
break;
case 'peck':
pond.quack_audio_set_pos(data.duck, data.x, data.y);
pond.quack_audio_peck(data.duck, data.onWater ? 1 : 0, data.duckling ? 1 : 0);
break;
case 'fixed':
pond.quack_audio_fixed(data.kind, data.x, data.y, data.gain);
break;
default:
break;
}
}
function sync(positions) {
if (!pond || !positions) return;
const slots = Math.min(pond.quack_audio_ducks(), positions.length);
for (let i = 0; i < slots; i++) {
pond.quack_audio_set_pos(i, positions[i][0], positions[i][1]);
}
}
function pump() {
while (pond && feed && queued < queue) {
// rendering may grow the module's memory, replacing its buffer, so
// look for the memory only afterwards and copy the block out
const ptr = pond.quack_audio_render();
const audio = new Float32Array(memory.buffer, ptr, 2 * block).slice();
feed.postMessage({ audio }, [audio.buffer]);
queued++;
}
}