macOS support (libusb Surface + virtual CoreMIDI), and meter falloff on all platforms - #1
Merged
Conversation
The Command|8's malformed MIDIStreaming input descriptor needs a different workaround on each platform. Linux patches snd-usb-audio; Windows relies on Digidesign's driver. CoreMIDI has no quirk mechanism, so the device's ports enumerate but stay inert and no MIDI-API backend can reach the input. MacosSurface therefore bypasses CoreMIDI on the device side entirely: it claims interface 1 over libusb and speaks USB-MIDI event packets directly, matching on VID/PID (0dba:8000) rather than a port name. Raw MIDI from the protocol encoders is packed into 4-byte packets on the way out and unpacked on the way in; the wake/keepalive contract is unchanged. MacosMidiPort goes the other way. macOS is the only supported platform that lets an application create MIDI endpoints, so the MCU side needs no loopback utility at all: it publishes a virtual source and destination via RtMidi and the DAW connects straight to them. Both endpoints are required - with only a source, a DAW sees an input with no matching output and control-surface support reports it cannot find a MIDI output. Claiming the interface takes it from CoreMIDI's class driver, so the binaries need root. Unprivileged processes cannot even enumerate the device (macOS hides USB devices a process may not touch), which makes "absent" and "not permitted" indistinguishable - open() reports both possibilities rather than guessing. Also add an __APPLE__ branch for the default MCU port names: the generic non-Windows default is "VirMIDI", which is a Linux virmidi name and wrong for ports we create ourselves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two problems, both visible on hardware and both affecting every platform. Meters never fell. Feedback::meter() sent the host's value and nothing more, so the display froze on whatever arrived last and stayed lit indefinitely once playback stopped. That is not a host bug: hosts transmit meter values sparsely (Reaper only when the quantised 0-12 level changes, roughly 1 Hz in practice) and expect the surface to supply the ballistics in between, the way real MCU hardware does in firmware. The host value is now treated as a peak - instant rise, then decay driven from Feedback::tick(), which Controller runs on every Surface tick. Default 1200 ms full-scale falloff, measured against captured Reaper meter traffic; set_meter_decay_ms(0) restores exact host-following. Levels were also mis-scaled. static_cast<int>(v * METER_ROWS) truncates, so 11/12 of full scale lit 5 of 6 LEDs and anything below 1/6 read as silence. Now rounds, with any non-zero signal guaranteed at least one lit LED so quiet material is distinguishable from nothing. Unchanged rows are no longer re-sent, so a steady level costs one write rather than one per tick. tests/test_feedback.cpp covers both fixes against a recording fake Surface, including the original symptom: full scale, then ticks with no host updates, asserting the meter reaches zero. Note that with falloff active a peak landing exactly on a half-row boundary renders one row low, because some decay elapses before the tick reads it. It is a sub-LED artifact at exact boundaries only, which is why the rounding tests disable decay to isolate the two behaviours. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
alphonsom added a commit
that referenced
this pull request
Aug 10, 2026
Brings in the macOS work and meter ballistics merged via PRs #1-#3. Clean merge, no conflicts; builds with no warnings and all three tests pass. README: Windows no longer requires Digidesign/Avid's driver. The dongle makes the surface enumerate as an ordinary class-compliant USB-MIDI device with no driver at all, and the ports look the same to this engine either way. Also spells out the exclusive-access trap, which is easy to hit and gives no useful error: a DAW holding the Command|8 ports stops the engine opening the device. The DAW talks to the engine over OSC or the MCU loopback pair, never to the surface directly -- the Command|8 speaks a proprietary protocol, so a DAW sending it generic MIDI just twitches the faders and leaves the display Offline, which is exactly what a first Windows attempt looks like. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the engine to macOS, and fixes two meter bugs that turned out to affect every platform.
Verified on hardware: a Command|8 driven from Reaper's native Mackie Control on macOS — faders bidirectional (10-bit in, motorised out), pan encoders, encoder rings, meters, LCD, buttons and LEDs.
macOS backend
The malformed MIDIStreaming input descriptor needs a different workaround per platform:
snd-usb-audioquirksnd-virmidiCoreMIDI has no quirk mechanism, so the device's ports enumerate but stay inert and no MIDI-API backend can reach the input.
MacosSurfacebypasses CoreMIDI on the device side and speaks USB-MIDI packets over libusb, matching VID/PID0dba:8000instead of a port name.MacosMidiPortgoes the other way. macOS is the only supported platform where an application can create MIDI endpoints, so the MCU side publishes its own virtual source and destination — no loopback utility. Both endpoints matter: with only a source, a DAW sees an input with no matching output and control-surface support reports it cannot find a MIDI output.src/protocol,src/feedback,src/controllerand both front-ends are untouched.Root is required
Claiming the interface takes it from CoreMIDI's class driver, and CoreMIDI reclaims it the moment anything releases it. Unprivileged processes cannot even enumerate the device, so "absent" and "not permitted" are indistinguishable —
open()reports both possibilities rather than guessing. The README covers this and why the alternatives (unloading the system driver, a codeless kext, DriverKit) are worse for a self-hosted tool.Meter fixes (all platforms)
Meters never fell.
meter()sent the host value and nothing more, so the display froze on the last value and stayed lit after playback stopped. Hosts transmit meter values sparsely — Reaper only when the quantised 0-12 level changes, ~1 Hz measured — and expect the surface to supply ballistics, as real MCU hardware does in firmware. The value is now a peak: instant rise, decay fromFeedback::tick(). Default 1200 ms, chosen against captured Reaper traffic;set_meter_decay_ms(0)disables it.Levels were mis-scaled.
static_cast<int>(v * METER_ROWS)truncates, so 11/12 of full scale lit 5 of 6 LEDs and anything below 1/6 read as silence. Now rounds, with any non-zero signal lighting at least one LED.Unchanged rows are no longer re-sent.
Testing
tests/test_feedback.cpp(new, wired into CTest) covers both fixes against a recording fakeSurface, including the original symptom: full scale, then ticks with no host updates, asserting it reaches zero. Both suites pass; clean build, no new warnings.Reviewer notes
APPLE-guarded, butfeedback.{hpp,cpp}andcontroller.cppare shared.__APPLE__branch for default MCU port names is a real fix, not cosmetic: the generic non-Windows default"VirMIDI"is a Linux virmidi name and wrong for ports we create ourselves.docs/PROTOCOL.mdis unchanged — no new protocol findings; this port was built against it and independently confirmed the wake handshake, 10-bit faders and(note, subid)button identity on hardware.🤖 Generated with Claude Code