diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..b5189db --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,173 @@ +# PeerSpeak Architecture Overview + +This document provides a concise, code-accurate guide to the design and implementation of PeerSpeak. It details the system boundaries, module interactions, audio paths, and networking protocols. + +--- + +## 1. High-Level Design & System Split + +PeerSpeak is a decentralized, full-mesh P2P voice chat client. The application is divided into two distinct execution environments to ensure high audio quality: + +``` ++--------------------------------------------------------------------------------+ +| Tokio Async Runtime (Multi-Threaded) | +| | +| +-------------------+ UiEvents +------------------+ | +| | Iced GUI |<--------------+ Core Loop | | +| | (src/app/mod.rs) | | (src/core/mod.rs)| | +| +---------+---------+ +--------+---------+ | +| | AppMessages | | +| v v | +| +---------+---------+ +--------+---------+ | +| | Core Controller | | Iroh Network & | | +| | (src/core/mod.rs) | | Gossip Router | | +| +-------------------+ +--------+---------+ | ++-------------------------------------------------|------------------------------+ + | + lock-free ring buffers (HeapRb) + | ++-------------------------------------------------|------------------------------+ +| Real-Time Audio | Thread (PipeWire) | +| v | +| +-------------------+ +--------+---------+ | +| | Capture Callback +-------------->| Playback Callback| | +| | (Mic Capture) | | (Speaker Out) | | +| +-------------------+ +------------------+ | ++--------------------------------------------------------------------------------+ +``` + +1. **Tokio Async Runtime (Multi-Threaded)**: Operates the P2P network layers (Iroh, Gossip), config management, state transitions, event demuxing, and the GUI event loop. +2. **Real-Time Audio Thread (PipeWire)**: Runs in the PipeWire hardware process context to drive the audio hardware callbacks. +3. **Lock-Free Safety Boundary**: To avoid priority inversion, audio drops, and stutters, the RT callback never allocates, locks mutexes, or performs blocking OS operations. Interaction with the async runtime is achieved using lock-free Single-Producer Single-Consumer (SPSC) ring buffers (`HeapRb` from the `ringbuf` crate) in [src/audio/pipewire_impl.rs](file:///home/mollusk/git/butter/peerspeak/src/audio/pipewire_impl.rs). + +--- + +## 2. Module Map + +All source code resides inside `src/`: + +* **[src/lib.rs](file:///home/mollusk/git/butter/peerspeak/src/lib.rs)**: Root of the library. Handles logging path resolution via `dirs` and exposes the append-based `log_msg` helper. +* **[src/main.rs](file:///home/mollusk/git/butter/peerspeak/src/main.rs)**: Tiny entry point that configures the Tokio runtime and boots the Iced GUI. +* **`src/app/`** + * **[src/app/mod.rs](file:///home/mollusk/git/butter/peerspeak/src/app/mod.rs)**: Handles GUI rendering via Iced. Defines Catppuccin theme styling, user screens (`Home`, `Room`, `Settings`), hotkey binding, per-peer volume sliders, and custom chime overrides with real-time `✓`/`✗` path validation. +* **`src/audio/`** + * **[src/audio/mod.rs](file:///home/mollusk/git/butter/peerspeak/src/audio/mod.rs)**: Declares audio hardware interfaces (`AudioBackend`, `AudioDevice`, `AudioCaptureCallback`) and global targets like `PLAYBACK_TARGET_SAMPLES`. + * **[src/audio/pipewire_impl.rs](file:///home/mollusk/git/butter/peerspeak/src/audio/pipewire_impl.rs)**: Implements playout and capture streams via PipeWire native APIs (`pipewire-rs`). Configures custom hardware buffer parameters to eliminate jitter. + * **[src/audio/pw_cli.rs](file:///home/mollusk/git/butter/peerspeak/src/audio/pw_cli.rs)**: Dynamically parses target nodes from `pw-cli list-objects Node` to support manual device selection. +* **`src/codec/`** + * **[src/codec/mod.rs](file:///home/mollusk/git/butter/peerspeak/src/codec/mod.rs)**: Abstract wrapper traits for audio compression. + * **[src/codec/opus_impl.rs](file:///home/mollusk/git/butter/peerspeak/src/codec/opus_impl.rs)**: Standardizes mono Opus encoding (frame size 960 samples @ 48kHz, 20ms) and decoding with built-in Packet Loss Concealment (PLC). +* **`src/core/`** + * **[src/core/mod.rs](file:///home/mollusk/git/butter/peerspeak/src/core/mod.rs)**: Implements the central room coordination, mixer logic, voice activity noise-gating, and the session shutdown sequencer. + * **[src/core/messages.rs](file:///home/mollusk/git/butter/peerspeak/src/core/messages.rs)**: Formulates the inter-thread messaging schemas (`CoreCommand` and `UiEvent`). + * **[src/core/jitter.rs](file:///home/mollusk/git/butter/peerspeak/src/core/jitter.rs)**: Provides a per-peer jitter buffer that reorders out-of-order frames, drops duplicate arrivals, and handles packet drops. +* **`src/network/`** + * **[src/network/mod.rs](file:///home/mollusk/git/butter/peerspeak/src/network/mod.rs)**: Formulates abstractions for low-latency P2P transports. + * **[src/network/iroh_impl.rs](file:///home/mollusk/git/butter/peerspeak/src/network/iroh_impl.rs)**: Integrates Iroh's endpoint system. Orchestrates connection supervisors, dial routines, and handshake logic. + * **[src/network/gossip.rs](file:///home/mollusk/git/butter/peerspeak/src/network/gossip.rs)**: Wraps `iroh-gossip` for discovery, room roster management, and state exchange. +* **[src/config.rs](file:///home/mollusk/git/butter/peerspeak/src/config.rs)**: Controls serialization and persistence of `AppConfig` to `~/.config/peerspeak/config.json`. +* **[src/notify.rs](file:///home/mollusk/git/butter/peerspeak/src/notify.rs)**: Operates system chime playback (`pw-play`/`paplay`/`aplay`) on background threads. Houses `expand_tilde` and `validate_custom_path` validation helpers. + +--- + +## 3. Playout and Capture Data Flow + +Audio processing runs on a 20ms clock (960 samples at 48kHz mono S16LE): + +``` + [ Mic Audio Capture ] + │ + ▼ (RT Callback) + [ Ring Buffer: capture_prod ] + │ + ▼ (Capture Thread) + [ Opus Encoder ] + │ + ▼ (Datagram Task) + [ Outbound Net Socket ] + │ + Network Link (QUIC Datagrams) + │ + ▼ + [ Inbound Net Socket ] + │ + ▼ (Datagram Task) + [ Per-Peer Jitter Buffer ] + │ + ▼ (Mixer Task 20ms Tick) + [ Opus Decoder / Mix PCM ] + │ + ▼ (Mixer Task) + [ Ring Buffer: playback_prod ] + │ + ▼ (RT Callback) + [ Speaker Playout ] +``` + +* **RT Playout Safety**: The playout process callback inside [src/audio/pipewire_impl.rs](file:///home/mollusk/git/butter/peerspeak/src/audio/pipewire_impl.rs#L64) pops mixed PCM samples directly from the SPSC ring buffer. No allocations or mutex locks are used. +* **Buffer Quantum Pinning**: Playout streams enforce an explicit `SPA_TYPE_OBJECT_ParamBuffers` configuration during stream setup, pinning the hardware buffer size to exactly 1024 frames (2048 bytes mono S16LE). This limits the driver from over-pulling beyond the SPSC ring buffer's capacity and eliminates playback crackle. +* **Jitter Buffers & Mix Loop**: Incoming UDP datagrams are parsed in the Core `datagram_task` and routed to the peer's `JitterBuffer` (`src/core/jitter.rs`). Every 20ms, the Core `mixer_task` runs a tick: + 1. It pulls from each active peer's jitter buffer. + 2. If a sequence gap exists, it asks the Opus decoder to perform Packet Loss Concealment (PLC). + 3. The decoded PCM samples are scaled by the peer's volume slider factor. + 4. All streams are mixed into a single mono channel, gated by the mic's RMS noise gate threshold, and pushed to the playout ring buffer. + +--- + +## 4. Network and P2P Transport + +P2P voice chats utilize a dual-plane architecture: +1. **Control Plane (iroh-gossip)**: Distributes room roster join/leaves and audio state updates (mute/deafen) using Gossip. +2. **Data Plane (QUIC Datagrams)**: Voice payloads bypass routing tables and are sent directly between peer endpoints via QUIC datagrams to minimize latency. + +### Dialer vs. Acceptor Rule +To prevent race conditions where both peers try to dial each other simultaneously, PeerSpeak enforces a deterministic identity rule in [src/network/iroh_impl.rs](file:///home/mollusk/git/butter/peerspeak/src/network/iroh_impl.rs#L148): +* The peer with the lexicographically **lower** `EndpointId` acts as the dialer. +* The peer with the lexicographically **higher** `EndpointId` accepts inbound connections. + +### Retained-Address Reconnect +To ensure reconnection reliability, the dialer caches each peer's complete `EndpointAddr` (direct IP endpoints + relay configurations) inside the `Shared.addrs` map. When a connection drops, the supervisor task's dial loop bypasses the gossip DHT lookup entirely and dials the cached address directly. This enables connections to recover even when gossip status is offline. + +--- + +## 5. Reconnect and Eviction Model + +Connection drops are managed via a dual-path armed grace timer to maintain a stable room state: + +``` + Drop Notice (gossip: PeerConnectionLost) ──┐ + ├──► [arm_grace_timer] ──► Evict after 45s + Drop Notice (transport: Connecting) ───────┘ (Idempotent check) +``` + +1. **Outage Triggers**: An outage is detected via two parallel vectors: + * Gossip raises a `RoomEvent::PeerConnectionLost` (due to gossip ping timeouts). + * Transport raises a `ConnEvent::Connecting` (due to QUIC connection loss). +2. **First-Dial Gate**: First-ever dial attempts (which emit `Connecting` during initial handshake) are ignored by the eviction logic. A peer must be present in the `seen_connected` set before a transport drop can trigger an eviction countdown. +3. **Idempotence**: The `arm_grace_timer` helper in [src/core/mod.rs](file:///home/mollusk/git/butter/peerspeak/src/core/mod.rs#L80) verifies if a grace timer is already running for the peer. If present, the request is a no-op. This ensures a flapping network connection cannot repeatedly reset the eviction clock. +4. **Eviction Execution**: When the 45-second `RECONNECT_GRACE` timer expires: + * The connection supervisor is aborted. + * The peer is removed from the room, and its audio buffers are cleared. + * The peer is scrubbed from the `seen_connected` set, ensuring that a future rejoin starts with a clean slate. +5. **Graceful Disconnects**: When a user clicks the "Leave" button, the client sends a `GOODBYE_CODE = 1` signal directly over the transport. The remote endpoint's supervisor processes this as a graceful shutdown, immediately evicting the peer and bypassing the 45s grace period. + +--- + +## 6. Core loop & UI Flow + +The core async orchestrator in `src/core/mod.rs` schedules five main concurrent tasks inside an `ActiveSession`: +* **`event_task`**: Processes gossip events, peer updates, and room roster modifications. +* **`conn_event_task`**: Monitors connection state changes, manages the `seen_connected` cache, and schedules grace eviction timers. +* **`mixer_task`**: Mixed peer audio playout pacing and RMS voice levels. +* **`datagram_task`**: Listens for raw incoming network datagrams and pushes them to peer jitter buffers. +* **`capture_thread`**: A dedicated blocking thread that handles microphone frame reads, runs Opus encoding, and submits outbound packets. + +Updates are forwarded back to the Iced GUI via `UiEvent` channels, keeping room lists, speaking indicators, settings values, and chime status validation up to date in real time. + +--- + +## 7. Config and Privacy + +* **Default Privacy**: PeerSpeak sets the `NetworkMode` config to `RelayNoDiscovery` by default. This forces P2P connectivity through Iroh's relay nodes for NAT traversal without publishing the peer's endpoint metadata to the public n0 DNS directory. +* **Persisted UI State**: Configuration values (devices, noise gates, custom chime file paths) are saved once when the settings page is exited via `AppMessage::NavigateBack`. +* **WAV Custom Overrides**: System chimes map to custom user paths defined in `AppConfig`. The paths undergo tilde expansion (`~` / `~/` -> `dirs::home_dir()`) and file validation checks, displaying a live checkmark or warning indicator in the Settings menu without blocking or writing to the disk during typing.