Control other devices/pedals via MIDI#113
Open
sastraxi wants to merge 56 commits into
Open
Conversation
382cb91 to
aa21b4b
Compare
sastraxi
commented
Apr 19, 2026
sastraxi
commented
Apr 19, 2026
Introduces ExternalMidiManager and ExternalMidiOut for sending MIDI messages to external devices (e.g. Source Audio C4, HX Stomp) on pedalboard load. Config is per-device with glob-based port auto-detection and per-pedalboard message override via config.yml. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
e557a1e to
8c68f3c
Compare
Any controller (footswitch, analog, encoder) can now direct its MIDI output to an external hardware device instead of the virtual port by adding a `midi_port:` field in hardware config. The port name must match one defined in the `external_midi:` section. Changes: - controller.py: add RoutingDestination/RoutingInfo dataclasses, AnalogDisplayInfo/FootswitchDisplayInfo TypedDicts, get_routing_info() and get_display_info() on Controller base class - analogmidicontrol.py: inherit Controller, add _clamp_endpoints(), _send_value(), send_current_value(), get_display_info(); refactor refresh() and initialize() to use these helpers Note: endpoint clamping logic duplicates fix/analog-endpoint-clamping which can be dropped once this merges. - hardware.py: add ExternalMidiOut wrapping in create_footswitches/ analog_controls/encoders; add __init_encoders_and_analog() for per-pedalboard routing updates; add sync_analog_controls() (respects per-control autosync flag); add __validate_midi_port(), _create_external_parameter() - pistomptre.py: pass midiout through add_encoder() signature - modhandler.py + mod.py: call sync_analog_controls() after reinit(); bind_current_pedalboard() now clears stale bindings, skips external controllers for plugin binding, and adds external controllers to display with synthetic parameters; parameter_value_commit() guards against external and audio parameters (modhandler only) - config template: document midi_port option for all control types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts: # pistomp/analogmidicontrol.py
# Conflicts: # modalapi/modhandler.py # pistomp/hardware.py
# Conflicts: # modalapi/modhandler.py # pistomp/analogmidicontrol.py # pistomp/hardware.py
# Conflicts: # pistomp/hardware.py
# Conflicts: # pistomp/hardware.py
parameter_value_commit uses bare EXTERNAL_INSTANCE_ID but only the ExternalMidi namespace alias is imported. Add the direct import so tests calling parameter_midi_change don't NameError. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# Conflicts: # modalapi/mod.py # modalapi/modhandler.py # pistomp/hardware.py
# Conflicts: # pistomp/hardware.py
5a4858e to
6d1d5d2
Compare
…evel - C2: _init_port no longer dels a cache key set only on success, so an open_port failure returns None instead of raising KeyError. - 4C: external_midi.close() now runs once in cleanup(); removed the duplicate call in both handlers' __del__. - 4E: a midi_port config typo logs at warning, not error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a class-level Controller.type default so the volume guard resolves on every controller (Footswitch -> None) without hasattr/getattr. mod.py now skips the volume control when clearing bindings, matching modhandler; drops the stale "align these two" TODO. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
C1: external_midi is None during create_*, so midi_port wiring there always fell back to virtual; __apply_midi_routing also had no footswitch branch and ran only for pedalboard cfg. Pull routing out of create_* and do it in one __apply_midi_routing pass (encoders, analog, footswitches) for both default and pedalboard cfg, via a single __resolve_midiout helper. reinit now routes the default cfg too. C3: open external ports eagerly at routing time, and back off failed ports so a disconnected device doesn't re-enumerate on every 10ms poll tick. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
External-routed controls aren't bound to any plugin parameter, so the plugin loop skipped them and they were invisible on v1. Port modhandler's external block to mod.py: bind a synthetic parameter and add an 'External' display entry. Extend the v1 mono draw_analog_assignments to render External entries as port:cc (it previously keyed only on type). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
S1 regression net: assert all shipped templates validate and the external_midi/midi_port surface is accepted (rejects non-string midi_port). Remove the never-implemented encoder shortpress_config feature from the guide; encoder buttons are UI-nav/longpress only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove C1-3 / 4A-G / 4D markers from code and test comments; history belongs in source control, not the source. Wording kept, markers dropped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10d8d49 to
70be97c
Compare
sastraxi
commented
Jun 8, 2026
sastraxi
commented
Jun 8, 2026
sastraxi
commented
Jun 8, 2026
A control is a source; it shouldn't know its destination. Routing detection piggybacked on the ExternalMidiOut wrapper the control held for sending (isinstance on self.midiout), fusing "where do I send" with "how do I send" and making the wrapper undeletable. Split them: Hardware owns external_routing (control -> RoutingInfo), the source of truth for the internal-vs-external split, rebuilt each reinit in __route_section. is_external/external_port_name read it. Controller drops get_routing_info; get_display_info returns own-presentation only. ControllerManager.bind queries the hardware registry and builds the External display entry from it. RoutingInfo/RoutingDestination survive as the registry's value vocabulary (the durable internal/external axis, future home of a v4 routing table). ExternalMidiOut is now purely a sender. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # tests/integration/conftest.py
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.
Any hardware control can now send its MIDI to an external device port (Source Audio C4, HX Stomp, etc.) instead of the virtual MIDI Through port. Also introduces a pedalboard-load message sending mechanism, allowing pedalboards to ensure external devices are in known state.
Includes #169
What's new
midi_port: <MIDI device name>to any footswitch, encoder, or analog/expression control, and its MIDI is sent there instead of to mod-ui, falling back if the device is unavailable.Testing
I've used this for a few months, both via init message and scrolling through presets on my C4 with Tweak1. This version has been simplified, decomposed, and tested more thoroughly than that one.
There are MacOS-only loopback tests that open an actual virtual MIDI port and assert bytes travel the full path for every routable control: footswitch press, tweak-encoder rotation, expression movement, plus the pedalboard-load messages and backoff.
Bugs discovered (mod-host)
Connecting/disconnecting an rtmidi output can trigger a crash in mod-host while it attempts to clean up when changing pedalboards. It's relatively rare and I only managed to trigger it by having a pedalboard with 54 (!) dead MIDI port references in the TTL. This will be worked on separately (see mod-audio/mod-host#96)