Merge branch 'gemini/doc-adaptive-jitter': document adaptive playout-delay controller
This commit is contained in:
+58
-5
@@ -60,7 +60,7 @@ All source code resides inside `src/`:
|
|||||||
* **`src/core/`**
|
* **`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/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/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/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. It also holds an *adaptive* playout delay that grows on jitter/loss and shrinks on a sustained clean run (see Section 4).
|
||||||
* **`src/network/`**
|
* **`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/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/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.
|
||||||
@@ -114,7 +114,60 @@ Audio processing runs on a 20ms clock (960 samples at 48kHz mono S16LE):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Network and P2P Transport
|
## 4. Adaptive Playout Delay Controller
|
||||||
|
|
||||||
|
To absorb network jitter (packet arrival variation) without incurring unnecessary latency, PeerSpeak replaces a fixed playout buffer depth with an adaptive feedback controller. The controller operates purely on buffer observations without requiring wall-clock synchronization or NTP.
|
||||||
|
|
||||||
|
### Controller State & Parameters
|
||||||
|
|
||||||
|
The controller tracks three key metrics per peer inside the [JitterBuffer](file:///home/mollusk/git/butter/peerspeak/src/core/jitter.rs#L62) struct in [src/core/jitter.rs](file:///home/mollusk/git/butter/peerspeak/src/core/jitter.rs):
|
||||||
|
* **Target Delay (`target_delay`)**: The number of frames that must accumulate in the buffer before playout is allowed to start (or restart after an underrun). Clamped to `[MIN_DELAY_FRAMES, MAX_DELAY_FRAMES]` (i.e., `[2, 12]` frames or ~40–240ms). Defaults to `DEFAULT_DELAY_FRAMES` (`3` frames, ~60ms).
|
||||||
|
* **Clean Run (`clean_run`)**: The number of consecutive real, in-order frames played without disruption. Used to drive the slow shrink phase.
|
||||||
|
* **Buffering Ticks (`buffering_ticks`)**: The number of 20ms mixer ticks spent in the buffering state since the first packet arrived.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Feedback Loop and Delay Adjustments
|
||||||
|
|
||||||
|
The controller employs a fast-grow / slow-shrink strategy: it reacts immediately to network disruptions (a one-frame increase per disruption event) but reclaims latency cautiously (a one-frame decrease, and only after a long clean run). Both adjustments are additive — the shrink's slowness comes from the `CLEAN_RUN_TO_SHRINK` gate, not from multiplicative scaling.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────┐
|
||||||
|
│ Packet Event │
|
||||||
|
└───────────┬────────────┘
|
||||||
|
│
|
||||||
|
┌────────────────────────┼────────────────────────┐
|
||||||
|
▼ (Late/PLC Gap) ▼ (Clean Playout) ▼ (Silence/Underrun)
|
||||||
|
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
|
||||||
|
│ Grow Target (+1) │ │ Increment clean_run │ │ Reset clean_run │
|
||||||
|
│ Reset clean_run │ │ If clean_run >= 250: │ │ Go Idle (re-buffer) │
|
||||||
|
└──────────────────────┘ │ Shrink Target (-1) │ │ Target Unchanged │
|
||||||
|
│ Reset clean_run │ └──────────────────────┘
|
||||||
|
└──────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Grow (Fast Increase)**:
|
||||||
|
* **Late Arrival**: If a packet arrives with a sequence number older than the next sequence to be played (`seq < next_seq`) in [insert](file:///home/mollusk/git/butter/peerspeak/src/core/jitter.rs#L121), it is discarded, and target delay grows by 1 frame (up to `12`) via [note_disruption](file:///home/mollusk/git/butter/peerspeak/src/core/jitter.rs#L104).
|
||||||
|
* **PLC Gap**: If the mixer polls a frame in [pop_frame](file:///home/mollusk/git/butter/peerspeak/src/core/jitter.rs#L144), the next sequence is missing, but later packets are already buffered, the decoder performs Packet Loss Concealment (PLC), and target delay grows by 1 frame (up to `12`) via [note_disruption](file:///home/mollusk/git/butter/peerspeak/src/core/jitter.rs#L104).
|
||||||
|
* *Rationale*: The jitter exceeded the current cushion. Growing the target delay ensures a deeper cushion next time the buffer primes.
|
||||||
|
2. **Shrink (Slow Decrease)**:
|
||||||
|
* **Clean Playout**: Each time a real, in-order frame is successfully popped and played in [pop_frame](file:///home/mollusk/git/butter/peerspeak/src/core/jitter.rs#L144), the controller increments `clean_run` via [note_clean](file:///home/mollusk/git/butter/peerspeak/src/core/jitter.rs#L111).
|
||||||
|
* When `clean_run` reaches `CLEAN_RUN_TO_SHRINK` (`250` frames, or ~5 seconds of continuous playback), the target delay is reduced by 1 frame (down to `2`), and `clean_run` is reset.
|
||||||
|
* *Rationale*: The link has been stable for a sustained period, indicating we can safely reclaim latency.
|
||||||
|
3. **Silence Neutrality**:
|
||||||
|
* If a talker pauses speaking, the buffer runs dry and goes idle. This is a benign underrun rather than a network anomaly.
|
||||||
|
* The controller transitions the playout state to buffering (`next_seq = None`) and resets `clean_run`, but **leaves `target_delay` untouched**. This avoids inflating playout latency during natural pauses in conversation.
|
||||||
|
4. **Buffer Overflow Resync**:
|
||||||
|
* If the number of buffered packets exceeds `MAX_BUFFERED_FRAMES` (`32` frames, ~640ms), the buffer has fallen too far behind.
|
||||||
|
* The oldest frame is discarded, the playout head (`next_seq`) is resynced to the oldest remaining packet sequence, and `clean_run` is reset. No change is made to `target_delay`.
|
||||||
|
5. **Prime-Timeout Safety Net**:
|
||||||
|
* When transitioning from idle to buffering, the mixer polls every 20ms and increments `buffering_ticks` if packets are present but their count is below `target_delay`.
|
||||||
|
* If `buffering_ticks` reaches `PRIME_TIMEOUT_TICKS` (`25` ticks, ~500ms), playout is primed and started immediately, even if the buffer length has not met `target_delay`.
|
||||||
|
* *Rationale*: This prevents short utterances (e.g., "yes", "okay") from being held indefinitely in a deep target cushion, capping the worst-case startup latency.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Network and P2P Transport
|
||||||
|
|
||||||
P2P voice chats utilize a dual-plane architecture:
|
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.
|
1. **Control Plane (iroh-gossip)**: Distributes room roster join/leaves and audio state updates (mute/deafen) using Gossip.
|
||||||
@@ -130,7 +183,7 @@ To ensure reconnection reliability, the dialer caches each peer's complete `Endp
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Reconnect and Eviction Model
|
## 6. Reconnect and Eviction Model
|
||||||
|
|
||||||
Connection drops are managed via a dual-path armed grace timer to maintain a stable room state:
|
Connection drops are managed via a dual-path armed grace timer to maintain a stable room state:
|
||||||
|
|
||||||
@@ -153,7 +206,7 @@ Connection drops are managed via a dual-path armed grace timer to maintain a sta
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Core loop & UI Flow
|
## 7. Core loop & UI Flow
|
||||||
|
|
||||||
The core async orchestrator in `src/core/mod.rs` schedules five main concurrent tasks inside an `ActiveSession`:
|
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.
|
* **`event_task`**: Processes gossip events, peer updates, and room roster modifications.
|
||||||
@@ -166,7 +219,7 @@ Updates are forwarded back to the Iced GUI via `UiEvent` channels, keeping room
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Config and Privacy
|
## 8. 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.
|
* **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`.
|
* **Persisted UI State**: Configuration values (devices, noise gates, custom chime file paths) are saved once when the settings page is exited via `AppMessage::NavigateBack`.
|
||||||
|
|||||||
Reference in New Issue
Block a user