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.
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
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:
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
akai.on('cc', (m) => {
if (m.channel !== 1) return;
// channel 1 only
});Sending
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)
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:
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.