/** * The instrawave side of the renderer seam. The three bundled instruments * are held at their default geometry and accuracy, so each has exactly one * bake, computed on demand and kept for the session (the drum's is a * couple of hundred MB; persisting it is future work). All per-voice * character travels as tuning, which is free: every voice bound to the * same instrument shares its bake, and one render call carries all their * events, each with the voice's tuning overrides and its strength scaling * the instrument's excitation amplitude (`vel`, or `amp` where there is no * strike). */ import { requestDevice } from 'instrawave/src/host/device.ts'; import { InstrumentSession } from 'instrawave/src/host/session.ts'; import { parseInstrument, paramValues, type InstrumentFile, type ParamSpec, type StimulusEvent, } from 'instrawave/src/format.ts'; import type { Bake } from 'instrawave/src/host/bake.ts'; import drumJson from 'instrawave/instruments/drum.instrument.json'; import dulcimerJson from 'instrawave/instruments/dulcimer.instrument.json'; import chimeJson from 'instrawave/instruments/chime.instrument.json'; export { PhaseCancelled } from 'instrawave/src/host/session.ts'; export type { StimulusEvent }; const files: InstrumentFile[] = [drumJson, dulcimerJson, chimeJson].map((j) => parseInstrument(JSON.stringify(j)), ); export type Progress = (done: number, total: number) => void; export class IwEngine { private device: GPUDevice | null = null; private bakes = new Map(); private insts = new Map(files.map((f) => [f.name, f])); names(): string[] { return [...this.insts.keys()]; } private inst(name: string): InstrumentFile { const f = this.insts.get(name); if (!f) throw new Error(`unknown instrument '${name}'`); return f; } /** The tuning parameters a binding may override: tuning-class params * that are also event parameters, so they can vary per hit. */ tuning(name: string): ParamSpec[] { const f = this.inst(name); return f.params.filter((p) => p.class === 'tuning' && f.render.eventParams.includes(p.key)); } /** The parameter a hit's strength scales: the strike speed where there * is one, the pluck amplitude otherwise. Linear equations, so it is a * pure scale. */ strengthParam(name: string): string { const ep = this.inst(name).render.eventParams; return ep.includes('vel') ? 'vel' : 'amp'; } defaultValue(name: string, key: string): number { const p = this.inst(name).params.find((q) => q.key === key); if (!p) throw new Error(`instrument '${name}' has no parameter '${key}'`); return p.value; } hasBake(name: string): boolean { return this.bakes.has(name); } async precompute(name: string, onProgress?: Progress, cancelled?: () => boolean): Promise { const inst = this.inst(name); this.device ??= await requestDevice(); const session = new InstrumentSession(this.device, inst, paramValues(inst)); this.bakes.set(name, await session.precompute(onProgress, cancelled)); } async render( name: string, seconds: number, events: StimulusEvent[], onProgress?: Progress, ): Promise<{ trace: Float32Array; dt: number }> { const inst = this.inst(name); const bake = this.bakes.get(name); if (!bake) throw new Error(`no bake for '${name}'; precompute first`); if (!this.device) throw new Error('no GPU device'); const session = new InstrumentSession(this.device, inst, paramValues(inst)); const { trace, dt } = await session.render(bake, seconds, events, [], onProgress); return { trace, dt }; } }