commit 57f248df365636f86328bb8cb584c9e34a4ca735
parent 0901577694888493cfc9dc03110f5399c1a186dc
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date: Mon, 1 Jun 2026 13:14:53 -0500
docs: log P3b multi-device plan + why it's branch-only
Captures the measured scope (78+41 static object-id refs, 22 channel-count
sites incl. ring buffer + realtime path), the refactor plan, and the reason it
stays off main: driver changes can't be behavior-tested without sudo install +
coreaudiod restart, so the refactor lives on p3b-multi-device and merges only
after a real installed multi-device round-trip passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat:
1 file changed, 54 insertions(+), 0 deletions(-)
diff --git a/docs/P3b-multi-device.md b/docs/P3b-multi-device.md
@@ -0,0 +1,54 @@
+# P3b — multiple dynamic devices (deferred, branch: `p3b-multi-device`)
+
+## Why this is on a branch, not main
+
+`main` is a working single-device Loopback replacement (rename + capture + combine +
+monitor, all verified). P3b is the one piece that requires rewriting BlackHole's **static
+object model**, and driver changes **cannot be behavior-tested without a sudo install +
+coreaudiod restart** — i.e. I can only build-check them, not run them. Half-building a
+4600-line C refactor blind is the failure mode that bit this project earlier (committing
+non-compiling / wrong-behaviour trees). So: the refactor lives on `p3b-multi-device`, main
+stays shippable, and we only merge once it's been installed-and-tested for real.
+
+## The scope (measured, not guessed)
+
+In `driver/upstream/BlackHole/BlackHole.c` (v0.6.1, 4620 lines):
+- **78** references to `kObjectID_Device`, **41** to `kObjectID_Device2`
+- A fixed object-ID enum: `Box=2, Device=3, Stream_Input=4, Volume_Input=5, Mute_Input=6,
+ Stream_Output=7, Volume_Output=8, Mute_Output=9, Pitch=10, Clock=11, Device2=12`
+- **22** uses of `kNumber_Of_Channels`, including ring-buffer `calloc` and the realtime IO
+ path (`vDSP_*`, `memcpy`) — so channel count is compile-time-baked into buffer math
+- One global `gRingBuffer`; per-device IO-running counters are hand-duplicated
+ (`gDevice_IOIsRunning`, `gDevice2_IOIsRunning`)
+
+## The refactor plan
+
+1. Replace the fixed object-ID enum with a **runtime registry**: each manifest device owns a
+ contiguous block of object IDs (device + its streams/volume/mute/clock controls), assigned
+ at init from the manifest.
+2. Replace the single-device globals with an **array of device structs** (`name`, `uid`,
+ `channels`, `gRingBuffer`, `gIOIsRunning`, sample-rate, volume state).
+3. In **every** property handler (`*_HasProperty / IsPropertySettable / GetPropertyDataSize /
+ GetPropertyData / SetPropertyData`) and `DoIOOperation`, switch on `objectID` → resolve to
+ the owning device struct instead of comparing against `kObjectID_Device`/`Device2`.
+4. Make `kNumber_Of_Channels`-derived sizing **per-device** (ring buffer, ASBD, channel
+ layout, bytes-per-frame).
+5. Publish the device list from the manifest at `BlackHole_Initialize`; the daemon already
+ writes `devices.json` and `hydra_cfg.c` already parses it (extend the reader from
+ first-device-name to the full array).
+6. Daemon side: `AddDevice` / `RemoveDevice` / `SetChannels` IPC + TUI affordances; rewrite
+ `devices.json` and trigger a reload.
+
+## Verification loop (the reason it's branch-only)
+
+Each iteration must: `./scripts/build-driver.sh` → `./scripts/install-driver.sh` (sudo +
+`killall coreaudiod`) → confirm in `system_profiler SPAudioDataType` / the TUI that the
+expected devices appear and route audio. There is no headless substitute. Budget for the
+single-device assumption being pervasive — expect this to be the largest single chunk of
+work in the project, and merge to main only after a real multi-device round-trip passes.
+
+## Lower-risk prerequisite already done
+
+P3a (manifest-driven **device name**) is on main and verified end-to-end on the host. It
+proves the daemon→manifest→driver-reader pipe works; P3b extends the reader from one name to
+the full device array.