hydra

Terminal replacement for Loopback — virtual audio devices and routing on macOS, from a ratatui TUI.
Log | Files | Refs | README | LICENSE

P3b-multi-device.md (3284B)


      1 # P3b — multiple dynamic devices (deferred, branch: `p3b-multi-device`)
      2 
      3 ## Why this is on a branch, not main
      4 
      5 `main` is a working single-device Loopback replacement (rename + capture + combine +
      6 monitor, all verified). P3b is the one piece that requires rewriting BlackHole's **static
      7 object model**, and driver changes **cannot be behavior-tested without a sudo install +
      8 coreaudiod restart** — i.e. I can only build-check them, not run them. Half-building a
      9 4600-line C refactor blind is the failure mode that bit this project earlier (committing
     10 non-compiling / wrong-behaviour trees). So: the refactor lives on `p3b-multi-device`, main
     11 stays shippable, and we only merge once it's been installed-and-tested for real.
     12 
     13 ## The scope (measured, not guessed)
     14 
     15 In `driver/upstream/BlackHole/BlackHole.c` (v0.6.1, 4620 lines):
     16 - **78** references to `kObjectID_Device`, **41** to `kObjectID_Device2`
     17 - A fixed object-ID enum: `Box=2, Device=3, Stream_Input=4, Volume_Input=5, Mute_Input=6,
     18   Stream_Output=7, Volume_Output=8, Mute_Output=9, Pitch=10, Clock=11, Device2=12`
     19 - **22** uses of `kNumber_Of_Channels`, including ring-buffer `calloc` and the realtime IO
     20   path (`vDSP_*`, `memcpy`) — so channel count is compile-time-baked into buffer math
     21 - One global `gRingBuffer`; per-device IO-running counters are hand-duplicated
     22   (`gDevice_IOIsRunning`, `gDevice2_IOIsRunning`)
     23 
     24 ## The refactor plan
     25 
     26 1. Replace the fixed object-ID enum with a **runtime registry**: each manifest device owns a
     27    contiguous block of object IDs (device + its streams/volume/mute/clock controls), assigned
     28    at init from the manifest.
     29 2. Replace the single-device globals with an **array of device structs** (`name`, `uid`,
     30    `channels`, `gRingBuffer`, `gIOIsRunning`, sample-rate, volume state).
     31 3. In **every** property handler (`*_HasProperty / IsPropertySettable / GetPropertyDataSize /
     32    GetPropertyData / SetPropertyData`) and `DoIOOperation`, switch on `objectID` → resolve to
     33    the owning device struct instead of comparing against `kObjectID_Device`/`Device2`.
     34 4. Make `kNumber_Of_Channels`-derived sizing **per-device** (ring buffer, ASBD, channel
     35    layout, bytes-per-frame).
     36 5. Publish the device list from the manifest at `BlackHole_Initialize`; the daemon already
     37    writes `devices.json` and `hydra_cfg.c` already parses it (extend the reader from
     38    first-device-name to the full array).
     39 6. Daemon side: `AddDevice` / `RemoveDevice` / `SetChannels` IPC + TUI affordances; rewrite
     40    `devices.json` and trigger a reload.
     41 
     42 ## Verification loop (the reason it's branch-only)
     43 
     44 Each iteration must: `./scripts/build-driver.sh` → `./scripts/install-driver.sh` (sudo +
     45 `killall coreaudiod`) → confirm in `system_profiler SPAudioDataType` / the TUI that the
     46 expected devices appear and route audio. There is no headless substitute. Budget for the
     47 single-device assumption being pervasive — expect this to be the largest single chunk of
     48 work in the project, and merge to main only after a real multi-device round-trip passes.
     49 
     50 ## Lower-risk prerequisite already done
     51 
     52 P3a (manifest-driven **device name**) is on main and verified end-to-end on the host. It
     53 proves the daemon→manifest→driver-reader pipe works; P3b extends the reader from one name to
     54 the full device array.