mseq is a lightweight MIDI sequencer framework written in Rust. It provides a flexible core for building sequencers that can run in standalone, master, or slave mode, with synchronization over standard MIDI clock and transport messages.
Add the crate with cargo add mseq, then implement a Conductor and hand it to run:
use mseq::{run, Conductor, Context, Instruction, MidiNote, Note};
struct MyConductor;
impl Conductor for MyConductor {
fn init(&mut self, ctx: &mut Context) -> Vec<Instruction> {
ctx.set_bpm(120);
// The sequencer starts paused: nothing plays until you call start().
ctx.start();
vec![]
}
fn update(&mut self, ctx: &mut Context) -> Vec<Instruction> {
// update() runs on every MIDI clock pulse, so there are 24 steps per quarter note.
if ctx.get_step() % 24 == 0 {
return vec![Instruction::PlayNote {
midi_note: MidiNote::new(Note::C, 4, 100),
len: 12,
channel_id: 1,
}];
}
vec![]
}
}
fn main() -> Result<(), mseq::MSeqError> {
// `None` asks the user to pick an output port, the empty Vec means no MIDI input.
run(MyConductor, None, Vec::new())
}You implement a Conductor, and optionally one or more Tracks. The engine calls init once at
startup, update on every MIDI clock pulse, and handle_input whenever a MIDI message comes in.
Every call receives a Context and returns a Vec<Instruction>: control changes and raw messages
are forwarded straight to the output, while notes are played on the step grid with their note-offs
scheduled for you. Transport (start, pause, resume, quit) is driven through the Context.
This is the whole surface you deal with:
A Conductor defines how your sequencer behaves:
Conductor::initis called once at startup, to set up state and emit initialInstructions (program changes, reset messages, and so on). Callctx.start()here to leave the initial pause.Conductor::updateis called at every clock tick, to advance the sequencer and emit the instructions for that tick.Conductor::handle_inputis called when aMidiMessagearrives, with a 0-basedinput_idtelling you which input it came from. While paused, onlyInstruction::MidiMessageis forwarded to the output, and every other instruction is dropped.
How the sequencer is clocked depends on the inputs you give to run:
- No input → runs standalone with its internal clock and transport, generating MIDI clock and transport messages but ignoring external MIDI input.
- Master mode → runs with its internal clock while also processing incoming MIDI events (except for external clock/transport).
- Slave mode → synchronizes playback to an external MIDI clock and responds to Start/Stop/Continue messages, dynamically adjusting BPM to match the clock source.
Sequencers can also be built around the Track trait, which describes step-based musical patterns. Each track produces a set of Instructions at a given step, so a track is usually played by calling play_step(ctx.get_step()) from update and returning the result.
The provided DeteTrack implements a deterministic looping track:
use mseq::{DeteTrack, MidiNote, Note};
// Two notes over 24 steps (one quarter note), looping, on MIDI channel 1.
let track = DeteTrack::new(
24,
vec![
// (note, start step, length in steps)
(MidiNote::new(Note::A, 4, 89), 0, 12),
(MidiNote::new(Note::C, 5, 89), 12, 12),
],
Note::A, // Root note, used as the reference for transposition
1,
"my_track",
);Implementing Track yourself is just as easy, from simple step sequencers to more complex algorithmic patterns.
run accepts a Vec<MidiInParam>, opening one MIDI input per entry. Each input gets its own queue and is identified by its 0-based position in the list, which is forwarded to Conductor::handle_input as input_id.
- An empty
Vecruns the sequencer standalone (no input). - At most one input acts as the clock/transport source: the first one with
slaveset totrue. Any otherslaveinputs are treated as message-only inputs (a warning is logged). - With multiple inputs, prefer setting an explicit
porton eachMidiInParamrather than leaving it asNone.
- Real-time MIDI clock generation and synchronization
- Master/slave transport control with Start/Stop/Continue handling
- Multiple MIDI inputs, each with its own queue and an
input_idfor routing - Flexible
Conductortrait for defining sequencer logic - Easy-to-implement tracks via the
Tracktrait - Thread-safe, minimal core designed for real-time responsiveness
- Step-based deterministic tracks with
DeteTrack
You can find ready-to-run examples in the examples directory. They demonstrate various usage patterns, from simple standalone sequencers to multi-track setups.