Skip to content

fix(coremidi): run CFRunLoop on dedicated OS thread for hot-plug notifications#3

Merged
leandrodaf merged 1 commit into
mainfrom
fix/coremidi-runloop-notifications
May 16, 2026
Merged

fix(coremidi): run CFRunLoop on dedicated OS thread for hot-plug notifications#3
leandrodaf merged 1 commit into
mainfrom
fix/coremidi-runloop-notifications

Conversation

@leandrodaf
Copy link
Copy Markdown
Owner

Problem

CoreMIDI delivers MIDIClientCreate notifications on the CFRunLoop of the thread that created the client. Go goroutines do not have a CFRunLoop by default — so hot-plug events (device connect/disconnect) were registered but never actually delivered.

Additionally, Stop() was calling client.Dispose(), which destroyed the notification channel entirely. This meant WatchDevices would go permanently blind after the first stop/start cycle.

Fix

internal/coremidi/client.go

  • NewClient now spawns a goroutine locked to a dedicated OS thread (runtime.LockOSThread)
  • Captures CFRunLoopGetCurrent() on that thread
  • Blocks the thread on a C helper runMIDIRunLoop() (uses CFRunLoopRunInMode loop, exits cleanly on kCFRunLoopRunStopped)
  • Dispose() calls CFRunLoopStop before deleting the cgo handle

internal/midi/mididarwin/client_darwin.go

  • Removed m.client.Dispose() from Stop() — the run loop and notification channel must survive between capture sessions
  • Added Close() for full teardown (calls Stop() + Dispose())

…fications

CoreMIDI delivers MIDIClientCreate notifications on the CFRunLoop of the
thread that created the client. Go goroutines have no CFRunLoop by default,
so hot-plug events were registered but never delivered.

Fix NewClient to:
- Lock the goroutine to a dedicated OS thread (runtime.LockOSThread)
- Capture CFRunLoopGetCurrent() on that thread
- Block the thread on runMIDIRunLoop() for the client lifetime
- Stop the run loop in Dispose() via CFRunLoopStop

Also remove m.client.Dispose() from Stop() so the notification run loop
survives stop/start capture cycles. Add Close() for full teardown.
@leandrodaf leandrodaf merged commit 1a6d681 into main May 16, 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.

1 participant