Support N MIDI inputs, each with its own queue - #3
Open
jeudine wants to merge 17 commits into
Open
Conversation
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>
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>
Change-Id: I61d41760aaa7461c3cbd3fa96ec52a934dab7f4c
feldspath
force-pushed
the
dev_multi_midi_inputs
branch
from
August 12, 2026 12:09
531fd6e to
d7c5a97
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_inputforward MIDI straight to the output.run()now takesmidi_in: Vec<MidiInParam>instead ofOption<MidiInParam>(empty vec = standalone/no input).Conductor::handle_input/Context::handle_inputgain a 0-basedinput_id(matching the input's position in theVec) so messages can be routed per source.slave. Extraslaveinputs are treated as message-only, with a warning.Direct forwarding from
handle_inputConductor::handle_inputstill returns aVec<Instruction>, and the pause filter now depends on the instruction:Instruction::MidiMessageis forwarded straight to the MIDI output, including while paused.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
InputResponsestruct, which has since been removed in favour of the singleVec<Instruction>.Correctness / cleanup
mem::takeso the real-time midir callback is never blocked while the conductor runs).NotifyQueuestruct to replace the(Arc<Mutex<..>>, Arc<Condvar>)tuple.pausefield fromContext.MidiMessage::is_transport; added slave-mode multi-input and pause-forwarding tests.DeteTrack::play_stepandget_notes_start_at_stepno longer panic on a zero-length track.DeteTrackderivesDefault, solen = 0was reachable from safe code and from deserialization, and both methods computedstep % self.len. They now log a warning and return no instruction.Documentation
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 themseqandmseq_coreREADMEs.[Conductor]links all rendered as literal text, since the file is not included bysrc/lib.rs. They now resolve to docs.rs.ctx.start(). The sequencer starts paused, which was the one gotcha that left a first conductor silent and was documented nowhere. Replaced theDeteTrack::default()snippet, which panicked, with a realDeteTrack::newcall.OverviewandConductor Traitsections, 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_tracksgained a track table, an example and its feature flags.Crate metadata and
no_stdmseq_tracksis now genuinelyno_stdwhen thestdfeature is off. It gated the file loaders but had no#![no_std]and pulledcsv,midly,tomlandfs-errunconditionally, so--no-default-featuresremoved most of the API without making the crate usable on a bare-metal target.midirfrommseq_tracks, which never referenced it.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_inputgains aninput_id: usizeparameter.mseq_tracksdependencies used only behindstdare now optional, so--no-default-featuresbuilds 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-depscargo 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