/ proof-of-concept / instrawave-compose
Sign in
proof-of-concept / instrawave-compose
instrawave-compose / src / render / instrawave.ts
144 lines · 5.3 KBCodeBlameHistory
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 2 * The instrawave side of the renderer seam. Each of the three bundled
3 * instruments carries one set of geometry and accuracy values (defaults
4 * plus the user's overrides, edited in the instrument panel), so each has
5 * exactly one live bake; the bake key tells a stale one from a current
6 * one, and serialized bakes round-trip through IndexedDB so a reload does
7 * not repeat the precompute. All per-voice character travels as tuning,
8 * which is free: every voice bound to the same instrument shares its bake,
9 * and one render call carries all their events, each with the voice's
10 * tuning overrides and its strength scaling the instrument's excitation
11 * amplitude (`vel`, or `amp` where there is no strike).
13import { requestDevice } from 'instrawave/src/host/device.ts';
14import { InstrumentSession } from 'instrawave/src/host/session.ts';
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 15import { bakeKey, serializeBake, type Bake } from 'instrawave/src/host/bake.ts';
16import { evalDerived } from 'instrawave/src/expr.ts';
18 parseInstrument,
19 paramValues,
20 type InstrumentFile,
21 type ParamSpec,
22 type StimulusEvent,
23} from 'instrawave/src/format.ts';
24import drumJson from 'instrawave/instruments/drum.instrument.json';
25import dulcimerJson from 'instrawave/instruments/dulcimer.instrument.json';
26import chimeJson from 'instrawave/instruments/chime.instrument.json';
28export { PhaseCancelled } from 'instrawave/src/host/session.ts';
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 29export { parseBake } from 'instrawave/src/host/bake.ts';
30export type { StimulusEvent, Bake };
32const files: InstrumentFile[] = [drumJson, dulcimerJson, chimeJson].map((j) =>
33 parseInstrument(JSON.stringify(j)),
34);
36export type Progress = (done: number, total: number) => void;
38export class IwEngine {
39 private device: GPUDevice | null = null;
40 private bakes = new Map<string, Bake>();
41 private insts = new Map(files.map((f) => [f.name, f]));
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 42 private overrides: Record<string, Record<string, number>> = {};
44 names(): string[] {
45 return [...this.insts.keys()];
46 }
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 48 file(name: string): InstrumentFile {
50 if (!f) throw new Error(`unknown instrument '${name}'`);
51 return f;
52 }
54 /** The tuning parameters a binding may override: tuning-class params
55 * that are also event parameters, so they can vary per hit. */
56 tuning(name: string): ParamSpec[] {
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 58 return f.params.filter((p) => p.class === 'tuning' && f.render.eventParams.includes(p.key));
59 }
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 61 /** Geometry and accuracy: the parameters the bake depends on. */
62 geometryParams(name: string): ParamSpec[] {
63 return this.file(name).params.filter((p) => p.class !== 'tuning');
64 }
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 66 /** The parameter a hit's strength scales: the strike speed where there
67 * is one, the pluck amplitude otherwise. Linear equations, so it is a
68 * pure scale. */
69 strengthParam(name: string): string {
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 70 const ep = this.file(name).render.eventParams;
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 71 return ep.includes('vel') ? 'vel' : 'amp';
72 }
74 defaultValue(name: string, key: string): number {
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 75 const p = this.file(name).params.find((q) => q.key === key);
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 76 if (!p) throw new Error(`instrument '${name}' has no parameter '${key}'`);
77 return p.value;
78 }
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 80 setOverrides(all: Record<string, Record<string, number>>): void {
81 this.overrides = all;
82 }
83 setOverride(name: string, key: string, value: number): void {
84 (this.overrides[name] ??= {})[key] = value;
85 }
86 dumpOverrides(): Record<string, Record<string, number>> {
87 return this.overrides;
88 }
90 values(name: string): Record<string, number> {
91 return paramValues(this.file(name), this.overrides[name] ?? {});
92 }
94 /** Params plus derived, fully evaluated (what the viewer draws from). */
95 scope(name: string): Record<string, number> {
96 const f = this.file(name);
97 return evalDerived(f.derived, this.values(name));
98 }
100 currentKey(name: string): Promise<string> {
101 return bakeKey(this.file(name), this.values(name));
102 }
104 async ensureDevice(): Promise<GPUDevice> {
105 return (this.device ??= await requestDevice());
106 }
108 /** True when an in-memory bake matches the current geometry. */
109 async bakeReady(name: string): Promise<boolean> {
110 const b = this.bakes.get(name);
111 return !!b && b.key === (await this.currentKey(name));
112 }
114 adoptBake(name: string, bake: Bake): void {
115 this.bakes.set(name, bake);
116 }
118 serializedBake(name: string): { key: string; bytes: Uint8Array } | null {
119 const b = this.bakes.get(name);
120 return b ? { key: b.key, bytes: serializeBake(b) } : null;
123 async precompute(name: string, onProgress?: Progress, cancelled?: () => boolean): Promise<void> {
125 const device = await this.ensureDevice();
126 const session = new InstrumentSession(device, inst, this.values(name));
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 127 this.bakes.set(name, await session.precompute(onProgress, cancelled));
128 }
130 async render(
131 name: string,
132 seconds: number,
133 events: StimulusEvent[],
134 onProgress?: Progress,
135 ): Promise<{ trace: Float32Array; dt: number }> {
138 if (!bake) throw new Error(`no bake for '${name}'; precompute first`);
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 139 const device = await this.ensureDevice();
140 const session = new InstrumentSession(device, inst, this.values(name));
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 141 const { trace, dt } = await session.render(bake, seconds, events, [], onProgress);
142 return { trace, dt };
143 }
↑↓ move↵openescclose