Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: [ main ]
branches: [ '**' ]

env:
CARGO_TERM_COLOR: always
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
/Cargo.lock
/.vscode
22 changes: 11 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,29 @@ members = ["mseq_core", "mseq_tracks"]

[package]
name = "mseq"
version = "2.2.4"
version = "3.0.0"
edition = "2024"
license = "LGPL-2.1"
readme = "README.md"
repository = "https://github.com/MF-Room/mseq"
authors = ["Julien Eudine <julien@eudine.fr>", "Marius Debussche <marius.debussche@gmail.com>"]
description = "Library for developing MIDI Sequencers."
description = "Framework for building MIDI sequencers, with clock and transport synchronization."
keywords = ["midi", "music", "sequencer"]
categories = ["multimedia"]

[dependencies]
mseq_core = "0.1.6"
mseq_tracks = "0.2.5"
spin_sleep = "1.2.1"
mseq_core = { version = "1.0.0", path = "mseq_core" }
mseq_tracks = { version = "1.0.0", path = "mseq_tracks" }
spin_sleep = "1.3.3"
thiserror = "2.0.18"
midir = "0.11.0"
promptly = "0.3.1"
serde = {version = "1.0.208", features = ["derive"] }
serde = {version = "1.0.228", features = ["derive"] }
csv = {version = "1.4.0"}
fs-err = "3.3.0"
log = "0.4.29"
itertools = "0.14.0"
fs-err = "3.3.1"
log = "0.4.33"
itertools = "0.15.0"

[dev-dependencies]
env_logger = "0.11.9"
rand = "0.10.1"
env_logger = "0.11.11"
rand = "0.10.2"
144 changes: 96 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,75 +7,123 @@

`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.

## Features
## Quick Start

Add the crate with `cargo add mseq`, then implement a [`Conductor`] and hand it to [`run`]:

```rust
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

- Real-time MIDI clock generation and synchronization
- Master/slave transport control with Start/Stop/Continue handling
- 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`]
You implement a [`Conductor`], and optionally one or more [`Track`]s. 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:

## Overview
<p align="center">
<img src="https://raw.githubusercontent.com/MF-Room/mseq/main/docs/architecture.svg"
alt="mseq_core: what the user implements and what the engine does with it" width="100%">
</p>

The sequencer is driven by a user-provided [`Conductor`] implementation, which defines how the sequencer initializes, progresses at each clock tick, and reacts to external MIDI messages.
## Conductor

- **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.
A [`Conductor`] defines how your sequencer behaves:

## Conductor Trait
- [`Conductor::init`] is called once at startup, to set up state and emit initial [`Instruction`]s (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.

A `Conductor` defines how your sequencer behaves:
How the sequencer is clocked depends on the inputs you give to [`run`]:

- [`Conductor::init`] → called once at startup to initialize state and produce initial [`Instruction`]s (e.g., send program changes or reset messages).
- [`Conductor::update`] → called at every clock tick to advance the sequencer state and emit the instructions for that tick (e.g., note on/off events).
- [`Conductor::handle_input`] → called when a new [`MidiMessage`] arrives, allowing the conductor to react to external inputs in real time.
- **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 provides a simple interface for describing step-based musical patterns. Each track produces a set of [`Instruction`]s at a given step.
Sequencers can also be built around the [`Track`] trait, which describes step-based musical patterns. Each track produces a set of [`Instruction`]s 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:

```rust
use mseq::{Track, DeteTrack, Instruction};

let mut track = DeteTrack::default();
// On each tick, play the instructions for the current step
let instructions: Vec<Instruction> = track.play_step(step);
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",
);
```
This makes it easy to implement custom track types, from simple step sequencers to more complex algorithmic patterns.

## Usage
The entry point of the crate is the [`run`] function:
Implementing [`Track`] yourself is just as easy, from simple step sequencers to more complex algorithmic patterns.

```rust
use mseq::{run, Conductor, Context, Instruction, MidiMessage};
## MIDI Inputs

struct MyConductor;
[`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`.

impl Conductor for MyConductor {
fn init(&mut self, _ctx: &mut Context) -> Vec<Instruction> {
vec![]
}

fn update(&mut self, _ctx: &mut Context) -> Vec<Instruction> {
vec![]
}
- 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`.

fn handle_input(&mut self, input: MidiMessage, _ctx: &Context) -> Vec<Instruction> {
vec![]
}
}
## Features

fn main() -> Result<(), mseq::MSeqError> {
let conductor = MyConductor;
let out_port = None;
let midi_in = None;
run(conductor, out_port, midi_in)
}
```
- 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](https://github.com/MF-Room/mseq/tree/main/examples) directory. They demonstrate various usage patterns, from simple standalone sequencers to multi-track setups.

[`Conductor`]: https://docs.rs/mseq/latest/mseq/trait.Conductor.html
[`Conductor::init`]: https://docs.rs/mseq/latest/mseq/trait.Conductor.html#tymethod.init
[`Conductor::update`]: https://docs.rs/mseq/latest/mseq/trait.Conductor.html#tymethod.update
[`Conductor::handle_input`]: https://docs.rs/mseq/latest/mseq/trait.Conductor.html#method.handle_input
[`Track`]: https://docs.rs/mseq/latest/mseq/trait.Track.html
[`DeteTrack`]: https://docs.rs/mseq/latest/mseq/struct.DeteTrack.html
[`Instruction`]: https://docs.rs/mseq/latest/mseq/enum.Instruction.html
[`MidiMessage`]: https://docs.rs/mseq/latest/mseq/enum.MidiMessage.html
[`run`]: https://docs.rs/mseq/latest/mseq/fn.run.html
90 changes: 90 additions & 0 deletions docs/architecture.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/acid_arp_track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ fn main() {
MyConductor { acid, arp },
// The midi port will be selected at runtime by the user
None,
None,
Vec::new(),
) {
println!("An error occured: {:?}", e);
}
Expand Down
2 changes: 1 addition & 1 deletion examples/clock_div_track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ fn main() {
MyConductor { clk_div },
// The midi port will be selected at runtime by the user
None,
None,
Vec::new(),
) {
println!("An error occured: {:?}", e);
}
Expand Down
2 changes: 1 addition & 1 deletion examples/impl_track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ fn main() {
},
// The midi port will be selected at runtime by the user
None,
None,
Vec::new(),
) {
println!("An error occured: {:?}", e);
}
Expand Down
2 changes: 1 addition & 1 deletion examples/midi_track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ fn main() {
MyConductor { track },
// The midi port will be selected at runtime by the user
None,
None,
Vec::new(),
) {
println!("An error occured: {:?}", e);
}
Expand Down
2 changes: 1 addition & 1 deletion examples/slave_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ fn main() {
MyConductor {},
// The midi port will be selected at runtime by the user
None,
Some(midi_in_param),
vec![midi_in_param],
) {
println!("An error occured: {:?}", e);
}
Expand Down
19 changes: 14 additions & 5 deletions mseq_core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
[package]
name = "mseq_core"
version = "0.1.6"
version = "1.0.0"
edition = "2024"
license = "LGPL-2.1"
readme = "README.md"
repository = "https://github.com/MF-Room/mseq/tree/main/mseq_core"
authors = ["Julien Eudine <julien@eudine.fr>", "Marius Debussche <marius.debussche@gmail.com>"]
description = "Library for developing MIDI Sequencers."
description = "Portable no_std core of the mseq MIDI sequencer framework."
keywords = ["midi", "music", "sequencer"]
categories = ["multimedia"]

[features]
# Internal-only feature that widens the visibility of a few crate internals so
# that integration tests can reach them.
# Not part of the public API and should not be enabled by consumers.
test-internals = []

[dependencies]
serde = {version = "1.0.208", default-features = false, features = ["derive", "alloc"] }
serde = {version = "1.0.228", default-features = false, features = ["derive", "alloc"] }
thiserror = { version="2.0.18", default-features = false }
hashbrown = "0.17.0"
log = "0.4.29"
hashbrown = "0.17.1"
log = "0.4.33"

[dev-dependencies]
mseq_core = { path = ".", features = ["test-internals"] }
Loading
Loading