Skip to content

MIDI input & output

A script addresses MIDI hardware by logical name, the name on the midi-in card in the Wiring view, not the USB port or the macOS device identifier. The project maps that logical name to whatever physical device is plugged in tonight, so the script that runs at home on a 49-key controller also runs at a venue on the 88-key stage rig.

typescript
const akai = midi.input('MIDI Mix');
const lights = midi.output('MIDI Mix');

midi.input(name) returns a handle whose on(...) handlers fire only while that device is the engine's open input. midi.output(name) lazy-opens the output the first time you send to it.

Receiving

typescript
interface SwMidiInput {
  on(event: 'cc',   fn: (m: SwMidiCcMsg) => void): void;
  on(event: 'note', fn: (m: SwMidiNoteMsg) => void): void;
}

type SwMidiCcMsg = {
  kind: 'cc';
  channel: number;    // 1..16
  controller: number; // 0..127
  value: number;      // 0..127
  normalized: number; // value / 127, 0..1
};

type SwMidiNoteMsg = {
  kind: 'note';
  channel: number;    // 1..16
  note: number;       // 0..127
  velocity: number;   // 0..127
  on: boolean;        // false for note-off
  normalized: number; // velocity / 127, 0..1
};

Only two events exist: 'cc' and 'note'. The 'note' event fires for both note-on and note-off, use the on boolean to tell them apart. The runtime does not deliver program change, pitch bend, or aftertouch to scripts (see Program Change below).

The normalized field is the canonical 0..1 amplitude, handy for writing straight to a widget without dividing by 127 yourself.

Press vs. release

Hardware buttons typically send velocity 127 on press and 0 on release for notes, or CC value 127 / 0 for CCs. The script sees both. Filter explicitly:

typescript
akai.on('cc', (m) => {
  if (m.value !== 127) return;   // ignore release
  // ...
});

akai.on('note', (m) => {
  if (!m.on || m.velocity === 0) return;  // presses only
  // ...
});

Channel filtering

typescript
akai.on('cc', (m) => {
  if (m.channel !== 1) return;
  // channel 1 only
});

Sending

typescript
interface SwMidiOutput {
  sendCC(channel: number, cc: number, value: number): void;       // 1..16, 0..127, 0..127
  sendNoteOn(channel: number, note: number, velocity: number): void;
  sendNoteOff(channel: number, note: number): void;
}

Those three are the whole output surface, there is no program-change, pitch-bend, or raw/SysEx send. Channel is clamped to 1..16 and data bytes to 0..127. The device is opened lazily on first send and held for the process lifetime, so LED feedback does not pay an open/close cost per message.

LED feedback (the typical pattern)

typescript
const out = midi.output('MIDI Mix');

// MIDI Mix mute row: each button's LED is on a different note.
const muteNote = (col: number) => 1 + col * 3; // 1, 4, 7, ...
const mutes = [...Array(8)].map((_, i) => widget(`MUTE_${i + 1}`));

mutes.forEach((w, i) => {
  w.on('change', (v) => out.sendNoteOn(1, muteNote(i), v > 0.5 ? 127 : 0));
});

onInit(() => {
  mutes.forEach((w, i) => out.sendNoteOn(1, muteNote(i), w.get() > 0.5 ? 127 : 0));
});

Re-emit on rack switch

LEDs reset to off when a different rack activates. If your script manages LEDs, re-emit them from onActivate:

typescript
onActivate(() => {
  mutes.forEach((w, i) => out.sendNoteOn(1, muteNote(i), w.get() > 0.5 ? 127 : 0));
});

Program Change and songs

MIDI Program Change is handled by the engine itself, not delivered to scripts. The engine maps an incoming Program Change to a song and recalls it (see Songs & setlists). That is a native binding you configure in the app, no script required, and there is no onProgramChange hook or midi.input(...).on('program-change', ...) event in the runtime.

Diagnostics

Every MIDI event reaching the engine is visible in the Diagnostics → MIDI Monitor tab. Toggle the chip for the device(s) you care about, events render with timestamps, channel, kind, and data bytes. Use it to confirm your script is seeing what you think it is.

Next: OSC.

Proprietary software, used under the Stagewright Software Licence.