MIDI input & output
Stagewright Script addresses MIDI hardware by logical name, the name on the midi-in card on the back canvas, not the USB port or the macOS device identifier. The project maps the 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 null if no midi-in card with that name exists. midi.output(name) lazy-opens the output the first time you write to it.
Receiving, MidiInputHandle
interface MidiInputHandle {
on(event: 'cc', fn: (m: CCEvent) => void): void;
on(event: 'note', fn: (m: NoteEvent) => void): void;
on(event: 'note-on', fn: (m: NoteEvent) => void): void;
on(event: 'note-off', fn: (m: NoteEvent) => void): void;
on(event: 'program-change', fn: (m: PCEvent) => void): void;
on(event: 'pitch-bend', fn: (m: PBEvent) => void): void;
on(event: 'aftertouch', fn: (m: ATEvent) => void): void;
off(event: '…', fn?: Function): void;
}
interface CCEvent { kind: 'cc'; channel: number; controller: number; value: number; }
interface NoteEvent { kind: 'note-on'|'note-off'; channel: number; note: number; velocity: number; }
interface PCEvent { kind: 'program-change'; channel: number; program: number; }
interface PBEvent { kind: 'pitch-bend'; channel: number; value: number; /* -8192..+8191 */ }
interface ATEvent { kind: 'aftertouch'; channel: number; value: number; }The 'note' event fires for BOTH note-on and note-off (use the kind field to tell them apart). 'note-on' and 'note-off' fire for those specifically; pick whichever is more readable.
Press vs. release
Hardware buttons typically send velocity 127 on press and 0 on release for note events, or CC value 127 / 0 for CCs. The script sees both. Filter explicitly:
akai.on('cc', (m) => {
if (m.value !== 127) return; // ignore release
// ...
});Or:
akai.on('note', (m) => {
if (m.kind === 'note-off' || m.velocity === 0) return;
// ...
});Channel filtering
akai.on('cc', (m) => {
if (m.channel !== 1) return;
// mute / solo / rec on channel 1 only
});Program Change → song / variation switch
Songs not in v1
Songs / Song Parts / Setlists are advertised on the corporate page but not yet in code. The onProgramChange hook fires; mapping to a song / variation is on the v2 roadmap. For v1, use Program Change to switch RACKS:
const pc = midi.input('Foot Controller');
const rackByPC = ['intro', 'verse', 'chorus', 'bridge', 'outro'];
pc.on('program-change', (m) => {
const name = rackByPC[m.program];
if (name) rack.activate(name);
});rack.activate(name) is a v2 helper; for v1, use the engine binding directly via the inspector or wait for the API surface.
Sending, MidiOutputHandle
interface MidiOutputHandle {
sendNoteOn (channel: number, note: number, velocity: number): void;
sendNoteOff(channel: number, note: number, velocity?: number): void;
sendCC (channel: number, controller: number, value: number): void;
sendProgramChange(channel: number, program: number): void;
sendPitchBend (channel: number, value: number): void; // -8192..+8191
sendRaw (bytes: number[]): void; // SysEx, anything else
}The output device opens on first use, so calling midi.output(...) on every render is cheap, no need to cache the handle in a global.
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 in onRackChange:
onRackChange(() => {
mutes.forEach((w, i) => out.sendNoteOn(1, muteNote(i), w.get() > 0.5 ? 127 : 0));
});SysEx / vendor-specific
sendRaw([...]) for everything the high-level helpers don't cover. Bytes are sent as-is, no framing, no checksumming.
// Akai MPK Mini: switch to bank B (vendor SysEx, look it up in the
// manufacturer's MIDI implementation chart).
out.sendRaw([0xf0, 0x47, 0x7f, 0x42, 0x09, 0x01, 0xf7]);Diagnostics
Every MIDI event hitting 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's seeing.
Next: Hooks reference.