Build a MIDI tool

No audio path: take note input, emit chords, and route the result to another tool or your DAW.

Scope note: the midi-tool template and the MIDI-only export target are part of the proposed CLI surface. The event ABI it is built on is real and shipped — everything below about AudioEvent describes types you can use today.

A MIDI tool has no io.inputs to read and writes no samples. It consumes note events and emits note events. Arpeggiators, chord generators, scale quantizers, velocity mappers, humanizers — all the same shape.

Start from the MIDI template

npx codex create my-chords --template midi-tool
cd my-chords
npm run dev

The dev UI gives you an on-screen keyboard and a device picker, and prints the event stream going in and coming out. That printout is the whole debugging story for a tool like this.

The event vocabulary is the same one

There is no separate MIDI type. A MIDI tool reads and writes the same AudioEvent union every kernel speaks:

type AudioEvent =
  | { kind: "param"; frameOffset: number; path: string; value: number }
  | { kind: "noteOn"; frameOffset: number; note: number; velocity: number }
  | { kind: "noteOff"; frameOffset: number; note: number };

That is deliberate. Because the vocabulary is shared, the output of a MIDI tool plugs into an instrument kernel with no adapter, and the DAW's MIDI stream plugs into your tool the same way. note is a MIDI note number, velocity is 0–1, and frameOffset is an integer frame inside the current block.

Emit a triad

Chord generation is the "hello world" here: for every incoming noteOn, emit three, at the root and two scale intervals above it.

process(state, _io, params, events): void {
  const quality = params["chord.quality"];   // 0 = major, 1 = minor
  const third = quality < 0.5 ? 4 : 3;
  const fifth = 7;

  for (let e = 0; e < events.length; e = e + 1) {
    const ev = events[e];
    if (ev.kind === "noteOn") {
      emitNoteOn(state, ev.frameOffset, ev.note, ev.velocity);
      emitNoteOn(state, ev.frameOffset, ev.note + third, ev.velocity);
      emitNoteOn(state, ev.frameOffset, ev.note + fifth, ev.velocity);
    } else if (ev.kind === "noteOff") {
      emitNoteOff(state, ev.frameOffset, ev.note);
      emitNoteOff(state, ev.frameOffset, ev.note + third);
      emitNoteOff(state, ev.frameOffset, ev.note + fifth);
    }
  }
}

Preserve frameOffset exactly. A chord whose notes land on different frames is a strummed chord, and if you did not mean to write a strummer you have written a bug that only shows up on tight material.

The output buffer is preallocated too

The kernel rules do not relax because you stopped touching audio. Your emitted events go into a fixed-size buffer allocated in init(), with a write cursor — not into an array you push onto.

init(_sampleRate: number): ChordState {
  const capacity = 256;
  return {
    outKind: new Int32Array(capacity),
    outFrame: new Int32Array(capacity),
    outNote: new Int32Array(capacity),
    outVelocity: new Float32Array(capacity),
    outCount: 0,
  };
}

Pick a capacity and drop events past it rather than growing. A dropped note is a bad afternoon; an allocation on the audio thread is a click in someone's mix.

Track what you sent

noteOff is where MIDI tools get interesting. If a parameter changes between a note's start and its end — the user switches major to minor mid-note — the intervals you would compute at noteOff no longer match the ones you emitted at noteOn, and you leave a hanging note.

The fix is to record the emitted note numbers per incoming note in state and release exactly those, rather than recomputing. Latch parameters at noteOn and use the latched values for the matching noteOff.

Routing and export

In the browser, chain a MIDI tool's output into any instrument's engine — same vocabulary, no glue. In a DAW, the export produces a MIDI-effect plugin that sits ahead of an instrument on the same track, so your DAW's own instruments work with it too.

Read How it works for where events sit in the pipeline, and Build a synth if you want something of your own to feed.