midi
stableBuild and parse MIDI messages, convert between note names and numbers, and assemble Standard MIDI File (SMF) structures for music sequencing applications.
use plugin midi Plugin snippets can be edited in the Playground, but native capabilities are executed locally with the Zolo CLI.
Functions (20)
- note_on Build a Note On MIDI message bytes
- note_off Build a Note Off MIDI message bytes
- control_change Build a Control Change MIDI message bytes
- program_change Build a Program Change MIDI message bytes
- pitch_bend Build a Pitch Bend MIDI message bytes
- channel_pressure Build a Channel Pressure (aftertouch) message
- poly_pressure Build a Polyphonic Key Pressure message
- all_notes_off Build an All Notes Off CC message
- all_sound_off Build an All Sound Off CC message
- reset_controllers Build a Reset All Controllers CC message
- parse_message Parse raw MIDI bytes into a typed table
- note_name Convert MIDI note number to name string
- note_number Convert note name string to MIDI number
- frequency Convert MIDI note number to Hz frequency
- tempo_to_bpm Convert microseconds-per-beat to BPM
- bpm_to_tempo Convert BPM to microseconds-per-beat
- encode_variable_length Encode integer as MIDI variable-length quantity
- smf_header Build a Standard MIDI File MThd header
- tempo_event Build a tempo meta event (FF 51 03)
- build_sequence Sort and delta-encode a list of timed events
From import to result
- 01Import only what you use
- 02Copy the smallest example
- 03Run it with the Zolo CLI
Overview¶
midi is a dependency-free toolkit for building, parsing, and sequencing MIDI
data as raw byte buffers. Channel-voice messages (note on/off, control change,
program change, pitch bend, aftertouch) are produced as bytes you can send to
any MIDI output, and incoming bytes can be decomposed with parse_message. The
plugin is stateless: there is no connection or device handle to manage — every
function is a pure transform from its arguments to a value, so you assemble and
inspect messages freely.
Beyond live messages, it also covers the music-theory and file-format layer:
convert between note names, note numbers, and frequencies; translate tempo
between BPM and microseconds-per-beat; and assemble Standard MIDI File (SMF)
structures with smf_header, tempo_event, encode_variable_length, and
build_sequence. Reach for it whenever you need to drive a synth, log MIDI
traffic, or write a .mid file by hand.
Common patterns¶
Play a note: build a Note On, then the matching Note Off.
use plugin midi::{note_on, note_off, note_name}
let n = 60
print("playing {note_name(n)}")
let on = note_on(0, n, 100)
let off = note_off(0, n, 0)Inspect raw MIDI bytes by parsing them back into a typed table:
use plugin midi::{control_change, parse_message}
let raw = control_change(0, 7, 90)
let msg = parse_message(raw)
print("{msg["type"]} ch={msg["channel"]} cc={msg["data1"]} val={msg["data2"]}")Assemble a delta-time encoded track for a Standard MIDI File:
use plugin midi::{smf_header, tempo_event, bpm_to_tempo, build_sequence, note_on, note_off}
let header = smf_header(0, 1, 480)
let tempo = tempo_event(bpm_to_tempo(120.0))
let seq = build_sequence([
#{"tick": 0, "message": note_on(0, 60, 100)},
#{"tick": 480, "message": note_off(0, 60, 0)}
])
print("events: {seq[1]["delta"]}")Build a Note On MIDI message bytes
Builds a 3-byte MIDI Note On message. channel is 0–15, note is 0–127, velocity is 0–127. Returns raw bytes suitable for sending to a MIDI output.
use plugin midi::{note_on, note_off}
let msg_on = note_on(0, 60, 100)
let msg_off = note_off(0, 60, 64)
print("Note On bytes: {msg_on}")Build a Note Off MIDI message bytes
Builds a 3-byte MIDI Note Off message (status byte 0x80 | channel).
use plugin midi::{note_on, note_off}
let on = note_on(0, 69, 127)
let off = note_off(0, 69, 0)Build a Control Change MIDI message bytes
Builds a 3-byte Control Change message. Common controllers: 7 = volume, 10 = pan, 64 = sustain pedal.
use plugin midi::{control_change}
let sustain_on = control_change(0, 64, 127)
let sustain_off = control_change(0, 64, 0)Set channel volume (CC 7) and pan (CC 10) in one go:
use plugin midi::{control_change}
let volume = control_change(0, 7, 100)
let pan_left = control_change(0, 10, 0)
let pan_right = control_change(0, 10, 127)Build a Program Change MIDI message bytes
Builds a 2-byte Program Change message to select a MIDI instrument patch (0–127).
use plugin midi::{program_change}
let piano = program_change(0, 0)
let strings = program_change(1, 48)Build a Pitch Bend MIDI message bytes
Builds a 3-byte Pitch Bend message. value is a 14-bit unsigned integer (0–16383); 8192 is center (no bend).
use plugin midi::{pitch_bend}
let center = pitch_bend(0, 8192)
let up = pitch_bend(0, 16383)
let down = pitch_bend(0, 0)Build a Channel Pressure (aftertouch) message
Builds a 2-byte Channel Pressure (monophonic aftertouch) message. pressure is 0–127.
use plugin midi::{channel_pressure}
let msg = channel_pressure(0, 80)Build a Polyphonic Key Pressure message
Builds a 3-byte Polyphonic Key Pressure message, applying aftertouch to a specific note.
use plugin midi::{poly_pressure}
let msg = poly_pressure(0, 60, 64)Build an All Notes Off CC message
Sends CC 123 with value 0 on the specified channel, signaling all playing notes to stop. Use for emergency note cutoff or when switching patches.
use plugin midi::{all_notes_off}
let msg = all_notes_off(0)Build an All Sound Off CC message
Sends CC 120 with value 0, cutting all audio immediately on the channel (harder than all_notes_off).
use plugin midi::{all_sound_off}
let msg = all_sound_off(0)Build a Reset All Controllers CC message
Sends CC 121 with value 0 to reset all controllers to their default values on the channel.
use plugin midi::{reset_controllers}
let msg = reset_controllers(0)Parse raw MIDI bytes into a typed table
Parses a raw 2- or 3-byte MIDI message into {type, channel, data1, data2}. Handles note_on, note_off (including velocity-zero note_on), aftertouch, control_change, program_change, channel_pressure, pitch_bend, and system messages.
use plugin midi::{note_on, parse_message}
let raw = note_on(0, 60, 100)
let msg = parse_message(raw)
print("{msg["type"]} ch={msg["channel"]} note={msg["data1"]} vel={msg["data2"]}")A Note On with velocity 0 is reported as note_off, matching the running-status convention:
use plugin midi::{note_on, parse_message}
let silent = note_on(0, 60, 0)
let msg = parse_message(silent)
print(msg["type"])Convert MIDI note number to name string
Converts a MIDI note number (0–127) to its name string such as "C4", "D#5", or "A-1".
use plugin midi::{note_name, note_number}
print(note_name(60))
print(note_name(69))
print(note_number("C4"))
print(note_number("A4"))Convert note name string to MIDI number
Converts a note name string like "C4" or "D#5" back to a MIDI note number.
use plugin midi::{note_number}
let n = note_number("A4")
print(n)It is the inverse of note_name, so the two round-trip cleanly:
use plugin midi::{note_name, note_number}
let name = note_name(72)
print("{name} = {note_number(name)}")Convert MIDI note number to Hz frequency
Calculates the frequency in Hz for a MIDI note number using the formula 440 * 2^((n-69)/12).
use plugin midi::{frequency, note_number}
let hz = frequency(69.0)
print("A4 = {hz} Hz")
let c4 = frequency(note_number("C4"))
print("C4 = {c4} Hz")Convert microseconds-per-beat to BPM
Converts a MIDI tempo value (microseconds per quarter note) to beats per minute.
use plugin midi::{tempo_to_bpm, bpm_to_tempo}
let bpm = tempo_to_bpm(500000.0)
print("{bpm} BPM")
let usec = bpm_to_tempo(120.0)
print("{usec} µs/beat")Convert BPM to microseconds-per-beat
Converts beats per minute to microseconds per quarter note, suitable for use in a MIDI tempo meta event.
use plugin midi::{bpm_to_tempo}
let usec = bpm_to_tempo(140.0)
print(usec)Encode integer as MIDI variable-length quantity
Encodes a non-negative integer as a MIDI variable-length quantity (VLQ), used for delta times in Standard MIDI Files.
use plugin midi::{encode_variable_length}
let vlq = encode_variable_length(0)
let vlq2 = encode_variable_length(300)Build a Standard MIDI File MThd header
Builds a 14-byte Standard MIDI File MThd header chunk. format is 0 (single track), 1 (multi-track synchronous), or 2 (multi-track sequential).
use plugin midi::{smf_header}
let header = smf_header(0, 1, 480)
print("Header bytes: {header}")Build a tempo meta event (FF 51 03)
Builds a MIDI tempo meta event (FF 51 03 tt tt tt), used inside a Standard MIDI File track.
use plugin midi::{tempo_event, bpm_to_tempo}
let usec = bpm_to_tempo(120.0)
let evt = tempo_event(usec)Sort and delta-encode a list of timed events
Takes a table of {tick, message} events, sorts them by tick, and returns a new table with {delta, tick, message} entries where delta is the time since the previous event. Use this to assemble delta-time encoded track data for SMF.
use plugin midi::{build_sequence, note_on, note_off}
let events = [
#{"tick": 0, "message": note_on(0, 60, 100)},
#{"tick": 480, "message": note_off(0, 60, 0)},
#{"tick": 480, "message": note_on(0, 64, 100)},
#{"tick": 960, "message": note_off(0, 64, 0)}
]
let seq = build_sequence(events)
let first = seq[1]
print("delta={first["delta"]} tick={first["tick"]}")