Skip to content
Open
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
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ keywords = ["midi", "music", "sequencer"]
categories = ["multimedia"]

[dependencies]
mseq_core = "0.1.6"
mseq_tracks = "0.2.5"
mseq_core = { version = "0.1.6", path = "mseq_core" }
mseq_tracks = { version = "0.2.5", path = "mseq_tracks" }
spin_sleep = "1.2.1"
thiserror = "2.0.18"
midir = "0.11.0"
Expand Down
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

- 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
Expand All @@ -30,7 +31,15 @@ A `Conductor` defines how your sequencer behaves:

- [`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.
- [`Conductor::handle_input`] → called when a new [`MidiMessage`] arrives, allowing the conductor to react to external inputs in real time. The `input_id` argument (0-based, matching the input's position in the list passed to [`run`]) identifies which input the message came from. It returns an [`InputResponse`] with two channels: `instructions` are processed by the controller (only while running), while `messages` are forwarded directly to the MIDI output, bypassing the controller, and are sent even while paused.

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

## Tracks

Expand All @@ -51,7 +60,7 @@ This makes it easy to implement custom track types, from simple step sequencers
The entry point of the crate is the [`run`] function:

```rust
use mseq::{run, Conductor, Context, Instruction, MidiMessage};
use mseq::{run, Conductor, Context, InputResponse, Instruction, MidiMessage};

struct MyConductor;

Expand All @@ -64,15 +73,17 @@ impl Conductor for MyConductor {
vec![]
}

fn handle_input(&mut self, input: MidiMessage, _ctx: &Context) -> Vec<Instruction> {
vec![]
fn handle_input(&mut self, _input_id: usize, _input: MidiMessage, _ctx: &Context) -> InputResponse {
// `instructions` go through the controller (only while running);
// `messages` are forwarded directly (always, even while paused).
InputResponse::default()
}
}

fn main() -> Result<(), mseq::MSeqError> {
let conductor = MyConductor;
let out_port = None;
let midi_in = None;
let out_port = None; // Ask the user for the output port
let midi_in = Vec::new(); // Run standalone (no MIDI input)
run(conductor, out_port, midi_in)
}
```
Expand Down
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
3 changes: 2 additions & 1 deletion mseq_core/src/bpm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ impl Bpm {
}

fn compute_period_us(bpm: u8) -> u64 {
60 * 1000000 / 24 / bpm as u64
// Guard against a 0 bpm, which would divide by zero.
60 * 1000000 / 24 / bpm.max(1) as u64
}

pub(crate) fn get_period_us(&self) -> u64 {
Expand Down
49 changes: 44 additions & 5 deletions mseq_core/src/conductor.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
use alloc::vec;
use alloc::vec::Vec;

use crate::{Context, MidiMessage, midi_controller::Instruction};

/// Output of [`Conductor::handle_input`].
///
/// Carries two independent channels:
/// - `instructions` are handed to the MIDI controller, where notes are buffered and
/// step-scheduled. They are executed only while the sequencer is running and are
/// dropped while paused.
/// - `messages` are forwarded directly to the MIDI output, bypassing the controller's
/// note buffering. They are always sent, including while paused.
#[derive(Default)]
pub struct InputResponse {
/// Instructions processed by the MIDI controller (buffered / step-scheduled).
/// Executed only while the sequencer is running; dropped while paused.
pub instructions: Vec<Instruction>,
/// MIDI messages forwarded directly to the output, bypassing the controller.
/// Always sent, including while paused.
pub messages: Vec<MidiMessage>,
}

/// Entry point for user-defined sequencer behavior.
///
/// The `Conductor` trait must be implemented by the user to define how their sequencer
Expand All @@ -26,6 +43,10 @@ pub trait Conductor {
/// This method is responsible for progressing the sequencer and producing
/// the set of instructions that should be executed at the current tick (e.g., sending MIDI events).
///
/// `update` is called on every tick, but while paused (via [`Context::pause`])
/// the returned instructions are dropped rather than sent to the MIDI output.
/// Use [`Context::is_paused`] if you want to alter behavior while paused.
///
/// # Returns
///
/// A `Vec<Instruction>` containing the actions to be passed to the MIDI controller
Expand All @@ -37,8 +58,21 @@ pub trait Conductor {
/// This method is called whenever a new [`MidiMessage`] is received.
/// It allows the conductor to react to external inputs by updating internal state or triggering events.
///
/// The returned `Vec<Instruction>` is passed directly to the MIDI controller or output backend,
/// allowing the conductor to immediately produce output in response to the input.
/// The returned [`InputResponse`] carries two channels:
///
/// - `instructions` are passed to the MIDI controller (buffered / step-scheduled).
/// They are executed only while the sequencer is running and are dropped while paused.
/// - `messages` are forwarded directly to the MIDI output, bypassing the controller.
/// They are always sent, including while paused.
///
/// Use [`Context::is_paused`] if you want to alter behavior while paused.
///
/// # Parameters
///
/// - `input_id`: 0-based index identifying which MIDI input produced the message. It matches the
/// position of the corresponding input in the list of inputs passed to the runtime. When a single
/// input is used, this is always `0`.
/// - `input`: The received [`MidiMessage`].
///
/// # Intercepted Messages
///
Expand All @@ -49,7 +83,12 @@ pub trait Conductor {
/// # Returns
///
/// A `Vec<Instruction>` to be sent to the MIDI output immediately.
fn handle_input(&mut self, _input: MidiMessage, _context: &Context) -> Vec<Instruction> {
vec![]
fn handle_input(
&mut self,
_input_id: usize,
_input: MidiMessage,
_context: &Context,
) -> InputResponse {
InputResponse::default()
}
}
56 changes: 37 additions & 19 deletions mseq_core/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::Conductor;
use crate::InputResponse;
use crate::Instruction;
use crate::MidiController;
use crate::MidiMessage;
Expand All @@ -24,7 +25,6 @@ pub struct Context {
step: u32,
running: bool,
on_pause: bool,
pause: bool,
sys_instructions: Vec<Instruction>,
}

Expand All @@ -40,7 +40,6 @@ impl Default for Context {
step: 0,
running: true,
on_pause: true,
pause: false,
sys_instructions: vec![],
}
}
Expand Down Expand Up @@ -69,9 +68,14 @@ impl Context {
}

/// Pauses the sequencer and send a MIDI stop message.
///
/// While paused, the step counter stops advancing, so step-driven tracks hold
/// their position. [`Conductor::update`] is still called every tick but its
/// returned instructions are dropped, and the instructions returned by
/// [`Conductor::handle_input`] are dropped too. The direct messages returned by
/// [`Conductor::handle_input`] are still forwarded.
pub fn pause(&mut self) {
self.on_pause = true;
self.pause = true;
self.sys_instructions.push(Instruction::StopAllNotes);
self.sys_instructions.push(Instruction::Stop);
}
Expand Down Expand Up @@ -120,9 +124,7 @@ impl Context {
conductor: &mut impl Conductor,
controller: &mut MidiController<impl MidiOut>,
) {
core::mem::take(&mut self.sys_instructions)
.into_iter()
.for_each(|instruction| controller.execute(instruction));
self.flush_sys_instructions(controller);

if self.on_pause {
conductor.update(self);
Expand All @@ -131,7 +133,18 @@ impl Context {
.update(self)
.into_iter()
.for_each(|instruction| controller.execute(instruction));
};
}
}

/// Immediately sends the pending system instructions (Start / Stop / Continue /
/// StopAllNotes) queued by [`start`](Self::start), [`pause`](Self::pause) and
/// [`resume`](Self::resume). The slave loop calls this so transport changes take
/// effect right away instead of waiting for the next external clock tick.
/// This function is not intended to be called directly by users.
pub fn flush_sys_instructions(&mut self, controller: &mut MidiController<impl MidiOut>) {
core::mem::take(&mut self.sys_instructions)
.into_iter()
.for_each(|instruction| controller.execute(instruction));
}

/// MIDI logic called after the clock tick.
Expand All @@ -142,8 +155,6 @@ impl Context {
if !self.on_pause {
self.step += 1;
controller.update(self.step);
} else if self.pause {
self.pause = false;
}
}

Expand All @@ -165,20 +176,27 @@ impl Context {
/// `handle_input` is used internally to enable code reuse across platforms and unify MIDI input processing.
pub fn handle_input(
&mut self,
input_id: usize,
conductor: &mut impl Conductor,
controller: &mut MidiController<impl MidiOut>,
input_queue: &mut InputQueue,
) {
if self.is_paused() {
input_queue
.drain(..)
.flat_map(|message| conductor.handle_input(message, self))
.for_each(drop);
} else {
input_queue
.drain(..)
.flat_map(|message| conductor.handle_input(message, self))
.for_each(|instruction| controller.execute(instruction));
let paused = self.is_paused();
for message in input_queue.drain(..) {
let InputResponse {
instructions,
messages,
} = conductor.handle_input(input_id, message, self);
// Messages are forwarded directly, even while paused.
for m in messages {
controller.send_message(m);
}
// Instructions go through the controller only while running.
if !paused {
for instruction in instructions {
controller.execute(instruction);
}
}
}
}
}
2 changes: 1 addition & 1 deletion mseq_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ mod midi_out;
mod note;
mod track;

pub use conductor::Conductor;
pub use conductor::{Conductor, InputResponse};
pub use context::*;
pub use midi::*;
pub use midi_controller::*;
Expand Down
14 changes: 14 additions & 0 deletions mseq_core/src/midi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,20 @@ pub enum MidiMessage {
}

impl MidiMessage {
/// Returns `true` for transport/system messages
/// ([`MidiMessage::Clock`], [`MidiMessage::Start`], [`MidiMessage::Continue`],
/// [`MidiMessage::Stop`]), which drive synchronization in slave mode, and `false`
/// for channel messages (note, CC, PC, pitch bend).
///
/// In slave mode these messages are intercepted from the clock source input and
/// are not forwarded to [`crate::Conductor::handle_input`].
pub fn is_transport(&self) -> bool {
matches!(
self,
MidiMessage::Clock | MidiMessage::Start | MidiMessage::Continue | MidiMessage::Stop
)
}

/// Parses a byte slice into a `MidiMessage` struct.
///
/// This function is not intended to be called directly by end users.
Expand Down
6 changes: 5 additions & 1 deletion mseq_core/src/midi_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,11 @@ impl<T: MidiOut> MidiController<T> {
}
}

fn send_message(&mut self, message: MidiMessage) {
/// Forwards a [`MidiMessage`] straight to the MIDI output, bypassing the
/// controller's note buffering and step scheduling.
///
/// This function is not intended to be called directly by the user.
pub(crate) fn send_message(&mut self, message: MidiMessage) {
if let Err(e) = self.midi_out.send_message(message) {
error!("MIDI: {e}");
}
Expand Down
Loading
Loading