Skip to content

Support N MIDI inputs, each with its own queue - #3

Open
jeudine wants to merge 17 commits into
mainfrom
dev_multi_midi_inputs
Open

Support N MIDI inputs, each with its own queue#3
jeudine wants to merge 17 commits into
mainfrom
dev_multi_midi_inputs

Conversation

@jeudine

@jeudine jeudine commented Jun 16, 2026

Copy link
Copy Markdown
Member

Summary

Generalizes MIDI input from a single optional port to N independent inputs, each with its own message queue and consumer thread, and lets handle_input forward MIDI straight to the output.

  • run() now takes midi_in: Vec<MidiInParam> instead of Option<MidiInParam> (empty vec = standalone/no input).
  • Conductor::handle_input / Context::handle_input gain a 0-based input_id (matching the input's position in the Vec) so messages can be routed per source.
  • At most one input is the clock/transport source: the first one marked slave. Extra slave inputs are treated as message-only, with a warning.
  • One consumer thread per input, each draining its own queue.

Direct forwarding from handle_input

Conductor::handle_input still returns a Vec<Instruction>, and the pause filter now depends on the instruction:

  • Instruction::MidiMessage is forwarded straight to the MIDI output, including while paused.
  • Every other instruction goes through the MidiController (buffered / step-scheduled) and is dropped while paused.

This lets a conductor echo or transform incoming MIDI immediately and even while paused, without the controller's clock-quantized note buffering. An earlier revision of this branch expressed it as a two-channel InputResponse struct, which has since been removed in favour of the single Vec<Instruction>.

Correctness / cleanup

  • Fixed a condvar lost-wakeup in the consumer threads and the slave loop (predicate loop + drain-into-local via mem::take so the real-time midir callback is never blocked while the conductor runs).
  • Introduced a NotifyQueue struct to replace the (Arc<Mutex<..>>, Arc<Condvar>) tuple.
  • Removed the dead pause field from Context.
  • Added MidiMessage::is_transport; added slave-mode multi-input and pause-forwarding tests.
  • DeteTrack::play_step and get_notes_start_at_step no longer panic on a zero-length track. DeteTrack derives Default, so len = 0 was reachable from safe code and from deserialization, and both methods computed step % self.len. They now log a warning and return no instruction.

Documentation

  • Added docs/architecture.svg, a single figure showing what the user implements (Conductor, Tracks) and what the engine does with it: the direct-forward path, the sequencer path, and transport. Embedded in both the mseq and mseq_core READMEs.
  • Fixed the root README, whose rustdoc-style [Conductor] links all rendered as literal text, since the file is not included by src/lib.rs. They now resolve to docs.rs.
  • Moved the usage example to the top of the README, made it play a note, and had it call ctx.start(). The sequencer starts paused, which was the one gotcha that left a first conductor silent and was documented nowhere. Replaced the DeteTrack::default() snippet, which panicked, with a real DeteTrack::new call.
  • Merged the README's Overview and Conductor Trait sections, which said the same thing twice.
  • mseq_core's README now says it has no MIDI I/O or run loop of its own; mseq_tracks gained a track table, an example and its feature flags.

Crate metadata and no_std

  • mseq_tracks is now genuinely no_std when the std feature is off. It gated the file loaders but had no #![no_std] and pulled csv, midly, toml and fs-err unconditionally, so --no-default-features removed most of the API without making the crate usable on a bare-metal target.
  • Dropped midir from mseq_tracks, which never referenced it.
  • Gave each crate its own description. All three shared "Library for developing MIDI Sequencers.", which made them indistinguishable on crates.io.

Breaking changes

  • run() signature: Option<MidiInParam> -> Vec<MidiInParam>.
  • Conductor::handle_input gains an input_id: usize parameter.
  • mseq_tracks dependencies used only behind std are now optional, so --no-default-features builds a smaller crate than before.

Testing

  • cargo fmt --all --check, cargo clippy --workspace (all-features + no-default-features, -D warnings)
  • RUSTDOCFLAGS="-W missing_docs -W rustdoc::all" cargo doc --workspace --no-deps
  • cargo test --workspace --all-features (unit, integration, doctests)
  • cargo build -p mseq_tracks --no-default-features --target thumbv7em-none-eabihf, which could not succeed before this branch

🤖 Generated with Claude Code

jeudine and others added 8 commits June 9, 2026 21:03
Replace the single optional MIDI input with a list of inputs. Each input
gets its own queue, condvar and midir connection, plus a dedicated consumer
thread that drains only that queue.

- run() now takes Vec<MidiInParam> (empty = standalone/no input)
- Conductor::handle_input and Context::handle_input gain an input_id (0-based
  index of the source input)
- Slave mode: the first input marked slave is the clock/transport source;
  the others are message-only inputs
- Per-input port prompt names the input being selected
- input_test now drives multiple independent inputs and asserts per-input
  routing via input-dependent transposition
- Patch mseq_core/mseq_tracks to local paths so the workspace builds against
  the new (breaking) core API during development

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a test asserting that in slave mode with multiple inputs, only the clock
of the slave input (input 0) advances the step: clocks arriving on a non-slave
input are dropped at routing and never reach the conductor or move the step.

Extract the transport/channel classification into MidiMessage::is_transport so
the production input callback and the test share the same routing logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Only one clock/transport source is supported, so log a warning when more than
one input has `slave: true`; the first one is used and the rest become
message-only inputs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Trigger the CI workflow on push to any branch instead of only main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Declare mseq_core and mseq_tracks with both a version and a local path so the
workspace builds against the local sources during development while still
publishing proper version requirements. Removes the patch.crates-io section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Consumer threads: park on the queue's own mutex/condvar (the pair the
  midir callback notifies) with a predicate loop, so messages that arrive
  before parking can't be lost; drain via an O(1) swap so the real-time
  callback isn't blocked while the conductor runs.
- run_slave: drain the transport queue, release it, then apply transport
  under the run lock alone, removing the only place these locks nested.
- Replace the (Arc<Mutex<InputQueue>>, Arc<Condvar>) tuple with a named
  NotifyQueue struct (queue/condvar fields, new()/push() helpers).
- Remove the unused Context::pause field and its dead branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jeudine
jeudine requested a review from feldspath June 16, 2026 21:33
@jeudine jeudine added the enhancement New feature or request label Jun 16, 2026
@jeudine jeudine self-assigned this Jun 16, 2026
jeudine and others added 4 commits June 16, 2026 23:37
Update the README to match the new multi-input API: the multiple-inputs
feature, an "input_id" note on handle_input, a "MIDI Inputs" section, and
the corrected usage example (handle_input gains input_id, midi_in is a Vec).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously process_pre_tick and handle_input called the conductor while
paused but discarded the returned instructions. Always execute them now;
pausing only freezes the step counter (step-driven tracks hold position),
it no longer silences update/handle_input output.

Update the Conductor::update, Conductor::handle_input and Context::pause
docs to describe the new pause semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Conductor::handle_input now returns an InputResponse with two channels:
- instructions: processed by the MIDI controller (buffered/step-scheduled),
  executed only while running and dropped while paused.
- messages: forwarded straight to the MIDI output, bypassing the controller,
  sent always including while paused.

This lets a conductor echo/transform incoming MIDI immediately and even while
paused, without the controller's clock-quantized note buffering.

Also revert update's pause behavior to dropping its instructions while paused
(undoing the previous "keep instructions while paused" change for update), and
update the related docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update third-party dependencies to their latest stable versions
(spin_sleep 1.3.3, serde 1.0.228, fs-err 3.3.1, log 0.4.33,
itertools 0.15, hashbrown 0.17.1, env_logger 0.11.11, rand 0.10.2).

Bump crate versions for publication: mseq 3.0.0, mseq_core 1.0.0,
mseq_tracks 1.0.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@AmdyTran AmdyTran left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Change-Id: I61d41760aaa7461c3cbd3fa96ec52a934dab7f4c
@feldspath
feldspath force-pushed the dev_multi_midi_inputs branch from 531fd6e to d7c5a97 Compare August 12, 2026 12:09
jeudine and others added 4 commits August 13, 2026 22:29
Remove "not intended to be called by the user" notes from private and
pub(crate) functions in MidiController, where visibility already enforces
it. Fix an unescaped backtick in TrackError::Midly that rustdoc warned
about, and drop stray trailing whitespace in MidiMessage::parse's docs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DeteTrack derives Default, so DeteTrack::default() and any deserialized
track can carry len = 0. Both play_step and the public
get_notes_start_at_step then computed step % self.len and panicked.

They now log a warning and return no instruction. The warning fires on
every call rather than once: a flag field would leak into DeteTrack's
Serialize, Deserialize, PartialEq and Eq derives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The std feature gated the file loaders, but the crate had no #![no_std]
and pulled csv, midly, toml and fs-err unconditionally, so
--no-default-features removed most of the API without making the crate
usable on a bare-metal target.

Every dependency used only behind cfg(feature = "std") is now optional
and enabled by that feature, serde drops down to default-features = false
with derive and alloc, and the crate declares no_std when std is off.
cargo build -p mseq_tracks --no-default-features --target
thumbv7em-none-eabihf now succeeds.

Also drop midir, which mseq_tracks never referenced, and give each crate
its own description: all three shared "Library for developing MIDI
Sequencers.", which made them indistinguishable on crates.io.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add docs/architecture.svg, a single figure showing what the user
implements (Conductor, Tracks) and what the engine does with it: the
direct-forward path, the sequencer path and transport. Both the mseq and
mseq_core READMEs embed it.

Fix the root README, whose rustdoc-style links all rendered as literal
[Conductor] text since the file is not included by src/lib.rs. Reference
definitions now point at docs.rs. Move the usage example to the top, make
it play a note, and have it call ctx.start(), the sequencer starting
paused being the one gotcha that made a first conductor silent. Replace
the DeteTrack::default() snippet, which panicked, with a real
DeteTrack::new call. Merge the Overview and Conductor Trait sections,
which said the same thing twice.

Give mseq_core a line saying it has no MIDI I/O or run loop of its own,
and mseq_tracks a track table, an example and its feature flags. Keep the
crate-level docs in sync with each README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants