Skip to content

Latest commit

 

History

233 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MSeq

doc crates.io CI License

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.

Quick Start

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())
}

Architecture

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:

mseq_core: what the user implements and what the engine does with it

Conductor

A Conductor defines how your sequencer behaves:

  • Conductor::init is called once at startup, to set up state and emit initial Instructions (program changes, reset messages, and so on). Call ctx.start() here to leave the initial pause.
  • Conductor::update is called at every clock tick, to advance the sequencer and emit the instructions for that tick.
  • Conductor::handle_input is called when a MidiMessage arrives, with a 0-based input_id telling you which input it came from. While paused, only Instruction::MidiMessage is 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.

Tracks

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.

MIDI Inputs

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 Vec runs the sequencer standalone (no input).
  • At most one input acts as the clock/transport source: the first one with slave set to true. Any other slave inputs are treated as message-only inputs (a warning is logged).
  • With multiple inputs, prefer setting an explicit port on each MidiInParam rather than leaving it as None.

Features

  • 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_id for routing
  • Flexible Conductor trait for defining sequencer logic
  • Easy-to-implement tracks via the Track trait
  • Thread-safe, minimal core designed for real-time responsiveness
  • Step-based deterministic tracks with DeteTrack

Examples

You can find ready-to-run examples in the examples directory. They demonstrate various usage patterns, from simple standalone sequencers to multi-track setups.

About

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.

Resources

Stars

10 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages