docs: add ARCHITECTURE.md overview

Code-accurate architecture guide: async/RT split, module map, audio data
flow, iroh/gossip transport, the dual-armed reconnect-eviction model, core
task layout, and config/privacy defaults.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 05:08:25 -04:00
co-authored by Claude Opus 4.8
parent 3b7e6e076e
commit fe4c7f9ee5
+173
View File
@@ -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.