import { executeCode, type RuntimeValue } from 'numbl'; import { mapDuration, mapPitch, mapVelocity, type SimManifest } from './manifest.ts'; /** * One note through one simulated instrument: build the parameter struct * from the model's own defaults plus the manifest's overrides, call the * entry function through numbl, and hand back the mono trace. Pure with * respect to the environment (no DOM, no worker), so the browser worker * and the node tests share it. */ export interface MFile { name: string; source: string; } export interface SimNoteOpts { pitch: number; // MIDI note number velocity: number; // 0..1 durationSec: number; sampleRate: number; quality: 'preview' | 'full'; settings: Record; // knob values; unknown keys are ignored } interface RuntimeTensorLike { kind: 'tensor'; data: Float64Array; } export function renderSimNote(manifest: SimManifest, mfiles: MFile[], opts: SimNoteOpts): Float32Array { const assigns: string[] = []; const setP = (key: string, value: number) => { if (!Number.isFinite(value)) throw new Error(`parameter ${key} is not finite`); assigns.push(`P.${key} = ${value};`); }; for (const [k, v] of Object.entries(manifest.fixed)) setP(k, v); for (const knob of manifest.knobs) { const v = opts.settings[knob.key]; setP(knob.key, v === undefined ? knob.default : Math.max(knob.min, Math.min(knob.max, v))); } if (opts.quality === 'preview') for (const [k, v] of Object.entries(manifest.preview)) setP(k, v); setP('sample_rate', opts.sampleRate); const pv = mapPitch(manifest, opts.pitch); if (pv) setP(pv.key, pv.value); const vv = mapVelocity(manifest, opts.velocity); setP(vv.key, vv.value); const dv = mapDuration(manifest, opts.durationSec); if (dv) setP(dv.key, dv.value); const code = [ `P = ${manifest.defaults}();`, ...assigns, `y_out = ${manifest.entry}(P);`, ].join('\n'); const r = executeCode(code, { displayResults: false }, mfiles, 'midiboot_note.m'); const y = r.variableValues.y_out as RuntimeValue as RuntimeTensorLike; if (!y || y.kind !== 'tensor') throw new Error(`${manifest.name}: entry '${manifest.entry}' did not return a tensor`); const out = new Float32Array(y.data.length); for (let i = 0; i < y.data.length; i++) out[i] = y.data[i]; return out; }