/ proof-of-concept / instrawave-compose
Sign in
proof-of-concept / instrawave-compose
instrawave-compose / src / render / instrawave.ts
96 lines · 3.6 KBCodeBlameHistory
2 * The instrawave side of the renderer seam. The three bundled instruments
3 * are held at their default geometry and accuracy, so each has exactly one
4 * bake, computed on demand and kept for the session (the drum's is a
5 * couple of hundred MB; persisting it is future work). All per-voice
6 * character travels as tuning, which is free: every voice bound to the
7 * same instrument shares its bake, and one render call carries all their
8 * events, each with the voice's tuning overrides and its strength scaling
9 * the instrument's excitation amplitude (`vel`, or `amp` where there is no
10 * strike).
11 */
12import { requestDevice } from 'instrawave/src/host/device.ts';
13import { InstrumentSession } from 'instrawave/src/host/session.ts';
14import {
15 parseInstrument,
16 paramValues,
17 type InstrumentFile,
18 type ParamSpec,
19 type StimulusEvent,
20} from 'instrawave/src/format.ts';
21import type { Bake } from 'instrawave/src/host/bake.ts';
22import drumJson from 'instrawave/instruments/drum.instrument.json';
23import dulcimerJson from 'instrawave/instruments/dulcimer.instrument.json';
24import chimeJson from 'instrawave/instruments/chime.instrument.json';
26export { PhaseCancelled } from 'instrawave/src/host/session.ts';
27export type { StimulusEvent };
29const files: InstrumentFile[] = [drumJson, dulcimerJson, chimeJson].map((j) =>
30 parseInstrument(JSON.stringify(j)),
31);
33export type Progress = (done: number, total: number) => void;
35export class IwEngine {
36 private device: GPUDevice | null = null;
37 private bakes = new Map<string, Bake>();
38 private insts = new Map(files.map((f) => [f.name, f]));
40 names(): string[] {
41 return [...this.insts.keys()];
42 }
44 private inst(name: string): InstrumentFile {
45 const f = this.insts.get(name);
46 if (!f) throw new Error(`unknown instrument '${name}'`);
47 return f;
48 }
50 /** The tuning parameters a binding may override: tuning-class params
51 * that are also event parameters, so they can vary per hit. */
52 tuning(name: string): ParamSpec[] {
53 const f = this.inst(name);
54 return f.params.filter((p) => p.class === 'tuning' && f.render.eventParams.includes(p.key));
55 }
57 /** The parameter a hit's strength scales: the strike speed where there
58 * is one, the pluck amplitude otherwise. Linear equations, so it is a
59 * pure scale. */
60 strengthParam(name: string): string {
61 const ep = this.inst(name).render.eventParams;
62 return ep.includes('vel') ? 'vel' : 'amp';
63 }
65 defaultValue(name: string, key: string): number {
66 const p = this.inst(name).params.find((q) => q.key === key);
67 if (!p) throw new Error(`instrument '${name}' has no parameter '${key}'`);
68 return p.value;
69 }
71 hasBake(name: string): boolean {
72 return this.bakes.has(name);
73 }
75 async precompute(name: string, onProgress?: Progress, cancelled?: () => boolean): Promise<void> {
76 const inst = this.inst(name);
77 this.device ??= await requestDevice();
78 const session = new InstrumentSession(this.device, inst, paramValues(inst));
79 this.bakes.set(name, await session.precompute(onProgress, cancelled));
80 }
82 async render(
83 name: string,
84 seconds: number,
85 events: StimulusEvent[],
86 onProgress?: Progress,
87 ): Promise<{ trace: Float32Array; dt: number }> {
88 const inst = this.inst(name);
89 const bake = this.bakes.get(name);
90 if (!bake) throw new Error(`no bake for '${name}'; precompute first`);
91 if (!this.device) throw new Error('no GPU device');
92 const session = new InstrumentSession(this.device, inst, paramValues(inst));
93 const { trace, dt } = await session.render(bake, seconds, events, [], onProgress);
94 return { trace, dt };
95 }
↑↓ move↵openescclose