/ simulated_instruments / instrument-format
Sign in
simulated_instruments / instrument-format
instrument-format
Go to fileHistoryFork
schemaExported-instrument format: spec, schema, types, build script
scriptsExported-instrument format: spec, schema, types, build script
tsExported-instrument format: spec, schema, types, build script
README.mdExported-instrument format: spec, schema, types, build script

instrument-format#

The exported-instrument format shared by the simulated instrument repositories and the hosts that play them, currently midiboot. A limitation of the earlier arrangement was that the host owned the mapping from MIDI to model parameters: a JSON manifest in each instrument repository declared which parameter received pitch, which received velocity, and the host applied those rules in TypeScript, so any mapping the manifest schema had not anticipated required a host change. Here we define a self-contained bundle in which the mapping itself is code written by the instrument author, in the same MATLAB syntax as the model and executed by the same interpreter, numbl. The host knows how to hand a note to the bundle and take audio back; it knows nothing about the model's parameters.

The bundle#

A bundle is one JSON file, format numbl-instrument-v1. It carries identity (id, name, optional description and source), a curated list of user-editable knobs (key, label, range, default, optional step and scale: "log"), and files, the complete set of .m sources needed to render a note: the driver functions defined below plus whatever model code they call. A bundle is self-contained by construction; nothing outside its files is on the numbl path when it runs. The machine-readable definitions are schema/numbl-instrument-v1.schema.json and ts/bundle.ts, which hosts vendor. One repository may export several bundles, typically variants of one model with different baked-in character.

The driver contract#

A bundle must contain render_note.m and may contain prepare.m:

state = prepare(params)    % optional, pitch-independent setup
y = render_note(note)      % one MIDI note to one audio vector

prepare receives a struct with every knob value (clamped to its range, defaults filled in) plus params.sample_rate and params.preview, and returns whatever precomputed state the renders want to share; a bundle without prepare.m gets no state field. render_note receives a struct with the fields below and returns a vector of samples at note.sample_rate, in arbitrary linear units. The vector's length is the driver's choice and may exceed duration_s (a struck instrument rings out); the host mixes it in at the note's onset and normalizes per track, so absolute level does not matter, only the relative level between notes.

Field Meaning
note.pitch MIDI note number; may be fractional in future hosts
note.velocity 0 to 1
note.duration_s note-on to note-off, seconds; advisory for instruments that ring freely
note.sample_rate output rate, Hz
note.preview 1 for the fast tier, 0 for full quality
note.params the same knob struct prepare saw
note.state prepare's return value, only when prepare.m exists

Hosts may add fields to these structs in later versions; drivers must ignore fields they do not use. Renders must be deterministic (fix any random seed from a constant or from the note's fields), because hosts cache one render per distinct note and reuse it wherever that note recurs. Hosts may also quantize velocity and duration before calling, trading fine dynamics for cache hits; midiboot uses eight velocity levels and quarter-second duration steps.

prepare runs once per batch of notes rendered together, not once per note, so it is the place for work that does not depend on pitch, velocity, or duration: tables, filter designs, anything derived from the knobs alone. The trade-off in the current midiboot host is that prepared state lives only within one rendering program, since numbl values are not carried between interpreter runs; a driver should therefore keep prepare cheap relative to a note render rather than relying on it being amortized indefinitely.

Building bundles#

An instrument repository lays its exports out as one directory per bundle:

export-src/<name>/export.json    identity and knobs
export-src/<name>/render_note.m  the driver (and prepare.m if used)
matlab/*.m                       the model, shared by every bundle

Running node scripts/build-exports.mjs from the repo root validates each export.json, inlines the drivers and the shared model sources, and writes exports/<id>.json, which is committed. The canonical copy of the script is scripts/build-exports.mjs; each instrument repo carries its own copy so it builds standalone. A host such as midiboot discovers committed bundles at build time and can also import a bundle file at runtime, so an export downloaded or built anywhere can be dropped into the app without rebuilding it.

Limitations#

Notes are rendered independently and mixed, so physics that couples successive notes (a string choked by the next pluck, a cymbal still swinging when struck again) is not expressible; a gesture-level contract would be the extension. There is no live low-latency voice in the contract, only offline rendering. Knob values are scalars; enumerated or boolean parameters would need a small schema addition. The format says nothing about licensing or signatures, which matters only once bundles travel beyond this vault.

↑↓ move↵openescclose