Skip to content

ALSA: automatically reconnect USB MIDI inputs and outputs - #374

Merged
insolace merged 1 commit into
thestk:masterfrom
madbrain76:fix/alsa-midi-auto-reconnect
Sep 21, 2026
Merged

insolace merged 1 commit into
thestk:masterfrom
madbrain76:fix/alsa-midi-auto-reconnect

Conversation

@madbrain76

Copy link
Copy Markdown
Contributor

Unplugging a USB MIDI device removes its ALSA sequencer subscriptions. Plugging it back in does not restore an application's open connections, even when the device returns with the same ALSA client address. A new address also makes reconnecting by the old numeric address unreliable.

Restore identifiable USB connections within the ALSA backend, without requiring applications to close and reopen their RtMidi objects.

Public behavior

  • Add supportsAutoReconnect(), setAutoReconnect(), and isAutoReconnectEnabled() to the C++ API. Recovery defaults on for ALSA builds with the required support; other backends report unsupported.
  • Disabling recovery preserves an existing subscription. Re-enabling it also checks for a device lost while recovery was disabled.
  • closePort() cancels recovery. isPortOpen() continues to describe the application's open connection, including temporary device absence.

Identity and recovery

  • Match USB vendor/product, serial number, and MIDI port number/name. Without a serial, match the USB location instead. Reject ambiguous identities and do not reconnect software ports by name alone.
  • Allow the matching device to return at a different ALSA client address.
  • Reconcile subscriptions after announcement overflow or monitor restart. Retry recoverable monitor, enumeration, and subscription failures.
  • Reset incomplete SysEx and input timestamp state on unsubscribe so a disconnected device's partial message cannot contaminate later input.

Threading and compatibility

  • Share one announcement client and worker across all input/output ports. Per-connection helpers retain the monitor through shared ownership.
  • Protect registry state and subscription lifetime with one mutex. Normal MIDI input and output do not acquire this mutex. Unregister before freeing subscriptions, and join the worker outside the lock.
  • Report background failures through std::cerr, without invoking an application's error callback from the monitor. Retry transient worker startup failures; report persistent failure on the calling thread.
  • Keep the helper implementation private to RtMidi.cpp. Compile USB identity support only with ALSA 1.1.1 or newer; older builds use a stub.
  • Use non-virtual API extensions and existing backend identification. Do not add public data members or alter existing virtual-table slots.

Tests and build integration

  • Add real ALSA sequencer tests with simulated USB identities and test-only linker wrappers for fault injection. No MIDI hardware is needed, and the wrappers are never linked into the production library.
  • Cover input/output delivery, reused and changed addresses, wrong and ambiguous identities, runtime controls, incomplete SysEx, concurrent port lifecycles, one shared worker, overflow, and failure recovery.
  • Exercise the capability API for both directions and the dummy backend.
  • Compile and run the regression test in native ALSA CMake and Autotools builds. Compile but do not execute it while cross-compiling. Report a skip when the sequencer or required ALSA support is unavailable.
  • Include the test sources, runners, and documentation in distributions.

Validation and limitations

CMake and Autotools regression runs pass locally. Address/undefined behavior sanitizer testing and a comparison of existing virtual tables were also performed during development. ThreadSanitizer reports existing ALSA input-thread shutdown races, reproduced against unmodified upstream; this patch does not address those separate races.

Physical USB hotplug testing passed in a rebuilt GrandOrgue using a Roland VK-88 through a UM-880. MIDI input recovered after unplugging and reconnecting the UM-880, with the external reconnect watcher disabled. Physical MIDI-output recovery has not yet been verified; output recovery is covered by the automated ALSA sequencer tests.

USB-location fallback cannot distinguish an identical replacement in the same socket. Enumeration remains a snapshot, and lost MIDI messages are not replayed. Ordinary patchbay unsubscribes alone do not trigger recovery, but reconciliation after lost announcements restores open intent.

See tests/README-hotplug.md for test details and remaining limitations.

Unplugging a USB MIDI device removes its ALSA sequencer subscriptions.
Plugging it back in does not restore an application's open connections,
even when the device returns with the same ALSA client address. A new
address also makes reconnecting by the old numeric address unreliable.

Restore identifiable USB connections within the ALSA backend, without
requiring applications to close and reopen their RtMidi objects.

Public behavior
---------------

- Add supportsAutoReconnect(), setAutoReconnect(), and
  isAutoReconnectEnabled() to the C++ API. Recovery defaults on for ALSA
  builds with the required support; other backends report unsupported.
- Disabling recovery preserves an existing subscription. Re-enabling it
  also checks for a device lost while recovery was disabled.
- closePort() cancels recovery. isPortOpen() continues to describe the
  application's open connection, including temporary device absence.

Identity and recovery
---------------------

- Match USB vendor/product, serial number, and MIDI port number/name.
  Without a serial, match the USB location instead. Reject ambiguous
  identities and do not reconnect software ports by name alone.
- Allow the matching device to return at a different ALSA client address.
- Reconcile subscriptions after announcement overflow or monitor restart.
  Retry recoverable monitor, enumeration, and subscription failures.
- Reset incomplete SysEx and input timestamp state on unsubscribe so a
  disconnected device's partial message cannot contaminate later input.

Threading and compatibility
--------------------------

- Share one announcement client and worker across all input/output ports.
  Per-connection helpers retain the monitor through shared ownership.
- Protect registry state and subscription lifetime with one mutex.
  Normal MIDI input and output do not acquire this mutex. Unregister
  before freeing subscriptions, and join the worker outside the lock.
- Report background failures through std::cerr, without invoking an
  application's error callback from the monitor. Retry transient worker
  startup failures; report persistent failure on the calling thread.
- Keep the helper implementation private to RtMidi.cpp. Compile USB
  identity support only with ALSA 1.1.1 or newer; older builds use a stub.
- Use non-virtual API extensions and existing backend identification.
  Do not add public data members or alter existing virtual-table slots.

Tests and build integration
---------------------------

- Add real ALSA sequencer tests with simulated USB identities and
  test-only linker wrappers for fault injection. No MIDI hardware is
  needed, and the wrappers are never linked into the production library.
- Cover input/output delivery, reused and changed addresses, wrong and
  ambiguous identities, runtime controls, incomplete SysEx, concurrent
  port lifecycles, one shared worker, overflow, and failure recovery.
- Exercise the capability API for both directions and the dummy backend.
- Compile and run the regression test in native ALSA CMake and Autotools
  builds. Compile but do not execute it while cross-compiling. Report a
  skip when the sequencer or required ALSA support is unavailable.
- Include the test sources, runners, and documentation in distributions.

Validation and limitations
--------------------------

CMake and Autotools regression runs pass locally. Address/undefined
behavior sanitizer testing and a comparison of existing virtual tables
were also performed during development. ThreadSanitizer reports existing
ALSA input-thread shutdown races, reproduced against unmodified upstream;
this patch does not address those separate races.

Physical USB hotplug testing passed in a rebuilt GrandOrgue using a
Roland VK-88 through a UM-880. MIDI input recovered after unplugging and
reconnecting the UM-880, with the external reconnect watcher disabled.
Physical MIDI-output recovery has not yet been verified; output recovery
is covered by the automated ALSA sequencer tests.

USB-location fallback cannot distinguish an identical replacement in the
same socket. Enumeration remains a snapshot, and lost MIDI messages are
not replayed. Ordinary patchbay unsubscribes alone do not trigger recovery,
but reconciliation after lost announcements restores open intent.

See tests/README-hotplug.md for test details and remaining limitations.
@FelipeFTN

Copy link
Copy Markdown

Pretty nice feature and very very useful in the GrandOrgue community! Thanks a lot ✨

@garyscavone could you please take a look at this changes? We would love to have it working in the next release of rtmidi ❤️

@insolace

Copy link
Copy Markdown
Collaborator

Reviewed and tested this on a second machine — Linux, alsa-lib 1.2.14, real USB MIDI hardware. Summary: it works, it fixes a failure that is currently silent, and we could not find a problem with it. Details below, then a few questions and some future-direction notes.

What we checked

  • CMake and Autotools both configure and build clean, no new warnings.
  • Your alsahotplug suite passes in both: ctest 2/2, make check 2/2. The Autotools all-local hook runs it during make as intended.
  • The ABI claim holds. The three new methods are non-virtual on RtMidi and MidiApi, per-connection state lives on the private AlsaMidiData in RtMidi.cpp, and the pre-1.1.1 stub reports isSupported() == false. No public data members, no vtable changes.
  • No conflict with four other ALSA/WinMM/CoreMIDI branches we have in flight; all merge clean on top of this.

Physical hotplug test

You noted output recovery had not been verified on hardware, so we tested that specifically — real mains removal via a switched outlet, not a USB-level reset.

Method: open one output and one input on a KMI malletStation Pro, then send a Universal Identity Request once a second and wait 600 ms for the reply. A round trip is the only reliable signal here, since a send during the outage raises nothing. The port is opened once and never reopened. Logged RtMidi's getPortCount(), isPortOpen() and port list alongside the kernel's own view from /proc/asound/seq/clients.

This branch: 16 s of replies at 5 ms, device removed, 16 s of misses, power restored, first reply 7 s later, then 88 consecutive replies with no gaps.

Unmodified master, same device and same 10 s outage: identical up to the outage, then 59 consecutive misses — it never recovered. From the moment the device returned, getPortCount() reported the port, the port list contained it, /proc/asound/seq/clients listed the client, and every sendMessage() returned without throwing and wrote nothing to stderr. No reply for 43 s. The subscription was silently dropped and nothing in the API indicated it.

So the bug reproduces against master and this branch fixes it. One caveat: in both runs the device returned at the same ALSA client address, so we exercised the same-address path. The changed-address path is covered by your test suite, not by ours.

Questions before merge

  1. all-local runs the test during make rather than only make check. Deliberate? It means a plain build executes a ~4 s ALSA test, which is guarded by NATIVE_BUILD and skips cleanly, but is a change in what make does.
  2. Background failures go to std::cerr. Reasonable for a monitor thread, but some hosts object to unconditional library output — worth a severity hook later, or is cerr the intended contract?
  3. The TSan races you mention in ALSA input-thread shutdown, reproduced against unmodified master: worth a separate issue so they are tracked independently of this PR? Happy to file it with what we can corroborate.

None of these block it from our point of view.

Future direction — not requests for this PR

Three things this opens up, mentioned only as context since the state is already tracked internally here:

A disconnect/reconnect notification. Recovery is currently invisible to the application: no notification, isPortOpen() stays true, and a send during the outage raises nothing because those paths are WARNING. For a console or synth that is right. For a firmware updater it is a hazard — a transfer can be interrupted with no detectable signal. An optional callback would cover it, though it would mean firing from the monitor thread, which is exactly the thing you deliberately avoided, so the threading contract needs care.

The same mechanism on macOS and Windows. No backend has any hotplug hook today, so this is the first. macOS looks like the easiest follow-up: MIDIClientCreate takes a notification proc that is currently NULL, and kMIDIPropertyUniqueID is a stable per-endpoint identifier, which would avoid the USB serial/location matching you had to do here. On Windows it depends on the backend — the newer device-watcher APIs make removal and arrival straightforward to observe, whereas WinMM has neither a stable device identity nor a notification path without a message pump, so it looks like the hardest case.

We would be glad to collaborate on either, rather than duplicate work: we have macOS and Windows hardware and can test, and we are happy to prototype the callback against this branch if it is of interest. Your design, your call on shape — just say if you would rather keep them.

@insolace insolace linked an issue Sep 13, 2026 that may be closed by this pull request
@madbrain76

Copy link
Copy Markdown
Contributor Author

Sorry for the late update here. I found actually that this PR only fixes half of my issue, which is which Grandorgue, that consumes rtmidi.

If I start GO with my MIDI interface powered on, the rtmidi fix allows subsequently powering off the interface, and back on, and this is completely transparent for GO, and presumably for other rtmidi apps as well.

However, it does not fix the case where GO starts with the MIDI interface disconnect. In that case, since GO never opens it at startup, the rtmidi auto-reconnect does not fix. Unfortunately, that requires a change at the application level, ie. in GO.

@insolace

Copy link
Copy Markdown
Collaborator

You are correct that at the application level you're still going to need to detect the device and make the initial connection, but there is still value in having the reconnect built into RtMidi. I think we should merge this into the next major release along with some functions that expose the metadata that's now available in WMS and CoreMIDI, things like serial numbers and usb VID and PID, but that would be a separate effort/PR.

I'm holding off on merging any significant PRs until @garyscavone has time to review and weigh in.

@madbrain76

Copy link
Copy Markdown
Contributor Author

You are correct that at the application level you're still going to need to detect the device and make the initial connection, but there is still value in having the reconnect built into RtMidi. I think we should merge this into the next major release along with some functions that expose the metadata that's now available in WMS and CoreMIDI, things like serial numbers and usb VID and PID, but that would be a separate effort/PR.

I'm holding off on merging any significant PRs until @garyscavone has time to review and weigh in.

Thanks. Of course, I did not mean to withdraw the PR. I think the fix should go in. I was just pointing out that it only solved 50% of my issue.

@garyscavone

Copy link
Copy Markdown
Contributor

As mentioned, I don't have time to support RtMidi. I have added Eric as a developer with administrative privileges and I'm happy to add other interested parties as well. My normal suggestions would be: 1. avoid making the code complicated and hard to support; and 2. try to keep all aspects of the API working across all supported MIDI subsystems. This particular PR seems a bit limited on the last point but hopefully there are ways to get similar functionality working for the other subsystems in the future. Feel free to approve it.

@insolace

Copy link
Copy Markdown
Collaborator

Thanks @garyscavone, I'll merge this and moving forward will use my best judgement.

@insolace insolace added this to the 6.1.0 milestone Sep 21, 2026
@insolace
insolace merged commit 390d5c5 into thestk:master Sep 21, 2026
5 checks passed
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.

Feature request: Detect/notify on port changes

4 participants