diff --git a/README.md b/README.md index cf60664..1f8428d 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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. diff --git a/src/mackie/mackie_main.cpp b/src/mackie/mackie_main.cpp index 4d243c3..527c4a1 100644 --- a/src/mackie/mackie_main.cpp +++ b/src/mackie/mackie_main.cpp @@ -49,6 +49,35 @@ int main(int argc, char** argv) { std::signal(SIGINT, on_sig); std::signal(SIGTERM, on_sig); + std::unique_ptr 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 @@ -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; }