Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,13 +118,20 @@ ctest --test-dir build
./build/command8-mackie # MCU bridge (no loopback needed)
```

No `sudo` needed: on the machine this was verified on (macOS 15.6, Intel,
Homebrew, libusb 1.0.30) `command8-monitor` claims the USB interface and gets
live fader/encoder input and LED feedback as a normal user. If your setup
instead reports "device not found" or a claim failure, it's most likely
another process already holding the interface (see below) or a stricter USB
permission policy on your machine — try `sudo` as a fallback in that case, and
consider a `LaunchDaemon` if you need it every run.
No `sudo` needed for `command8-monitor` or `command8-reaper`: on the machine
this was verified on (macOS 15.6, Intel, Homebrew, libusb 1.0.30)
`command8-monitor` claims the USB interface and gets live fader/encoder input
and LED feedback as a normal user, and `command8-reaper` does the same and
additionally reclaims the interface unprivileged after an unplug/replug cycle
(re-tested directly, not inferred). Neither binary ever touches RtMidi/CoreMIDI
(`command8-reaper` only speaks OSC over liblo, and rebuilds its backend fresh
on every reconnect) — unlike `command8-mackie` below, which does and pays for
it on replug. If your setup instead reports "device not found" or a claim
failure, it's most likely another process already holding the interface (see
below) or a stricter USB permission policy on your machine — try `sudo` as a
fallback in that case, and consider a `LaunchDaemon` if you need it every run.

`command8-mackie` is different — see below.

If another Command|8 bridge is already running, stop it first: the interface is
exclusive.
Expand All@@ -136,6 +143,20 @@ destination, both named **`Command|8`**; point your DAW's Mackie Control
surface at that name for *both* its input and its output. Rename with
`--mcu-recv`/`--mcu-send` if you want something else.

**Sudo-free at launch, but not across a replug.** Publishing those virtual
ports means this process has a CoreMIDI client for its whole life, and once
that's true, this process's own future claims of the physical device race
CoreMIDI's in-process device-notification handling for the same interface —
and lose. So `command8-mackie` opens the Command|8 unprivileged fine on first
launch, but if the device is unplugged and replugged while it's running, it
cannot reclaim the interface again without root (verified: this isn't a
race that resolves with more retries or more time, and tearing down and
rebuilding the virtual ports around the reclaim attempt doesn't help either
— it's a standing condition for the rest of that process's life). If you need
replug resilience, run it under `sudo` from the start; if you don't (or you're
fine restarting it after a reconnect), it's the only one of the three bridges
that's usually unprivileged.

Publishing both endpoints matters: with only a source, a DAW sees an input with
no matching output and control-surface support reports that it cannot find a
MIDI output.
Expand Down
49 changes: 44 additions & 5 deletions src/mackie/mackie_main.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -49,6 +49,35 @@ int main(int argc, char** argv) {
std::signal(SIGINT, on_sig);
std::signal(SIGTERM, on_sig);

std::unique_ptr<command8::Surface> surface;

#ifdef __APPLE__
// Claim the Command|8 *before* touching RtMidi/CoreMIDI (constructed below
// via MackieBackend's MidiPort). Once a CoreMIDI client has ever existed
// in this process, this process's own future libusb claims of the device
// race CoreMIDI's in-process device-notification handling for the same
// interface and lose -- so this ordering only buys the very first claim.
// It does NOT make replug recovery below sudo-free: once MackieBackend
// constructs its RtMidi client, a claim lost to a later unplug cannot be
// regained without root from this process, even if that client is torn
// down first (verified: tearing down and rebuilding the CoreMIDI client
// around the reclaim attempt does not help, and the failure does not
// clear with time -- it is a standing condition for the rest of this
// process's life, not a transient race). A full fix would need a
// privilege-separated helper process to hold the libusb claim, which is
// more than this ordering trick can give us; for now, treat first launch
// as sudo-free and replug-while-running as still requiring it.
// Linux/Windows don't share this problem at all (their MidiPort never
// touches the physical device), so they keep the original order below:
// fail fast on a missing MCU port without first blocking on hardware that
// may not even be plugged in yet.
std::printf("command8-mackie: waiting for the Command|8...\n");
surface = command8::make_surface();
g_surface = surface.get();
while (!g_stop && !surface->open(port_match)) nap(2);
if (g_stop) { std::printf("\nbye\n"); return 0; }
#endif

command8::MackieBackend backend(recv_match, send_match);
if (!backend.ok()) {
#ifdef _WIN32
Expand All@@ -64,19 +93,29 @@ int main(int argc, char** argv) {
#endif
return 1;
}

#ifdef __APPLE__
std::printf("command8-mackie: MCU up.\n");
#else
std::printf("command8-mackie: MCU up; waiting for the Command|8...\n");
surface = command8::make_surface();
g_surface = surface.get();
while (!g_stop && !surface->open(port_match)) nap(2);
if (g_stop) { std::printf("\nbye\n"); return 0; }
#endif

while (!g_stop) {
auto surface = command8::make_surface();
g_surface = surface.get();
if (!surface->open(port_match)) { g_surface = nullptr; nap(2); continue; }
command8::Controller controller(*surface, backend); // attaches Feedback
controller.run(); // blocks
backend.stop(); // stop rx before fb dies
surface->close();
g_surface = nullptr;
if (!g_stop) std::printf("command8-mackie: device removed; waiting...\n");
if (g_stop) break;
std::printf("command8-mackie: device removed; waiting...\n");
surface = command8::make_surface();
g_surface = surface.get();
while (!g_stop && !surface->open(port_match)) nap(2);
}
g_surface = nullptr;
std::printf("\nbye\n");
return 0;
}
Loading