Skip to content

feat: WatchDevices — MIDI device hot-plug detection#2

Merged
leandrodaf merged 4 commits into
mainfrom
feat/watch-devices
May 15, 2026
Merged

feat: WatchDevices — MIDI device hot-plug detection#2
leandrodaf merged 4 commits into
mainfrom
feat/watch-devices

Conversation

@leandrodaf
Copy link
Copy Markdown
Owner

Summary

Adds WatchDevices(ctx context.Context) (<-chan DeviceEvent, error) to the ClientMIDI interface, enabling consumers to react in real time when a MIDI device is connected or disconnected.

New API

// DeviceEventType describes what happened to a MIDI device.
type DeviceEventType int

const (
    DeviceAdded   DeviceEventType = iota
    DeviceRemoved
)

// DeviceEvent is emitted by WatchDevices when the set of MIDI devices changes.
type DeviceEvent struct {
    Type   DeviceEventType
    Device DeviceInfo
}

// WatchDevices returns a channel that emits DeviceEvent values on hot-plug.
// The channel is closed when ctx is cancelled.
WatchDevices(ctx context.Context) (<-chan DeviceEvent, error)

Per-platform implementation

Platform Mechanism Latency
macOS Native CoreMIDI MIDINotifyProc callback via CGo (cgo.Handle pattern) Instant
Linux Polling ListDevices() every 2 s ≤ 2 s
Windows Polling midiInGetNumDevs every 2 s ≤ 2 s
Stubs / no-CGo Channel closed on ctx cancel, no events

Changes

  • sdk/contracts/midi.goDeviceEventType, DeviceEvent, WatchDevices added to ClientMIDI
  • sdk/contracts/mock.goWatchDevicesFunc / WatchDevicesCalls added to MockMIDIClient
  • internal/coremidi/client.go — CGo export goMidiNotify, static C bridge notifyBridgeFn, cgo.Handle lifecycle, Client.NotifyCh, Dispose()
  • internal/midi/mididarwin/client_darwin.goWatchDevices using CoreMIDI notify channel + diffDevices helper; Stop() calls Dispose()
  • internal/midi/midilinux/client_linux_cgo.goWatchDevices with 2 s polling + diffDevices
  • internal/midi/midiwindows/client_windows.goWatchDevices with 2 s polling + diffDevices
  • All stubs / dummy clients updated to satisfy the interface

Tests

  • mididarwin and midiwindows dummy clients: 100% coverage
  • sdk/contracts mock: WatchDevices func, error, cancel, call counter
  • midilinux CGo client: channel returned, cancel, diffDevices add/remove/unchanged (via export_test.go shim)

Notes

  • cgo.Handle.Delete() is called in ClientMid.Stop() (darwin) via client.Dispose() — no handle leak
  • Device identity comparison uses Name + Manufacturer; adequate for single-device use case
  • go mod tidy does not accept -tags; tidy was run without build tags

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

leandrodaf and others added 4 commits May 15, 2026 12:30
- Add DeviceEventType, DeviceEvent to sdk/contracts/midi.go
- Add WatchDevices(ctx) to ClientMIDI interface and MockMIDIClient
- macOS: register CoreMIDI MIDINotifyProc via cgo.Handle; coremidi.Client
  now exposes NotifyCh <-chan int32 with native notifications
- Linux: 2-second polling goroutine using ListDevices diff
- Windows: 2-second polling goroutine using ListDevices diff
- All dummy/stub implementations return a channel closed on ctx.Done()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
sdk/contracts:
- MockMIDIClient: expand struct doc, document WatchDevicesFunc /
  WatchDevicesCalls fields and WatchDevices method

internal/coremidi:
- NewClient: document CGo handle lifecycle and NotifyCh

internal/midi/mididarwin (darwin + dummy):
- ClientMid: struct-level doc
- NewMIDIClient, ListDevices, SelectDevice, handleMIDIMessage,
  closeOutCh, Stop, StartCapture: doc comments
- dummyMIDIClient: struct doc; WatchDevices stub doc

internal/midi/midilinux (cgo + nocgo + dummy):
- NewMIDIClient, ListDevices, SelectDevice, closeOutCh, StartCapture: docs
- midiParser / feed: doc comments
- stubClient, dummyMIDIClient: struct docs; WatchDevices stub docs

internal/midi/midiwindows (windows + dummy):
- HMIDIIN, midiInCaps, ClientMid: type/struct docs
- NewMIDIClient, ListDevices, SelectDevice, closeOutCh, StartCapture,
  midiInCallback, Stop: doc comments
- dummyMIDIClient: struct doc; WatchDevices stub doc

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
sdk/contracts/mock_test.go:
- WatchDevices uses WatchDevicesFunc when set
- WatchDevices returns configured error
- WatchDevices default closes channel on context cancel
- WatchDevices increments WatchDevicesCalls

mididarwin/client_dummy_test.go:
- WatchDevices closes channel on context cancel
- WatchDevices emits no events (stub)

midilinux/client_dummy_test.go:
- WatchDevices closes channel on context cancel
- WatchDevices emits no events (stub)

midilinux/client_linux_cgo_test.go (new):
- WatchDevices returns non-nil channel
- WatchDevices closes channel on context cancel
- diffDevices: adds new device event (via export_test.go shim)
- diffDevices: removes gone device event
- diffDevices: no events when list unchanged

midilinux/export_test.go (new):
- DiffDevicesExported shim for white-box testing

midiwindows/client_dummy_test.go:
- WatchDevices closes channel on context cancel
- WatchDevices emits no events (stub)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Windows (client_windows.go):
- stopCapture() function body was missing its declaration line, causing
  'syntax error: non-declaration statement outside function body' at line 308.
  Add 'func (m *ClientMid) stopCapture() error {' before the body.

macOS (coremidi/client.go):
- cgo.Handle has no Pointer() method; replace h.Pointer() with
  unsafe.Pointer(uintptr(h)), which is the correct way to pass a
  cgo.Handle to a C function expecting void*.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@leandrodaf leandrodaf merged commit 7e21b8a into main May 15, 2026
8 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