Skip to content

Add a MIDI monitor for diagnosing controller input - #565

Draft
diamondsea11 wants to merge 1 commit into
rerdavies:mainfrom
diamondsea11:upstream-pr/midi-monitor
Draft

Add a MIDI monitor for diagnosing controller input#565
diamondsea11 wants to merge 1 commit into
rerdavies:mainfrom
diamondsea11:upstream-pr/midi-monitor

Conversation

@diamondsea11

Copy link
Copy Markdown

Draft — the diagnostics half of the MIDI work, which you said you had no objection to. Deliberately carries none of the Control Hub / controller-profile / per-snapshot-action material that overlaps with your MIDI Mappings roadmap.

Two things up front, so you can judge how hard to look at it:

  • The C++ has not been compiled. I don't have a Linux build environment. The TypeScript is clean under tsc -b --force. Everything else is code review only.
  • This is not an extraction from my fork like the others. In my fork the monitor lives inside the Control Hub dialog, which is exactly the part you asked to defer, so I rewrote it standalone against main. That means it's new code, not code that has been running on my Pi.

The problem

There is no way to see what a controller is actually sending. The only path that reports incoming MIDI to a client is MIDI learn, and by design it reports only what it can bind to: note-on with non-zero velocity, and control change minus bank select and CC LSB. Everything else is dropped twice — once in the realtime filter in ProcessMidiMonitor, and again in PiPedalModel::OnNotifyMidiListen. So when a footswitch appears to do nothing, there is nothing to look at, and no way to tell "the device sent nothing" from "the device sent a program change and PiPedal ignored it".

The change

A monitor mode alongside MIDI learn rather than a parallel pipeline:

  • MidiListener gains listenForAllEvents. The filter in OnNotifyMidiListen becomes per-listener, so learn clients receive exactly the set of messages they received before — the note-off and non-note/CC drops are preserved for them verbatim.
  • A new monitorMidiEvents socket message reuses the existing listener list; cancelled with the existing cancelListenForMidiEvent. The listenForMidiEvent message is untouched, so there is no wire-compatibility question for existing clients.
  • SetMonitorAllMidiEvents on the audio host makes the realtime filter pass messages through unmodified while a monitor is attached, and only then.
  • The three places that separately recomputed the listen flag now share UpdateMidiListenerState().

The UI is a read-only full-screen dialog reached from a toolbar button in System MIDI Bindings. Newest-first list of decoded messages (time, channel, type, data), pause and clear. Bounded to 200 entries so a controller streaming CCs can't grow it without limit.

Things worth your judgement

  • Placement. I put the entry point in the System MIDI Bindings toolbar because that's where someone goes when a controller misbehaves. If MIDI Mappings is about to become something else, this button should probably move with it.
  • Realtime cost. While a monitor is open, every channel-voice message writes a MidiNotifyBody to the ring buffer instead of only bindable ones. For a controller that streams CCs that's a higher rate than learn ever produced. It's bounded by MIDI bandwidth and only happens while the dialog is open, but you know that ring buffer's headroom better than I do — if you'd rather it coalesced or rate-limited, say so.
  • I left CancelMonitorAtomOutput's midiEventListeners.size() check alone. It looks like a copy-paste (it's in the atom-output path), but it's pre-existing and unrelated, and I didn't want to bury it in this diff.

Happy to drop the UI entirely and land just the backend if you'd rather build the dialog yourself.

There is currently no way to see what a MIDI controller is actually sending.
The only path that reports incoming MIDI to a client is MIDI learn, which by
design shows just the messages it can bind to: note-on with non-zero velocity
and control change (minus bank select and CC LSB). Program change, note-off,
pitch bend, aftertouch and channel pressure are dropped in two places -- the
realtime filter in AudioHost and again in PiPedalModel::OnNotifyMidiListen --
so when a footswitch "does nothing", there is nothing to look at.

Adds a monitor mode alongside MIDI learn rather than a second pipeline:

- MidiListener gains listenForAllEvents. The filter in OnNotifyMidiListen
  becomes per-listener, so learn clients receive exactly the same set of
  messages as before.
- New monitorMidiEvents socket message reuses the existing listener list and
  is cancelled with cancelListenForMidiEvent.
- SetMonitorAllMidiEvents on the audio host makes the realtime filter pass
  messages through unmodified while a monitor is attached.
- The three places that recomputed the listen flag now share
  UpdateMidiListenerState().

The UI is a read-only, full-screen dialog reached from a toolbar button in
System MIDI Bindings: newest-first list of decoded messages with channel,
type and data, plus pause and clear. Bounded to 200 entries so a controller
streaming CCs cannot grow it without limit.

No change to MIDI learn behaviour, to bindings, or to what reaches plugins.

Verified with npx tsc -b --force. The C++ has not been compiled -- I have no
Linux build environment here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
diamondsea11 pushed a commit to diamondsea11/pipedal that referenced this pull request Aug 6, 2026
…draft

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants