Skip to content

Control other devices/pedals via MIDI#113

Open
sastraxi wants to merge 56 commits into
TreeFallSound:pistomp-v3from
sastraxi:feat/external-midi
Open

Control other devices/pedals via MIDI#113
sastraxi wants to merge 56 commits into
TreeFallSound:pistomp-v3from
sastraxi:feat/external-midi

Conversation

@sastraxi

@sastraxi sastraxi commented Jan 7, 2026

Copy link
Copy Markdown
Collaborator

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

  1. The main feaure is per-control routing. Add 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.
  2. An external MIDI manager class provides port enumeration/auto-detect, eager open at routing time, send-with-fallback wrapper, and a pedalboard-load message burst (send_messages_for_pedalboard) for syncing external gear on bundle change.
  3. This branch introduces a controller manager class, shared by mod/modhandler, as well as extracting out Current into its own (shared) module.
  4. Finally, external controllers show on the LCD, just like they would if they were assigned to a parameter in MOD. Externally-routed controls get a synthetic parameter to do so and work in all hardware versions.
  5. Config files, parser, and hardware have been updated to pass-through configuration

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)

When ExternalMidiManager._init_port() opens a direct rtmidi port to the C4, it creates
a new ALSA sequencer client (RtMidiOut Client, client 132). JACK sees this as a new
JackPortIsTerminal|JackPortIsPhysical MIDI port and fires mod-host's async
PortRegistration callback, which enqueues a POSTPONED_JACK_MIDI_CONNECT event.

If the user switches pedalboards before that event is drained, mod-host's main thread is
simultaneously running effects_remove()lilv_instance_free()dlclose(). The
async connect event and the teardown race on the ALSA/JACK heap, causing corruption.
Subsequent dlclose() calls then SIGSEGV on the corrupted state.

Sonnet 4.6

@sastraxi sastraxi force-pushed the feat/external-midi branch from 382cb91 to aa21b4b Compare January 7, 2026 06:18
Comment thread modalapi/modhandler.py Outdated
Comment thread typings/rtmidi/_rtmidi.pyi
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>
@sastraxi sastraxi force-pushed the feat/external-midi branch from e557a1e to 8c68f3c Compare April 19, 2026 19:20
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>
@sastraxi sastraxi changed the title Add External MIDI sync Add external MIDI device synchronization on pedalboard load Apr 19, 2026
sastraxi and others added 22 commits April 23, 2026 23:10
# 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
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
@sastraxi sastraxi force-pushed the feat/external-midi branch from 5a4858e to 6d1d5d2 Compare June 6, 2026 18:14
sastraxi and others added 13 commits June 6, 2026 14:26
…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>
@sastraxi sastraxi changed the title Add external MIDI device synchronization on pedalboard load External MIDI routing for hardware controls Jun 7, 2026
@sastraxi sastraxi force-pushed the feat/external-midi branch from 10d8d49 to 70be97c Compare June 8, 2026 01:36
@sastraxi sastraxi changed the title External MIDI routing for hardware controls Control other pedals via MIDI Jun 8, 2026
@sastraxi sastraxi changed the title Control other pedals via MIDI Control other devices/pedals via MIDI Jun 8, 2026
Comment thread modalapi/mod.py
Comment thread modalapi/mod.py Outdated
Comment thread pistomp/analogmidicontrol.py
sastraxi and others added 4 commits June 9, 2026 17:48
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant