That was my answer above. I entered your original post to Claude, and Claude suggested another workaround using hardware:
Short answer: not with the stock Vangelis UI. A global binding always includes the source device and the MIDI channel, and each control can hold only one non-ZynStep binding at a time. You can’t add a second controller to it, and you can’t make it device-agnostic without a code change.
Raw extraction: zyngine/zynthian_chain_manager.py, vangelis branch, fetched just now
- Binding tables (from
__init__):
absolute_midi_cc_binding, keyed by 24-bit ZMOP,CHAN,CC. The comment says ZMOP, but the code actually stores the ZMIP (input device).
chain_midi_cc_binding, keyed by 24-bit CHAIN,CHAN,CC.
add_midi_learn(), absolute branch: key = (zmip << 16) | (midi_chan << 8) | midi_cc
add_midi_learn(), first action: calls remove_midi_learn_from_zctrl(zctrl, chain=True, abs=True, ...). This means every new learn wipes the previous chain and absolute bindings for that control. Only ZynStep mappings survive.
midi_control_change(), absolute lookup: key = (zmip << 16) | key_low. This is an exact match. There is no wildcard or “any device” fallback.
midi_control_change(), chain lookup: only checks self.active_chain.chain_id, using either (chain, chan, cc) or the channel-unbound (chain, 0xff, cc).
What that means
- [verified-from-source] Global learn binds the specific input device + channel + CC. That explains why the control options page shows the controller’s name next to CC 107.
- [verified-from-source] Learning again from the second controller replaces the first binding instead of adding to it. So “learn it from both” can’t work.
- [verified-from-source] Chain learn ignores the device, but the binding still includes the MIDI channel, unless the binding was stored channel-unbound (0xff). So the poster’s “any controller sends CC 107” holds only if the controllers share a channel.
- [inferred] The poster also needs both controllers on the same channel for any global solution, because channel sits in the absolute key too.
Workarounds
- Merge before Zynthian sees it. Put both controllers through one input device: a hardware MIDI merger into the DIN input, a controller’s MIDI-thru/merge feature, or a multi-port interface that presents one merged port. Zynthian then sees one zmip, and a single global learn covers both. Both controllers must send on the same channel. [inferred, untested]
- Code change to add a wildcard device. [sketch, untested]
python
# in midi_control_change(), replacing the absolute-binding block
for zm in ((zmip, 0xFF) if zmip != ZMIP_STEP_INDEX else (zmip,)):
try:
for zctrl in self.absolute_midi_cc_binding[(zm << 16) | key_low]:
zctrl.midi_control_change(cc_val)
except KeyError:
pass
The lookup side is the easy part. Two pieces would still be missing:
- UI: some option (“Global learn – any device”) that calls
add_midi_learn(chan, cc, zctrl, zmip=0xFF).
- Snapshots: a check that save/restore survives. [from memory/inferred] I haven’t read how the state manager serialises the zmip; it probably maps it to a device ID, and 0xFF would need special handling. I also haven’t verified that 0xFF can’t collide with a real zmip index.
This looks like a good feature request for riban/jofemodo, or a PR if you want one.
Draft forum reply
You’ve read the behaviour correctly. In Vangelis, a global (absolute) MIDI learn binds the source device + MIDI channel + CC, and learning the same control again from a second device replaces the first binding rather than adding to it. Chain learn ignores the device but only works on the selected chain. So there’s currently no UI way to get “any device, any chain selection.”
Practical workaround: merge the two controllers into a single MIDI input before they reach Zynthian (hardware merger, MIDI thru/merge on one controller, or an interface that presents a merged port), send on the same channel, then global-learn once.
A proper fix would be an “any device” option for global learn. The lookup in zynthian_chain_manager.midi_control_change() is an exact match on the device index, so it would need a wildcard fallback plus UI and snapshot support. Might be worth a feature request.
If you want, I can read the state manager’s snapshot code next to firm up the PR scope.
Sources: