2 * The bake: everything the precompute produced, in one binary file.
3 *
4 * A bake is valid for exactly one setting of the geometry and accuracy
5 * parameters of exactly one instrument file. The key is a SHA-256 over the
6 * instrument's name, its full precompute definition (derived expressions,
7 * shaders, schedule), and the geometry and accuracy values; editing any of
8 * those, including the instrument file itself, invalidates the bake, while
9 * tuning values are deliberately absent from the hash.
10 *
11 * Container: an 8-byte magic, a u32 header length, a JSON header (the key,
12 * the evaluated scope for reference, and each array's offset and length),
13 * then the raw f32 data. Same bytes in the browser and on disk.
14 */
15import type { InstrumentFile } from '../format.ts';
16import { bakeRelevantValues } from '../format.ts';
18const MAGIC = 'IWBAKE1\n';
20export interface Bake {
21 key: string;
22 instrument: string;
23 /** The evaluated scope the precompute ran with, for reference. */
24 scope: Record<string, number>;
25 arrays: Record<string, Float32Array>;
26}
28/** The bake key for an instrument under given parameter values. */
29export async function bakeKey(
30 inst: InstrumentFile,
31 values: Record<string, number>,
32): Promise<string> {
33 const subject = JSON.stringify({
34 name: inst.name,
35 derived: inst.derived,
36 shaders: inst.shaders,
37 precompute: inst.precompute,
38 values: bakeRelevantValues(inst, values),
39 });
40 const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(subject));
41 return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
42}
44export function serializeBake(bake: Bake): Uint8Array {
45 const entries = Object.entries(bake.arrays);
46 let offset = 0;
47 const header = {
48 key: bake.key,
49 instrument: bake.instrument,
50 scope: bake.scope,
51 arrays: entries.map(([name, a]) => {
52 const e = { name, offset, length: a.length };
53 offset += a.length;
54 return e;
55 }),
56 };
57 // Padded to 4 bytes with spaces (JSON whitespace), so the f32 data that
58 // follows is aligned and the header parses as-is.
59 const head = new TextEncoder().encode(
60 JSON.stringify(header) + ' '.repeat((4 - (JSON.stringify(header).length % 4)) % 4),
61 );
62 const padded = head.length;
63 const total = MAGIC.length + 4 + padded + 4 * offset;
64 const out = new Uint8Array(total);
65 const view = new DataView(out.buffer);
66 new TextEncoder().encodeInto(MAGIC, out);
67 view.setUint32(MAGIC.length, padded, true);
68 out.set(head, MAGIC.length + 4);
69 let pos = MAGIC.length + 4 + padded;
70 for (const [, a] of entries) {
71 out.set(new Uint8Array(a.buffer, a.byteOffset, a.byteLength), pos);
72 pos += a.byteLength;
73 }
74 return out;
75}
77export function parseBake(data: Uint8Array): Bake {
78 const magic = new TextDecoder().decode(data.subarray(0, MAGIC.length));
79 if (magic !== MAGIC) throw new Error('not an instrawave bake file');
80 const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
81 const headerLen = view.getUint32(MAGIC.length, true);
82 const header = JSON.parse(
83 new TextDecoder().decode(data.subarray(MAGIC.length + 4, MAGIC.length + 4 + headerLen)),
84 ) as {
85 key: string;
86 instrument: string;
87 scope: Record<string, number>;
88 arrays: { name: string; offset: number; length: number }[];
89 };
90 const base = MAGIC.length + 4 + headerLen;
91 const arrays: Record<string, Float32Array> = {};
92 for (const a of header.arrays) {
93 // Copy out so the result owns aligned storage whatever the container's
94 // alignment was.
95 const bytes = data.subarray(base + 4 * a.offset, base + 4 * (a.offset + a.length));
96 const f = new Float32Array(a.length);
97 new Uint8Array(f.buffer).set(bytes);
98 arrays[a.name] = f;
99 }
100 return { key: header.key, instrument: header.instrument, scope: header.scope, arrays };
101}