Commit graph

3 commits

Author SHA1 Message Date
atagen
9edd809416 stage 4 (a–d): IPC server, ops, broadcast
Phase 4 first four checkpoints — daemon now serves the wire protocol
specified in IPC.md and broadcasts events to subscribers.

  4a IPC server skeleton
     UnixListener at $XDG_RUNTIME_DIR/headroom/control.sock, accept
     thread, per-connection thread, hello-on-connect, codec
     round-trip, 0600 perms with stale-socket detection. Caught and
     fixed a sigprocmask ordering bug: block SIGTERM/SIGINT
     process-wide BEFORE the IPC accept thread spawns, otherwise it
     inherits the unblocked mask and the signal takes the default
     disposition before pipewire's signalfd can read it.

  4b Read-only ops + shared state
     Arc<Mutex<DaemonState>> (parking_lot) for cross-thread daemon
     state. RoutingState moved off Rc<RefCell<>>-only and reads
     profile from the shared lock. Captures the headroom-processed
     node id via the registry. Implements: status, profile.list,
     profile.show, route.list, setting.get (serde-roundtrip dotted
     lookup), setting.list (flattened).

  4c Mutating ops
     profile.use (idempotent no-op until 4e ships the disk loader),
     profile.reload (empty list till 4e), route.set/unset with
     single-app user-rule replace semantics, setting.set with serde
     round-trip type-safety, bypass.set. CLI fix:
     allow_hyphen_values so 'headroom set foo.bar -0.5' works.

  4d Subscriptions + broadcast
     Per-connection split into reader thread + writer thread, joined
     by a bounded crossbeam_channel<ServerFrame>(64). Broadcaster in
     DaemonState fans out events via try_send; bounded queues drop
     on overflow with per-(subscriber, topic) counters and a
     daemon::overflow flush event piggybacked onto the next
     successful publish.

     Live events wired: daemon::started, daemon::shutdown,
     routing::rule_changed, routing::stream_routed,
     routing::stream_removed. CLI 'monitor [topics]' command
     subscribes by topic list.

Workspace deps unchanged; uses already-declared crossbeam-channel,
parking_lot. Sinks/SinkInfo gained Default derives.

Tests: 97 passing (28 dsp, 20 ipc, 45 core, 4 client). Clippy clean
at default level under -D warnings.

Remaining Phase 4 punch-list (recommended order):
  4e profile TOML loader + hot reload (notify-debouncer-mini)
  4h preferred_real_sink tracking
  4i target.object routing reliability on real WirePlumber
  4f slow AGC loop with ebur128
  4g meters publishing
  4j auto-promote to default sink (optional flag)
2026-05-19 23:14:18 +10:00
atagen
ae83310772 stage 3: daemon core
Phase 3 — bring up the daemon end-to-end through six checkpoints:

  3a Module skeleton (error, profile, routing, runtime, pw/*)
  3b Pure routing engine + 13 tests (no PipeWire dep)
  3c PwContext: main loop, sigprocmask-block SIGTERM/SIGINT before
     add_signal_local so signalfd actually picks them up
  3d headroom-processed virtual sink via the adapter factory with
     factory.name=support.null-audio-sink
  3e Filter: two pw_streams (capture from monitor / playback to real
     sink) with an rtrb SPSC ring between them. DSP chain
     (Compressor → two-tier Limiter) runs in the playback callback.
     Allocation-free; #![forbid(unsafe_code)] preserved via
     bytemuck::try_cast_slice for the byte↔f32 reinterpretation.
  3f Registry watcher binds the default metadata, evaluates new
     Stream/Output/Audio nodes against profile rules, writes
     target.object for processed routes. Self-stream guard skips
     anything whose node.name starts with 'headroom-filter'.

Workspace deps added: pipewire = { features = ["v0_3_44"] } for the
modern TARGET_OBJECT key, libspa, rtrb, nix (sigprocmask), bytemuck.

Tests: 65 passing (28 dsp, 20 ipc, 4 client, 13 core). Clippy clean
at default level under -D warnings.

PLAN.md §5 renumbered to fix stale subsection labels (was 4.1–4.4
from before the per-app insertion).

Known limitations punted to Phase 4 (documented in commit history
and team memory):
  - WirePlumber doesn't always honor late target.object writes once
    a stream is already linked (timing race).
  - preferred_real_sink dynamic tracking stubbed.
  - No auto-promote of headroom-processed to system default.
  - application.process.binary occasionally arrives in late metadata
    updates after the global registers; routing logs show '?' until
    we add a re-read.
2026-05-19 22:15:49 +10:00
atagen
ca1910de60 stage 2 2026-05-19 16:33:09 +10:00