midi: bridge the Paula serial port to host MIDI (CoreMIDI/ALSA/WinMM)#105
midi: bridge the Paula serial port to host MIDI (CoreMIDI/ALSA/WinMM)#105hobbo91 wants to merge 3 commits into
Conversation
Add an optional `midi` build feature that bridges Paula's serial port to the host MIDI system, with three native backends -- CoreMIDI (macOS), the ALSA sequencer (Linux), and WinMM (Windows) -- all raw FFI, no wrapper crate. A default build compiles no MIDI code: the whole module is behind #[cfg(feature = "midi")]. The emulator core only sees the existing SerialSink. MidiSerialSink adds the two things every backend shares: the scheduled-send contract (Emulator publishes a SerialTimeAnchor, Paula stamps each byte with the cck it left the wire on, and the sink maps that to a host Instant so each message is delivered at its emitted time rather than when a frame flushes) and a MidiFramer that reassembles the serial byte stream into whole MIDI messages (running status, SysEx, real-time passthrough). Active Sensing is forwarded by default and stripped only under COPPERLINE_MIDI_STRIP_ACTIVE_SENSE. Each backend honours the contract with its own scheduling primitive: a CoreMIDI packet timestamp, an ALSA real-time queue event, and -- since WinMM has no timestamp -- a scheduler thread that fires midiOutShortMsg / midiOutLongMsg when a message comes due. Layout-sensitive FFI structs are pinned with compile-time size/offset asserts (CoreMIDI's packed(4) packet list, snd_seq_event_t, and WinMM's packed MIDIHDR). Selection: --list-midi, --midi-out / --midi-in (imply --serial midi), the [serial] config block, a launcher Serial tab, and a live in-window MIDI In / MIDI Out menu. Off by default the binary carries zero MIDI symbols and the feature adds no crates. No STATE_VERSION change. Verified: clippy -D warnings and fmt clean in both feature states; 1277 tests with --features midi (1269 default), 0 failures; an ignored midi_through_loopback timing test per backend, verified passing on macOS (IAC Driver bus), Linux (snd-seq-dummy "Midi Through") and Windows (loopMIDI), plus real-synth output and live MIDI-in checks on each OS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an optional midi Cargo feature that bridges Paula’s emulated serial port to the host MIDI stack (CoreMIDI / ALSA sequencer / WinMM), preserving guest byte timing by scheduling host delivery against a published serial time anchor. This integrates into configuration, CLI, and UI while keeping default builds MIDI-free.
Changes:
- Introduces
src/midi/*with a sharedMidiSerialSink+ framer and native OS backends (plus stub for unsupported targets). - Extends the serial plumbing (
SerialSinktimestamping +SerialTimeAnchor) and wires Paula/Bus/Emulator to publish the time anchor. - Adds config/CLI/UI selection for serial mode and MIDI endpoints (launcher Serial tab + runtime menu items in MIDI mode).
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/video/window.rs | Adds MIDI-aware runtime menu wiring and device cycling hooks. |
| src/video/ui.rs | Makes the runtime menu dynamic (conditionally includes MIDI items) and updates hit-testing/drawing accordingly. |
| src/video/launcher.rs | Adds a Serial launcher tab (MIDI builds) and config editing for serial/MIDI endpoint selection. |
| src/serial.rs | Adds SerialTimeAnchor and timestamps serial writes so timing-sensitive sinks can schedule output. |
| src/midi/mod.rs | Implements MidiSerialSink, message framing, endpoint cycling, and the backend trait. |
| src/midi/coremidi.rs | macOS CoreMIDI backend via raw FFI with timestamped packet scheduling. |
| src/midi/alsa.rs | Linux ALSA sequencer backend via raw FFI with real-time queue scheduling and input thread. |
| src/midi/winmm.rs | Windows WinMM backend via raw FFI with a scheduler thread to honor send instants. |
| src/midi/stub.rs | Stub backend for unsupported targets (enumerates nothing, refuses to open). |
| src/main.rs | Adds --list-midi, --serial, --midi-in/out parsing and feature-gated help output. |
| src/lib.rs | Exposes the midi module behind #[cfg(feature = "midi")]. |
| src/emulator.rs | Builds the serial sink from config and republishes the serial time anchor after re-anchoring. |
| src/config.rs | Adds [serial] config (mode + endpoints), CLI overrides, parsing, and tests. |
| src/chipset/paula.rs | Threads emit-time color-clock stamping through serial TX and updates serial tests accordingly. |
| src/bus.rs | Publishes the serial time anchor to Paula and exposes the MIDI sink for runtime device switching. |
| src/bus/tests.rs | Updates test serial sink impl for the new write_byte signature. |
| README.md | Documents the optional MIDI feature, selection, and usage examples. |
| docs/internals/peripherals.md | Documents the MIDI serial bridge design and backend model. |
| copperline.example.toml | Adds commented [serial] configuration examples and CLI notes. |
| Cargo.toml | Adds the midi feature flag (off by default). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
LinuxJedi
left a comment
There was a problem hiding this comment.
Excellent feature. Happy to have it compiled in by default. Please do that and address the issues Copilot flagged
Make the MIDI bridge part of the default build (`default = ["midi"]`), as requested on the PR; `--no-default-features` still compiles a MIDI-free build (0 MIDI symbols, no MIDI framework linked). Also address the review comments: - Only query the runtime menu's device labels when the menu is open, rather than allocating two Strings and touching the bus on every frame. - Pre-size the runtime menu's item Vec so appending the MIDI and trailing items never reallocates. - Fix the peripherals internals doc, which still described Windows as falling back to the stub: WinMM is a native backend now, and the stub is for other targets only. Note WinMM's scheduler thread and packed MIDIHDR alongside the CoreMIDI/ALSA layout notes. - Reword the README MIDI section for the built-in-by-default change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Thanks! Compiled in by default now (default = ["midi"], opt out with --no-default-features), and addressed all three Copilot comments — stale Windows/stub doc, per-frame menu-label work, and the menu-items Vec sizing. clippy, fmt and the tests all pass with and without the feature. |
Move the MIDI In / MIDI Out entries above Pixel Aspect (and below Joystick input) so the device switches sit with the other input-related items rather than between Pixel Aspect and Warp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Love the work you are doing with this project!
I actually think Copperline is a much better candidate for this use case, especially for folks who are still using Amiga's for making (mostly Jungle) music with OctaMED and real samplers or wanting to integrate a virtual Amiga into a software DAW. I did something similar with vAmiga a while back.
Summary
Adds an optional
midibuild feature that bridges Paula's serial port to the host's MIDI system, so an Amiga sequencer, tracker, or game can drives real (or virtual) instruments -- or is played from a host MIDI keyboard -- over the emulated serial line ([serial] mode = "midi", or--serial midi). A default build compiles no MIDI code: the wholemidimodule is behind#[cfg(feature = "midi")]. Three native backends, one per OS, all raw FFI with no wrapper crate.Serial timing is treated as seriously as everything else: each byte is delivered at the host instant it left the emulated wire, not whenever a frame's worth of bytes happens to flush.
The bridge (platform-agnostic)
The emulator core only ever sees the existing
SerialSink.MidiSerialSinkimplements it and owns the two things every backend shares:Emulatorpublishes aSerialTimeAnchor(the host instant of emulated colour-clock 0 on the audio/video-paced timeline); Paula stamps each transmitted byte with thecckit finished shifting out; the sink maps that to a hostInstantand hands it tobackend.send(msg, at). The guest's byte timing survives to the wire instead of collapsing to the flush moment.MidiFramerreassembles whole messages -- running status, SysEx accumulation, interleaved real-time bytes passed straight through -- before anything is sent.Active Sensing (
0xFE) is forwarded by default (a real Amiga passes it down the line) and stripped only underCOPPERLINE_MIDI_STRIP_ACTIVE_SENSE=1.COPPERLINE_MIDI_DEBUG=1|2traces byte-flow counts / decoded messages per direction.Backends (raw FFI,
cfg(target_os))Each talks to the platform API directly and honours the contract with that platform's own scheduling primitive:
mach_absolute_time); input arrives on a CoreMIDI callback thread into a lock-free SPSC ring. TheMIDIPacketListmirror is#[repr(C, packed(4))]-- CoreMIDI packs to 4, and natural alignment mis-locates every received packet.snd_seq_event_tschedule fields are written by hand (thesnd_seq_ev_*helpers are header-only inlines) against a mirror pinned with compile-timeoffset_of!asserts.midiOut*carries no timestamp, so a scheduler thread holds a priority queue keyed by hostInstantand callsmidiOutShortMsg/midiOutLongMsgonly when a message comes due (timeBeginPeriod(1)for ~1 ms wait granularity); input follows the CoreMIDI callback model. Every import and the callback areextern "system", andMIDIHDRis#[repr(C, packed(4))](112 bytes on 64-bit,lpNextat offset 28 -- verified against<mmeapi.h>, not just self-consistent asserts).Other targets keep the existing stub
Selection and UI
--list-midiprints host endpoints;--midi-out NAME/--midi-in NAME(case-insensitive substring, either optional) imply--serial midi; the same three keys live under[serial]. Devices are also picked in a new launcher Serial tab (present only in amidibuild) and swapped live from the in-window MIDI In / MIDI Out menu (shown only when serial is in MIDI mode).--helplists the flags in amidibuild.Built in by default (opt-out is zero-cost)
default = ["midi"]).cargo build --no-default-featurescompiles a MIDI-free binary: 0 MIDI symbols (nmclean), no MIDI framework linked.midi = []; the input ring'sringbufwas already a dependency of the audio path).STATE_VERSIONchange -- all MIDI state is transient host-side handles; nothing enters a save state.Packaging
Nothing here touches the Flatpak/AppImage/Homebrew plumbing. Two decisions are yours if you want to enable the feature in a release build: the ALSA sequencer needs
/dev/snd, which the current Flatpakfinish-args(--socket=pulseaudio,--device=dri/input) don't grant -- it would want--device=all; and no CI job builds--features midiyet. Happy to follow up with a Flatpak-permissions patch or a feature-build CI matrix if you'd like them.Verification / Real world testing
cargo build/cargo build --features midi,cargo clippy --all-targets -- -D warnings(and with--features midi),cargo fmt --check-- all clean. Tests: 1269 default, 1277 with--features midi, 0 failures; the MIDI-only tests coverMidiFramerreassembly (running status, SysEx, interleaved real-time, stray-byte drop) and[serial]mode parsing.midi_through_loopbackintegration test per backend round-trips a note scheduled 300 ms out through a loopback port and asserts arrival >= 250 ms -- the schedule was honoured, not collapsed to "now".cargo test --features midi -- --ignored. Verified passing: macOS (IAC Driver bus), Linux (snd-seq-dummy "Midi Through"), Windows (loopMIDI, x64).mt32.drvdriver.docs/internals/peripherals.md; the Paula-serial row of the emulation table updated (it described stdout only).Screenshots