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.