/** * The bake: everything the precompute produced, in one binary file. * * A bake is valid for exactly one setting of the geometry and accuracy * parameters of exactly one instrument file. The key is a SHA-256 over the * instrument's name, its full precompute definition (derived expressions, * shaders, schedule), and the geometry and accuracy values; editing any of * those, including the instrument file itself, invalidates the bake, while * tuning values are deliberately absent from the hash. * * Container: an 8-byte magic, a u32 header length, a JSON header (the key, * the evaluated scope for reference, and each array's offset and length), * then the raw f32 data. Same bytes in the browser and on disk. */ import type { InstrumentFile } from '../format.ts'; import { bakeRelevantValues } from '../format.ts'; const MAGIC = 'IWBAKE1\n'; export interface Bake { key: string; instrument: string; /** The evaluated scope the precompute ran with, for reference. */ scope: Record; arrays: Record; } /** The bake key for an instrument under given parameter values. */ export async function bakeKey( inst: InstrumentFile, values: Record, ): Promise { const subject = JSON.stringify({ name: inst.name, derived: inst.derived, shaders: inst.shaders, precompute: inst.precompute, values: bakeRelevantValues(inst, values), }); const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(subject)); return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join(''); } export function serializeBake(bake: Bake): Uint8Array { const entries = Object.entries(bake.arrays); let offset = 0; const header = { key: bake.key, instrument: bake.instrument, scope: bake.scope, arrays: entries.map(([name, a]) => { const e = { name, offset, length: a.length }; offset += a.length; return e; }), }; // Padded to 4 bytes with spaces (JSON whitespace), so the f32 data that // follows is aligned and the header parses as-is. const head = new TextEncoder().encode( JSON.stringify(header) + ' '.repeat((4 - (JSON.stringify(header).length % 4)) % 4), ); const padded = head.length; const total = MAGIC.length + 4 + padded + 4 * offset; const out = new Uint8Array(total); const view = new DataView(out.buffer); new TextEncoder().encodeInto(MAGIC, out); view.setUint32(MAGIC.length, padded, true); out.set(head, MAGIC.length + 4); let pos = MAGIC.length + 4 + padded; for (const [, a] of entries) { out.set(new Uint8Array(a.buffer, a.byteOffset, a.byteLength), pos); pos += a.byteLength; } return out; } export function parseBake(data: Uint8Array): Bake { const magic = new TextDecoder().decode(data.subarray(0, MAGIC.length)); if (magic !== MAGIC) throw new Error('not an instrawave bake file'); const view = new DataView(data.buffer, data.byteOffset, data.byteLength); const headerLen = view.getUint32(MAGIC.length, true); const header = JSON.parse( new TextDecoder().decode(data.subarray(MAGIC.length + 4, MAGIC.length + 4 + headerLen)), ) as { key: string; instrument: string; scope: Record; arrays: { name: string; offset: number; length: number }[]; }; const base = MAGIC.length + 4 + headerLen; const arrays: Record = {}; for (const a of header.arrays) { // Copy out so the result owns aligned storage whatever the container's // alignment was. const bytes = data.subarray(base + 4 * a.offset, base + 4 * (a.offset + a.length)); const f = new Float32Array(a.length); new Uint8Array(f.buffer).set(bytes); arrays[a.name] = f; } return { key: header.key, instrument: header.instrument, scope: header.scope, arrays }; }