Skip to main content

pcmflux/
lib.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! pcmflux: PulseAudio/PipeWire audio capture with Opus encoding, plus mic-uplink
8//! playback, exposed as a pure-Rust PyO3 extension (the audio sibling of pixelflux).
9//!
10//! The capture thread pulls S16LE PCM fragments from a PulseAudio record stream,
11//! reassembles them into fixed-size Opus frames, encodes them (mono/stereo via the
12//! `opus` crate, 5.1/7.1 surround via the multistream API), optionally wraps them in
13//! RFC 2198 RED framing — redundant copies of recent frames, so a client on a lossy
14//! transport rebuilds a dropped packet from the next one it receives instead of stalling
15//! for a retransmit — and hands each frame to a delivery thread that runs the Python
16//! callback off the audio path, so a slow or GIL-blocked callback can never stall the
17//! PulseAudio pump. The playback path mirrors this in reverse: it decodes the Opus mic
18//! uplink and writes PCM into a virtual sink.
19//!
20//! Concurrency design (the invariants below are load-bearing):
21//!   - A lifecycle mutex serializes joining/reassigning the capture thread.
22//!   - A single stop_state atomic is the one source of truth (0 = running, -1 =
23//!     external stop, a positive value = self-stop recorded under the issuing thread's
24//!     tid — the delivery thread's for a callback-issued stop). The
25//!     external -1 is stored INSIDE that lock immediately before join, so a stop can
26//!     never be lost between observing a live thread and asking it to stop. A
27//!     re-entrant self-start undoes only its own self-stop via one compare-exchange,
28//!     so a racing external stop is never clobbered.
29//!   - The PulseAudio mainloop is pumped with a bounded ~20ms timeout, so a stop is
30//!     observed within ~20ms even if the audio source delivers no data (is wedged).
31//!   - The GIL is released around join, because joining the capture thread transitively
32//!     joins the delivery thread, whose in-flight Python callback needs the GIL; holding
33//!     it while joining would deadlock.
34//!   - A callback may itself call stop/start; the callback runs on the delivery thread,
35//!     so that re-entrant case is detected via the delivery thread's OS tid (the capture
36//!     thread's tid is checked too) and short-circuits without joining — a join from
37//!     inside the callback would cycle (stopper joins capture, capture joins delivery,
38//!     delivery is the stopper).
39
40use pyo3::buffer::PyUntypedBuffer;
41use pyo3::prelude::*;
42use pyo3::types::{PyBytes, PyString};
43use std::collections::VecDeque;
44use std::sync::atomic::{
45    AtomicBool, AtomicI32, AtomicI64, AtomicU8, AtomicU64, AtomicUsize, Ordering,
46};
47use std::sync::{Arc, Condvar, Mutex, OnceLock, Weak};
48use std::thread::{JoinHandle, ThreadId};
49use std::time::{Duration, Instant};
50
51use libpulse_binding as pulse;
52use pulse::callbacks::ListResult;
53use pulse::channelmap::Map as ChannelMap;
54use pulse::context::{Context, FlagSet as CtxFlags};
55use pulse::def::BufferAttr;
56use pulse::mainloop::standard::Mainloop;
57use pulse::sample::{Format, Spec};
58use pulse::stream::{FlagSet as StreamFlags, PeekResult, Stream};
59use pulse::time::MicroSeconds;
60
61/// Non-panicking `println!` replacement that swallows write errors (e.g. EPIPE) instead
62/// of panicking, so a broken output pipe can't unwind the capture thread or a callback.
63macro_rules! plog {
64    ($($arg:tt)*) => {{
65        use std::io::Write;
66        let _ = writeln!(std::io::stdout().lock(), $($arg)*);
67    }};
68}
69/// Non-panicking `eprintln!` replacement — the stderr sibling of `plog!`.
70macro_rules! elog {
71    ($($arg:tt)*) => {{
72        use std::io::Write;
73        let _ = writeln!(std::io::stderr().lock(), $($arg)*);
74    }};
75}
76
77mod ogg_sink;
78
79/// Returns the calling thread's OS tid (`gettid` syscall) so a stop/start issued from
80/// inside the Python callback can detect it is on the capture thread and avoid self-joining.
81#[inline]
82fn gettid() -> i64 {
83    unsafe { libc::syscall(libc::SYS_gettid) as i64 }
84}
85
86/// `start_state`: no worker has run yet, or the last run stopped cleanly.
87const ST_IDLE: u8 = 0;
88/// `start_state` handshake: startup in progress.
89const ST_STARTING: u8 = 1;
90/// `start_state` handshake: the hot loop is running.
91const ST_RUNNING: u8 = 2;
92/// `start_state`: the last run ended in error (startup or mid-run); `last_error` says why.
93const ST_FAILED: u8 = 3;
94
95/// Opus target-bitrate bounds (bits/s); the initial `opus_bitrate` and every live update
96/// are clamped into this range so the encoder never sees a value it would reject.
97const OPUS_BITRATE_MIN: i32 = 6000;
98const OPUS_BITRATE_MAX: i32 = 510000;
99
100/// `stop_state` sentinel: no stop pending (running).
101const STOP_NONE: i64 = 0;
102/// `stop_state` sentinel: authoritative external stop (-1). Any positive value
103/// instead is the OS tid of a capture thread that self-stopped from its callback.
104const STOP_EXTERNAL: i64 = -1;
105
106/// PulseAudio mainloop pump timeout (~20 ms) — upper bound on how long a pending
107/// stop can go unobserved even when the audio source delivers nothing.
108const PUMP_TIMEOUT_US: u64 = 20 * 1000;
109/// Max Opus output packet size in bytes per stream; sizes the emit buffer pool.
110const MAX_OPUS_PACKET: usize = 4000;
111
112/// Max RED timestamp offset — 14-bit ceiling in 48 kHz samples from the primary.
113const RED_MAX_OFFSET: u64 = 16383;
114/// Max RED block length — 10-bit ceiling in bytes.
115const RED_MAX_LEN: usize = 1023;
116/// RED block payload type.
117const RED_BLOCK_PT: u8 = 0;
118/// Max redundant copies per frame and RED history depth.
119const RED_MAX_DISTANCE: i32 = 4;
120/// Bit of a header-carrying capture's second byte that marks where the stream pauses by the
121/// sender's choice. Alone, as `[0x01, WS_QUIET]` with no Opus, it is sent where a sound ends
122/// (and when the session drops after sound): a player plays out what it holds at once
123/// rather than waiting for more, and running dry after it is not a late delivery. It is set
124/// as well on the silent frame sent right behind that mark, whose Opus carries the end of
125/// the sound the encoder held back, so a player appends that frame to the sound rather than
126/// taking it for a new one. Never part of `n_red`, which fits below it (`RED_MAX_DISTANCE`).
127const WS_QUIET: u8 = 0x80;
128
129/// Test-only reference implementation of the WS audio frame body — builds the
130/// full RFC 2198 RED framing + primary Opus packet for byte-for-byte assertion
131/// against the pooled in-place `write_ws_prefix_into` on the hot path.
132///
133/// RED carries redundant copies of recent frames alongside each primary, so a client on a
134/// lossy transport can rebuild a dropped packet from the next one it receives with no
135/// retransmit — that packet-loss concealment is the whole reason the layout below exists.
136///
137/// `history` is oldest-first (front = oldest, largest timestamp offset back from
138/// `primary_pts`). The layout is chosen by `emit_header` and `red_distance`:
139///
140/// 1. **Header omitted** (`!emit_header`): the raw primary Opus packet, with no framing
141///    bytes — the header is what carries the redundant-block count, so without it there
142///    is no `[0x01,n]` prefix.
143/// 2. **`red_distance == 0`, or no usable redundancy**: the 2-byte `[0x01, 0x00]` framing
144///    then the primary. On this wire `n_red == 0` must mean **exactly** those two bytes,
145///    so a first frame after a (re)start or a gated silence (empty history) collapses here
146///    too; emitting a lone primary-only RED header instead would be mis-stripped by the
147///    client's `n_red == 0` path and corrupt the frame.
148/// 3. **`red_distance > 0` with usable history**: the full RFC 2198 RED framing —
149///    - `0x01` audio-chunk tag, then `n_red` (count of redundant blocks that actually fit).
150///    - The primary timestamp (low 32 bits, big-endian), letting the client order and dedup
151///      recovered frames against what it has already played.
152///    - One 4-byte header per redundant block, oldest-first: `0x80 | PT` (F bit set,
153///      i.e. another block follows), then `(offset14 << 10) | len10` big-endian.
154///    - The 1-byte primary block header (`PT`, F bit clear).
155///    - The block payloads in that same order: redundant oldest-first, then the primary.
156///
157/// A redundant block whose 48 kHz sample offset overflows `RED_MAX_OFFSET` (14 bits) or
158/// whose byte length overflows `RED_MAX_LEN` (10 bits) is skipped, so `n_red` counts only
159/// what fit. The second byte's top bit is not part of `n_red`: it is `WS_QUIET`, set by the
160/// emit path on the silent frame that ends a sound.
161#[cfg(test)]
162fn build_ws_body(
163    primary: &[u8],
164    primary_pts: u64,
165    history: &VecDeque<(Vec<u8>, u64)>,
166    red_distance: usize,
167    emit_header: bool,
168) -> Vec<u8> {
169    if !emit_header {
170        return primary.to_vec();
171    }
172    if red_distance == 0 {
173        let mut out = Vec::with_capacity(2 + primary.len());
174        out.push(0x01);
175        out.push(0x00);
176        out.extend_from_slice(primary);
177        return out;
178    }
179    let start = history.len().saturating_sub(red_distance);
180    let mut blocks: Vec<(&[u8], u32)> = Vec::with_capacity(red_distance);
181    for (data, pts) in history.iter().skip(start) {
182        let offset = primary_pts.saturating_sub(*pts);
183        if offset > RED_MAX_OFFSET || data.len() > RED_MAX_LEN {
184            continue;
185        }
186        blocks.push((data.as_slice(), offset as u32));
187    }
188    if blocks.is_empty() {
189        let mut out = Vec::with_capacity(2 + primary.len());
190        out.push(0x01);
191        out.push(0x00);
192        out.extend_from_slice(primary);
193        return out;
194    }
195    let redundant_bytes: usize = blocks.iter().map(|(d, _)| d.len()).sum();
196    let mut out = Vec::with_capacity(6 + 4 * blocks.len() + 1 + redundant_bytes + primary.len());
197    out.push(0x01);
198    out.push(blocks.len() as u8);
199    out.extend_from_slice(&(primary_pts as u32).to_be_bytes());
200    for (data, offset) in &blocks {
201        out.push(0x80 | (RED_BLOCK_PT & 0x7F));
202        let word = ((offset & 0x3FFF) << 10) | (data.len() as u32 & 0x3FF);
203        out.push((word >> 16) as u8);
204        out.push((word >> 8) as u8);
205        out.push(word as u8);
206    }
207    out.push(RED_BLOCK_PT & 0x7F);
208    for (data, _) in &blocks {
209        out.extend_from_slice(data);
210    }
211    out.extend_from_slice(primary);
212    out
213}
214
215/// Worst-case byte length of the WS frame prefix (tag + n_red + pts + headers + payloads).
216const RED_PREFIX_MAX: usize =
217    6 + 4 * RED_MAX_DISTANCE as usize + 1 + RED_MAX_DISTANCE as usize * RED_MAX_LEN;
218
219/// Emit the RFC 2198 RED framing prefix into `buf` in-place and return its length,
220/// so the encoder can serialize the Opus packet directly after it with no scratch
221/// buffer and no per-frame allocation. The runtime counterpart of `build_ws_body`.
222///
223/// 1. **Header omitted**: returns `0`; the caller emits the raw primary with no prefix.
224/// 2. **`red_distance > 0` with usable history**: writes the full RED header — tag,
225///    `n_red`, the primary timestamp (low 32 bits, big-endian), one 4-byte header per
226///    redundant block (F bit set, then `(offset14 << 10) | len10`), the 1-byte primary
227///    header (F bit clear), and the redundant block payloads oldest-first. Usable blocks
228///    are selected by history index into a fixed `RED_MAX_DISTANCE` array — the distance is
229///    clamped at ingest, so this allocates nothing.
230/// 3. **`red_distance == 0`, or no usable redundancy**: the 2-byte `[0x01, 0x00]` framing.
231///
232/// `buf` must hold at least `RED_PREFIX_MAX` bytes when `emit_header && red_distance > 0`,
233/// and at least 2 bytes otherwise.
234fn write_ws_prefix_into(
235    buf: &mut [u8],
236    primary_pts: u64,
237    history: &VecDeque<(Vec<u8>, u64)>,
238    red_distance: usize,
239    emit_header: bool,
240) -> usize {
241    if !emit_header {
242        return 0;
243    }
244    if red_distance > 0 {
245        debug_assert!(red_distance <= RED_MAX_DISTANCE as usize);
246        let start = history.len().saturating_sub(red_distance);
247        let mut idx = [0usize; RED_MAX_DISTANCE as usize];
248        let mut n = 0usize;
249        for (i, (data, pts)) in history.iter().enumerate().skip(start) {
250            let offset = primary_pts.saturating_sub(*pts);
251            if offset <= RED_MAX_OFFSET && data.len() <= RED_MAX_LEN {
252                idx[n] = i;
253                n += 1;
254            }
255        }
256        if n > 0 {
257            buf[0] = 0x01;
258            buf[1] = n as u8;
259            buf[2..6].copy_from_slice(&(primary_pts as u32).to_be_bytes());
260            let mut i = 6;
261            for &k in &idx[..n] {
262                let (data, pts) = &history[k];
263                let offset = primary_pts.saturating_sub(*pts) as u32;
264                buf[i] = 0x80 | (RED_BLOCK_PT & 0x7F);
265                let word = ((offset & 0x3FFF) << 10) | (data.len() as u32 & 0x3FF);
266                buf[i + 1] = (word >> 16) as u8;
267                buf[i + 2] = (word >> 8) as u8;
268                buf[i + 3] = word as u8;
269                i += 4;
270            }
271            buf[i] = RED_BLOCK_PT & 0x7F;
272            i += 1;
273            for &k in &idx[..n] {
274                let data = &history[k].0;
275                buf[i..i + data.len()].copy_from_slice(data);
276                i += data.len();
277            }
278            return i;
279        }
280    }
281    buf[0] = 0x01;
282    buf[1] = 0x00;
283    2
284}
285
286/// Capture/encode settings, snapshotted from `AudioCaptureSettings` at start.
287#[derive(Clone)]
288struct Settings {
289    device_name: Option<String>,
290    sample_rate: u32,
291    channels: i32,
292    opus_bitrate: i32,
293    frame_duration_ms: f64,
294    use_vbr: bool,
295    opus_complexity: Option<i32>,
296    use_silence_gate: bool,
297    debug_logging: bool,
298    latency_ms: i32,
299    omit_audio_header: bool,
300    red_distance: i32,
301    output_socket: String,
302}
303
304/// Highest Opus encoder complexity.
305const OPUS_COMPLEXITY_MAX: i32 = 10;
306
307/// True if `ms` is a valid Opus frame duration (2.5, 5, 10, 20, 40, or 60 ms).
308///
309/// The comparison is done in tenths of a millisecond (`round(ms * 10)`) so the fractional
310/// 2.5 ms case is accepted exactly, without float-equality pitfalls.
311fn valid_opus_duration(ms: f64) -> bool {
312    matches!((ms * 10.0).round() as i64, 25 | 50 | 100 | 200 | 400 | 600)
313}
314
315/// Normalize a Python `device_name` (`str | bytes | None`) into `Option<String>`,
316/// mapping both `None` and the empty string to `None` (meaning the system default).
317/// Shared by the capture and playback settings extractors.
318///
319/// An interior NUL is rejected here with `ValueError`: the name becomes a C string for
320/// libpulse on the worker thread, where it could only surface as a panic.
321fn parse_device_name(dev_obj: &Bound<'_, PyAny>) -> PyResult<Option<String>> {
322    if dev_obj.is_none() {
323        return Ok(None);
324    }
325    let v: String = if let Ok(st) = dev_obj.cast::<PyString>() {
326        st.to_str()?.to_string()
327    } else if let Ok(b) = dev_obj.cast::<PyBytes>() {
328        String::from_utf8_lossy(b.as_bytes()).into_owned()
329    } else {
330        dev_obj.extract()?
331    };
332    if v.contains('\0') {
333        return Err(pyo3::exceptions::PyValueError::new_err(
334            "device_name must not contain NUL bytes",
335        ));
336    }
337    Ok(if v.is_empty() { None } else { Some(v) })
338}
339
340/// Read a Python `AudioCaptureSettings` into a Rust `Settings` by attribute name,
341/// rejecting with `ValueError` what the worker could only fail on later.
342///
343/// `opus_bitrate` is clamped into the Opus range exactly as `update_audio_bitrate`
344/// clamps a live update; `red_distance` into `[0, RED_MAX_DISTANCE]` — it selects how
345/// many redundant Opus copies each frame carries, and cannot exceed the RFC 2198
346/// history depth. `latency_ms == 0` selects the default fragment size.
347fn extract_settings(s: &Bound<'_, PyAny>) -> PyResult<Settings> {
348    let device_name = parse_device_name(&s.getattr("device_name")?)?;
349    let parsed = Settings {
350        device_name,
351        sample_rate: s.getattr("sample_rate")?.extract()?,
352        channels: s.getattr("channels")?.extract()?,
353        opus_bitrate: s
354            .getattr("opus_bitrate")?
355            .extract::<i32>()?
356            .clamp(OPUS_BITRATE_MIN, OPUS_BITRATE_MAX),
357        frame_duration_ms: s.getattr("frame_duration_ms")?.extract()?,
358        use_vbr: s.getattr("use_vbr")?.extract()?,
359        opus_complexity: s.getattr("opus_complexity")?.extract()?,
360        use_silence_gate: s.getattr("use_silence_gate")?.extract()?,
361        debug_logging: s.getattr("debug_logging")?.extract()?,
362        latency_ms: s.getattr("latency_ms")?.extract()?,
363        omit_audio_header: s.getattr("omit_audio_header")?.extract()?,
364        red_distance: s
365            .getattr("red_distance")?
366            .extract::<i32>()?
367            .clamp(0, RED_MAX_DISTANCE),
368        output_socket: s.getattr("output_socket")?.extract()?,
369    };
370    check_opus_sample_rate(parsed.sample_rate)?;
371    if !valid_opus_duration(parsed.frame_duration_ms) {
372        return value_error(format!(
373            "frame_duration_ms must be one of 2.5, 5, 10, 20, 40, or 60 (got {})",
374            parsed.frame_duration_ms
375        ));
376    }
377    if let Some(c) = parsed.opus_complexity
378        && !(0..=OPUS_COMPLEXITY_MAX).contains(&c)
379    {
380        return value_error(format!(
381            "opus_complexity must be None or between 0 and {OPUS_COMPLEXITY_MAX} (got {c})"
382        ));
383    }
384    if !matches!(parsed.channels, 1 | 2 | 6 | 8) {
385        return value_error(format!(
386            "channels must be 1, 2, 6, or 8 (got {})",
387            parsed.channels
388        ));
389    }
390    if parsed.latency_ms < 0 {
391        return value_error(format!(
392            "latency_ms must be >= 0 (got {}); 0 selects the default fragment size",
393            parsed.latency_ms
394        ));
395    }
396    Ok(parsed)
397}
398
399/// Build a `ValueError` for a rejected settings field.
400fn value_error<T>(msg: String) -> PyResult<T> {
401    Err(pyo3::exceptions::PyValueError::new_err(msg))
402}
403
404/// Opus codecs only run at 8, 12, 16, 24, or 48 kHz; anything else would fail encoder or
405/// decoder creation on the worker, so it is rejected up front.
406fn check_opus_sample_rate(rate: u32) -> PyResult<()> {
407    if matches!(rate, 8000 | 12000 | 16000 | 24000 | 48000) {
408        Ok(())
409    } else {
410        value_error(format!(
411            "sample_rate must be 8000, 12000, 16000, 24000, or 48000 (got {rate})"
412        ))
413    }
414}
415
416/// Playback settings, snapshotted from the Python `AudioPlaybackSettings` at
417/// start so the playback thread owns an immutable copy for the run's lifetime.
418#[derive(Clone)]
419struct PbSettings {
420    device_name: Option<String>,
421    sample_rate: u32,
422    channels: i32,
423    latency_ms: i32,
424    max_buffer_bytes: usize,
425    debug_logging: bool,
426}
427
428/// Read a Python `AudioPlaybackSettings` into a Rust `PbSettings` by attribute name,
429/// rejecting with `ValueError` what the playback thread could only fail on later.
430///
431/// `latency_ms` must be positive: it sizes the sink buffer, and a zero target would make
432/// PulseAudio start playback with nothing prebuffered and underrun on every write.
433/// `max_buffer_bytes` must be positive: it bounds the drop-oldest queue, and a zero bound
434/// would discard every chunk as soon as it was queued.
435fn extract_pb_settings(s: &Bound<'_, PyAny>) -> PyResult<PbSettings> {
436    let parsed = PbSettings {
437        device_name: parse_device_name(&s.getattr("device_name")?)?,
438        sample_rate: s.getattr("sample_rate")?.extract()?,
439        channels: s.getattr("channels")?.extract()?,
440        latency_ms: s.getattr("latency_ms")?.extract()?,
441        max_buffer_bytes: s.getattr("max_buffer_bytes")?.extract()?,
442        debug_logging: s.getattr("debug_logging")?.extract()?,
443    };
444    check_opus_sample_rate(parsed.sample_rate)?;
445    if !matches!(parsed.channels, 1 | 2) {
446        return value_error(format!(
447            "playback channels must be 1 or 2 (got {})",
448            parsed.channels
449        ));
450    }
451    if parsed.latency_ms <= 0 {
452        return value_error(format!(
453            "latency_ms must be > 0 (got {})",
454            parsed.latency_ms
455        ));
456    }
457    if parsed.max_buffer_bytes == 0 {
458        return value_error("max_buffer_bytes must be > 0 (got 0)".to_string());
459    }
460    Ok(parsed)
461}
462
463/// Python-facing capture/encode configuration read by `start_capture`.
464///
465/// Declared `#[pyclass(dict)]` so callers may stash extra attributes on instances; the
466/// fields below are the ones read by attribute name in `extract_settings`. `device_name`
467/// accepts `str | bytes | None`.
468#[pyclass(dict)]
469struct AudioCaptureSettings {
470    #[pyo3(get, set)]
471    device_name: Py<PyAny>,
472    #[pyo3(get, set)]
473    sample_rate: u32,
474    #[pyo3(get, set)]
475    channels: i32,
476    #[pyo3(get, set)]
477    opus_bitrate: i32,
478    #[pyo3(get, set)]
479    frame_duration_ms: f64,
480    #[pyo3(get, set)]
481    use_vbr: bool,
482    /// Opus encoder complexity, 0 (cheapest) to 10; out-of-range values raise `ValueError` at
483    /// start. `None` leaves the encoder at libopus's default. Applied when the encoder is
484    /// created, so it does not change during a running capture.
485    #[pyo3(get, set)]
486    opus_complexity: Option<i32>,
487    #[pyo3(get, set)]
488    use_silence_gate: bool,
489    #[pyo3(get, set)]
490    debug_logging: bool,
491    #[pyo3(get, set)]
492    latency_ms: i32,
493    #[pyo3(get, set)]
494    omit_audio_header: bool,
495    #[pyo3(get, set)]
496    red_distance: i32,
497    /// Unix socket path on which the capture serves its packets as an Ogg Opus stream to
498    /// every consumer that connects; empty serves none. A path that cannot be bound fails the
499    /// start like any other startup error.
500    #[pyo3(get, set)]
501    output_socket: String,
502}
503
504#[pymethods]
505impl AudioCaptureSettings {
506    #[new]
507    fn new(py: Python<'_>) -> Self {
508        AudioCaptureSettings {
509            device_name: py.None(),
510            sample_rate: 48000,
511            channels: 2,
512            opus_bitrate: 128000,
513            frame_duration_ms: 20.0,
514            use_vbr: true,
515            opus_complexity: None,
516            use_silence_gate: true,
517            debug_logging: false,
518            latency_ms: 0,
519            omit_audio_header: false,
520            red_distance: 0,
521            output_socket: String::new(),
522        }
523    }
524}
525
526/// Python-facing mic-playback configuration read by `AudioPlayback.start`.
527///
528/// Defaults match the client mic wire (S16LE / mono / 24 kHz). `max_buffer_bytes` is the
529/// single byte bound on the drop-oldest playback queue (~2 s at 24 kHz mono s16), and
530/// `device_name` accepts `str | bytes | None`.
531#[pyclass]
532struct AudioPlaybackSettings {
533    #[pyo3(get, set)]
534    device_name: Py<PyAny>,
535    #[pyo3(get, set)]
536    sample_rate: u32,
537    #[pyo3(get, set)]
538    channels: i32,
539    #[pyo3(get, set)]
540    latency_ms: i32,
541    #[pyo3(get, set)]
542    max_buffer_bytes: usize,
543    #[pyo3(get, set)]
544    debug_logging: bool,
545}
546
547#[pymethods]
548impl AudioPlaybackSettings {
549    #[new]
550    fn new(py: Python<'_>) -> Self {
551        AudioPlaybackSettings {
552            device_name: PyString::new(py, "input").into_any().unbind(),
553            sample_rate: 24000,
554            channels: 1,
555            latency_ms: 40,
556            max_buffer_bytes: 96000,
557            debug_logging: false,
558        }
559    }
560}
561
562/// Zero-copy buffer-protocol result type handed to the Python callback.
563///
564/// Owns its `Vec<u8>` and exposes it read-only through the buffer protocol, so Python can
565/// read the encoded frame without a copy. When the last Python reference is released and
566/// the frame is dropped, a pooled buffer is recycled back to the capture thread (see the
567/// `Drop` impl), keeping the steady-state emit path allocation-free.
568#[pyclass]
569struct AudioFrame {
570    data: Vec<u8>,
571    pts: u64,
572    /// Channels the frame's Opus carries: the capture's own, or 2 for its stereo companion.
573    channels: u8,
574    /// Set when the buffer came from a capture's `BufferPool`; recycled to it on drop.
575    pool: Option<Arc<BufferPool>>,
576}
577
578impl Drop for AudioFrame {
579    fn drop(&mut self) {
580        if let Some(pool) = self.pool.take() {
581            pool.put(std::mem::take(&mut self.data));
582        }
583    }
584}
585
586#[pymethods]
587impl AudioFrame {
588    fn __len__(&self) -> usize {
589        self.data.len()
590    }
591
592    #[getter]
593    fn pts(&self) -> u64 {
594        self.pts
595    }
596
597    /// Channels the frame's Opus carries: the capture's own count, or 2 for a frame of the
598    /// stereo companion (`AudioCapture.set_stereo_companion`), which shares its pts.
599    #[getter]
600    fn channels(&self) -> u8 {
601        self.channels
602    }
603
604    /// Expose the owned bytes to Python's buffer protocol without a copy.
605    ///
606    /// `PyBuffer_FillInfo` INCREFs `slf` into `view->obj`, pinning the `Vec` alive until
607    /// every `memoryview` / slice over it is released. The view is readonly, so the
608    /// consumer cannot mutate the encoded frame.
609    unsafe fn __getbuffer__(
610        slf: PyRefMut<'_, Self>,
611        view: *mut pyo3::ffi::Py_buffer,
612        flags: std::os::raw::c_int,
613    ) -> PyResult<()> {
614        let r = unsafe {
615            pyo3::ffi::PyBuffer_FillInfo(
616                view,
617                slf.as_ptr(),
618                slf.data.as_ptr() as *mut std::os::raw::c_void,
619                slf.data.len() as pyo3::ffi::Py_ssize_t,
620                1,
621                flags,
622            )
623        };
624        if r != 0 {
625            return Err(PyErr::fetch(slf.py()));
626        }
627        Ok(())
628    }
629
630    unsafe fn __releasebuffer__(&self, _view: *mut pyo3::ffi::Py_buffer) {}
631}
632
633/// Shared state for one capture (or playback) run: the lifecycle state machine plus the
634/// per-frame settings mirrors the worker reads on the hot path — all lock-free except the
635/// rarely touched `last_error`.
636///
637/// The lifecycle is driven by two atomics — `stop_state` (the single source of truth for
638/// "should this run stop") and `start_state` (IDLE → STARTING → RUNNING, then FAILED with a
639/// `last_error` or back to IDLE on a clean stop) — plus `capture_tid` and `deliver_tid`, the
640/// worker and delivery threads' OS tids used to detect a re-entrant stop/start issued from
641/// inside the Python callback (which runs on the delivery thread). The remaining atomics
642/// mirror settings the worker consults each frame without re-snapshotting `Settings`, so
643/// `update_audio_bitrate` can retune the encoder mid-run without locking; the silence and
644/// header flags are published once at start and only read per frame.
645struct Inner {
646    /// Single lifecycle source of truth: `STOP_NONE` (running), `STOP_EXTERNAL`, or a
647    /// positive tid meaning the run self-stopped from inside its own callback (recorded
648    /// under the issuing thread's tid — the delivery thread's). A re-entrant start clears
649    /// only its own self-stop via compare-exchange, so it can never clobber an external
650    /// stop that raced in mid-join (which would strand it).
651    stop_state: AtomicI64,
652    started_ok: AtomicBool,
653    start_state: AtomicU8,
654    /// Why the last run failed (`start_state == ST_FAILED`), for Python's `last_error`.
655    /// Written before `ST_FAILED` is published and cleared when a new run is spawned, so
656    /// a failure that lands after the start handshake returned — the retry ladder giving
657    /// up, a mid-run reconnect budget spent, a worker panic — is still observable.
658    last_error: Mutex<Option<String>>,
659    /// OS tid of the running capture thread; `0` when no worker is live.
660    capture_tid: AtomicI64,
661    /// OS tid of the running delivery thread (the one that invokes the Python callback);
662    /// `0` when none is live. Checked by the re-entrancy guards alongside `capture_tid`,
663    /// because a stop/start issued from inside the callback executes on THIS thread — a
664    /// join from it would cycle (stopper joins capture, capture joins delivery).
665    deliver_tid: AtomicI64,
666    /// Lock-free per-frame settings mirrors, re-read by the worker each frame.
667    /// `opus_bitrate` is republished by `update_audio_bitrate`; the rest are published
668    /// once, by the run that starts.
669    opus_bitrate: AtomicI32,
670    use_silence_gate: AtomicBool,
671    debug_logging: AtomicBool,
672    emit_audio_header: AtomicBool,
673    /// Whether a surround capture also emits every frame downmixed to stereo; toggled live.
674    stereo_companion: AtomicBool,
675}
676
677impl Inner {
678    fn new() -> Self {
679        Inner {
680            stop_state: AtomicI64::new(STOP_NONE),
681            started_ok: AtomicBool::new(false),
682            start_state: AtomicU8::new(ST_IDLE),
683            last_error: Mutex::new(None),
684            capture_tid: AtomicI64::new(0),
685            deliver_tid: AtomicI64::new(0),
686            opus_bitrate: AtomicI32::new(128000),
687            use_silence_gate: AtomicBool::new(true),
688            debug_logging: AtomicBool::new(false),
689            emit_audio_header: AtomicBool::new(true),
690            stereo_companion: AtomicBool::new(false),
691        }
692    }
693
694    /// Request an authoritative external stop, stored unconditionally.
695    ///
696    /// The single source of truth for stopping a run. External stops win every race: they
697    /// are published (inside the lifecycle lock, immediately before join) with a plain
698    /// store, so a concurrent self-stop/self-start — which only ever compare-exchanges
699    /// state it owns — can never clobber one and strand the join forever.
700    fn request_external_stop(&self) {
701        self.stop_state.store(STOP_EXTERNAL, Ordering::Release);
702    }
703
704    /// Record a re-entrant self-stop from inside the run's own callback.
705    ///
706    /// Only transitions `STOP_NONE -> me` via compare-exchange, so it never overwrites a
707    /// pending external stop (which must win the join).
708    fn request_self_stop(&self, me: i64) {
709        let _ =
710            self.stop_state
711                .compare_exchange(STOP_NONE, me, Ordering::AcqRel, Ordering::Acquire);
712    }
713
714    /// Undo a re-entrant self-stop (self-start from inside the callback).
715    ///
716    /// Compare-exchanges `me -> STOP_NONE`, clearing the stop only if this same thread
717    /// still owns it. If an external stop landed in between, the CAS fails and that stop
718    /// stands.
719    fn undo_self_stop(&self, me: i64) {
720        let _ =
721            self.stop_state
722                .compare_exchange(me, STOP_NONE, Ordering::AcqRel, Ordering::Acquire);
723    }
724
725    /// Clear the stop state back to running. Only ever called under the lifecycle
726    /// lock (after join, before spawn), where no external stop can be in flight — the
727    /// lost-stop invariant that lets this be an unconditional store.
728    fn clear_stop(&self) {
729        self.stop_state.store(STOP_NONE, Ordering::Release);
730    }
731
732    /// True once any stop (external or self) is pending; the hot loops poll this.
733    fn stop_pending(&self) -> bool {
734        self.stop_state.load(Ordering::Acquire) != STOP_NONE
735    }
736
737    /// True while a worker is (or is still becoming) live: the startup handshake
738    /// is in flight, or the hot loop is running with no stop pending.
739    ///
740    /// Goes false the moment the worker fails, is stopped, or dies mid-run — the hot loop
741    /// clears `started_ok` before breaking on error, even with no stop pending and
742    /// `start_state` still `RUNNING`. Producers (e.g. `AudioPlayback::write`) gate on this
743    /// so they surface a dead stream instead of feeding state nothing services.
744    fn worker_alive(&self) -> bool {
745        self.start_state.load(Ordering::Acquire) == ST_STARTING || self.running()
746    }
747
748    /// True while the worker is connected and running with no stop pending — the
749    /// Python `is_capturing` / `is_running` getters.
750    fn running(&self) -> bool {
751        self.started_ok.load(Ordering::Acquire) && !self.stop_pending()
752    }
753
754    /// Lifecycle phase for Python's `state`: `"idle"` (no run yet, or the last run stopped
755    /// cleanly), `"starting"` (worker spawned, first PulseAudio session not yet up),
756    /// `"running"` (connected, or reconnecting mid-run, with no stop pending), or
757    /// `"failed"` (the last run ended in error; `last_error` says why). A run that was
758    /// stopped reads `"idle"` again even while its thread is still winding down.
759    fn state_name(&self) -> &'static str {
760        match self.start_state.load(Ordering::Acquire) {
761            ST_STARTING => "starting",
762            ST_FAILED => "failed",
763            ST_RUNNING if self.running() => "running",
764            _ => "idle",
765        }
766    }
767
768    /// Mark the run dead with a reason: logs `msg` to stderr, records it for `last_error`,
769    /// and only then publishes `ST_FAILED`, so a reader that observes the failed state also
770    /// finds its message. Every terminal worker error goes through here.
771    fn fail(&self, msg: String) {
772        elog!("[pcmflux] ERROR: {msg}");
773        *self.last_error.lock().unwrap_or_else(|e| e.into_inner()) = Some(msg);
774        self.started_ok.store(false, Ordering::Release);
775        self.start_state.store(ST_FAILED, Ordering::Release);
776    }
777
778    /// The message of the last run's failure, or `None` while no run has failed since the
779    /// last (re)start.
780    fn last_error(&self) -> Option<String> {
781        self.last_error
782            .lock()
783            .unwrap_or_else(|e| e.into_inner())
784            .clone()
785    }
786
787    /// Forget the previous run's failure; called by `spawn_worker` as a new run is armed.
788    fn clear_error(&self) {
789        *self.last_error.lock().unwrap_or_else(|e| e.into_inner()) = None;
790    }
791
792    /// True when the calling thread is one of this run's own threads — the capture
793    /// worker or the delivery thread that runs the Python callback — i.e. the call is a
794    /// re-entrant stop/start/drop from inside the callback. Such a caller must never
795    /// join: teardown has the capture thread join the delivery thread, so a join from
796    /// either one closes a cycle and deadlocks.
797    fn is_own_thread(&self, me: i64) -> bool {
798        self.capture_tid.load(Ordering::Acquire) == me
799            || self.deliver_tid.load(Ordering::Acquire) == me
800    }
801}
802
803/// Per-`AudioCapture` shared handle: the lock-free `Inner` state plus the
804/// lifecycle-locked join handle for the capture thread.
805struct Shared {
806    inner: Arc<Inner>,
807    /// Lifecycle lock: serializes take/join/reassign of the capture thread's handle.
808    thread: Mutex<Option<JoinHandle<()>>>,
809}
810
811/// Process-wide registry of live captures, swept at interpreter exit. Holds `Weak`
812/// references so it keeps nothing alive on its own.
813static REGISTRY: OnceLock<Mutex<Vec<Weak<Shared>>>> = OnceLock::new();
814/// Lazily initialize and return the capture registry.
815fn registry() -> &'static Mutex<Vec<Weak<Shared>>> {
816    REGISTRY.get_or_init(|| Mutex::new(Vec::new()))
817}
818
819/// Join the worker `slot` holds, having set the external stop INSIDE the caller's lock
820/// immediately before `join()` — the set-before-join ordering the lost-stop invariant rests on
821/// — and clear `capture_tid` so a later start sees no stale owner. Every stop path ends here.
822fn join_held_worker(slot: &mut Option<JoinHandle<()>>, inner: &Inner) {
823    if let Some(handle) = slot.take() {
824        inner.request_external_stop();
825        let _ = handle.join();
826        inner.capture_tid.store(0, Ordering::Release);
827    }
828}
829
830/// `join_held_worker` behind the lifecycle lock, for the stop paths that want nothing else from
831/// it. A lock poisoned by an unrelated panic is taken anyway: it must never be what leaves a
832/// worker thread running into interpreter finalization.
833fn stop_and_join(slot: &Mutex<Option<JoinHandle<()>>>, inner: &Inner) {
834    join_held_worker(&mut slot.lock().unwrap_or_else(|e| e.into_inner()), inner);
835}
836
837/// Locked takeover + spawn of a worker thread, shared by the capture and playback
838/// starts. Returns the new thread's id, or `None` if the spawn failed.
839///
840/// Under the lifecycle lock, in order:
841///
842/// 1. **Stop and join any prior worker** through `join_held_worker`, under this lock.
843/// 2. **Reset the lifecycle state**: clears `stop_state` back to `STOP_NONE` (done ONLY
844///    here, under the lock, after the join and before the spawn, where no external stop
845///    can be in flight), forgets the previous run's `last_error`, and arms the startup
846///    handshake at `ST_STARTING`.
847/// 3. **Spawn `body` on a named thread**: the thread applies a best-effort `nice` boost
848///    (audio must not stutter when the captured workload saturates the CPU; EPERM without
849///    `CAP_SYS_NICE` is silently a no-op), publishes its OS tid into `capture_tid` for the
850///    re-entrancy guard, runs `body`, then clears the tid on exit.
851///
852/// The returned `ThreadId` is the identity a failed start later hands to
853/// `join_failed_start`, so a losing start tears down only the thread it spawned.
854fn spawn_worker(
855    slot: &Mutex<Option<JoinHandle<()>>>,
856    inner: &Arc<Inner>,
857    name: &str,
858    body: impl FnOnce() + Send + 'static,
859) -> Option<ThreadId> {
860    let mut guard = slot.lock().unwrap_or_else(|e| e.into_inner());
861    join_held_worker(&mut guard, inner);
862    inner.clear_stop();
863    inner.clear_error();
864    inner.started_ok.store(false, Ordering::Release);
865    inner.start_state.store(ST_STARTING, Ordering::Release);
866    let t_inner = inner.clone();
867    match std::thread::Builder::new()
868        .name(name.into())
869        .spawn(move || {
870            unsafe {
871                let _ = libc::setpriority(libc::PRIO_PROCESS, gettid() as libc::id_t, -15);
872            }
873            t_inner.capture_tid.store(gettid(), Ordering::Release);
874            // A worker panic must flip the liveness contract (started_ok/start_state):
875            // an unguarded unwind would leave is_capturing reporting true forever with
876            // no frames flowing and no error anywhere.
877            if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)) {
878                let what = payload
879                    .downcast_ref::<&str>()
880                    .map(|s| s.to_string())
881                    .or_else(|| payload.downcast_ref::<String>().cloned())
882                    .unwrap_or_else(|| "unknown panic payload".to_string());
883                t_inner.fail(format!("worker thread panicked: {what}"));
884            }
885            t_inner.capture_tid.store(0, Ordering::Release);
886        }) {
887        Ok(h) => {
888            let id = h.thread().id();
889            *guard = Some(h);
890            Some(id)
891        }
892        Err(_) => None,
893    }
894}
895
896/// Failed-start teardown: stop and join the thread `spawned` by THIS start attempt,
897/// but only if it still owns the slot.
898///
899/// A concurrent start may have already joined this thread and published a live replacement,
900/// which must not be torn down. The guard is an identity check: `ThreadId`s are never reused
901/// within a process, so it cannot false-match. When it does own the slot, the teardown is
902/// `join_held_worker`'s, like every other stop path.
903fn join_failed_start(slot: &Mutex<Option<JoinHandle<()>>>, inner: &Inner, spawned: ThreadId) {
904    let mut guard = slot.lock().unwrap_or_else(|e| e.into_inner());
905    if guard.as_ref().map(|h| h.thread().id()) != Some(spawned) {
906        return;
907    }
908    join_held_worker(&mut guard, inner);
909}
910
911/// Startup handshake shared by the capture and playback starts: with the GIL released,
912/// waits up to ~2 s for the worker this call spawned (`spawned`) to publish `RUNNING` or
913/// `FAILED`. A worker still `STARTING` when the window closes counts as started — its
914/// retry ladder legitimately runs longer than this — and a failure after that is exposed
915/// through `state` / `last_error` instead. On `FAILED`, `join_failed_start` tears down only
916/// that thread (identity-checked, sparing a concurrent winner) and the worker's recorded
917/// error is raised as `RuntimeError`.
918fn await_start(
919    py: Python<'_>,
920    slot: &Mutex<Option<JoinHandle<()>>>,
921    inner: &Inner,
922    spawned: ThreadId,
923    what: &str,
924) -> PyResult<()> {
925    let mut state = inner.start_state.load(Ordering::Acquire);
926    if state == ST_STARTING {
927        py.detach(|| {
928            for _ in 0..200 {
929                state = inner.start_state.load(Ordering::Acquire);
930                if state != ST_STARTING {
931                    break;
932                }
933                std::thread::sleep(Duration::from_millis(10));
934            }
935        });
936    }
937    if state == ST_FAILED {
938        py.detach(|| join_failed_start(slot, inner, spawned));
939        let why = inner
940            .last_error()
941            .unwrap_or_else(|| "unknown error".to_string());
942        return Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
943            "{what} failed to start: {why}"
944        )));
945    }
946    Ok(())
947}
948
949/// Bounded, drop-oldest byte queue for the mic-PCM handoff into the virtual
950/// "input" sink — the whole of the playback path's buffering in a single bound.
951///
952/// `push` runs on the Python side with the GIL released; `drain_upto` and
953/// `splice_head` run on the playback thread. Overflow discards the OLDEST bytes, keeping
954/// the newest window (mic audio is drift-tolerant and stale samples are worthless); the
955/// depth that stands below the bound is cut by the playback thread (`StandingDepth`).
956struct PlayQueue {
957    buf: Mutex<VecDeque<u8>>,
958    /// Bounds (re)published by `start()`; atomics so a restart can reconfigure the
959    /// `Arc`-shared queue in place without swapping it. `frame_bytes >= 1` (set at `new()`).
960    max_bytes: AtomicUsize,
961    frame_bytes: AtomicUsize,
962}
963
964impl PlayQueue {
965    fn new() -> Self {
966        PlayQueue {
967            buf: Mutex::new(VecDeque::new()),
968            max_bytes: AtomicUsize::new(96000),
969            frame_bytes: AtomicUsize::new(2),
970        }
971    }
972
973    /// Apply this run's byte bound and frame alignment, and drop any stale audio
974    /// left from a prior run. `frame_bytes` is floored to 1; the bound is floored
975    /// to a whole-frame multiple (min one frame) so overflow drops can never split
976    /// a sample frame.
977    fn configure(&self, max_bytes: usize, frame_bytes: usize) {
978        let fb = frame_bytes.max(1);
979        self.frame_bytes.store(fb, Ordering::Relaxed);
980        self.max_bytes
981            .store((max_bytes / fb * fb).max(fb), Ordering::Relaxed);
982        self.clear();
983    }
984
985    /// Drop everything queued, keeping the bounds. Used when a run starts and whenever the
986    /// playback session is reopened, since audio buffered across an outage is stale.
987    fn clear(&self) {
988        self.buf.lock().unwrap_or_else(|e| e.into_inner()).clear();
989    }
990
991    /// Append client PCM, dropping the OLDEST whole frames once the queue passes
992    /// the byte bound so the newest audio is always retained. Drops stay
993    /// frame-aligned: trimming mid-frame would phase-shift every later drain into
994    /// interleaved garbage.
995    fn push(&self, data: &[u8]) {
996        let max = self.max_bytes.load(Ordering::Relaxed);
997        let fb = self.frame_bytes.load(Ordering::Relaxed);
998        let mut q = self.buf.lock().unwrap_or_else(|e| e.into_inner());
999        q.extend(data.iter().copied());
1000        let over = q.len().saturating_sub(max);
1001        if over > 0 {
1002            let drop = (over.div_ceil(fb) * fb).min(q.len());
1003            q.drain(..drop);
1004        }
1005    }
1006
1007    /// Drain up to `n` bytes into `out`, clamped to what is queued and floored to a
1008    /// whole frame, since a PA write must be a multiple of the sample-spec frame size.
1009    /// Returns the bytes left queued.
1010    fn drain_upto(&self, n: usize, out: &mut Vec<u8>) -> usize {
1011        let fb = self.frame_bytes.load(Ordering::Relaxed);
1012        let mut q = self.buf.lock().unwrap_or_else(|e| e.into_inner());
1013        let mut take = n.min(q.len());
1014        take -= take % fb;
1015        out.clear();
1016        out.extend(q.drain(..take));
1017        q.len()
1018    }
1019
1020    /// Cut `cut` bytes out right behind the head, the next audio the sink plays: the
1021    /// `fade` bytes at the head, which continue what the sink already holds, are blended
1022    /// linearly into the `fade` bytes after the cut, so the seam is a short crossfade
1023    /// rather than a step. Both are floored to whole frames, and nothing is cut unless
1024    /// `cut + fade` bytes are queued.
1025    fn splice_head(&self, cut: usize, fade: usize) {
1026        let fb = self.frame_bytes.load(Ordering::Relaxed);
1027        let (cut, fade) = (cut / fb * fb, fade / fb * fb);
1028        let mut q = self.buf.lock().unwrap_or_else(|e| e.into_inner());
1029        if cut == 0 || cut + fade > q.len() {
1030            return;
1031        }
1032        let channels = (fb / 2).max(1);
1033        let frames = (fade / fb) as f32;
1034        for i in (0..fade).step_by(2) {
1035            let w = ((i / 2 / channels) as f32 + 1.0) / (frames + 1.0);
1036            let a = i16::from_le_bytes([q[i], q[i + 1]]) as f32;
1037            let b = i16::from_le_bytes([q[cut + i], q[cut + i + 1]]) as f32;
1038            let [lo, hi] = ((a + (b - a) * w).round() as i16).to_le_bytes();
1039            q[cut + i] = lo;
1040            q[cut + i + 1] = hi;
1041        }
1042        q.drain(..cut);
1043    }
1044}
1045
1046/// The playback queue's standing depth, and when to cut it.
1047///
1048/// The bytes left queued after each drain are tracked at their minimum over a window of
1049/// one second of written audio. What stayed queued at every drain of a whole window was
1050/// never needed to ride out the uplink's jitter, which the sink's own buffer covers, so it
1051/// is pure delay: a burst after a network stall or a stalled tab, a client clock running
1052/// fast of the sink, or a sink that resumed late leaves it there, and with the drain rate
1053/// equal to the arrival rate it would stay for the rest of the session. A window that
1054/// closes with at least `min_cut` of it above `keep` cuts down to `keep`, the length of
1055/// the splice's crossfade; smaller excess waits, so drift is shed in a few cuts rather than
1056/// a splice every window.
1057struct StandingDepth {
1058    window: usize,
1059    keep: usize,
1060    min_cut: usize,
1061    frame_bytes: usize,
1062    written: usize,
1063    min_left: usize,
1064}
1065
1066impl StandingDepth {
1067    /// Size the window, the crossfade kept, and the least cut for a stream of
1068    /// `bytes_per_sec` in frames of `frame_bytes`: one second, 2 ms, and 10 ms.
1069    fn new(bytes_per_sec: usize, frame_bytes: usize) -> Self {
1070        let fb = frame_bytes.max(1);
1071        let frames = |ms: usize| (bytes_per_sec * ms / 1000 / fb).max(1) * fb;
1072        StandingDepth {
1073            window: frames(1000),
1074            keep: frames(2),
1075            min_cut: frames(10),
1076            frame_bytes: fb,
1077            written: 0,
1078            min_left: usize::MAX,
1079        }
1080    }
1081
1082    /// Start a fresh window, as after the queue was cleared.
1083    fn reset(&mut self) {
1084        self.written = 0;
1085        self.min_left = usize::MAX;
1086    }
1087
1088    /// Record one drain that wrote `written` bytes and left `left` queued; returns the
1089    /// bytes to cut when this drain closes a window over a standing excess.
1090    fn after_drain(&mut self, written: usize, left: usize) -> Option<usize> {
1091        self.written += written;
1092        self.min_left = self.min_left.min(left);
1093        if self.written < self.window {
1094            return None;
1095        }
1096        let excess = self.min_left.saturating_sub(self.keep);
1097        self.reset();
1098        (excess >= self.min_cut).then(|| excess / self.frame_bytes * self.frame_bytes)
1099    }
1100}
1101
1102/// Turns the mic uplink back into PCM: the client always sends the mic as Opus, so
1103/// every inbound packet must be decoded before it can be queued for the virtual sink. It
1104/// decodes one packet to interleaved S16LE PCM, reusing a scratch buffer across calls to
1105/// stay off the per-decode allocation path. Lives behind a `Mutex` on `PbShared` and is
1106/// driven from `write` / `write_red`.
1107struct OpusPlaybackDecoder {
1108    dec: OpusDec,
1109    channels: usize,
1110    pcm: Vec<i16>,
1111    /// Inbound scratch for a buffer-protocol payload, copied here under the GIL so the
1112    /// off-GIL decode never reads a buffer another Python thread could be mutating.
1113    packet: Vec<u8>,
1114    /// RFC 2198 RED recovery cursor: the timestamp of the last frame decoded, so a
1115    /// redundant copy of a dropped frame is decoded exactly once, in order. `None` until
1116    /// the first RED frame arrives.
1117    last_ts: Option<i64>,
1118}
1119
1120/// Owning wrapper over a raw `OpusDecoder`.
1121///
1122/// The pointer is only touched from the thread holding the enclosing `Mutex`, which is what
1123/// makes the `unsafe impl Send` sound; `Drop` destroys the C decoder.
1124struct OpusDec {
1125    st: *mut opusic_sys::OpusDecoder,
1126}
1127
1128unsafe impl Send for OpusDec {}
1129
1130impl Drop for OpusDec {
1131    fn drop(&mut self) {
1132        unsafe { opusic_sys::opus_decoder_destroy(self.st) }
1133    }
1134}
1135
1136impl OpusPlaybackDecoder {
1137    /// Create a mono/stereo Opus decoder for the mic uplink; `None` if creation
1138    /// fails. `channels <= 1` decodes as mono, otherwise stereo.
1139    fn new(sample_rate: u32, channels: i32) -> Option<Self> {
1140        let mut err = 0;
1141        let st = unsafe {
1142            opusic_sys::opus_decoder_create(
1143                sample_rate as i32,
1144                if channels <= 1 { 1 } else { 2 },
1145                &mut err,
1146            )
1147        };
1148        if st.is_null() || err != 0 {
1149            return None;
1150        }
1151        let dec = OpusDec { st };
1152        Some(OpusPlaybackDecoder {
1153            dec,
1154            channels: channels.max(1) as usize,
1155            pcm: Vec::new(),
1156            packet: Vec::new(),
1157            last_ts: None,
1158        })
1159    }
1160
1161    /// Decode one Opus packet and return the interleaved S16LE PCM as bytes, or `None`
1162    /// for an empty or undecodable packet.
1163    ///
1164    /// The scratch `pcm` buffer is grown once to `5760 * channels` — an Opus packet decodes
1165    /// to at most 120 ms, which is 5760 samples per channel at 48 kHz — then reused across
1166    /// calls, and the result is a view straight over it, so a decode never allocates. The
1167    /// view borrows the decoder, so callers must queue it before decoding the next packet.
1168    /// Reinterpreting the samples as S16LE bytes assumes a little-endian host, exactly as
1169    /// the capture side does when it fills `accum` from PulseAudio fragments.
1170    fn decode_to_pcm(&mut self, packet: &[u8]) -> Option<&[u8]> {
1171        if packet.is_empty() {
1172            return None;
1173        }
1174        let cap = 5760 * self.channels;
1175        if self.pcm.len() < cap {
1176            self.pcm.resize(cap, 0);
1177        }
1178        let samples = unsafe {
1179            opusic_sys::opus_decode(
1180                self.dec.st,
1181                packet.as_ptr(),
1182                packet.len() as i32,
1183                self.pcm.as_mut_ptr(),
1184                (cap / self.channels) as i32,
1185                0,
1186            )
1187        };
1188        if samples < 0 {
1189            return None;
1190        }
1191        let n = samples as usize * self.channels;
1192        Some(bytemuck::cast_slice(&self.pcm[..n]))
1193    }
1194
1195    /// Reconstruct the mic uplink across packet loss: recover any frames the sender
1196    /// dropped from the redundant copies RED carries, and decode each new frame exactly once
1197    /// into `queue`. This is what lets a lossy UDP/WebRTC uplink play through gaps without ever
1198    /// waiting for a retransmit. It all runs off the GIL — the work is pure byte-slicing plus
1199    /// Opus decode with no Python state, so releasing the GIL keeps the mic path from
1200    /// serializing behind the rest of the interpreter.
1201    ///
1202    /// 1. **Parse the block headers**: walk the redundant headers (F bit set) collecting each
1203    ///    block's 14-bit timestamp offset and 10-bit length, then consume the 1-byte primary
1204    ///    header (F bit clear). A truncated payload, or more redundancy than `RED_MAX_DISTANCE`,
1205    ///    bails out.
1206    /// 2. **Resolve block boundaries**: turn the headers into `(ts, start, len)` triples,
1207    ///    oldest-first, where each redundant `ts` is `primary_ts - offset` and the primary is
1208    ///    whatever bytes remain.
1209    /// 3. **Anchor the first packet**: with no prior `last_ts`, decode only the primary and set
1210    ///    `last_ts` to it — its trailing redundancy describes frames never played, so it is not
1211    ///    replayed.
1212    /// 4. **Recover and advance**: otherwise decode every block whose `ts` is strictly newer
1213    ///    than `last_ts` (in oldest-first order, so a gap left by a dropped packet is filled
1214    ///    before the primary), pushing PCM to `queue` and advancing `last_ts`. Blocks at or
1215    ///    below `last_ts` are already-played duplicates and are skipped — the timestamp dedup
1216    ///    that makes redundancy free of double-decoding under no loss.
1217    fn decode_red_into_queue(&mut self, payload: &[u8], primary_ts: i64, queue: &PlayQueue) {
1218        let n = payload.len();
1219        let mut i = 0usize;
1220        let mut offs = [0i64; RED_MAX_DISTANCE as usize];
1221        let mut lens = [0usize; RED_MAX_DISTANCE as usize];
1222        let mut nh = 0usize;
1223        while i < n && (payload[i] & 0x80) != 0 {
1224            if i + 4 > n || nh >= RED_MAX_DISTANCE as usize {
1225                return;
1226            }
1227            let field = ((payload[i + 1] as u32) << 16)
1228                | ((payload[i + 2] as u32) << 8)
1229                | (payload[i + 3] as u32);
1230            offs[nh] = ((field >> 10) & 0x3FFF) as i64;
1231            lens[nh] = (field & 0x3FF) as usize;
1232            nh += 1;
1233            i += 4;
1234        }
1235        if i >= n {
1236            return;
1237        }
1238        i += 1;
1239
1240        let mut frames = [(0i64, 0usize, 0usize); RED_MAX_DISTANCE as usize + 1];
1241        let mut nf = 0usize;
1242        for k in 0..nh {
1243            if i + lens[k] > n {
1244                return;
1245            }
1246            if lens[k] > 0 {
1247                frames[nf] = (primary_ts - offs[k], i, lens[k]);
1248                nf += 1;
1249            }
1250            i += lens[k];
1251        }
1252        frames[nf] = (primary_ts, i, n - i);
1253        nf += 1;
1254
1255        if self.last_ts.is_none() {
1256            let (ts, start, len) = frames[nf - 1];
1257            if len > 0
1258                && let Some(pcm) = self.decode_to_pcm(&payload[start..start + len])
1259            {
1260                queue.push(pcm);
1261            }
1262            self.last_ts = Some(ts);
1263            return;
1264        }
1265        let mut last = self.last_ts.unwrap();
1266        for &(ts, start, len) in frames.iter().take(nf) {
1267            // RTP timestamps are 32-bit and wrap; compare serial-number style so a
1268            // wraparound isn't mistaken for "already played" (ts > last would fail
1269            // for every frame after the rollover).
1270            let newer = ((ts.wrapping_sub(last)) as i32) > 0;
1271            if len > 0 && newer {
1272                if let Some(pcm) = self.decode_to_pcm(&payload[start..start + len]) {
1273                    queue.push(pcm);
1274                }
1275                last = ts;
1276            }
1277        }
1278        self.last_ts = Some(last);
1279    }
1280}
1281
1282/// Per-`AudioPlayback` shared handle — the mirror of `Shared` for the playback path.
1283///
1284/// Reuses the capture lifecycle core on `Inner` (the `stop_state` protocol, the start
1285/// handshake, and the worker-tid re-entrancy guard); the Opus/silence settings mirrors on
1286/// `Inner` are unused here. Adds the bounded PCM `queue` and the always-Opus mic decoder.
1287struct PbShared {
1288    inner: Arc<Inner>,
1289    thread: Mutex<Option<JoinHandle<()>>>,
1290    queue: Arc<PlayQueue>,
1291    /// The mic uplink is always Opus; `write` / `write_red` decode each packet through this
1292    /// before enqueuing PCM. Set at `start()`; `None` only before a successful start.
1293    opus_dec: Mutex<Option<OpusPlaybackDecoder>>,
1294}
1295
1296/// Process-wide registry of live playbacks, swept by the same atexit sweep as
1297/// captures. Holds `Weak` references so it keeps nothing alive on its own.
1298static PLAYBACK_REGISTRY: OnceLock<Mutex<Vec<Weak<PbShared>>>> = OnceLock::new();
1299/// Lazily initialize and return the playback registry.
1300fn playback_registry() -> &'static Mutex<Vec<Weak<PbShared>>> {
1301    PLAYBACK_REGISTRY.get_or_init(|| Mutex::new(Vec::new()))
1302}
1303
1304/// Run one bounded iteration of a PulseAudio standard mainloop:
1305/// `prepare(timeout_us)` → `poll` → `dispatch`. Returns `false` if any stage errors.
1306///
1307/// The `timeout_us` bound is what makes a pending stop observable within ~20 ms even when
1308/// the audio source delivers nothing, since every loop that calls this re-checks
1309/// `stop_pending()` between pumps.
1310fn pump(ml: &mut pulse::mainloop::standard::Mainloop, timeout_us: u64) -> bool {
1311    if ml.prepare(Some(MicroSeconds(timeout_us))).is_err() {
1312        return false;
1313    }
1314    if ml.poll().is_err() {
1315        return false;
1316    }
1317    if ml.dispatch().is_err() {
1318        return false;
1319    }
1320    true
1321}
1322
1323/// Chromium's multistream-Opus surround layout for a channel count, as
1324/// `(streams, coupled, mapping)`; `None` for anything but 5.1 (6) or 7.1 (8).
1325///
1326/// The same tables are advertised in the WebRTC SDP (`multiopus`), so the browser's decoder
1327/// inverts exactly the stream/coupling/mapping this encoder applies.
1328fn multiopus_layout(channels: i32) -> Option<(i32, i32, &'static [u8])> {
1329    match channels {
1330        6 => Some((4, 2, &[0, 4, 1, 2, 3, 5])),
1331        8 => Some((5, 3, &[0, 6, 1, 2, 3, 4, 5, 7])),
1332        _ => None,
1333    }
1334}
1335
1336/// The speaker positions the surround encoder takes its input channels as, in order, as a
1337/// PulseAudio channel map: Chromium's 5.1 and 7.1 orders (left, right, center, LFE, then the
1338/// surround pair, and at 7.1 the back pair before the side one), which its `multiopus`
1339/// decoder hands to the output layout. The record stream asks for this map, so the sound
1340/// server remixes the source's own layout into it. Without one, libpulse gives a stream its
1341/// AIFF default, which at six channels names front-left-of-center, front-right-of-center, and
1342/// rear-center instead of the surround pair and LFE, and at eight has no map at all, so the
1343/// stream cannot even be created. `None` for mono and stereo, whose defaults are right.
1344fn surround_channel_map(channels: i32) -> Option<&'static str> {
1345    match channels {
1346        6 => Some("front-left,front-right,front-center,lfe,rear-left,rear-right"),
1347        8 => Some(
1348            "front-left,front-right,front-center,lfe,rear-left,rear-right,side-left,side-right",
1349        ),
1350        _ => None,
1351    }
1352}
1353
1354/// Fold one surround frame (`surround_channel_map` order) to interleaved stereo, `dst`
1355/// holding the frame's samples per channel twice: each side takes its front channel at
1356/// unity and the center and its surround channels at -3 dB (ITU-R BS.775, the LFE left
1357/// out), saturating rather than wrapping, as a browser's own speaker downmix of the
1358/// surround stream sounds.
1359fn downmix_stereo(src: &[i16], channels: usize, dst: &mut [i16]) {
1360    const K: f32 = std::f32::consts::FRAC_1_SQRT_2;
1361    for (f, out) in dst.as_chunks_mut::<2>().0.iter_mut().enumerate() {
1362        let s = &src[f * channels..(f + 1) * channels];
1363        let center = K * s[2] as f32;
1364        let mut left = s[0] as f32 + center + K * s[4] as f32;
1365        let mut right = s[1] as f32 + center + K * s[5] as f32;
1366        if channels >= 8 {
1367            left += K * s[6] as f32;
1368            right += K * s[7] as f32;
1369        }
1370        out[0] = left.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
1371        out[1] = right.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
1372    }
1373}
1374
1375/// One encode surface over both Opus APIs: the single-stream C encoder for mono/stereo, and
1376/// the multistream one for 6/8-channel surround.
1377enum PcmEncoder {
1378    Stereo(MonoOpus),
1379    Multi(MultiOpus),
1380}
1381
1382/// Owning wrapper over a raw `OpusEncoder` (the mono/stereo single-stream encoder).
1383///
1384/// Owned by the capture thread exactly as `MultiOpus` is; `Drop` destroys the C encoder.
1385struct MonoOpus {
1386    st: *mut opusic_sys::OpusEncoder,
1387}
1388
1389unsafe impl Send for MonoOpus {}
1390
1391impl Drop for MonoOpus {
1392    fn drop(&mut self) {
1393        unsafe { opusic_sys::opus_encoder_destroy(self.st) }
1394    }
1395}
1396
1397/// Owning wrapper over a raw `OpusMSEncoder` (the surround multistream encoder).
1398///
1399/// The raw pointer is only ever touched from the capture thread that owns the enclosing
1400/// `RunState`, which is what makes the `unsafe impl Send` below sound; `Drop` destroys the
1401/// C encoder.
1402struct MultiOpus {
1403    st: *mut opusic_sys::OpusMSEncoder,
1404}
1405
1406unsafe impl Send for MultiOpus {}
1407
1408impl Drop for MultiOpus {
1409    fn drop(&mut self) {
1410        unsafe { opusic_sys::opus_multistream_encoder_destroy(self.st) }
1411    }
1412}
1413
1414impl PcmEncoder {
1415    /// Build the Opus encoder for a channel count, selecting the API by width.
1416    ///
1417    /// - **Mono/stereo** (`channels <= 2`): the single-stream C encoder in
1418    ///   `RESTRICTED_LOWDELAY`; a failure to apply the initial bitrate or VBR mode is logged
1419    ///   but not fatal.
1420    /// - **Surround** (6/8): the raw multistream C encoder created from `multiopus_layout`
1421    ///   in `RESTRICTED_LOWDELAY`; an unsupported channel count is a hard error.
1422    ///
1423    /// Bitrate, VBR, and a `Some` complexity are applied at creation; `None` sends no complexity
1424    /// request, leaving libopus's default. Bitrate can be retuned live via `set_bitrate`.
1425    fn new(
1426        sample_rate: u32,
1427        channels: i32,
1428        vbr: bool,
1429        bitrate: i32,
1430        complexity: Option<i32>,
1431    ) -> Result<Self, String> {
1432        if channels <= 2 {
1433            let mut err = 0;
1434            let st = unsafe {
1435                opusic_sys::opus_encoder_create(
1436                    sample_rate as i32,
1437                    if channels == 1 { 1 } else { 2 },
1438                    opusic_sys::OPUS_APPLICATION_RESTRICTED_LOWDELAY,
1439                    &mut err,
1440                )
1441            };
1442            if st.is_null() || err != 0 {
1443                return Err(format!("opus_encoder_create() failed: {err}"));
1444            }
1445            let enc = MonoOpus { st };
1446            unsafe {
1447                if opusic_sys::opus_encoder_ctl(st, opusic_sys::OPUS_SET_BITRATE_REQUEST, bitrate)
1448                    != 0
1449                {
1450                    elog!("[pcmflux] WARNING: failed to apply initial bitrate");
1451                }
1452                if opusic_sys::opus_encoder_ctl(
1453                    st,
1454                    opusic_sys::OPUS_SET_VBR_REQUEST,
1455                    i32::from(vbr),
1456                ) != 0
1457                {
1458                    elog!("[pcmflux] WARNING: failed to apply VBR mode");
1459                }
1460                if let Some(c) = complexity
1461                    && opusic_sys::opus_encoder_ctl(st, opusic_sys::OPUS_SET_COMPLEXITY_REQUEST, c)
1462                        != 0
1463                {
1464                    elog!("[pcmflux] WARNING: failed to apply complexity");
1465                }
1466            }
1467            return Ok(PcmEncoder::Stereo(enc));
1468        }
1469        let (streams, coupled, mapping) = multiopus_layout(channels)
1470            .ok_or_else(|| format!("unsupported surround channel count {channels}"))?;
1471        unsafe {
1472            let mut err: i32 = 0;
1473            let st = opusic_sys::opus_multistream_encoder_create(
1474                sample_rate as i32,
1475                channels,
1476                streams,
1477                coupled,
1478                mapping.as_ptr(),
1479                opusic_sys::OPUS_APPLICATION_RESTRICTED_LOWDELAY,
1480                &mut err,
1481            );
1482            if st.is_null() || err != 0 {
1483                return Err(format!("opus_multistream_encoder_create() failed: {err}"));
1484            }
1485            if opusic_sys::opus_multistream_encoder_ctl(
1486                st,
1487                opusic_sys::OPUS_SET_BITRATE_REQUEST,
1488                bitrate,
1489            ) != 0
1490            {
1491                elog!("[pcmflux] WARNING: failed to apply initial surround bitrate");
1492            }
1493            if opusic_sys::opus_multistream_encoder_ctl(
1494                st,
1495                opusic_sys::OPUS_SET_VBR_REQUEST,
1496                vbr as i32,
1497            ) != 0
1498            {
1499                elog!("[pcmflux] WARNING: failed to apply surround VBR mode");
1500            }
1501            if let Some(c) = complexity
1502                && opusic_sys::opus_multistream_encoder_ctl(
1503                    st,
1504                    opusic_sys::OPUS_SET_COMPLEXITY_REQUEST,
1505                    c,
1506                ) != 0
1507            {
1508                elog!("[pcmflux] WARNING: failed to apply surround complexity");
1509            }
1510            Ok(PcmEncoder::Multi(MultiOpus { st }))
1511        }
1512    }
1513
1514    /// Encode one interleaved-PCM frame into `out`, returning the packet byte length.
1515    ///
1516    /// Dispatches to the `opus` crate (mono/stereo) or the raw multistream encode (surround,
1517    /// which needs the explicit `frame_size_per_channel`). Either error surfaces as a `String`.
1518    fn encode(
1519        &mut self,
1520        pcm: &[i16],
1521        frame_size_per_channel: usize,
1522        out: &mut [u8],
1523    ) -> Result<usize, String> {
1524        match self {
1525            PcmEncoder::Stereo(enc) => unsafe {
1526                let n = opusic_sys::opus_encode(
1527                    enc.st,
1528                    pcm.as_ptr(),
1529                    frame_size_per_channel as i32,
1530                    out.as_mut_ptr(),
1531                    out.len() as i32,
1532                );
1533                if n < 0 {
1534                    Err(format!("opus_encode() failed: {n}"))
1535                } else {
1536                    Ok(n as usize)
1537                }
1538            },
1539            PcmEncoder::Multi(ms) => unsafe {
1540                let n = opusic_sys::opus_multistream_encode(
1541                    ms.st,
1542                    pcm.as_ptr(),
1543                    frame_size_per_channel as i32,
1544                    out.as_mut_ptr(),
1545                    out.len() as i32,
1546                );
1547                if n < 0 {
1548                    Err(format!("opus_multistream_encode() failed: {n}"))
1549                } else {
1550                    Ok(n as usize)
1551                }
1552            },
1553        }
1554    }
1555
1556    /// The encoder's look-ahead in input samples: what a decoder discards first.
1557    fn lookahead(&mut self) -> i32 {
1558        match self {
1559            PcmEncoder::Stereo(enc) => unsafe {
1560                let mut value: i32 = 0;
1561                let ret = opusic_sys::opus_encoder_ctl(
1562                    enc.st,
1563                    opusic_sys::OPUS_GET_LOOKAHEAD_REQUEST,
1564                    &mut value as *mut i32,
1565                );
1566                if ret == 0 { value } else { 0 }
1567            },
1568            PcmEncoder::Multi(ms) => unsafe {
1569                let mut value: i32 = 0;
1570                let ret = opusic_sys::opus_multistream_encoder_ctl(
1571                    ms.st,
1572                    opusic_sys::OPUS_GET_LOOKAHEAD_REQUEST,
1573                    &mut value as *mut i32,
1574                );
1575                if ret == 0 { value } else { 0 }
1576            },
1577        }
1578    }
1579
1580    /// Retune the encoder's target bitrate live (bits/s), for either API.
1581    fn set_bitrate(&mut self, bits: i32) -> Result<(), String> {
1582        match self {
1583            PcmEncoder::Stereo(enc) => unsafe {
1584                let ret = opusic_sys::opus_encoder_ctl(
1585                    enc.st,
1586                    opusic_sys::OPUS_SET_BITRATE_REQUEST,
1587                    bits,
1588                );
1589                if ret != 0 {
1590                    Err(format!("ctl error {ret}"))
1591                } else {
1592                    Ok(())
1593                }
1594            },
1595            PcmEncoder::Multi(ms) => unsafe {
1596                let ret = opusic_sys::opus_multistream_encoder_ctl(
1597                    ms.st,
1598                    opusic_sys::OPUS_SET_BITRATE_REQUEST,
1599                    bits,
1600                );
1601                if ret != 0 {
1602                    Err(format!("ctl error {ret}"))
1603                } else {
1604                    Ok(())
1605                }
1606            },
1607        }
1608    }
1609}
1610
1611/// Backing store for the delivery ring: `Some(queue)` while open, `None` once closed
1612/// so `pop` wakes and returns `None` for a clean shutdown.
1613type FrameQueue = Option<VecDeque<(Vec<u8>, u64, u8)>>;
1614
1615/// Bounded, drop-oldest hand-off from the capture thread to the Python delivery
1616/// thread, so a slow or GIL-blocked callback can never stall the PulseAudio pump.
1617///
1618/// The capture thread `push`es encoded `(frame, pts)` pairs; the delivery thread blocks in
1619/// `pop`. Stale audio is worthless, so overflow past `capacity` (a few frames of slack)
1620/// discards the OLDEST frame and bumps `dropped`. `close` empties the queue to `None` and
1621/// wakes the consumer so it exits.
1622struct DeliveryRing {
1623    q: Mutex<FrameQueue>,
1624    cv: Condvar,
1625    dropped: AtomicU64,
1626    capacity: usize,
1627}
1628
1629impl DeliveryRing {
1630    /// Create an open ring pre-sized to `capacity` frames.
1631    fn new(capacity: usize) -> Self {
1632        Self {
1633            q: Mutex::new(Some(VecDeque::with_capacity(capacity))),
1634            cv: Condvar::new(),
1635            dropped: AtomicU64::new(0),
1636            capacity,
1637        }
1638    }
1639
1640    /// Enqueue one encoded frame, dropping the oldest (and bumping `dropped`) if the
1641    /// ring is at capacity, then wake the consumer. A no-op once closed.
1642    fn push(&self, data: Vec<u8>, pts: u64, channels: u8) -> Option<Vec<u8>> {
1643        let mut g = self.q.lock().unwrap_or_else(|e| e.into_inner());
1644        let Some(q) = g.as_mut() else {
1645            return Some(data);
1646        };
1647        if q.len() >= self.capacity {
1648            q.pop_front();
1649            self.dropped.fetch_add(1, Ordering::Relaxed);
1650        }
1651        q.push_back((data, pts, channels));
1652        self.cv.notify_one();
1653        None
1654    }
1655
1656    /// Block until a frame is available and return it, or return `None` once the ring
1657    /// is closed and drained — the delivery thread's loop condition.
1658    fn pop(&self) -> Option<(Vec<u8>, u64, u8)> {
1659        let mut g = self.q.lock().unwrap_or_else(|e| e.into_inner());
1660        loop {
1661            {
1662                let q = g.as_mut()?;
1663                if let Some(item) = q.pop_front() {
1664                    return Some(item);
1665                }
1666            }
1667            g = self.cv.wait(g).unwrap_or_else(|e| e.into_inner());
1668        }
1669    }
1670
1671    /// Close the ring: drop any queued frames and wake every waiter so `pop` returns
1672    /// `None`, and hand every later `push` its buffer back. Called during capture teardown
1673    /// to join the delivery thread, and up front by a run that has no callback.
1674    fn close(&self) {
1675        *self.q.lock().unwrap_or_else(|e| e.into_inner()) = None;
1676        self.cv.notify_all();
1677    }
1678}
1679
1680/// Owns the delivery thread for the lifetime of one capture run and tears it down on
1681/// `Drop`, so teardown also happens when the capture thread UNWINDS.
1682///
1683/// Closing the ring is the delivery thread's only wake-up: skip it and the thread parks
1684/// in `pop()` forever, pinning the Python callback and leaving `deliver_tid` set to a tid
1685/// the OS may hand to an unrelated thread (whose `stop_capture` would then be mistaken
1686/// for a re-entrant self-stop and silently do nothing).
1687struct DeliveryThread<'a> {
1688    ring: Arc<DeliveryRing>,
1689    inner: &'a Inner,
1690    join: Option<JoinHandle<()>>,
1691}
1692
1693impl Drop for DeliveryThread<'_> {
1694    fn drop(&mut self) {
1695        self.ring.close();
1696        if let Some(j) = self.join.take() {
1697            let _ = j.join();
1698        }
1699        self.inner.deliver_tid.store(0, Ordering::Release);
1700    }
1701}
1702
1703/// Recycles outgoing frame buffers from dropped `AudioFrame`s back to the capture
1704/// thread, so the steady-state emit path allocates nothing.
1705///
1706/// INVARIANT: every buffer is born as `vec![0u8; buf_size]`, so bytes `[0, buf_size)` stay
1707/// initialized for the allocation's whole lifetime — `truncate` only shortens `len`, never
1708/// de-initializes memory. That is what makes `restore`'s `set_len` back to `buf_size` sound.
1709struct BufferPool {
1710    bufs: Mutex<Vec<Vec<u8>>>,
1711    buf_size: usize,
1712}
1713
1714impl BufferPool {
1715    /// Cap on pooled buffers. Outstanding frames rarely exceed the delivery-ring
1716    /// capacity plus a few Python-held references; anything past this goes back to the
1717    /// allocator rather than growing the pool unboundedly.
1718    const MAX_POOLED: usize = 16;
1719
1720    /// Create an empty pool that hands out (and accepts) `buf_size`-byte buffers.
1721    fn new(buf_size: usize) -> Self {
1722        Self {
1723            bufs: Mutex::new(Vec::new()),
1724            buf_size,
1725        }
1726    }
1727
1728    /// Take a fully initialized buffer of exactly `buf_size` length, recycling a
1729    /// pooled one when available. The runtime path takes through `PoolTaker`; this direct,
1730    /// locking form serves the unit tests.
1731    #[cfg(test)]
1732    fn take(&self) -> Vec<u8> {
1733        let recycled = self.bufs.lock().unwrap_or_else(|e| e.into_inner()).pop();
1734        match recycled {
1735            Some(v) => self.restore(v),
1736            None => vec![0u8; self.buf_size],
1737        }
1738    }
1739
1740    /// Restore a recycled buffer to full `buf_size` length via `set_len`.
1741    ///
1742    /// Sound per the pool invariant: the bytes were written at allocation and truncation
1743    /// does not de-initialize them, so extending `len` back to `buf_size` never exposes
1744    /// uninitialized memory.
1745    fn restore(&self, mut v: Vec<u8>) -> Vec<u8> {
1746        debug_assert!(v.capacity() >= self.buf_size);
1747        unsafe { v.set_len(self.buf_size) };
1748        v
1749    }
1750
1751    /// Return a buffer to the pool, unless it is undersized or the pool is already at
1752    /// `MAX_POOLED` (in which case it is dropped to the allocator).
1753    fn put(&self, v: Vec<u8>) {
1754        if v.capacity() < self.buf_size {
1755            return;
1756        }
1757        let mut g = self.bufs.lock().unwrap_or_else(|e| e.into_inner());
1758        if g.len() < Self::MAX_POOLED {
1759            g.push(v);
1760        }
1761    }
1762
1763    /// Move every pooled buffer into `into` under a single lock — the batched refill
1764    /// for the sole-consumer `PoolTaker`, whose empty local stash is `into`.
1765    fn drain_into(&self, into: &mut Vec<Vec<u8>>) {
1766        let mut g = self.bufs.lock().unwrap_or_else(|e| e.into_inner());
1767        std::mem::swap(&mut *g, into);
1768    }
1769}
1770
1771/// Sole-consumer view over the shared `BufferPool` for the capture thread.
1772///
1773/// Refills are batched into a `local` stash, so the per-frame `take` is lock-free in the
1774/// steady state, while returns from the delivery/Python side (`AudioFrame` drops) still go
1775/// back through the shared pool. Only the capture thread owns a `PoolTaker`.
1776struct PoolTaker {
1777    pool: Arc<BufferPool>,
1778    local: Vec<Vec<u8>>,
1779}
1780
1781impl PoolTaker {
1782    /// Wrap a shared pool with an empty local stash.
1783    fn new(pool: Arc<BufferPool>) -> Self {
1784        Self {
1785            pool,
1786            local: Vec::new(),
1787        }
1788    }
1789
1790    /// Take one `buf_size` buffer: pop from the local stash, batch-refilling it from
1791    /// the shared pool (one lock) only when empty, and allocating fresh when both are empty.
1792    fn take(&mut self) -> Vec<u8> {
1793        if self.local.is_empty() {
1794            self.pool.drain_into(&mut self.local);
1795        }
1796        match self.local.pop() {
1797            Some(v) => self.pool.restore(v),
1798            None => vec![0u8; self.pool.buf_size],
1799        }
1800    }
1801
1802    /// Return a buffer on an error path without locking — it stays in the local stash.
1803    fn put(&mut self, v: Vec<u8>) {
1804        if v.capacity() >= self.pool.buf_size && self.local.len() < BufferPool::MAX_POOLED {
1805            self.local.push(v);
1806        }
1807    }
1808}
1809
1810/// Per-run encode/deliver state, living on the capture thread's stack for the
1811/// lifetime of one capture. Holds the encoder, the frame-reassembly buffers, the outgoing
1812/// buffer recycler, the RED redundancy history, and the running debug-log counters.
1813struct RunState<'a> {
1814    inner: &'a Inner,
1815    ring: &'a DeliveryRing,
1816    encoder: PcmEncoder,
1817    frame_size_per_channel: usize,
1818    channels: usize,
1819    /// Reassembly buffer for exactly one Opus frame (`i16` samples, filled byte-wise from
1820    /// the incoming PulseAudio fragments).
1821    accum: Vec<i16>,
1822    /// A zeroed reference of the same length as `accum`; comparing `accum == silence_ref`
1823    /// lowers to a single vectorized memcmp for the silence gate, versus a scalar per-sample
1824    /// scan.
1825    silence_ref: Vec<i16>,
1826    pcm_fill_bytes: usize,
1827    /// Outgoing-buffer recycler, shared with delivered `AudioFrame`s whose drop refills it.
1828    pool: PoolTaker,
1829    /// RFC 2198 redundancy history: the last `red_distance` emitted `(opus, pts)` frames,
1830    /// oldest-first. Per-run — reset on start, and the frame size is fixed for a run.
1831    red_history: VecDeque<(Vec<u8>, u64)>,
1832    /// Retired `red_history` buffers, reused for the next entry so the steady state (a
1833    /// silence gap included) allocates nothing. Bounded by `red_distance`: every buffer
1834    /// is either in the history or here.
1835    red_spare: Vec<Vec<u8>>,
1836    red_distance: usize,
1837    /// Whether the last frame was emitted as sound, so the gate lets the silent frame after it
1838    /// through when that frame carries the end of the sound (always on a header capture, behind
1839    /// the bare `WS_QUIET` mark and marked itself; on a raw capture when `tail_sounding`), and a
1840    /// session that drops after it sends the bare mark once.
1841    sounding: bool,
1842    /// Whether that frame of sound held sound in its last `lookahead` samples, the part the
1843    /// encoder holds back until the next frame.
1844    tail_sounding: bool,
1845    /// The encoder's lookahead in samples per channel (`PcmEncoder::lookahead`).
1846    lookahead: usize,
1847    /// A surround capture's stereo encoder and its downmix scratch, for
1848    /// `Inner::stereo_companion`; `None` for mono and stereo captures.
1849    companion: Option<(PcmEncoder, Vec<i16>)>,
1850    /// The bitrate the companion encoder was last given; follows the capture's.
1851    companion_bitrate: i32,
1852    total_samples_processed: u64,
1853    /// The Ogg Opus stream on the output socket, and the 48 kHz samples one input
1854    /// sample is worth for its granule positions.
1855    ogg: Option<ogg_sink::OggSink>,
1856    ogg_scale: u64,
1857    first_sound_detected: bool,
1858    current_applied_bitrate: i32,
1859    chunks_read: u64,
1860    chunks_silent: u64,
1861    chunks_encoded: u64,
1862    bytes_encoded: u64,
1863}
1864
1865impl<'a> RunState<'a> {
1866    /// Feed one PulseAudio PCM fragment into the reassembly buffer, emitting a frame
1867    /// each time `accum` fills to exactly one Opus frame.
1868    ///
1869    /// Fragments arrive at arbitrary byte boundaries, so this copies from `src` into `accum`
1870    /// at `pcm_fill_bytes`, calling `emit_frame` (and resetting the fill cursor) whenever a
1871    /// full `frame_size_per_channel * channels * 2`-byte chunk accumulates, and loops until
1872    /// `src` is drained. Any partial remainder is carried into the next fragment.
1873    fn feed(&mut self, mut src: &[u8]) {
1874        let chunk_bytes = self.frame_size_per_channel * self.channels * 2;
1875        while !src.is_empty() {
1876            let want = chunk_bytes - self.pcm_fill_bytes;
1877            let take = want.min(src.len());
1878            {
1879                let dst: &mut [u8] = bytemuck::cast_slice_mut(&mut self.accum);
1880                dst[self.pcm_fill_bytes..self.pcm_fill_bytes + take].copy_from_slice(&src[..take]);
1881            }
1882            self.pcm_fill_bytes += take;
1883            src = &src[take..];
1884            if self.pcm_fill_bytes == chunk_bytes {
1885                self.emit_frame();
1886                self.pcm_fill_bytes = 0;
1887            }
1888        }
1889    }
1890
1891    /// Encode one reassembled frame and hand it to the delivery thread. The heart of
1892    /// the capture encode path.
1893    ///
1894    /// 1. **Dynamic bitrate**: re-reads the `opus_bitrate` mirror and reconfigures the encoder
1895    ///    only when it changed, so a live bitrate update costs nothing on unchanged frames.
1896    /// 2. **Timestamp**: `pts` is the running 48 kHz-domain sample count
1897    ///    (`total_samples_processed`) *before* this frame, then advanced by
1898    ///    `frame_size_per_channel` — a monotonic per-frame timestamp used for RED offsets and
1899    ///    client-side ordering.
1900    /// 3. **Silence gate**: when enabled, a frame equal to the zeroed `silence_ref` is counted
1901    ///    and dropped (no Opus is sent), so pure silence costs no bandwidth. The first one after
1902    ///    sound can carry the end of that sound, the part the encoder held back (its
1903    ///    lookahead). A header capture always sends it: the bare `WS_QUIET` mark goes first,
1904    ///    then that frame with `WS_QUIET` set. A raw Opus capture sends it only when the sound
1905    ///    reached into that lookahead (`tail_sounding`): a WebRTC receiver's jitter buffer
1906    ///    fades the sound after a pause in from what it concealed, and concealing from a frame
1907    ///    of silence faded more short sounds than concealing from the sound itself, while a
1908    ///    frame that ends a sound loses nothing of it. The first non-silent frame logs once.
1909    /// 4. **Encode in place**: `write_ws_prefix_into` writes the RFC 2198 RED framing prefix
1910    ///    (which depends only on `pts` + history) into a pooled buffer, and the Opus packet is
1911    ///    encoded DIRECTLY after it — no assembly copy, and the buffer recycles through the
1912    ///    pool, so the steady state allocates nothing. An encode error or a zero-length packet
1913    ///    returns the buffer to the pool and drops the frame.
1914    /// 5. **Retain redundancy**: with `red_distance > 0`, the just-encoded primary is copied
1915    ///    onto `red_history` (bounded, oldest-first) to serve as a future redundant copy,
1916    ///    into the buffer the retiring entry hands back.
1917    /// 6. **Hand off**: the truncated buffer is pushed to the `DeliveryRing`; the capture
1918    ///    thread itself never touches the GIL.
1919    fn emit_frame(&mut self) {
1920        self.chunks_read += 1;
1921
1922        let requested = self.inner.opus_bitrate.load(Ordering::Relaxed);
1923        if requested != self.current_applied_bitrate {
1924            match self.encoder.set_bitrate(requested) {
1925                Ok(()) => {
1926                    plog!(
1927                        "[pcmflux] Dynamic Bitrate Update: {} -> {} kbps",
1928                        self.current_applied_bitrate / 1000,
1929                        requested / 1000
1930                    );
1931                    self.current_applied_bitrate = requested;
1932                }
1933                Err(e) => {
1934                    // current_applied_bitrate stays put, so the next frame retries the
1935                    // rejected value instead of latching it as if it had been applied.
1936                    elog!("[pcmflux] Failed to update bitrate ({requested}): {e:?}");
1937                }
1938            }
1939        }
1940
1941        let pts = self.total_samples_processed;
1942        self.total_samples_processed += self.frame_size_per_channel as u64;
1943
1944        let emit_header = self.inner.emit_audio_header.load(Ordering::Relaxed);
1945        let silent =
1946            self.inner.use_silence_gate.load(Ordering::Relaxed) && self.accum == self.silence_ref;
1947        if silent && !(self.sounding && (emit_header || self.tail_sounding)) {
1948            self.sounding = false;
1949            self.chunks_silent += 1;
1950            // Flush the RED backlog: if these pre-silence frames were kept, the first
1951            // packet after a long quiet stretch would ship minutes-old audio as
1952            // "redundant" data, and a receiver could reconstruct it into the gap. The
1953            // emptied buffers are kept for reuse, so a silence gap costs no allocations.
1954            self.red_spare
1955                .extend(self.red_history.drain(..).map(|(v, _)| v));
1956            return;
1957        }
1958        if !self.first_sound_detected {
1959            plog!("[pcmflux] First non-silent audio chunk detected! Encoding...");
1960            self.first_sound_detected = true;
1961        }
1962
1963        let n = self.frame_size_per_channel * self.channels;
1964        let mut data = self.pool.take();
1965        let prefix = write_ws_prefix_into(
1966            &mut data,
1967            pts,
1968            &self.red_history,
1969            self.red_distance,
1970            emit_header,
1971        );
1972        let encoded = match self.encoder.encode(
1973            &self.accum[..n],
1974            self.frame_size_per_channel,
1975            &mut data[prefix..],
1976        ) {
1977            Ok(b) => b,
1978            Err(e) => {
1979                elog!("[pcmflux] ERROR: {e}");
1980                self.pool.put(data);
1981                return;
1982            }
1983        };
1984        if encoded == 0 {
1985            self.pool.put(data);
1986            return;
1987        }
1988        self.sounding = !silent;
1989        let held = self.lookahead.min(self.frame_size_per_channel) * self.channels;
1990        self.tail_sounding = !silent && self.accum[n - held..n].iter().any(|&v| v != 0);
1991        self.chunks_encoded += 1;
1992        self.bytes_encoded += encoded as u64;
1993        if let Some(sink) = self.ogg.as_mut() {
1994            sink.write_packet(
1995                &data[prefix..prefix + encoded],
1996                self.total_samples_processed * self.ogg_scale,
1997            );
1998        }
1999        if self.red_distance > 0 {
2000            let mut slot = if self.red_history.len() >= self.red_distance {
2001                self.red_history
2002                    .pop_front()
2003                    .map(|(v, _)| v)
2004                    .unwrap_or_default()
2005            } else {
2006                self.red_spare.pop().unwrap_or_default()
2007            };
2008            slot.clear();
2009            slot.extend_from_slice(&data[prefix..prefix + encoded]);
2010            self.red_history.push_back((slot, pts));
2011        }
2012        data.truncate(prefix + encoded);
2013        if silent && emit_header {
2014            self.push_mark(pts);
2015            data[1] |= WS_QUIET;
2016        }
2017
2018        if let Some(data) = self.ring.push(data, pts, self.channels as u8) {
2019            self.pool.put(data);
2020        }
2021        if self.inner.stereo_companion.load(Ordering::Relaxed) {
2022            self.emit_companion(pts, emit_header, silent && emit_header);
2023        }
2024    }
2025
2026    /// Tell the delivery thread the stream pauses when the session drops after a frame of
2027    /// sound: a header capture pushes the bare `[0x01, WS_QUIET]` (stamped with the `pts` the
2028    /// pause starts at), since no frame is left to carry the mark, so a player plays out what
2029    /// it holds rather than waiting for more. A raw Opus capture has nothing to frame it with,
2030    /// and its RTP timestamps carry the pause already.
2031    fn emit_quiet(&mut self, pts: u64) {
2032        if std::mem::take(&mut self.sounding)
2033            && self.inner.emit_audio_header.load(Ordering::Relaxed)
2034        {
2035            self.push_mark(pts);
2036        }
2037    }
2038
2039    /// Push the bare `[0x01, WS_QUIET]` stamped `pts`.
2040    fn push_mark(&mut self, pts: u64) {
2041        let mut data = self.pool.take();
2042        data[0] = 0x01;
2043        data[1] = WS_QUIET;
2044        data.truncate(2);
2045        if let Some(data) = self.ring.push(data, pts, self.channels as u8) {
2046            self.pool.put(data);
2047        }
2048    }
2049
2050    /// Emit the stereo companion of the frame just emitted: the frame downmixed
2051    /// (`downmix_stereo`), encoded with the companion's own stereo encoder at the capture's
2052    /// bitrate, and handed to the delivery thread right after it with the same `pts` and
2053    /// `channels == 2`. On the wire it carries the plain two-byte header, never RED, which
2054    /// the surround stream's history serves, with `WS_QUIET` set when `ending` (the companion
2055    /// of the silent frame that ends a sound).
2056    fn emit_companion(&mut self, pts: u64, emit_header: bool, ending: bool) {
2057        let fpc = self.frame_size_per_channel;
2058        let channels = self.channels;
2059        let bitrate = self.current_applied_bitrate;
2060        let Some((encoder, pcm)) = self.companion.as_mut() else {
2061            return;
2062        };
2063        if self.companion_bitrate != bitrate && encoder.set_bitrate(bitrate).is_ok() {
2064            self.companion_bitrate = bitrate;
2065        }
2066        downmix_stereo(&self.accum[..fpc * channels], channels, pcm);
2067        let mut data = self.pool.take();
2068        let prefix = write_ws_prefix_into(&mut data, pts, &VecDeque::new(), 0, emit_header);
2069        match encoder.encode(pcm, fpc, &mut data[prefix..]) {
2070            Ok(n) if n > 0 => {
2071                data.truncate(prefix + n);
2072                if ending {
2073                    data[1] |= WS_QUIET;
2074                }
2075                if let Some(data) = self.ring.push(data, pts, 2) {
2076                    self.pool.put(data);
2077                }
2078            }
2079            Ok(_) => self.pool.put(data),
2080            Err(e) => {
2081                elog!("[pcmflux] ERROR: stereo companion: {e}");
2082                self.pool.put(data);
2083            }
2084        }
2085    }
2086}
2087
2088/// Own one whole capture run end to end on a dedicated thread — connect PulseAudio,
2089/// encode, and deliver — until stopped. It runs on its own thread because the PulseAudio
2090/// mainloop must be pumped continuously and independently of Python: sharing the caller's
2091/// thread would tie capture cadence to the GIL and let any Python stall starve the audio.
2092/// The body handed to `spawn_worker`.
2093///
2094/// Publishes `start_state` for the handshake (`RUNNING` on entering the hot loop, `FAILED`
2095/// with a `last_error` on any terminal error, `IDLE` on a clean stop) and returns when
2096/// `stop_state` leaves `STOP_NONE` or on a fatal error. The startup sequence, in order:
2097///
2098/// 1. **Seed the mirrors**: copies the settings snapshot (already validated by
2099///    `extract_settings`) into the `Inner` per-frame atomics.
2100/// 2. **Buffer attr / latency**: a configured `latency_ms` uses `ADJUST_LATENCY` with
2101///    `fragsize` set to that latency; otherwise `fragsize` is floored at ~20 ms, which yields
2102///    a prompt first frame and avoids PipeWire's ~2 s default fragment.
2103/// 3. **Connect + probe**: drives the context to `Ready` on the bounded pump (re-checking
2104///    `stop_pending` each turn), and up-front validates a NAMED device via an introspect
2105///    probe — an async `connect_record` would not fail synchronously on a bad name. The probe
2106///    closure is called from C inside mainloop dispatch, so its body is `catch_unwind`-guarded
2107///    to keep a panic from unwinding across the FFI boundary.
2108/// 4. **Encoder + record stream**: creates the `PcmEncoder` (mono/stereo or surround) and
2109///    drives the record stream to `Ready`.
2110/// 5. **Delivery thread**: spawns the delivery thread that pops from the `DeliveryRing` and
2111///    runs the Python callback there, so GIL stalls cannot back up the PA pump; a callback
2112///    error is reported as an unraisable exception and never propagates into the loop. The
2113///    buffer pool is sized to the worst-case body — RED prefix plus a max Opus packet, scaled
2114///    by stream count for surround (one self-delimited packet per stream). A run without a
2115///    callback spawns no delivery thread: its ring starts closed, so every encoded frame goes
2116///    back to the pool and only the output socket is served.
2117/// 6. **Hot loop**: `pump`s on the ~20 ms bound, then drains every buffered fragment via
2118///    peek/discard (a `Hole` is an xrun — the read index is just advanced), feeding each into
2119///    `RunState`. A stop is observed within the pump bound even when the source is wedged. On
2120///    exit it disconnects the stream, drops the encoder, closes and joins the delivery ring,
2121///    and reports any dropped stale frames.
2122///
2123/// One PulseAudio session for capture: mainloop, context, and the record stream,
2124/// all recreated together on reconnect.
2125/// Drop order matters (declaration order): the stream must die first, then its
2126/// owning context, then the mainloop both pulse threads pump — the reverse of
2127/// the build. A wrong order is a use-after-free on the libpulse side.
2128struct PaCaptureSession {
2129    stream: Stream,
2130    /// Must outlive the stream (the connection owns it); never read after open.
2131    #[allow(dead_code)]
2132    context: Context,
2133    mainloop: Mainloop,
2134}
2135
2136/// Why a session failed to open: drives the retry policy of the caller.
2137enum SessionOpenError {
2138    /// The named source is absent at startup (misconfiguration-ish); the caller gives it
2139    /// only its short bring-up window rather than the full startup retry budget.
2140    DeviceNotFound(String),
2141    /// Server down, busy, or a bring-up race: retryable.
2142    Transient(String),
2143    /// stop_pending observed while opening; caller must shut down cleanly.
2144    Aborted,
2145}
2146
2147/// Open a capture session: mainloop + context + record stream driven to `Ready` on the
2148/// bounded pump, honoring `stop_pending` at every turn. A NAMED device is validated by
2149/// an introspect probe on every call (an async connect_record would not fail
2150/// synchronously on a bad name, and on reconnect this is also what notices the device
2151/// reappearing after an outage). `device_was_present` distinguishes initial bring-up
2152/// from reconnect: a named device missing at startup is probably a misconfiguration, so
2153/// it is reported as `DeviceNotFound` and the caller spends only a short window on it;
2154/// the same device vanishing mid-run (PulseAudio/PipeWire restart kills every source) is
2155/// transient and must be retried or audio never comes back.
2156fn pa_capture_session_open(
2157    inner: &Inner,
2158    spec: &Spec,
2159    device: Option<&str>,
2160    attr: &BufferAttr,
2161    adjust_latency: bool,
2162    device_was_present: bool,
2163) -> Result<PaCaptureSession, SessionOpenError> {
2164    let tr = |e: &str| SessionOpenError::Transient(e.to_string());
2165    let mut mainloop = match Mainloop::new() {
2166        Some(m) => m,
2167        None => return Err(tr("pa_mainloop_new() failed")),
2168    };
2169    let mut context = match Context::new(&mainloop, "pcmflux") {
2170        Some(c) => c,
2171        None => return Err(tr("pa_context_new() failed")),
2172    };
2173    if context.connect(None, CtxFlags::NOFLAGS, None).is_err() {
2174        return Err(tr("pa_context_connect() failed"));
2175    }
2176    loop {
2177        let st = context.get_state();
2178        if st == pulse::context::State::Ready {
2179            break;
2180        }
2181        if !st.is_good() {
2182            return Err(tr("PulseAudio context connection failed"));
2183        }
2184        if inner.stop_pending() {
2185            return Err(SessionOpenError::Aborted);
2186        }
2187        if !pump(&mut mainloop, PUMP_TIMEOUT_US) {
2188            return Err(tr("mainloop iterate failed during connect"));
2189        }
2190    }
2191
2192    if let Some(dev) = device {
2193        let probe = Arc::new(Mutex::new((false, false)));
2194        let p2 = probe.clone();
2195        let op = context
2196            .introspect()
2197            .get_source_info_by_name(dev, move |res| {
2198                let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2199                    let mut g = p2.lock().unwrap_or_else(|e| e.into_inner());
2200                    match res {
2201                        ListResult::Item(_) => g.0 = true,
2202                        ListResult::End | ListResult::Error => g.1 = true,
2203                    }
2204                }));
2205            });
2206        loop {
2207            if probe.lock().unwrap_or_else(|e| e.into_inner()).1 {
2208                break;
2209            }
2210            if inner.stop_pending() {
2211                drop(op);
2212                return Err(SessionOpenError::Aborted);
2213            }
2214            if !context.get_state().is_good() {
2215                drop(op);
2216                return Err(tr("context failed during source probe"));
2217            }
2218            if !pump(&mut mainloop, PUMP_TIMEOUT_US) {
2219                drop(op);
2220                return Err(tr("mainloop iterate failed during source probe"));
2221            }
2222        }
2223        drop(op);
2224        if !probe.lock().unwrap_or_else(|e| e.into_inner()).0 {
2225            let msg = format!("PulseAudio source not found: '{dev}'");
2226            return Err(if device_was_present {
2227                // Mid-run: the server was just restarted; its sources are all
2228                // gone for now, not misconfigured.
2229                SessionOpenError::Transient(msg)
2230            } else {
2231                SessionOpenError::DeviceNotFound(msg)
2232            });
2233        }
2234    }
2235
2236    let map = surround_channel_map(spec.channels as i32)
2237        .and_then(|m| ChannelMap::new_from_string(m).ok());
2238    let mut stream = match Stream::new(&mut context, "Audio Capture", spec, map.as_ref()) {
2239        Some(s) => s,
2240        None => return Err(tr("pa_stream_new() failed")),
2241    };
2242    let flags = if adjust_latency {
2243        StreamFlags::ADJUST_LATENCY
2244    } else {
2245        StreamFlags::NOFLAGS
2246    };
2247    if stream.connect_record(device, Some(attr), flags).is_err() {
2248        return Err(tr("pa_stream_connect_record() failed"));
2249    }
2250    loop {
2251        let st = stream.get_state();
2252        if st == pulse::stream::State::Ready {
2253            break;
2254        }
2255        if !st.is_good() {
2256            return Err(SessionOpenError::Transient(format!(
2257                "PulseAudio record stream failed (device '{}')",
2258                device.unwrap_or("default")
2259            )));
2260        }
2261        if inner.stop_pending() {
2262            return Err(SessionOpenError::Aborted);
2263        }
2264        if !pump(&mut mainloop, PUMP_TIMEOUT_US) {
2265            return Err(tr("mainloop iterate failed during stream connect"));
2266        }
2267    }
2268    Ok(PaCaptureSession {
2269        stream,
2270        context,
2271        mainloop,
2272    })
2273}
2274
2275fn capture_run(inner: &Arc<Inner>, settings: &Settings, callback: Option<&Py<PyAny>>) {
2276    inner
2277        .opus_bitrate
2278        .store(settings.opus_bitrate, Ordering::Relaxed);
2279    inner
2280        .use_silence_gate
2281        .store(settings.use_silence_gate, Ordering::Relaxed);
2282    inner
2283        .debug_logging
2284        .store(settings.debug_logging, Ordering::Relaxed);
2285    inner
2286        .emit_audio_header
2287        .store(!settings.omit_audio_header, Ordering::Relaxed);
2288
2289    // Sample rate, channel count, and frame duration were validated by `extract_settings`.
2290    let spec = Spec {
2291        format: Format::S16le,
2292        rate: settings.sample_rate,
2293        channels: settings.channels as u8,
2294    };
2295
2296    let mut attr = BufferAttr {
2297        maxlength: u32::MAX,
2298        tlength: u32::MAX,
2299        prebuf: u32::MAX,
2300        minreq: u32::MAX,
2301        fragsize: u32::MAX,
2302    };
2303    let adjust_latency = settings.latency_ms > 0;
2304    if adjust_latency {
2305        attr.fragsize = spec.usec_to_bytes(MicroSeconds(settings.latency_ms as u64 * 1000)) as u32;
2306    } else {
2307        attr.fragsize = spec.usec_to_bytes(MicroSeconds(20 * 1000)) as u32;
2308    }
2309
2310    let device = settings.device_name.as_deref();
2311
2312    let encoder = match PcmEncoder::new(
2313        settings.sample_rate,
2314        settings.channels,
2315        settings.use_vbr,
2316        settings.opus_bitrate,
2317        settings.opus_complexity,
2318    ) {
2319        Ok(e) => e,
2320        Err(e) => {
2321            inner.fail(e);
2322            return;
2323        }
2324    };
2325    plog!(
2326        "[pcmflux] SUCCESS: Opus encoder created ({} ch).",
2327        settings.channels
2328    );
2329
2330    let frame_size_per_channel =
2331        (settings.sample_rate as f64 * settings.frame_duration_ms / 1000.0) as usize;
2332    let channels = settings.channels as usize;
2333
2334    let ring = Arc::new(DeliveryRing::new(8));
2335    // Surround encodes one self-delimited packet per multistream stream (4 for 5.1, 5 for
2336    // 7.1), so the worst-case body scales with the stream count of the actual layout.
2337    let max_pkt = multiopus_layout(settings.channels).map_or(1, |(streams, _, _)| streams as usize)
2338        * MAX_OPUS_PACKET;
2339    let pool = Arc::new(BufferPool::new(RED_PREFIX_MAX + max_pkt));
2340    let delivery = match callback {
2341        None => {
2342            ring.close();
2343            None
2344        }
2345        Some(callback) => {
2346            let deliver_ring = Arc::clone(&ring);
2347            let deliver_pool = Arc::clone(&pool);
2348            let deliver_inner = Arc::clone(inner);
2349            let deliver_cb: Py<PyAny> = Python::attach(|py| callback.clone_ref(py));
2350            let spawned = std::thread::Builder::new()
2351                .name("pcmflux-deliver".into())
2352                .spawn(move || {
2353                    unsafe {
2354                        let _ = libc::setpriority(libc::PRIO_PROCESS, gettid() as libc::id_t, -10);
2355                    }
2356                    deliver_inner.deliver_tid.store(gettid(), Ordering::Release);
2357                    while let Some((data, pts, channels)) = deliver_ring.pop() {
2358                        Python::attach(|py| {
2359                            let frame = match Py::new(
2360                                py,
2361                                AudioFrame {
2362                                    data,
2363                                    pts,
2364                                    channels,
2365                                    pool: Some(Arc::clone(&deliver_pool)),
2366                                },
2367                            ) {
2368                                Ok(f) => f,
2369                                Err(e) => {
2370                                    elog!("[pcmflux] AudioFrame alloc failed: {e:?}");
2371                                    return;
2372                                }
2373                            };
2374                            if let Err(e) = deliver_cb.call1(py, (frame,)) {
2375                                e.write_unraisable(py, Some(deliver_cb.bind(py)));
2376                            }
2377                        });
2378                    }
2379                    deliver_inner.deliver_tid.store(0, Ordering::Release);
2380                });
2381            match spawned {
2382                Ok(join) => Some(DeliveryThread {
2383                    ring: Arc::clone(&ring),
2384                    inner,
2385                    join: Some(join),
2386                }),
2387                Err(e) => {
2388                    inner.fail(format!("delivery thread spawn failed: {e}"));
2389                    return;
2390                }
2391            }
2392        }
2393    };
2394
2395    let ogg_scale = 48_000 / settings.sample_rate.max(1) as u64;
2396    let mut encoder = encoder;
2397    let ogg = if settings.output_socket.is_empty() {
2398        None
2399    } else {
2400        match ogg_sink::OggSink::bind(
2401            &settings.output_socket,
2402            &ogg_sink::OpusHead {
2403                channels: channels as u8,
2404                pre_skip: (encoder.lookahead().max(0) as u64 * ogg_scale).min(u16::MAX as u64)
2405                    as u16,
2406                input_sample_rate: settings.sample_rate,
2407                mapping: multiopus_layout(channels as i32).map(|(streams, coupled, table)| {
2408                    (streams as u8, coupled as u8, table.to_vec())
2409                }),
2410            },
2411        ) {
2412            Ok(sink) => Some(sink),
2413            Err(e) => {
2414                inner.fail(e);
2415                return;
2416            }
2417        }
2418    };
2419    let lookahead = encoder.lookahead().max(0) as usize;
2420    let mut run = RunState {
2421        inner,
2422        ring: &ring,
2423        encoder,
2424        frame_size_per_channel,
2425        channels,
2426        accum: vec![0i16; frame_size_per_channel * channels],
2427        silence_ref: vec![0i16; frame_size_per_channel * channels],
2428        pcm_fill_bytes: 0,
2429        pool: PoolTaker::new(Arc::clone(&pool)),
2430        red_history: VecDeque::new(),
2431        red_spare: Vec::new(),
2432        red_distance: settings.red_distance.max(0) as usize,
2433        sounding: false,
2434        tail_sounding: false,
2435        lookahead,
2436        companion: if channels > 2 {
2437            match PcmEncoder::new(
2438                settings.sample_rate,
2439                2,
2440                settings.use_vbr,
2441                settings.opus_bitrate,
2442                settings.opus_complexity,
2443            ) {
2444                Ok(e) => Some((e, vec![0i16; frame_size_per_channel * 2])),
2445                Err(e) => {
2446                    elog!("[pcmflux] stereo companion unavailable: {e}");
2447                    None
2448                }
2449            }
2450        } else {
2451            None
2452        },
2453        companion_bitrate: settings.opus_bitrate,
2454        total_samples_processed: 0,
2455        ogg,
2456        ogg_scale,
2457        first_sound_detected: false,
2458        current_applied_bitrate: settings.opus_bitrate,
2459        chunks_read: 0,
2460        chunks_silent: 0,
2461        chunks_encoded: 0,
2462        bytes_encoded: 0,
2463    };
2464
2465    let mut last_log = Instant::now();
2466
2467    // Session loop: the record stream CAN die mid-run (PulseAudio/PipeWire restart,
2468    // source unplugged) and a plain `break` there leaves audio dead until some
2469    // unrelated settings change restarts the capture. Reopen with backoff instead.
2470    // Three retry budgets, shortest first:
2471    //   - DEVICE_WAIT_TRIES: a start that finds the NAMED source missing. The sink whose
2472    //     monitor is being recorded may still be materializing (container bring-up, or a
2473    //     capture start that raced a server restart), but a misconfigured name must still
2474    //     surface quickly, so this window is only a few seconds.
2475    //   - START_TRIES: any other failure to bring the first session up.
2476    //   - RECONNECT_TRIES: a mid-run reconnect, which has to outlast a whole
2477    //     PulseAudio/PipeWire restart or audio never comes back.
2478    const DEVICE_WAIT_TRIES: u32 = 6;
2479    const START_TRIES: u32 = 12;
2480    const RECONNECT_TRIES: u32 = 40;
2481    let mut session: Option<PaCaptureSession> = None;
2482    let mut ever_connected = false;
2483    let mut tries: u32 = 0;
2484    let mut backoff_ms: u64 = 250;
2485    let mut terminal_error: Option<String> = None;
2486
2487    loop {
2488        if inner.stop_pending() {
2489            break;
2490        }
2491        if session.is_none() {
2492            let opened = pa_capture_session_open(
2493                inner,
2494                &spec,
2495                device,
2496                &attr,
2497                adjust_latency,
2498                ever_connected,
2499            );
2500            let cap = match (&opened, ever_connected) {
2501                (Err(SessionOpenError::DeviceNotFound(_)), _) => DEVICE_WAIT_TRIES,
2502                (_, true) => RECONNECT_TRIES,
2503                (_, false) => START_TRIES,
2504            };
2505            match opened {
2506                Ok(s) => {
2507                    session = Some(s);
2508                    tries = 0;
2509                    backoff_ms = 250;
2510                    if !ever_connected {
2511                        ever_connected = true;
2512                        inner.started_ok.store(true, Ordering::Release);
2513                        inner.start_state.store(ST_RUNNING, Ordering::Release);
2514                        plog!(
2515                            "[pcmflux] Capture loop started. Device: {}, Rate: {}, Channels: {}, Bitrate: {} kbps, \
2516                             VBR: {}, Silence Gate: {}",
2517                            device.unwrap_or("system_default"),
2518                            settings.sample_rate,
2519                            settings.channels,
2520                            settings.opus_bitrate / 1000,
2521                            if settings.use_vbr { "On" } else { "Off" },
2522                            if settings.use_silence_gate {
2523                                "On"
2524                            } else {
2525                                "Off"
2526                            }
2527                        );
2528                    } else {
2529                        plog!("[pcmflux] audio capture reconnected; resuming.");
2530                    }
2531                }
2532                Err(SessionOpenError::Aborted) => break,
2533                Err(SessionOpenError::DeviceNotFound(e)) | Err(SessionOpenError::Transient(e)) => {
2534                    tries += 1;
2535                    if tries >= cap {
2536                        terminal_error = Some(e);
2537                        break;
2538                    }
2539                    elog!(
2540                        "[pcmflux] audio capture open failed ({e}); retry {tries}/{cap} in {backoff_ms}ms"
2541                    );
2542                    let mut slept = 0u64;
2543                    while slept < backoff_ms && !inner.stop_pending() {
2544                        std::thread::sleep(Duration::from_millis(50));
2545                        slept += 50;
2546                    }
2547                    backoff_ms = (backoff_ms * 2).min(5000);
2548                    continue;
2549                }
2550            }
2551        }
2552        let s = session.as_mut().expect("session checked above");
2553        if !pump(&mut s.mainloop, PUMP_TIMEOUT_US) {
2554            elog!("[pcmflux] ERROR: mainloop iterate failed; reopening the session.");
2555            session = None;
2556            // Drop the partially reassembled frame: the next fragments come from after
2557            // the outage, and stitching them onto pre-outage PCM would emit one frame
2558            // with a discontinuity in the middle, charged to the wrong pts.
2559            run.pcm_fill_bytes = 0;
2560            let at = run.total_samples_processed;
2561            run.emit_quiet(at);
2562            continue;
2563        }
2564        let sstate = s.stream.get_state();
2565        if sstate != pulse::stream::State::Ready {
2566            elog!("[pcmflux] record stream lost; reopening the session.");
2567            session = None;
2568            run.pcm_fill_bytes = 0;
2569            let at = run.total_samples_processed;
2570            run.emit_quiet(at);
2571            continue;
2572        }
2573
2574        loop {
2575            let mut discard = false;
2576            let mut done = false;
2577            match s.stream.peek() {
2578                Ok(PeekResult::Empty) => done = true,
2579                Ok(PeekResult::Hole(_)) => discard = true,
2580                Ok(PeekResult::Data(buf)) => {
2581                    run.feed(buf);
2582                    discard = true;
2583                }
2584                Err(_) => {
2585                    elog!("[pcmflux] ERROR: pa_stream_peek() failed.");
2586                    done = true;
2587                }
2588            }
2589            if discard {
2590                let _ = s.stream.discard();
2591            }
2592            if done {
2593                break;
2594            }
2595        }
2596
2597        if run.inner.debug_logging.load(Ordering::Relaxed) {
2598            let elapsed = last_log.elapsed();
2599            if elapsed >= Duration::from_secs(2) {
2600                let secs = elapsed.as_secs_f64();
2601                let kbps = (run.bytes_encoded * 8) as f64 / (secs * 1000.0);
2602                let silent_pct = if run.chunks_read > 0 {
2603                    100.0 * run.chunks_silent as f64 / run.chunks_read as f64
2604                } else {
2605                    0.0
2606                };
2607                plog!(
2608                    "[pcmflux] Status | Read: {}, Silent: {} ({:.1}%), Encoded: {}, Rate: {:.2} kbps",
2609                    run.chunks_read,
2610                    run.chunks_silent,
2611                    silent_pct,
2612                    run.chunks_encoded,
2613                    kbps
2614                );
2615                last_log = Instant::now();
2616                run.chunks_read = 0;
2617                run.chunks_silent = 0;
2618                run.chunks_encoded = 0;
2619                run.bytes_encoded = 0;
2620            }
2621        }
2622    }
2623
2624    if let Some(e) = terminal_error {
2625        inner.fail(format!(
2626            "audio capture could not {} (last error: {e}); stopping.",
2627            if ever_connected {
2628                "stay connected"
2629            } else {
2630                "connect"
2631            }
2632        ));
2633    } else {
2634        plog!("[pcmflux] Stop requested. Cleaning up capture loop...");
2635        inner.started_ok.store(false, Ordering::Release);
2636        // A stop before the first session came up must also resolve the startup
2637        // handshake, or the waiting `start_capture` polls out with the run still STARTING.
2638        inner.start_state.store(ST_IDLE, Ordering::Release);
2639    }
2640    if let Some(s) = session.as_mut() {
2641        let _ = s.stream.disconnect();
2642    }
2643    drop(session);
2644    drop(run);
2645    drop(delivery);
2646    let dropped = ring.dropped.load(Ordering::Relaxed);
2647    if dropped > 0 {
2648        plog!("[pcmflux] Delivery ring dropped {dropped} stale frame(s) to a slow consumer.");
2649    }
2650    plog!("[pcmflux] Audio capture loop finished. Resources released.");
2651}
2652
2653/// One PulseAudio session for playback: mainloop, context, and the playback stream, all
2654/// recreated together on reconnect — the mirror of `PaCaptureSession`, with the same
2655/// drop-order requirement (stream first, then its context, then the mainloop).
2656struct PaPlaybackSession {
2657    stream: Stream,
2658    /// Must outlive the stream (the connection owns it); never read after open.
2659    #[allow(dead_code)]
2660    context: Context,
2661    mainloop: Mainloop,
2662}
2663
2664/// Open a playback session: mainloop + context + playback stream driven to `Ready` on the
2665/// bounded pump, honoring `stop_pending` at every turn. The mirror of
2666/// `pa_capture_session_open`.
2667///
2668/// Every failure is `Transient`: `connect_playback` resolves a sink name asynchronously,
2669/// so a wrong device name and a server that is still coming up are the same failed stream
2670/// state here, and the caller's retry budget is what bounds either one.
2671fn pa_playback_session_open(
2672    inner: &Inner,
2673    spec: &Spec,
2674    device: Option<&str>,
2675    attr: &BufferAttr,
2676) -> Result<PaPlaybackSession, SessionOpenError> {
2677    let tr = |e: &str| SessionOpenError::Transient(e.to_string());
2678    let mut mainloop = match Mainloop::new() {
2679        Some(m) => m,
2680        None => return Err(tr("pa_mainloop_new() failed (playback)")),
2681    };
2682    let mut context = match Context::new(&mainloop, "pcmflux") {
2683        Some(c) => c,
2684        None => return Err(tr("pa_context_new() failed (playback)")),
2685    };
2686    if context.connect(None, CtxFlags::NOFLAGS, None).is_err() {
2687        return Err(tr("pa_context_connect() failed (playback)"));
2688    }
2689    loop {
2690        let st = context.get_state();
2691        if st == pulse::context::State::Ready {
2692            break;
2693        }
2694        if !st.is_good() {
2695            return Err(tr("PulseAudio context connection failed (playback)"));
2696        }
2697        if inner.stop_pending() {
2698            return Err(SessionOpenError::Aborted);
2699        }
2700        if !pump(&mut mainloop, PUMP_TIMEOUT_US) {
2701            return Err(tr("mainloop iterate failed during connect (playback)"));
2702        }
2703    }
2704
2705    let mut stream = match Stream::new(&mut context, "Microphone Playback", spec, None) {
2706        Some(s) => s,
2707        None => return Err(tr("pa_stream_new() failed (playback)")),
2708    };
2709    if stream
2710        .connect_playback(device, Some(attr), StreamFlags::ADJUST_LATENCY, None, None)
2711        .is_err()
2712    {
2713        return Err(SessionOpenError::Transient(format!(
2714            "pa_stream_connect_playback() failed (device '{}')",
2715            device.unwrap_or("default")
2716        )));
2717    }
2718    loop {
2719        let st = stream.get_state();
2720        if st == pulse::stream::State::Ready {
2721            break;
2722        }
2723        if !st.is_good() {
2724            return Err(SessionOpenError::Transient(format!(
2725                "PulseAudio playback stream failed (device '{}')",
2726                device.unwrap_or("default")
2727            )));
2728        }
2729        if inner.stop_pending() {
2730            return Err(SessionOpenError::Aborted);
2731        }
2732        if !pump(&mut mainloop, PUMP_TIMEOUT_US) {
2733            return Err(tr(
2734                "mainloop iterate failed during stream connect (playback)",
2735            ));
2736        }
2737    }
2738    Ok(PaPlaybackSession {
2739        stream,
2740        context,
2741        mainloop,
2742    })
2743}
2744
2745/// Drive one whole mic-playback run on the playback thread. The body handed to
2746/// `spawn_worker`; the mirror of `capture_run` for the uplink.
2747///
2748/// This thread solely owns the PA playback stream, so writes are serialized structurally
2749/// with no executor. It mirrors `capture_run`'s lifecycle: `start_state` goes `RUNNING`
2750/// once the first session is up, `FAILED` (with a `last_error`) when the run gives up, and
2751/// `IDLE` on a clean stop; it returns when `stop_state` leaves `STOP_NONE` or the retry
2752/// budget is spent.
2753///
2754/// **Session loop**: the sink can die under a live stream (PulseAudio/PipeWire restart,
2755/// sink removed). Breaking out there would leave the mic uplink dead until something
2756/// upstream noticed and restarted the whole playback, losing every packet in between, so
2757/// the session is reopened with the same backoff and budgets capture uses.
2758///
2759/// **Buffer sizing and the prebuf timing rule** (load-bearing): `tlength` is the target
2760/// latency in bytes, and `prebuf` is a quarter of it, floored to one frame. `prebuf` must
2761/// NOT be zero: with `prebuf == 0` PulseAudio starts playback instantly, the realtime read
2762/// pointer then runs ahead of the write index, and every `SeekMode::Relative` write lands
2763/// "in the past" — the server silently discards it forever (observed as bytes flowing at
2764/// exactly realtime rate while the sink monitor stayed silent). A quarter-buffer prebuf makes
2765/// the stream wait for data before starting and re-prebuffer after each underrun, so a late
2766/// chunk plays slightly delayed instead of vanishing.
2767///
2768/// **Hot loop**: `pump`s on the ~20 ms bound, then, whenever the server is writable, drains
2769/// that many bytes from the `PlayQueue` (clamped and frame-aligned) and writes them with
2770/// `free_cb = None`, so PA copies the bytes and the `scratch` buffer is reused next
2771/// iteration. Newly queued bytes are picked up on the next pump — no cross-thread wakeup is
2772/// needed, mirroring capture's poll style — and a stop is observed within the pump bound.
2773/// Each drain feeds `StandingDepth`, and depth that stood through a whole window is
2774/// spliced out of the queue's head, so the uplink's delay returns to the sink's own
2775/// buffer after a burst and does not grow with a client clock running fast.
2776fn playback_run(inner: &Inner, settings: &PbSettings, queue: &PlayQueue) {
2777    inner
2778        .debug_logging
2779        .store(settings.debug_logging, Ordering::Relaxed);
2780
2781    // Sample rate and channel count were validated by `extract_pb_settings`.
2782    let spec = Spec {
2783        format: Format::S16le,
2784        rate: settings.sample_rate,
2785        channels: settings.channels as u8,
2786    };
2787
2788    let device = settings.device_name.as_deref();
2789    plog!(
2790        "[pcmflux] Attempting to connect playback to PulseAudio device: {} (latency {}ms)",
2791        device.unwrap_or("system_default"),
2792        settings.latency_ms
2793    );
2794
2795    let tlength = spec.usec_to_bytes(MicroSeconds(settings.latency_ms.max(0) as u64 * 1000)) as u32;
2796    let attr = BufferAttr {
2797        maxlength: u32::MAX,
2798        tlength,
2799        prebuf: (tlength / 4).max(spec.frame_size() as u32),
2800        minreq: u32::MAX,
2801        fragsize: u32::MAX,
2802    };
2803
2804    // Retry budgets, matching capture: a start gets a short window, a mid-run reconnect
2805    // one long enough to outlast a whole PulseAudio/PipeWire restart.
2806    const START_TRIES: u32 = 12;
2807    const RECONNECT_TRIES: u32 = 40;
2808    let mut session: Option<PaPlaybackSession> = None;
2809    let mut ever_connected = false;
2810    let mut tries: u32 = 0;
2811    let mut backoff_ms: u64 = 250;
2812    let mut terminal_error: Option<String> = None;
2813
2814    let mut scratch: Vec<u8> = Vec::new();
2815    let mut bytes_written: u64 = 0;
2816    let mut bytes_cut: u64 = 0;
2817    let mut writable_hits: u64 = 0;
2818    let mut last_pb_log = Instant::now();
2819    let mut standing = StandingDepth::new(spec.bytes_per_second(), spec.frame_size());
2820
2821    loop {
2822        if inner.stop_pending() {
2823            break;
2824        }
2825        if session.is_none() {
2826            match pa_playback_session_open(inner, &spec, device, &attr) {
2827                Ok(s) => {
2828                    session = Some(s);
2829                    tries = 0;
2830                    backoff_ms = 250;
2831                    if !ever_connected {
2832                        ever_connected = true;
2833                        inner.started_ok.store(true, Ordering::Release);
2834                        inner.start_state.store(ST_RUNNING, Ordering::Release);
2835                        plog!(
2836                            "[pcmflux] Playback loop started. Device: {}, Rate: {}, Channels: {}, Latency: {}ms",
2837                            device.unwrap_or("system_default"),
2838                            settings.sample_rate,
2839                            settings.channels,
2840                            settings.latency_ms
2841                        );
2842                    } else {
2843                        // Mic audio queued during the outage is stale; playing it out
2844                        // would only push that much extra latency into the uplink.
2845                        queue.clear();
2846                        standing.reset();
2847                        plog!("[pcmflux] audio playback reconnected; resuming.");
2848                    }
2849                }
2850                Err(SessionOpenError::Aborted) => break,
2851                Err(SessionOpenError::DeviceNotFound(e)) | Err(SessionOpenError::Transient(e)) => {
2852                    tries += 1;
2853                    let cap = if ever_connected {
2854                        RECONNECT_TRIES
2855                    } else {
2856                        START_TRIES
2857                    };
2858                    if tries >= cap {
2859                        terminal_error = Some(e);
2860                        break;
2861                    }
2862                    elog!(
2863                        "[pcmflux] audio playback open failed ({e}); retry {tries}/{cap} in {backoff_ms}ms"
2864                    );
2865                    let mut slept = 0u64;
2866                    while slept < backoff_ms && !inner.stop_pending() {
2867                        std::thread::sleep(Duration::from_millis(50));
2868                        slept += 50;
2869                    }
2870                    backoff_ms = (backoff_ms * 2).min(5000);
2871                    continue;
2872                }
2873            }
2874        }
2875        let s = session.as_mut().expect("session checked above");
2876        if !pump(&mut s.mainloop, PUMP_TIMEOUT_US) {
2877            elog!("[pcmflux] ERROR: mainloop iterate failed; reopening the playback session.");
2878            session = None;
2879            continue;
2880        }
2881        if s.stream.get_state() != pulse::stream::State::Ready {
2882            elog!("[pcmflux] playback stream lost; reopening the session.");
2883            session = None;
2884            continue;
2885        }
2886        if let Some(can) = s.stream.writable_size()
2887            && can > 0
2888        {
2889            writable_hits += 1;
2890            let left = queue.drain_upto(can, &mut scratch);
2891            if !scratch.is_empty() {
2892                if let Err(e) = s
2893                    .stream
2894                    .write(&scratch, None, 0, pulse::stream::SeekMode::Relative)
2895                {
2896                    elog!("[pcmflux] ERROR: pa_stream_write() failed: {e:?}");
2897                } else {
2898                    bytes_written += scratch.len() as u64;
2899                }
2900            }
2901            if let Some(cut) = standing.after_drain(scratch.len(), left) {
2902                queue.splice_head(cut, standing.keep);
2903                bytes_cut += cut as u64;
2904            }
2905        }
2906        if inner.debug_logging.load(Ordering::Relaxed) && last_pb_log.elapsed().as_secs() >= 1 {
2907            plog!(
2908                "[pcmflux] Playback | writable_hits: {writable_hits}, bytes_written: {bytes_written}, queued: {}, standing depth cut: {bytes_cut}",
2909                queue.buf.lock().map(|q| q.len()).unwrap_or(0)
2910            );
2911            last_pb_log = Instant::now();
2912        }
2913    }
2914
2915    if let Some(e) = terminal_error {
2916        inner.fail(format!(
2917            "audio playback could not {} (last error: {e}); stopping.",
2918            if ever_connected {
2919                "stay connected"
2920            } else {
2921                "connect"
2922            }
2923        ));
2924    } else {
2925        plog!("[pcmflux] Stop requested. Cleaning up playback loop...");
2926        inner.started_ok.store(false, Ordering::Release);
2927        // A stop before the first session came up must also resolve the startup
2928        // handshake, so `worker_alive` stops reporting a STARTING run that will never run.
2929        inner.start_state.store(ST_IDLE, Ordering::Release);
2930    }
2931    if let Some(s) = session.as_mut() {
2932        let _ = s.stream.disconnect();
2933    }
2934    drop(session);
2935    plog!("[pcmflux] Audio playback loop finished. Resources released.");
2936}
2937
2938/// Python-facing capture handle. Owns the `Shared` lifecycle state and exposes
2939/// `start_capture` / `stop_capture` / `update_audio_bitrate` / `is_capturing` to Python.
2940#[pyclass]
2941struct AudioCapture {
2942    shared: Arc<Shared>,
2943}
2944
2945impl AudioCapture {
2946    fn inner(&self) -> &Arc<Inner> {
2947        &self.shared.inner
2948    }
2949}
2950
2951#[pymethods]
2952impl AudioCapture {
2953    #[new]
2954    fn new() -> Self {
2955        AudioCapture {
2956            shared: Arc::new(Shared {
2957                inner: Arc::new(Inner::new()),
2958                thread: Mutex::new(None),
2959            }),
2960        }
2961    }
2962
2963    /// Start (or restart) audio capture, delivering encoded frames to `callback`; with no
2964    /// callback the run serves only its `output_socket` and no frame ever reaches Python.
2965    ///
2966    /// 1. **Re-entrancy guard**: if called on one of the run's own threads — the delivery
2967    ///    thread (where the Python callback actually executes) or the capture thread — it
2968    ///    cannot join/recreate the run it is part of (the capture thread joins the delivery
2969    ///    thread on teardown, so a join from either closes a cycle), so it just undoes a
2970    ///    nested SELF-stop and returns. That undo is a compare-exchange that clears the stop
2971    ///    ONLY if this thread still owns it — if an external stop stored `STOP_EXTERNAL`
2972    ///    meanwhile, the CAS fails and that stop stands (clearing it would strand its
2973    ///    in-flight join forever).
2974    /// 2. **Spawn with the GIL released**: `spawn_worker` stops/joins any prior thread and
2975    ///    spawns the new one via `py.detach`, because the lifecycle lock and `join()` must not
2976    ///    be held while holding the GIL — joining the capture thread transitively joins the
2977    ///    delivery thread, whose in-flight callback needs the GIL, so that would deadlock.
2978    ///    The stop/clear ordering (the lost-stop invariant) lives in `spawn_worker`.
2979    /// 3. **Register** the handle for the atexit sweep (best-effort), pruning dead weaks.
2980    /// 4. **Startup handshake** (`await_start`): waits up to ~2 s (GIL released) for the
2981    ///    thread to publish `RUNNING` or `FAILED`, returning `Ok` while it is still
2982    ///    `STARTING` — the retry ladder can run longer than the window, so a later failure
2983    ///    is observed through `state` / `last_error`. On `FAILED`, `join_failed_start` tears
2984    ///    down ONLY the thread this call spawned (identity-checked) — a concurrent start may
2985    ///    already own the slot with a live run that must survive — and `last_error` is raised.
2986    #[pyo3(signature = (settings, callback = None))]
2987    fn start_capture(
2988        &self,
2989        py: Python<'_>,
2990        settings: &Bound<'_, PyAny>,
2991        callback: Option<Py<PyAny>>,
2992    ) -> PyResult<()> {
2993        let inner = self.inner().clone();
2994
2995        let me = gettid();
2996        if inner.is_own_thread(me) {
2997            inner.undo_self_stop(me);
2998            return Ok(());
2999        }
3000
3001        let parsed = extract_settings(settings)?;
3002
3003        let shared = &self.shared;
3004        let inner_ref = &inner;
3005        let t_inner = inner.clone();
3006        let body = move || capture_run(&t_inner, &parsed, callback.as_ref());
3007        let spawned =
3008            py.detach(move || spawn_worker(&shared.thread, inner_ref, "pcmflux-capture", body));
3009        let my_thread = match spawned {
3010            Some(id) => id,
3011            None => {
3012                return Err(pyo3::exceptions::PyRuntimeError::new_err(
3013                    "capture thread spawn failed",
3014                ));
3015            }
3016        };
3017
3018        if let Ok(mut reg) = registry().lock() {
3019            reg.retain(|w| w.strong_count() > 0);
3020            reg.push(Arc::downgrade(&self.shared));
3021        }
3022
3023        await_start(py, &self.shared.thread, &inner, my_thread, "audio capture")
3024    }
3025
3026    /// Stop audio capture, joining the capture thread.
3027    ///
3028    /// A re-entrant stop from one of the run's own threads — the delivery thread (where the
3029    /// Python callback executes) or the capture thread — only records a self-stop: a join
3030    /// from inside the run would cycle (the capture thread joins the delivery thread on
3031    /// teardown). It must not clobber an external stop already in effect, which has to win
3032    /// the join. Otherwise it takes the lifecycle lock and joins with the GIL released (via
3033    /// `py.detach`): joining the capture thread transitively joins the delivery thread,
3034    /// whose in-flight callback needs the GIL, so holding it would deadlock. The
3035    /// authoritative external stop is set INSIDE the lock immediately before the join, so
3036    /// it wins over any concurrent self-stop.
3037    fn stop_capture(&self, py: Python<'_>) {
3038        let inner = self.inner();
3039        let me = gettid();
3040        if inner.is_own_thread(me) {
3041            inner.request_self_stop(me);
3042            return;
3043        }
3044        let shared = &self.shared;
3045        py.detach(|| stop_and_join(&shared.thread, inner));
3046    }
3047
3048    /// Set the live Opus target bitrate (bits/s) via the atomic mirror; the capture
3049    /// loop applies it on the next frame, without a restart. Values are clamped to the
3050    /// valid Opus range so an out-of-range request can never wedge the encoder.
3051    fn update_audio_bitrate(&self, bps: i32) {
3052        let clamped = bps.clamp(OPUS_BITRATE_MIN, OPUS_BITRATE_MAX);
3053        self.inner().opus_bitrate.store(clamped, Ordering::Relaxed);
3054    }
3055
3056    /// Have a surround capture also deliver every frame downmixed to stereo, live: each
3057    /// surround frame is followed by its companion, the same `pts` and `channels == 2`,
3058    /// encoded by a stereo encoder of its own at the capture's bitrate, for consumers
3059    /// that decode no multistream Opus. Off by default, it keeps its setting across
3060    /// restarts, and a mono or stereo capture ignores it.
3061    fn set_stereo_companion(&self, enabled: bool) {
3062        self.inner()
3063            .stereo_companion
3064            .store(enabled, Ordering::Relaxed);
3065    }
3066
3067    /// True while a capture worker is connected and running with no stop pending; false
3068    /// while still starting and after a failure — see `state` to tell those apart.
3069    #[getter]
3070    fn is_capturing(&self) -> bool {
3071        self.inner().running()
3072    }
3073
3074    /// Lifecycle phase: `"idle"`, `"starting"`, `"running"`, or `"failed"`. A run that
3075    /// fails after `start_capture` returned (its retry ladder gave up, or a mid-run
3076    /// reconnect budget was spent) reads `"failed"` with the reason in `last_error`.
3077    #[getter]
3078    fn state(&self) -> &'static str {
3079        self.inner().state_name()
3080    }
3081
3082    /// Why the last run failed, or `None` while no run has failed since the last start.
3083    #[getter]
3084    fn last_error(&self) -> Option<String> {
3085        self.inner().last_error()
3086    }
3087}
3088
3089impl Drop for AudioCapture {
3090    /// Best-effort stop on GC/dealloc: the re-entrant case (running on the run's own
3091    /// delivery or capture thread) records a self-stop only (never clobbering a pending
3092    /// external stop); otherwise it takes the lifecycle lock and joins the capture thread
3093    /// with the GIL released, matching `stop_capture`.
3094    fn drop(&mut self) {
3095        let inner = &self.shared.inner;
3096        let me = gettid();
3097        if inner.is_own_thread(me) {
3098            inner.request_self_stop(me);
3099            return;
3100        }
3101        let shared = &self.shared;
3102        Python::attach(|py| py.detach(|| stop_and_join(&shared.thread, inner)));
3103    }
3104}
3105
3106/// Python-facing mic-playback handle, symmetric to `AudioCapture`.
3107///
3108/// Same lifecycle protocol, but a PA playback stream instead of a record stream, and a
3109/// bounded drop-oldest queue fed by `write` / `write_red` instead of a Python callback.
3110/// Python never holds the PA handle — only the playback thread touches it — so a
3111/// close-versus-inflight-write use-after-free is structurally impossible here.
3112#[pyclass]
3113struct AudioPlayback {
3114    shared: Arc<PbShared>,
3115}
3116
3117impl AudioPlayback {
3118    fn inner(&self) -> &Arc<Inner> {
3119        &self.shared.inner
3120    }
3121
3122    /// Run `decode` on this run's Opus decoder with the bytes of a bytes-like `data`, off
3123    /// the GIL. The shared body of `write` and `write_red`.
3124    ///
3125    /// Gated on `worker_alive`: it raises once no playback thread services the queue (start
3126    /// failure, stop, or a PA outage the session loop could not reconnect through), so the
3127    /// caller's reopen-on-error path engages instead of the audio being swallowed silently.
3128    /// A reconnect in progress stays "alive" and keeps queueing.
3129    ///
3130    /// A `bytes` payload is immutable, so it is borrowed in place across the GIL release.
3131    /// Any other bytes-like object — a C-contiguous buffer-protocol exporter of any item
3132    /// format, the same set CPython's own `y*` argument parsing accepts (`memoryview`,
3133    /// `bytearray`, `array`, NumPy, an `AudioFrame`, ...) — may be mutated by another Python
3134    /// thread once the GIL is dropped, so its bytes are copied under the GIL into the
3135    /// decoder's reusable scratch buffer first; an Opus packet is a few hundred bytes, and
3136    /// the steady state allocates nothing.
3137    fn decode_packet(
3138        &self,
3139        py: Python<'_>,
3140        data: &Bound<'_, PyAny>,
3141        decode: impl FnOnce(&mut OpusPlaybackDecoder, &[u8], &PlayQueue) + Send,
3142    ) -> PyResult<()> {
3143        if !self.inner().worker_alive() {
3144            return Err(pyo3::exceptions::PyRuntimeError::new_err(
3145                "audio playback is not running (stream failed, stopped, or never started)",
3146            ));
3147        }
3148        let queue = &*self.shared.queue;
3149        if let Ok(b) = data.cast::<PyBytes>() {
3150            let packet = b.as_bytes();
3151            py.detach(|| {
3152                let mut dec = self
3153                    .shared
3154                    .opus_dec
3155                    .lock()
3156                    .unwrap_or_else(|e| e.into_inner());
3157                if let Some(d) = dec.as_mut() {
3158                    decode(d, packet, queue);
3159                }
3160            });
3161            return Ok(());
3162        }
3163        let buf = PyUntypedBuffer::get(data)?;
3164        if !buf.is_c_contiguous() {
3165            return Err(pyo3::exceptions::PyBufferError::new_err(
3166                "a contiguous bytes-like object is required",
3167            ));
3168        }
3169        let mut dec = self
3170            .shared
3171            .opus_dec
3172            .lock()
3173            .unwrap_or_else(|e| e.into_inner());
3174        let Some(d) = dec.as_mut() else {
3175            return Ok(());
3176        };
3177        let mut packet = std::mem::take(&mut d.packet);
3178        packet.clear();
3179        // A C-contiguous Py_buffer exposes exactly `len_bytes()` readable bytes at `buf`,
3180        // whatever its item format; the export is held (and the GIL, so no Python thread
3181        // can resize or mutate the exporter) for the duration of this copy.
3182        let n = buf.len_bytes();
3183        if n > 0 {
3184            packet.extend_from_slice(unsafe {
3185                std::slice::from_raw_parts(buf.buf_ptr() as *const u8, n)
3186            });
3187        }
3188        drop(buf);
3189        py.detach(|| decode(d, &packet, queue));
3190        d.packet = packet;
3191        Ok(())
3192    }
3193}
3194
3195#[pymethods]
3196impl AudioPlayback {
3197    #[new]
3198    fn new() -> Self {
3199        AudioPlayback {
3200            shared: Arc::new(PbShared {
3201                inner: Arc::new(Inner::new()),
3202                thread: Mutex::new(None),
3203                queue: Arc::new(PlayQueue::new()),
3204                opus_dec: Mutex::new(None),
3205            }),
3206        }
3207    }
3208
3209    /// Start (or restart) mic playback into the virtual sink. The playback mirror of
3210    /// `start_capture`.
3211    ///
3212    /// Same shape as capture: a re-entrant start from the playback thread just undoes a
3213    /// nested self-stop; the worker is spawned with the GIL released (the stop/clear ordering
3214    /// lives in `spawn_worker`); the handle is registered for the atexit sweep; and the ~2 s
3215    /// `await_start` handshake raises a `FAILED` start (with `last_error`) after tearing
3216    /// down only the thread THIS call spawned (identity-checked, sparing a concurrent
3217    /// winner), while a start still in its retry ladder returns `Ok` and is watched through
3218    /// `state` / `last_error`. Before spawning, it applies this run's byte bound + frame
3219    /// alignment to the queue (dropping any stale audio) and creates the Opus decoder up
3220    /// front, since the mic uplink is always Opus and `write` / `write_red` decode packets
3221    /// to PCM off the GIL for this same run.
3222    fn start(&self, py: Python<'_>, settings: &Bound<'_, PyAny>) -> PyResult<()> {
3223        let inner = self.inner().clone();
3224
3225        let me = gettid();
3226        if inner.capture_tid.load(Ordering::Acquire) == me {
3227            inner.undo_self_stop(me);
3228            return Ok(());
3229        }
3230
3231        let parsed = extract_pb_settings(settings)?;
3232        let frame_bytes = (parsed.channels.max(1) as usize) * 2;
3233        self.shared
3234            .queue
3235            .configure(parsed.max_buffer_bytes, frame_bytes);
3236
3237        let decoder = OpusPlaybackDecoder::new(parsed.sample_rate, parsed.channels);
3238        if decoder.is_none() {
3239            return Err(pyo3::exceptions::PyRuntimeError::new_err(
3240                "failed to create Opus decoder for playback",
3241            ));
3242        }
3243        *self
3244            .shared
3245            .opus_dec
3246            .lock()
3247            .unwrap_or_else(|e| e.into_inner()) = decoder;
3248
3249        let shared = &self.shared;
3250        let inner_ref = &inner;
3251        let queue = self.shared.queue.clone();
3252        let t_inner = inner.clone();
3253        let body = move || playback_run(&t_inner, &parsed, &queue);
3254        let spawned =
3255            py.detach(move || spawn_worker(&shared.thread, inner_ref, "pcmflux-playback", body));
3256        let my_thread = match spawned {
3257            Some(id) => id,
3258            None => {
3259                return Err(pyo3::exceptions::PyRuntimeError::new_err(
3260                    "playback thread spawn failed",
3261                ));
3262            }
3263        };
3264
3265        if let Ok(mut reg) = playback_registry().lock() {
3266            reg.retain(|w| w.strong_count() > 0);
3267            reg.push(Arc::downgrade(&self.shared));
3268        }
3269
3270        await_start(py, &self.shared.thread, &inner, my_thread, "audio playback")
3271    }
3272
3273    /// Push one Opus mic packet for playback. The steady-state hot path.
3274    ///
3275    /// `data` is any bytes-like object (`bytes`, `memoryview`, `bytearray`, an
3276    /// `AudioFrame`, ...); see `decode_packet` for the liveness gate and how the payload is
3277    /// borrowed. The decode runs with the GIL released — it touches no Python state, so
3278    /// dropping the GIL lets it run concurrently with the rest of the app — and a bad packet
3279    /// is dropped rather than corrupting the stream. It never blocks on PA (drop-oldest
3280    /// happens inside `PlayQueue::push`).
3281    fn write(&self, py: Python<'_>, data: &Bound<'_, PyAny>) -> PyResult<()> {
3282        self.decode_packet(py, data, |dec, packet, queue| {
3283            if let Some(pcm) = dec.decode_to_pcm(packet) {
3284                queue.push(pcm);
3285            }
3286        })
3287    }
3288
3289    /// Play one RFC 2198 RED mic frame from the WebRTC/UDP uplink, recovering across any
3290    /// packet loss on the way in. The lossy-transport counterpart of `write`.
3291    ///
3292    /// The payload is de-framed, loss-recovered, and decoded entirely off the GIL by
3293    /// `decode_red_into_queue` (see there for why RED exists and why the decode runs off the
3294    /// GIL). `primary_ts` is the packet's monotonic RTP timestamp; the redundant blocks carry
3295    /// offsets back from it. Accepts any bytes-like `data` and is gated on `worker_alive`
3296    /// exactly like `write` (see `decode_packet`).
3297    fn write_red(&self, py: Python<'_>, data: &Bound<'_, PyAny>, primary_ts: i64) -> PyResult<()> {
3298        self.decode_packet(py, data, move |dec, packet, queue| {
3299            dec.decode_red_into_queue(packet, primary_ts, queue);
3300        })
3301    }
3302
3303    /// Stop mic playback, joining the playback thread.
3304    ///
3305    /// A re-entrant stop from the playback thread records a self-stop only (it cannot
3306    /// self-join) and never clobbers an external stop already in effect. Otherwise it joins
3307    /// with the GIL released so a slow PA disconnect cannot stall the interpreter; `stop`
3308    /// returns only once the thread is joined and the sink is released.
3309    fn stop(&self, py: Python<'_>) {
3310        let inner = self.inner();
3311        let me = gettid();
3312        if inner.capture_tid.load(Ordering::Acquire) == me {
3313            inner.request_self_stop(me);
3314            return;
3315        }
3316        let shared = &self.shared;
3317        py.detach(|| stop_and_join(&shared.thread, inner));
3318    }
3319
3320    /// True while a playback worker is connected and running with no stop pending; false
3321    /// while still starting and after a failure — see `state` to tell those apart.
3322    #[getter]
3323    fn is_running(&self) -> bool {
3324        self.inner().running()
3325    }
3326
3327    /// Lifecycle phase: `"idle"`, `"starting"`, `"running"`, or `"failed"`, the playback
3328    /// mirror of `AudioCapture.state`.
3329    #[getter]
3330    fn state(&self) -> &'static str {
3331        self.inner().state_name()
3332    }
3333
3334    /// Why the last run failed, or `None` while no run has failed since the last start.
3335    #[getter]
3336    fn last_error(&self) -> Option<String> {
3337        self.inner().last_error()
3338    }
3339}
3340
3341impl Drop for AudioPlayback {
3342    /// Best-effort stop on GC/dealloc; symmetric to `AudioCapture::drop`.
3343    fn drop(&mut self) {
3344        let inner = &self.shared.inner;
3345        let me = gettid();
3346        if inner.capture_tid.load(Ordering::Acquire) == me {
3347            inner.request_self_stop(me);
3348            return;
3349        }
3350        let shared = &self.shared;
3351        Python::attach(|py| py.detach(|| stop_and_join(&shared.thread, inner)));
3352    }
3353}
3354
3355/// atexit sweep: stop and join every live capture and playback before interpreter
3356/// shutdown, so no worker thread is still calling into Python during finalization.
3357///
3358/// Snapshots the two `Weak` registries into strong references (skipping any already
3359/// dropped), then hands each to `stop_and_join` with the GIL released. Registered on
3360/// `atexit` from the module init.
3361#[pyfunction]
3362fn _stop_all_captures(py: Python<'_>) {
3363    let snapshot: Vec<Arc<Shared>> = match registry().lock() {
3364        Ok(reg) => reg.iter().filter_map(|w| w.upgrade()).collect(),
3365        Err(_) => Vec::new(),
3366    };
3367    for shared in snapshot {
3368        py.detach(|| stop_and_join(&shared.thread, &shared.inner));
3369    }
3370    let pb_snapshot: Vec<Arc<PbShared>> = match playback_registry().lock() {
3371        Ok(reg) => reg.iter().filter_map(|w| w.upgrade()).collect(),
3372        Err(_) => Vec::new(),
3373    };
3374    for shared in pb_snapshot {
3375        py.detach(|| stop_and_join(&shared.thread, &shared.inner));
3376    }
3377}
3378
3379#[cfg(test)]
3380mod tests {
3381    use super::*;
3382
3383    /// A lifecycle lock poisoned by an unrelated panic must not be what leaves a worker
3384    /// thread running into interpreter finalization: `stop_and_join` takes the lock anyway,
3385    /// so the handle is still taken and joined. The join itself would hang if the stop were
3386    /// not set before it.
3387    #[test]
3388    fn stop_and_join_takes_a_poisoned_lifecycle_lock() {
3389        use std::sync::Arc;
3390        let inner = Arc::new(Inner::new());
3391        let inner_c = inner.clone();
3392        let worker = std::thread::spawn(move || {
3393            while !inner_c.stop_pending() {
3394                std::thread::yield_now();
3395            }
3396        });
3397        let slot = Arc::new(Mutex::new(Some(worker)));
3398        let poisoner = slot.clone();
3399        let _ = std::thread::spawn(move || {
3400            let _guard = poisoner.lock().unwrap();
3401            panic!("poison");
3402        })
3403        .join();
3404        assert!(slot.is_poisoned());
3405        stop_and_join(&slot, &inner);
3406        assert!(slot.lock().unwrap_or_else(|e| e.into_inner()).is_none());
3407        assert_eq!(inner.capture_tid.load(Ordering::Acquire), 0);
3408    }
3409
3410    /// Timestamps that wrap the 32-bit RTP range still decode in order — the
3411    /// post-rollover frame is not mistaken for an already-played duplicate.
3412    #[test]
3413    fn red_playback_timestamp_wraparound() {
3414        let f = opus_frames(3);
3415        let mut dec = OpusPlaybackDecoder::new(24000, 1).unwrap();
3416        let q = PlayQueue::new();
3417        q.configure(1 << 20, 2);
3418        let wrap = (u32::MAX as i64) - 100;
3419        dec.decode_red_into_queue(&build_red_payload(&[], &f[0]), wrap, &q);
3420        // One 20 ms step past the 32-bit rollover (mod 2^32): ts is numerically far
3421        // BELOW `wrap`, so a plain `ts > last` treats it as an already-played frame.
3422        let next = (wrap.wrapping_add(480)) & 0xFFFF_FFFF;
3423        dec.decode_red_into_queue(&build_red_payload(&[(480, &f[0])], &f[1]), next, &q);
3424        let mut out = Vec::new();
3425        q.drain_upto(1 << 20, &mut out);
3426        assert_eq!(
3427            out.len(),
3428            2 * FRAME_PCM_BYTES,
3429            "frame after the 32-bit wrap was dropped as a duplicate"
3430        );
3431    }
3432
3433    /// The re-entrancy guard must recognize BOTH of a run's own threads: the
3434    /// capture worker AND the delivery thread — the Python callback executes on the
3435    /// delivery thread, so a stop/start it issues arrives with the delivery tid, and
3436    /// treating it as external would join into the capture→delivery join cycle and
3437    /// deadlock.
3438    #[test]
3439    fn reentrancy_guard_matches_delivery_thread() {
3440        let inner = Inner::new();
3441        let me = gettid();
3442        assert!(
3443            !inner.is_own_thread(me),
3444            "no run live: nothing should match"
3445        );
3446        inner.deliver_tid.store(me, Ordering::Release);
3447        assert!(
3448            inner.is_own_thread(me),
3449            "delivery tid must short-circuit the guard"
3450        );
3451        inner.deliver_tid.store(0, Ordering::Release);
3452        inner.capture_tid.store(me, Ordering::Release);
3453        assert!(
3454            inner.is_own_thread(me),
3455            "capture tid must still short-circuit the guard"
3456        );
3457    }
3458
3459    /// Encode the same noisy PCM with `complexity` and return the concatenated packets.
3460    fn encode_noise(channels: i32, complexity: Option<i32>) -> Vec<u8> {
3461        let ch = channels as usize;
3462        let frame = 960usize;
3463        let mut enc = PcmEncoder::new(48000, channels, true, 96000, complexity).expect("encoder");
3464        let mut seed = 0x2545_f491u32;
3465        let mut bytes = Vec::new();
3466        for _ in 0..8 {
3467            let pcm: Vec<i16> = (0..frame * ch)
3468                .map(|_| {
3469                    seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
3470                    ((seed >> 16) as i16) / 4
3471                })
3472                .collect();
3473            let mut out = vec![0u8; 4 * MAX_OPUS_PACKET];
3474            let n = enc.encode(&pcm, frame, &mut out).expect("encode");
3475            bytes.extend_from_slice(&out[..n]);
3476        }
3477        bytes
3478    }
3479
3480    /// The complexity reaches both encoder APIs: 0 and 10 produce different packets from the
3481    /// same input, and leaving it unset produces exactly what libopus's default of 9 does, so
3482    /// the default sends no request.
3483    #[test]
3484    fn opus_complexity_reaches_the_encoder() {
3485        for channels in [2, 6] {
3486            let low = encode_noise(channels, Some(0));
3487            let high = encode_noise(channels, Some(10));
3488            assert_ne!(
3489                low, high,
3490                "complexity 0 and 10 encoded identically ({channels} ch)"
3491            );
3492            assert_eq!(
3493                encode_noise(channels, None),
3494                encode_noise(channels, Some(9))
3495            );
3496        }
3497    }
3498
3499    /// `opus_complexity` is `None` by default, accepts 0 through 10, and rejects anything
3500    /// else with `ValueError`.
3501    #[test]
3502    fn opus_complexity_is_validated() {
3503        Python::initialize();
3504        Python::attach(|py| {
3505            let settings = Bound::new(py, AudioCaptureSettings::new(py)).unwrap();
3506            let settings = settings.as_any();
3507            assert_eq!(extract_settings(settings).unwrap().opus_complexity, None);
3508            for ok in [0, 5, 10] {
3509                settings.setattr("opus_complexity", ok).unwrap();
3510                assert_eq!(
3511                    extract_settings(settings).unwrap().opus_complexity,
3512                    Some(ok)
3513                );
3514            }
3515            for bad in [-1, 11] {
3516                settings.setattr("opus_complexity", bad).unwrap();
3517                let err = extract_settings(settings).err().expect("out of range");
3518                assert!(err.is_instance_of::<pyo3::exceptions::PyValueError>(py));
3519            }
3520        });
3521    }
3522
3523    /// Encode 5.1 with a tone only on FC (input channel 2), decode with the same
3524    /// layout, and verify the energy comes back on that same channel — proving the
3525    /// `multiopus_layout` tables are self-consistent end to end.
3526    #[test]
3527    fn multiopus_surround_roundtrip() {
3528        let channels = 6usize;
3529        let frame = 480usize;
3530        let mut enc = PcmEncoder::new(48000, channels as i32, true, 256000, None).expect("encoder");
3531        let mut pcm = vec![0i16; frame * channels];
3532        for i in 0..frame {
3533            let v =
3534                (8000.0 * (2.0 * std::f64::consts::PI * 440.0 * i as f64 / 48000.0).sin()) as i16;
3535            pcm[i * channels + 2] = v;
3536        }
3537        let mut out = vec![0u8; 4 * MAX_OPUS_PACKET];
3538        let mut n = 0;
3539        for _ in 0..3 {
3540            n = enc.encode(&pcm, frame, &mut out).expect("encode");
3541        }
3542        assert!(n > 0, "surround encode produced no bytes");
3543
3544        unsafe {
3545            let (streams, coupled, mapping) = multiopus_layout(channels as i32).unwrap();
3546            let mut err = 0;
3547            let dec = opusic_sys::opus_multistream_decoder_create(
3548                48000,
3549                channels as i32,
3550                streams,
3551                coupled,
3552                mapping.as_ptr(),
3553                &mut err,
3554            );
3555            assert!(!dec.is_null() && err == 0, "decoder create failed: {err}");
3556            let mut decoded = vec![0i16; frame * channels];
3557            let got = opusic_sys::opus_multistream_decode(
3558                dec,
3559                out.as_ptr(),
3560                n as i32,
3561                decoded.as_mut_ptr(),
3562                frame as i32,
3563                0,
3564            );
3565            opusic_sys::opus_multistream_decoder_destroy(dec);
3566            assert_eq!(got, frame as i32, "decode length mismatch");
3567            let mut rms = vec![0f64; channels];
3568            for i in 0..frame {
3569                for (c, r) in rms.iter_mut().enumerate() {
3570                    let s = decoded[i * channels + c] as f64;
3571                    *r += s * s;
3572                }
3573            }
3574            let loudest = rms
3575                .iter()
3576                .enumerate()
3577                .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3578                .unwrap()
3579                .0;
3580            assert_eq!(loudest, 2, "tone did not come back on FC: rms={rms:?}");
3581        }
3582    }
3583
3584    /// The stereo fold: the fronts pass at unity to their own side, the center and each
3585    /// surround channel at -3 dB to theirs, the LFE nowhere, and a sum past full scale
3586    /// saturates instead of wrapping.
3587    #[test]
3588    fn downmix_stereo_follows_bs775() {
3589        let one = |ch: usize, pos: usize, v: i16| {
3590            let mut src = vec![0i16; ch];
3591            src[pos] = v;
3592            let mut dst = [0i16; 2];
3593            downmix_stereo(&src, ch, &mut dst);
3594            dst
3595        };
3596        assert_eq!(one(6, 0, 10000), [10000, 0]);
3597        assert_eq!(one(6, 1, 10000), [0, 10000]);
3598        assert_eq!(one(6, 2, 10000), [7071, 7071]);
3599        assert_eq!(one(6, 3, 10000), [0, 0]);
3600        assert_eq!(one(6, 4, 10000), [7071, 0]);
3601        assert_eq!(one(6, 5, 10000), [0, 7071]);
3602        assert_eq!(one(8, 6, 10000), [7071, 0]);
3603        assert_eq!(one(8, 7, 10000), [0, 7071]);
3604        let mut dst = [0i16; 2];
3605        downmix_stereo(&[30000, -30000, 0, 0, 30000, -30000], 6, &mut dst);
3606        assert_eq!(dst, [i16::MAX, i16::MIN]);
3607    }
3608
3609    /// With the companion on, a surround capture hands every frame over twice: the
3610    /// multistream packet with the capture's channel count, then its stereo fold with the
3611    /// same pts, which a plain stereo decoder plays with the center tone on both sides; with
3612    /// it off, only the surround frame goes. The fold of the silent frame that ends a sound
3613    /// carries that frame's quiet bit.
3614    #[test]
3615    fn stereo_companion_follows_each_surround_frame() {
3616        let inner = Inner::new();
3617        let ring = DeliveryRing::new(64);
3618        let frame = 480usize;
3619        let pool = Arc::new(BufferPool::new(RED_PREFIX_MAX + 4 * MAX_OPUS_PACKET));
3620        let mut run = RunState {
3621            inner: &inner,
3622            ring: &ring,
3623            encoder: PcmEncoder::new(48000, 6, true, 256000, None).expect("encoder"),
3624            frame_size_per_channel: frame,
3625            channels: 6,
3626            accum: vec![0i16; frame * 6],
3627            silence_ref: vec![0i16; frame * 6],
3628            pcm_fill_bytes: 0,
3629            pool: PoolTaker::new(Arc::clone(&pool)),
3630            red_history: VecDeque::new(),
3631            red_spare: Vec::new(),
3632            red_distance: 0,
3633            sounding: false,
3634            tail_sounding: false,
3635            lookahead: 120,
3636            companion: Some((
3637                PcmEncoder::new(48000, 2, true, 256000, None).expect("stereo"),
3638                vec![0i16; frame * 2],
3639            )),
3640            companion_bitrate: 256000,
3641            total_samples_processed: 0,
3642            ogg: None,
3643            ogg_scale: 1,
3644            first_sound_detected: true,
3645            current_applied_bitrate: 256000,
3646            chunks_read: 0,
3647            chunks_silent: 0,
3648            chunks_encoded: 0,
3649            bytes_encoded: 0,
3650        };
3651        let mut pcm = vec![0i16; frame * 6];
3652        for i in 0..frame {
3653            pcm[i * 6 + 2] =
3654                ((2.0 * std::f64::consts::PI * 440.0 * i as f64 / 48000.0).sin() * 8000.0) as i16;
3655        }
3656        run.feed(bytemuck::cast_slice(&pcm));
3657        inner.stereo_companion.store(true, Ordering::Relaxed);
3658        for _ in 0..4 {
3659            run.feed(bytemuck::cast_slice(&pcm));
3660        }
3661        let tone_frames = ring.q.lock().unwrap().as_ref().expect("ring open").len();
3662        run.feed(bytemuck::cast_slice(&vec![0i16; frame * 6]));
3663        let queued = ring.q.lock().unwrap();
3664        let frames: Vec<(Vec<u8>, u64, u8)> = queued
3665            .as_ref()
3666            .expect("ring open")
3667            .iter()
3668            .cloned()
3669            .collect();
3670        let tags: Vec<(u64, u8, bool)> = frames
3671            .iter()
3672            .map(|(d, p, c)| (*p, *c, d[1] & WS_QUIET != 0))
3673            .collect();
3674        assert_eq!(
3675            tags,
3676            vec![
3677                (0, 6, false),
3678                (480, 6, false),
3679                (480, 2, false),
3680                (960, 6, false),
3681                (960, 2, false),
3682                (1440, 6, false),
3683                (1440, 2, false),
3684                (1920, 6, false),
3685                (1920, 2, false),
3686                (2400, 6, true),
3687                (2400, 6, true),
3688                (2400, 2, true)
3689            ]
3690        );
3691        let mut dec = OpusPlaybackDecoder::new(48000, 2).expect("decoder");
3692        let mut last = Vec::new();
3693        for (data, _, c) in &frames[..tone_frames] {
3694            if *c == 2 {
3695                last = dec.decode_to_pcm(&data[2..]).expect("decodes").to_vec();
3696            }
3697        }
3698        let samples: &[i16] = bytemuck::cast_slice(&last);
3699        let rms = |ch: usize| {
3700            (samples
3701                .iter()
3702                .skip(ch)
3703                .step_by(2)
3704                .map(|&v| (v as f64).powi(2))
3705                .sum::<f64>()
3706                / frame as f64)
3707                .sqrt()
3708        };
3709        assert!(
3710            rms(0) > 2000.0 && rms(1) > 2000.0,
3711            "center tone on both sides: {} {}",
3712            rms(0),
3713            rms(1)
3714        );
3715    }
3716
3717    /// Every surround layout the encoder takes has a channel map libpulse parses, with one
3718    /// position per channel and the front pair, center, and LFE where the encoder reads them.
3719    #[test]
3720    fn surround_channel_maps_parse() {
3721        use pulse::channelmap::Position;
3722        for channels in [6, 8] {
3723            let map = ChannelMap::new_from_string(surround_channel_map(channels).unwrap())
3724                .expect("parses");
3725            assert_eq!(map.len() as i32, channels);
3726            let pos = map.get();
3727            assert_eq!(
3728                &pos[..4],
3729                &[
3730                    Position::FrontLeft,
3731                    Position::FrontRight,
3732                    Position::FrontCenter,
3733                    Position::Lfe
3734                ]
3735            );
3736        }
3737        assert!(surround_channel_map(2).is_none() && surround_channel_map(1).is_none());
3738    }
3739
3740    /// `valid_opus_duration` accepts exactly the six legal Opus frame durations and
3741    /// rejects everything else (including the near-misses 3, 15, 25, 30 ms).
3742    #[test]
3743    fn opus_durations() {
3744        for ms in [2.5, 5.0, 10.0, 20.0, 40.0, 60.0] {
3745            assert!(valid_opus_duration(ms));
3746        }
3747        for ms in [0.0, 1.0, 3.0, 15.0, 25.0, 30.0, 50.0, 100.0] {
3748            assert!(!valid_opus_duration(ms));
3749        }
3750    }
3751
3752    /// The Opus rates, refused up front rather than at encoder creation: 44.1 kHz is the
3753    /// one a caller reaches for that Opus has never run at.
3754    #[test]
3755    fn opus_sample_rates() {
3756        for rate in [8000, 12000, 16000, 24000, 48000] {
3757            assert!(check_opus_sample_rate(rate).is_ok());
3758        }
3759        for rate in [0, 11025, 22050, 44100, 96000] {
3760            assert!(check_opus_sample_rate(rate).is_err());
3761        }
3762    }
3763
3764    /// The samples-per-channel and PCM-byte arithmetic matches the wire cases:
3765    /// 48 kHz / 20 ms / stereo is 960 samples/ch and 3840 bytes, and 24 kHz / 10 ms / mono
3766    /// is 240 samples and 480 bytes.
3767    #[test]
3768    fn frame_geometry() {
3769        let fspc = (48000usize * 20) / 1000;
3770        assert_eq!(fspc, 960);
3771        assert_eq!(fspc * 2 * 2, 3840);
3772        let m = (24000usize * 10) / 1000;
3773        assert_eq!(m, 240);
3774        assert_eq!(m * 2, 480);
3775    }
3776
3777    /// `red_distance == 0` produces exactly the 2-byte `[0x01, 0x00]` + opus framing,
3778    /// and the omit-header path returns the raw opus with no prefix at all.
3779    #[test]
3780    fn red_zero_is_byte_identical() {
3781        let opus = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42];
3782        let hist: VecDeque<(Vec<u8>, u64)> = VecDeque::new();
3783
3784        let legacy_expected = {
3785            let mut v = vec![0x01u8, 0x00];
3786            v.extend_from_slice(&opus);
3787            v
3788        };
3789        assert_eq!(build_ws_body(&opus, 4096, &hist, 0, true), legacy_expected);
3790        assert_eq!(build_ws_body(&opus, 4096, &hist, 0, false), opus);
3791    }
3792
3793    /// `red_distance == 2`: parse the emitted body back (`n_red` 4-byte headers, the
3794    /// 1-byte primary header, block data split by their lengths) and assert the primary and
3795    /// the two redundant blocks round-trip with the expected oldest-first offsets 1920 & 960.
3796    #[test]
3797    fn red_two_roundtrips() {
3798        let f_n2 = vec![0xA0, 0xA1, 0xA2];
3799        let f_n1 = vec![0xB0, 0xB1, 0xB2, 0xB3];
3800        let primary = vec![0xC0, 0xC1];
3801        let mut hist: VecDeque<(Vec<u8>, u64)> = VecDeque::new();
3802        hist.push_back((f_n2.clone(), 0));
3803        hist.push_back((f_n1.clone(), 960));
3804
3805        let body = build_ws_body(&primary, 1920, &hist, 2, true);
3806        assert_eq!(body[0], 0x01);
3807        let n_red = body[1] as usize;
3808        assert_eq!(n_red, 2);
3809        assert_eq!(
3810            u32::from_be_bytes([body[2], body[3], body[4], body[5]]),
3811            1920
3812        );
3813
3814        let mut idx = 6;
3815        let mut offsets = Vec::new();
3816        let mut lens = Vec::new();
3817        for _ in 0..n_red {
3818            assert_eq!(body[idx] & 0x80, 0x80, "redundant header F bit must be set");
3819            let word = ((body[idx + 1] as u32) << 16)
3820                | ((body[idx + 2] as u32) << 8)
3821                | (body[idx + 3] as u32);
3822            offsets.push((word >> 10) & 0x3FFF);
3823            lens.push((word & 0x3FF) as usize);
3824            idx += 4;
3825        }
3826        assert_eq!(body[idx] & 0x80, 0x00, "primary header F bit must be clear");
3827        idx += 1;
3828
3829        assert_eq!(offsets, vec![1920, 960]);
3830        assert_eq!(lens, vec![f_n2.len(), f_n1.len()]);
3831
3832        let b0 = &body[idx..idx + lens[0]];
3833        idx += lens[0];
3834        let b1 = &body[idx..idx + lens[1]];
3835        idx += lens[1];
3836        let prim = &body[idx..];
3837        assert_eq!(b0, &f_n2[..]);
3838        assert_eq!(b1, &f_n1[..]);
3839        assert_eq!(prim, &primary[..]);
3840    }
3841
3842    /// Test helper: build an RFC 2198 RED payload — redundant blocks oldest-first,
3843    /// each with a 14-bit timestamp offset back from the primary, then the primary. Mirrors
3844    /// the wire format `decode_red_into_queue` parses (the mic-uplink counterpart of
3845    /// `build_ws_body`).
3846    fn build_red_payload(reds: &[(u64, &[u8])], primary: &[u8]) -> Vec<u8> {
3847        let mut v = Vec::new();
3848        for (off, blk) in reds {
3849            let field = (((*off as u32) & 0x3FFF) << 10) | ((blk.len() as u32) & 0x3FF);
3850            v.push(0x80 | (RED_BLOCK_PT & 0x7F));
3851            v.push((field >> 16) as u8);
3852            v.push((field >> 8) as u8);
3853            v.push(field as u8);
3854        }
3855        v.push(RED_BLOCK_PT & 0x7F);
3856        for (_, blk) in reds {
3857            v.extend_from_slice(blk);
3858        }
3859        v.extend_from_slice(primary);
3860        v
3861    }
3862
3863    /// Test helper: `n` distinct valid 20 ms mono Opus packets at 24 kHz
3864    /// (480 samples/frame).
3865    fn opus_frames(n: usize) -> Vec<Vec<u8>> {
3866        let mut enc = PcmEncoder::new(24000, 1, true, 24000, None).unwrap();
3867        (0..n)
3868            .map(|s| {
3869                let pcm: Vec<i16> = (0..480)
3870                    .map(|k| ((k * (s as i32 + 1)) % 4000 - 2000) as i16)
3871                    .collect();
3872                let mut out = vec![0u8; 4000];
3873                let len = enc.encode(&pcm, 480, &mut out).unwrap();
3874                out.truncate(len);
3875                out
3876            })
3877            .collect()
3878    }
3879
3880    /// Test constant: each decoded 20 ms mono frame is 480 samples * 2 bytes.
3881    const FRAME_PCM_BYTES: usize = 480 * 2;
3882
3883    /// A dropped middle packet is recovered from the next packet's redundancy: the
3884    /// redundant copy of the gap frame is decoded, while the redundant copy of an
3885    /// already-played frame is not (timestamp dedup). Exercises the off-GIL RED playback path
3886    /// end to end — anchor on packet 1 (ts 1000), drop packet 2 (ts 1480), then packet 3
3887    /// (ts 1960) carries redundant copies of 1000 and 1480, so the output is exactly the
3888    /// three frames 1000 + 1480 + 1960.
3889    #[test]
3890    fn red_playback_recovers_lost_frame() {
3891        let f = opus_frames(3);
3892        let mut dec = OpusPlaybackDecoder::new(24000, 1).unwrap();
3893        let q = PlayQueue::new();
3894        q.configure(1 << 20, 2);
3895
3896        dec.decode_red_into_queue(&build_red_payload(&[], &f[0]), 1000, &q);
3897        let pkt3 = build_red_payload(&[(960, &f[0]), (480, &f[1])], &f[2]);
3898        dec.decode_red_into_queue(&pkt3, 1960, &q);
3899
3900        let mut out = Vec::new();
3901        q.drain_upto(1 << 20, &mut out);
3902        assert_eq!(out.len(), 3 * FRAME_PCM_BYTES);
3903        assert_eq!(dec.last_ts, Some(1960));
3904    }
3905
3906    /// With no loss, redundancy is pure overhead: every frame decodes exactly once and
3907    /// the redundant copies are dropped, so three packets yield exactly three frames.
3908    #[test]
3909    fn red_playback_no_double_decode() {
3910        let f = opus_frames(3);
3911        let mut dec = OpusPlaybackDecoder::new(24000, 1).unwrap();
3912        let q = PlayQueue::new();
3913        q.configure(1 << 20, 2);
3914
3915        dec.decode_red_into_queue(&build_red_payload(&[], &f[0]), 1000, &q);
3916        dec.decode_red_into_queue(&build_red_payload(&[(480, &f[0])], &f[1]), 1480, &q);
3917        dec.decode_red_into_queue(&build_red_payload(&[(480, &f[1])], &f[2]), 1960, &q);
3918
3919        let mut out = Vec::new();
3920        q.drain_upto(1 << 20, &mut out);
3921        assert_eq!(out.len(), 3 * FRAME_PCM_BYTES);
3922        assert_eq!(dec.last_ts, Some(1960));
3923    }
3924
3925    /// A too-old block (offset > 16383, overflowing the 14-bit field) and an oversize
3926    /// block (len > 1023, overflowing the 10-bit field) are both skipped, so `n_red` counts
3927    /// only the one in-range block that fit the RFC 2198 fields.
3928    #[test]
3929    fn red_skips_oversize_and_too_old() {
3930        let too_old = vec![0x11u8; 3];
3931        let oversize = vec![0x22u8; 1100];
3932        let good = vec![0x33u8; 5];
3933        let primary = vec![0x44u8; 2];
3934        let primary_pts = 100_000u64;
3935
3936        let mut hist: VecDeque<(Vec<u8>, u64)> = VecDeque::new();
3937        hist.push_back((too_old, primary_pts - 20_000));
3938        hist.push_back((oversize, primary_pts - 1920));
3939        hist.push_back((good.clone(), primary_pts - 960));
3940
3941        let body = build_ws_body(&primary, primary_pts, &hist, 4, true);
3942        assert_eq!(body[0], 0x01);
3943        assert_eq!(body[1], 1, "only the in-range block survives");
3944        assert_eq!(
3945            u32::from_be_bytes([body[2], body[3], body[4], body[5]]),
3946            primary_pts as u32
3947        );
3948
3949        let word = ((body[7] as u32) << 16) | ((body[8] as u32) << 8) | (body[9] as u32);
3950        assert_eq!((word >> 10) & 0x3FFF, 960);
3951        let len = (word & 0x3FF) as usize;
3952        assert_eq!(len, good.len());
3953        assert_eq!(
3954            body[10] & 0x80,
3955            0x00,
3956            "primary header follows the one redundant header"
3957        );
3958        assert_eq!(&body[11..11 + len], &good[..]);
3959        assert_eq!(&body[11 + len..], &primary[..]);
3960    }
3961
3962    /// `red_distance > 0` with no usable history (e.g. the first frame after a
3963    /// (re)start) collapses to exactly the 2-byte `[0x01, 0x00]` + opus framing — the same
3964    /// bytes as `n_red == 0`, not a primary-only RED header. The client's `n_red == 0` path
3965    /// strips exactly 2 bytes, so emitting a lone primary-only RED header here would be
3966    /// mis-stripped and corrupt the frame; the body is asserted to be exactly those 2 bytes
3967    /// plus the opus.
3968    #[test]
3969    fn red_empty_history_collapses_to_bare_header() {
3970        let opus = vec![0x77u8; 4];
3971        let hist: VecDeque<(Vec<u8>, u64)> = VecDeque::new();
3972        let body = build_ws_body(&opus, 960, &hist, 2, true);
3973        assert_eq!(body[0], 0x01);
3974        assert_eq!(body[1], 0x00);
3975        assert_eq!(&body[2..], &opus[..]);
3976        assert_eq!(body.len(), 2 + opus.len());
3977    }
3978
3979    /// The emit path pauses a header stream exactly once per gated run: frames of sound
3980    /// around a stretch of digital silence go out with their Opus, then the bare
3981    /// `[0x01, WS_QUIET]` where the sound ends, then the first silent frame (it carries the end
3982    /// of the sound) with `WS_QUIET` set too, the frames after it send nothing, and sound
3983    /// resumes with the plain header (RED on or off, since the gate empties the history). A raw
3984    /// Opus capture sends the same frames with no marker, the silent one because the tone ran
3985    /// into the encoder's lookahead.
3986    #[test]
3987    fn gated_silence_sends_one_quiet_frame() {
3988        for (red, header) in [(0usize, true), (2, true), (0, false)] {
3989            let inner = Inner::new();
3990            inner.emit_audio_header.store(header, Ordering::Relaxed);
3991            let ring = DeliveryRing::new(64);
3992            let frame = 480usize;
3993            let pool = Arc::new(BufferPool::new(RED_PREFIX_MAX + MAX_OPUS_PACKET));
3994            let mut run = RunState {
3995                inner: &inner,
3996                ring: &ring,
3997                encoder: PcmEncoder::new(48000, 2, true, 64000, None).expect("encoder"),
3998                frame_size_per_channel: frame,
3999                channels: 2,
4000                accum: vec![0i16; frame * 2],
4001                silence_ref: vec![0i16; frame * 2],
4002                pcm_fill_bytes: 0,
4003                pool: PoolTaker::new(Arc::clone(&pool)),
4004                red_history: VecDeque::new(),
4005                red_spare: Vec::new(),
4006                red_distance: red,
4007                sounding: false,
4008                tail_sounding: false,
4009                lookahead: 120,
4010                companion: None,
4011                companion_bitrate: 64000,
4012                total_samples_processed: 0,
4013                ogg: None,
4014                ogg_scale: 1,
4015                first_sound_detected: true,
4016                current_applied_bitrate: 64000,
4017                chunks_read: 0,
4018                chunks_silent: 0,
4019                chunks_encoded: 0,
4020                bytes_encoded: 0,
4021            };
4022            let tone: Vec<i16> = (0..frame * 2)
4023                .map(|i| ((i as f64 * 0.3).sin() * 8000.0) as i16)
4024                .collect();
4025            let quiet = vec![0i16; frame * 2];
4026            for pcm in [&quiet, &tone, &tone, &quiet, &quiet, &quiet, &tone, &tone] {
4027                run.feed(bytemuck::cast_slice(pcm));
4028            }
4029            let queued = ring.q.lock().unwrap();
4030            let frames = queued.as_ref().expect("ring open");
4031            let pts: Vec<u64> = frames.iter().map(|(_, p, _)| *p).collect();
4032            let quiet_frames: Vec<&Vec<u8>> = frames
4033                .iter()
4034                .map(|(d, _, _)| d)
4035                .filter(|d| d.len() == 2)
4036                .collect();
4037            if header {
4038                assert_eq!(pts, vec![480, 960, 1440, 1440, 2880, 3360], "red={red}");
4039                assert_eq!(quiet_frames, vec![&vec![0x01, WS_QUIET]], "red={red}");
4040                assert_eq!(
4041                    frames[2].0.len(),
4042                    2,
4043                    "the bare mark goes where the sound ends"
4044                );
4045                assert!(
4046                    frames[3].0.len() > 2 && frames[3].0[1] & WS_QUIET != 0,
4047                    "the silent frame behind it carries the mark too"
4048                );
4049                assert!(
4050                    frames
4051                        .iter()
4052                        .enumerate()
4053                        .all(|(i, (d, _, _))| i == 2 || i == 3 || d[1] & WS_QUIET == 0)
4054                );
4055                assert_eq!(frames[4].0[1], 0x00, "sound resumes with the plain header");
4056            } else {
4057                assert_eq!(pts, vec![480, 960, 1440, 2880, 3360]);
4058                assert!(quiet_frames.is_empty() && frames.iter().all(|(d, _, _)| d.len() > 2));
4059            }
4060        }
4061    }
4062
4063    /// A sound in the last 2.5 ms of the frame before the gate closes reaches a header
4064    /// capture's decoder: the encoder holds that much back until the next frame, so the frame
4065    /// that ends the sound is sent, behind the bare mark and marked itself.
4066    #[test]
4067    fn a_header_capture_sends_the_tail_of_a_sound_before_the_gate() {
4068        let inner = Inner::new();
4069        inner.emit_audio_header.store(true, Ordering::Relaxed);
4070        let ring = DeliveryRing::new(64);
4071        let frame = 480usize;
4072        let pool = Arc::new(BufferPool::new(RED_PREFIX_MAX + MAX_OPUS_PACKET));
4073        let mut run = RunState {
4074            inner: &inner,
4075            ring: &ring,
4076            encoder: PcmEncoder::new(48000, 2, true, 128000, None).expect("encoder"),
4077            frame_size_per_channel: frame,
4078            channels: 2,
4079            accum: vec![0i16; frame * 2],
4080            silence_ref: vec![0i16; frame * 2],
4081            pcm_fill_bytes: 0,
4082            pool: PoolTaker::new(Arc::clone(&pool)),
4083            red_history: VecDeque::new(),
4084            red_spare: Vec::new(),
4085            red_distance: 0,
4086            sounding: false,
4087            tail_sounding: false,
4088            lookahead: 120,
4089            companion: None,
4090            companion_bitrate: 128000,
4091            total_samples_processed: 0,
4092            ogg: None,
4093            ogg_scale: 1,
4094            first_sound_detected: true,
4095            current_applied_bitrate: 128000,
4096            chunks_read: 0,
4097            chunks_silent: 0,
4098            chunks_encoded: 0,
4099            bytes_encoded: 0,
4100        };
4101        let mut click = vec![0i16; frame * 2];
4102        for i in frame - 96..frame {
4103            let v =
4104                ((i as f64 * 2.0 * std::f64::consts::PI * 1500.0 / 48000.0).sin() * 16000.0) as i16;
4105            click[i * 2] = v;
4106            click[i * 2 + 1] = v;
4107        }
4108        let quiet = vec![0i16; frame * 2];
4109        for pcm in [&quiet, &click, &quiet, &quiet, &quiet] {
4110            run.feed(bytemuck::cast_slice(pcm));
4111        }
4112        let queued = ring.q.lock().unwrap();
4113        let marked: Vec<(u64, usize)> = queued
4114            .as_ref()
4115            .expect("ring open")
4116            .iter()
4117            .filter(|(d, _, _)| d[1] & WS_QUIET != 0)
4118            .map(|(d, p, _)| (*p, d.len().min(3)))
4119            .collect();
4120        assert_eq!(
4121            marked,
4122            vec![(960, 2), (960, 3)],
4123            "the bare mark, then the frame that ends the sound"
4124        );
4125        let mut decoder = OpusPlaybackDecoder::new(48000, 2).expect("decoder");
4126        let mut peak = 0i32;
4127        for (data, _, _) in queued.as_ref().expect("ring open") {
4128            if data.len() <= 2 {
4129                continue;
4130            }
4131            let pcm: &[i16] =
4132                bytemuck::cast_slice(decoder.decode_to_pcm(&data[2..]).expect("decode"));
4133            peak = peak.max(pcm.iter().map(|&v| (v as i32).abs()).max().unwrap_or(0));
4134        }
4135        assert!(peak > 8000, "the sound's tail decodes at {peak}");
4136    }
4137
4138    /// A raw capture sends the silent frame after a sound only when the sound reached into the
4139    /// encoder's lookahead, the part that frame carries: a burst in the last 2 ms of a frame is
4140    /// followed by the silent frame and decodes whole, and a burst that ended before the
4141    /// lookahead is not followed by it.
4142    #[test]
4143    fn a_raw_capture_sends_the_tail_only_when_the_sound_reached_the_lookahead() {
4144        for late in [true, false] {
4145            let inner = Inner::new();
4146            inner.emit_audio_header.store(false, Ordering::Relaxed);
4147            let ring = DeliveryRing::new(64);
4148            let frame = 480usize;
4149            let pool = Arc::new(BufferPool::new(RED_PREFIX_MAX + MAX_OPUS_PACKET));
4150            let mut run = RunState {
4151                inner: &inner,
4152                ring: &ring,
4153                encoder: PcmEncoder::new(48000, 2, true, 128000, None).expect("encoder"),
4154                frame_size_per_channel: frame,
4155                channels: 2,
4156                accum: vec![0i16; frame * 2],
4157                silence_ref: vec![0i16; frame * 2],
4158                pcm_fill_bytes: 0,
4159                pool: PoolTaker::new(Arc::clone(&pool)),
4160                red_history: VecDeque::new(),
4161                red_spare: Vec::new(),
4162                red_distance: 0,
4163                sounding: false,
4164                tail_sounding: false,
4165                lookahead: 120,
4166                companion: None,
4167                companion_bitrate: 128000,
4168                total_samples_processed: 0,
4169                ogg: None,
4170                ogg_scale: 1,
4171                first_sound_detected: true,
4172                current_applied_bitrate: 128000,
4173                chunks_read: 0,
4174                chunks_silent: 0,
4175                chunks_encoded: 0,
4176                bytes_encoded: 0,
4177            };
4178            let mut click = vec![0i16; frame * 2];
4179            let burst = if late { frame - 96..frame } else { 96..192 };
4180            for i in burst {
4181                let v = ((i as f64 * 2.0 * std::f64::consts::PI * 1500.0 / 48000.0).sin() * 16000.0)
4182                    as i16;
4183                click[i * 2] = v;
4184                click[i * 2 + 1] = v;
4185            }
4186            let quiet = vec![0i16; frame * 2];
4187            for pcm in [&quiet, &click, &quiet, &quiet, &quiet] {
4188                run.feed(bytemuck::cast_slice(pcm));
4189            }
4190            let queued = ring.q.lock().unwrap();
4191            let frames = queued.as_ref().expect("ring open");
4192            let pts: Vec<u64> = frames.iter().map(|(_, p, _)| *p).collect();
4193            if !late {
4194                assert_eq!(
4195                    pts,
4196                    vec![480],
4197                    "a burst that ended before the lookahead needs no tail"
4198                );
4199                continue;
4200            }
4201            assert_eq!(
4202                pts,
4203                vec![480, 960],
4204                "the silent frame carrying the burst's end follows it"
4205            );
4206            let mut decoder = OpusPlaybackDecoder::new(48000, 2).expect("decoder");
4207            let mut peak = 0i32;
4208            for (data, _, _) in frames {
4209                let pcm: &[i16] =
4210                    bytemuck::cast_slice(decoder.decode_to_pcm(data).expect("decode"));
4211                peak = peak.max(pcm.iter().map(|&v| (v as i32).abs()).max().unwrap_or(0));
4212            }
4213            assert!(peak > 8000, "the burst decodes at {peak}");
4214        }
4215    }
4216
4217    /// A session that drops after sound leaves no frame to carry the mark, so a header capture
4218    /// sends the bare `[0x01, WS_QUIET]` once; one that drops in silence, or a raw capture,
4219    /// sends nothing.
4220    #[test]
4221    fn a_dropped_session_after_sound_sends_the_bare_mark_once() {
4222        for header in [true, false] {
4223            let inner = Inner::new();
4224            inner.emit_audio_header.store(header, Ordering::Relaxed);
4225            let ring = DeliveryRing::new(64);
4226            let frame = 480usize;
4227            let pool = Arc::new(BufferPool::new(RED_PREFIX_MAX + MAX_OPUS_PACKET));
4228            let mut run = RunState {
4229                inner: &inner,
4230                ring: &ring,
4231                encoder: PcmEncoder::new(48000, 2, true, 64000, None).expect("encoder"),
4232                frame_size_per_channel: frame,
4233                channels: 2,
4234                accum: vec![0i16; frame * 2],
4235                silence_ref: vec![0i16; frame * 2],
4236                pcm_fill_bytes: 0,
4237                pool: PoolTaker::new(Arc::clone(&pool)),
4238                red_history: VecDeque::new(),
4239                red_spare: Vec::new(),
4240                red_distance: 0,
4241                sounding: false,
4242                tail_sounding: false,
4243                lookahead: 120,
4244                companion: None,
4245                companion_bitrate: 64000,
4246                total_samples_processed: 0,
4247                ogg: None,
4248                ogg_scale: 1,
4249                first_sound_detected: true,
4250                current_applied_bitrate: 64000,
4251                chunks_read: 0,
4252                chunks_silent: 0,
4253                chunks_encoded: 0,
4254                bytes_encoded: 0,
4255            };
4256            run.emit_quiet(0);
4257            let tone: Vec<i16> = (0..frame * 2)
4258                .map(|i| ((i as f64 * 0.3).sin() * 8000.0) as i16)
4259                .collect();
4260            run.feed(bytemuck::cast_slice(&tone));
4261            run.emit_quiet(480);
4262            run.emit_quiet(480);
4263            let queued = ring.q.lock().unwrap();
4264            let bare: Vec<&Vec<u8>> = queued
4265                .as_ref()
4266                .expect("ring open")
4267                .iter()
4268                .map(|(d, _, _)| d)
4269                .filter(|d| d.len() == 2)
4270                .collect();
4271            if header {
4272                assert_eq!(bare, vec![&vec![0x01, WS_QUIET]]);
4273            } else {
4274                assert!(bare.is_empty());
4275            }
4276        }
4277    }
4278
4279    /// The all-zero comparison behind the silence gate: an all-zero buffer reads as
4280    /// silent, and a single non-zero sample makes it non-silent.
4281    #[test]
4282    fn silence_detection() {
4283        let silent = vec![0i16; 960 * 2];
4284        assert!(silent.iter().all(|&s| s == 0));
4285        let mut not = silent.clone();
4286        not[123] = 7;
4287        assert!(!not.iter().all(|&s| s == 0));
4288    }
4289
4290    /// Deterministic lost-stop probe against the real `stop_state` protocol.
4291    ///
4292    /// A stand-in "capture thread" hammers `request_self_stop` + `undo_self_stop` (the
4293    /// re-entrant callback pattern) while another thread issues `request_external_stop` and
4294    /// waits for the loop's `stop_pending()` to observe it. The external stop must never be
4295    /// cleared by a self-start, so the thread always observes `STOP_EXTERNAL` and the join
4296    /// returns (no hang). The bounded spin count is the in-test watchdog; 5000 iterations
4297    /// shake out the race.
4298    #[test]
4299    fn external_stop_never_lost_to_self_restart() {
4300        use std::sync::Arc;
4301        let me: i64 = 987654;
4302        for _ in 0..5000 {
4303            let inner = Arc::new(Inner::new());
4304            let inner_c = inner.clone();
4305            let observed = Arc::new(AtomicBool::new(false));
4306            let observed_c = observed.clone();
4307            let h = std::thread::spawn(move || {
4308                let mut spins: u64 = 0;
4309                loop {
4310                    inner_c.request_self_stop(me);
4311                    inner_c.undo_self_stop(me);
4312                    if inner_c.stop_pending()
4313                        && inner_c.stop_state.load(Ordering::Acquire) == STOP_EXTERNAL
4314                    {
4315                        observed_c.store(true, Ordering::Release);
4316                        return;
4317                    }
4318                    spins += 1;
4319                    assert!(
4320                        spins < 50_000_000,
4321                        "external stop was lost (would hang the join)"
4322                    );
4323                }
4324            });
4325            inner.request_external_stop();
4326            h.join().unwrap();
4327            assert!(observed.load(Ordering::Acquire));
4328            assert_eq!(inner.stop_state.load(Ordering::Acquire), STOP_EXTERNAL);
4329            assert!(inner.stop_pending());
4330        }
4331    }
4332
4333    /// Pushing past the byte bound drops the OLDEST bytes, keeping the newest window:
4334    /// an 8-byte bound fed 12 bytes retains the last 8, and a second drain comes back empty.
4335    #[test]
4336    fn playqueue_drop_oldest_keeps_newest() {
4337        let q = PlayQueue::new();
4338        q.configure(8, 2);
4339        q.push(&[1, 2, 3, 4, 5, 6]);
4340        q.push(&[7, 8, 9, 10, 11, 12]);
4341        let mut out = Vec::new();
4342        q.drain_upto(100, &mut out);
4343        assert_eq!(out, vec![5, 6, 7, 8, 9, 10, 11, 12]);
4344        q.drain_upto(100, &mut out);
4345        assert!(out.is_empty());
4346    }
4347
4348    /// `drain_upto` clamps to the requested count AND floors to a whole frame, so a PA
4349    /// write never gets a partial frame: with 4-byte frames, a request of 7 yields 4 bytes,
4350    /// the next drain yields the next 4, and the trailing 2 bytes are withheld as a partial
4351    /// frame.
4352    #[test]
4353    fn playqueue_drain_is_frame_aligned() {
4354        let q = PlayQueue::new();
4355        q.configure(1000, 4);
4356        q.push(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
4357        let mut out = Vec::new();
4358        q.drain_upto(7, &mut out);
4359        assert_eq!(out, vec![0, 1, 2, 3]);
4360        q.drain_upto(100, &mut out);
4361        assert_eq!(out, vec![4, 5, 6, 7]);
4362        q.drain_upto(100, &mut out);
4363        assert!(out.is_empty());
4364    }
4365
4366    /// `configure()` applies the new bounds and drops stale audio from a prior run, so
4367    /// bytes queued before it are gone afterward.
4368    #[test]
4369    fn playqueue_configure_resets() {
4370        let q = PlayQueue::new();
4371        q.push(&[1, 2, 3, 4]);
4372        q.configure(96000, 2);
4373        let mut out = Vec::new();
4374        q.drain_upto(100, &mut out);
4375        assert!(out.is_empty(), "configure must clear stale audio");
4376    }
4377
4378    /// A byte bound that is not a whole-frame multiple must not let overflow drops
4379    /// split a sample frame — a mid-frame trim would phase-shift every later drain
4380    /// into interleaved garbage. The bound floors to whole frames and drops are made
4381    /// in whole frames.
4382    #[test]
4383    fn playqueue_misaligned_bound_never_splits_frames() {
4384        let q = PlayQueue::new();
4385        // A 9-byte bound over 4-byte frames floors to 8, which is two whole frames.
4386        q.configure(9, 4);
4387        q.push(&[0, 1, 2, 3]);
4388        q.push(&[4, 5, 6, 7]);
4389        // With 12 bytes queued against a bound of 8, exactly the oldest frame is dropped.
4390        q.push(&[8, 9, 10, 11]);
4391        let mut out = Vec::new();
4392        q.drain_upto(100, &mut out);
4393        assert_eq!(out, vec![4, 5, 6, 7, 8, 9, 10, 11]);
4394    }
4395
4396    /// A splice drops exactly the cut, blends the head into the audio after the cut over
4397    /// the fade (first blended sample next to the head, last next to the tail), leaves the
4398    /// rest untouched, and does nothing when the queue cannot hold the cut and the fade.
4399    #[test]
4400    fn playqueue_splice_crossfades_the_seam() {
4401        let q = PlayQueue::new();
4402        q.configure(1 << 20, 2);
4403        let head: Vec<i16> = vec![1000; 48];
4404        let tail: Vec<i16> = (0..200).map(|i| -2000 + i).collect();
4405        q.push(bytemuck::cast_slice(&head));
4406        q.push(bytemuck::cast_slice(&[0i16; 52]));
4407        q.push(bytemuck::cast_slice(&tail));
4408        q.splice_head(200, 96);
4409        let mut out = Vec::new();
4410        assert_eq!(q.drain_upto(1 << 20, &mut out), 0);
4411        let got: Vec<i16> = bytemuck::cast_slice::<u8, i16>(&out).to_vec();
4412        assert_eq!(got.len(), 200);
4413        assert!(
4414            (got[0] - 1000).abs() < 70,
4415            "first sample stays near the head: {}",
4416            got[0]
4417        );
4418        assert!(
4419            (got[47] - tail[47]).abs() < 70,
4420            "last blended sample reaches the tail: {}",
4421            got[47]
4422        );
4423        assert!(
4424            got.windows(2).take(48).all(|w| (w[1] - w[0]).abs() < 70),
4425            "no step inside the fade"
4426        );
4427        assert_eq!(&got[48..], &tail[48..]);
4428
4429        let short = PlayQueue::new();
4430        short.configure(1 << 20, 2);
4431        short.push(&[1, 0, 2, 0, 3, 0]);
4432        short.splice_head(4, 4);
4433        let mut out = Vec::new();
4434        short.drain_upto(100, &mut out);
4435        assert_eq!(out, vec![1, 0, 2, 0, 3, 0]);
4436    }
4437
4438    /// The queue against the real sink cadence, simulated: 20 ms packets arrive on a
4439    /// client clock, the sink drains 10 ms whenever it has room, and `StandingDepth` cuts
4440    /// what stood through a window. A 500 ms stall followed by its burst is shed within two
4441    /// windows, and a client clock 0.1 % fast (1 ms more per second than the sink plays)
4442    /// never lets the depth past the 10 ms cut threshold and a packet over ten minutes;
4443    /// without the cut both would hold their excess (the burst) or grow without bound.
4444    #[test]
4445    fn standing_depth_is_shed_after_bursts_and_drift() {
4446        fn run(stall: bool, skew: f64, seconds: f64, cut: bool) -> (usize, usize) {
4447            let (rate, fb) = (24_000usize, 2usize);
4448            let q = PlayQueue::new();
4449            q.configure(96_000, fb);
4450            let mut standing = StandingDepth::new(rate * fb, fb);
4451            let packet = vec![0u8; 960];
4452            let mut out = Vec::new();
4453            let (mut t, mut next_arrival, mut k) = (0.0f64, 0.0f64, 0u64);
4454            let (mut worst_late, mut end) = (0usize, 0usize);
4455            let mut held = 0u64;
4456            while t < seconds {
4457                while next_arrival <= t {
4458                    let in_stall = stall && (5.0..5.5).contains(&next_arrival);
4459                    if in_stall {
4460                        held += 1;
4461                    } else {
4462                        for _ in 0..=held {
4463                            q.push(&packet);
4464                        }
4465                        held = 0;
4466                    }
4467                    k += 1;
4468                    next_arrival = k as f64 * 0.020 / (1.0 + skew);
4469                }
4470                let left = q.drain_upto(480, &mut out);
4471                if cut && let Some(n) = standing.after_drain(out.len(), left) {
4472                    q.splice_head(n, standing.keep);
4473                }
4474                let depth = q.buf.lock().unwrap().len();
4475                if t > 8.0 {
4476                    worst_late = worst_late.max(depth);
4477                }
4478                end = depth;
4479                t += 0.010;
4480            }
4481            (worst_late, end)
4482        }
4483        let (late, _) = run(true, 0.0, 30.0, true);
4484        assert!(
4485            late <= 960 + 480,
4486            "burst shed by 8 s: {late} bytes still standing"
4487        );
4488        let (_, held) = run(true, 0.0, 30.0, false);
4489        assert!(held >= 20_000, "without the cut the burst stands: {held}");
4490        let (drift, _) = run(false, 0.001, 600.0, true);
4491        assert!(drift <= 480 + 960 + 96, "drift bounded: {drift} bytes");
4492        let (_, grown) = run(false, 0.001, 600.0, false);
4493        assert!(grown >= 25_000, "without the cut drift grows: {grown}");
4494    }
4495
4496    /// `worker_alive` (which gates `AudioPlayback::write`) tracks the lifecycle: it is
4497    /// false before any worker, true through the startup handshake and the healthy run, and
4498    /// goes false the moment the hot loop's error exit clears `started_ok` — even with no stop
4499    /// pending and `start_state` still `RUNNING` (the silent mid-run death case). A pending
4500    /// external stop also reads as not-alive.
4501    #[test]
4502    fn worker_alive_tracks_loop_error_exit() {
4503        let inner = Inner::new();
4504        assert!(!inner.worker_alive(), "no worker yet");
4505
4506        inner.start_state.store(ST_STARTING, Ordering::Release);
4507        assert!(inner.worker_alive(), "startup handshake counts as alive");
4508
4509        inner.started_ok.store(true, Ordering::Release);
4510        inner.start_state.store(ST_RUNNING, Ordering::Release);
4511        assert!(inner.worker_alive());
4512
4513        inner.started_ok.store(false, Ordering::Release);
4514        assert_eq!(inner.stop_state.load(Ordering::Acquire), STOP_NONE);
4515        assert!(!inner.worker_alive(), "dead loop must be observable");
4516
4517        inner.started_ok.store(true, Ordering::Release);
4518        inner.request_external_stop();
4519        assert!(!inner.worker_alive());
4520    }
4521
4522    /// `state_name` / `last_error` follow the lifecycle the Python getters expose: idle
4523    /// before any run, starting through the handshake, running once connected, and
4524    /// failed — with the reason recorded BEFORE the state flips — after `fail`. A new run
4525    /// armed by `spawn_worker` forgets the previous failure.
4526    #[test]
4527    fn state_and_last_error_track_lifecycle() {
4528        let inner = Arc::new(Inner::new());
4529        assert_eq!(inner.state_name(), "idle");
4530        assert_eq!(inner.last_error(), None);
4531
4532        inner.start_state.store(ST_STARTING, Ordering::Release);
4533        assert_eq!(inner.state_name(), "starting");
4534        assert!(!inner.running());
4535
4536        inner.started_ok.store(true, Ordering::Release);
4537        inner.start_state.store(ST_RUNNING, Ordering::Release);
4538        assert_eq!(inner.state_name(), "running");
4539        assert!(inner.running());
4540
4541        inner.request_external_stop();
4542        assert_eq!(
4543            inner.state_name(),
4544            "idle",
4545            "a pending stop is no longer running"
4546        );
4547        inner.clear_stop();
4548
4549        inner.fail("source vanished".to_string());
4550        assert_eq!(inner.state_name(), "failed");
4551        assert_eq!(inner.last_error().as_deref(), Some("source vanished"));
4552        assert!(!inner.running());
4553        assert!(!inner.worker_alive());
4554
4555        let slot: Mutex<Option<JoinHandle<()>>> = Mutex::new(None);
4556        let wi = inner.clone();
4557        spawn_worker(&slot, &inner, "fresh", move || {
4558            wi.started_ok.store(false, Ordering::Release);
4559            wi.start_state.store(ST_IDLE, Ordering::Release);
4560        })
4561        .unwrap();
4562        slot.lock().unwrap().take().unwrap().join().unwrap();
4563        assert_eq!(
4564            inner.last_error(),
4565            None,
4566            "a new run must start with no stale error"
4567        );
4568        assert_eq!(inner.state_name(), "idle", "a clean stop reads idle again");
4569    }
4570
4571    /// A worker body that panics is caught by `spawn_worker`, which marks the run failed
4572    /// and records the panic message as `last_error`, so the death is observable from
4573    /// Python instead of leaving a silently dead capture.
4574    #[test]
4575    fn worker_panic_records_last_error() {
4576        let inner = Arc::new(Inner::new());
4577        let slot: Mutex<Option<JoinHandle<()>>> = Mutex::new(None);
4578        spawn_worker(&slot, &inner, "panicky", || panic!("boom")).unwrap();
4579        slot.lock().unwrap().take().unwrap().join().unwrap();
4580        assert_eq!(inner.state_name(), "failed");
4581        assert_eq!(
4582            inner.last_error().as_deref(),
4583            Some("worker thread panicked: boom")
4584        );
4585        assert!(!inner.worker_alive());
4586        assert_eq!(inner.capture_tid.load(Ordering::Acquire), 0);
4587    }
4588
4589    /// A LOSING start's failed-start cleanup must never tear down a WINNING start's
4590    /// freshly spawned thread.
4591    ///
4592    /// Drives the bad interleaving directly: loser L's worker fails, winner W takes the slot
4593    /// over (joins dead L, spawns live W) BEFORE L runs its `ST_FAILED` cleanup. That late
4594    /// `join_failed_start` must be an identity-mismatch no-op, leaving W running and still
4595    /// tear-down-able through the ordinary external stop path.
4596    #[test]
4597    fn failed_start_cleanup_spares_concurrent_winner() {
4598        let inner = Arc::new(Inner::new());
4599        let slot: Mutex<Option<JoinHandle<()>>> = Mutex::new(None);
4600
4601        let li = inner.clone();
4602        let l_id = spawn_worker(&slot, &inner, "loser", move || {
4603            li.started_ok.store(false, Ordering::Release);
4604            li.start_state.store(ST_FAILED, Ordering::Release);
4605        })
4606        .unwrap();
4607        while inner.start_state.load(Ordering::Acquire) != ST_FAILED {
4608            std::thread::yield_now();
4609        }
4610
4611        let wi = inner.clone();
4612        let w_id = spawn_worker(&slot, &inner, "winner", move || {
4613            wi.started_ok.store(true, Ordering::Release);
4614            wi.start_state.store(ST_RUNNING, Ordering::Release);
4615            while !wi.stop_pending() {
4616                std::thread::sleep(Duration::from_millis(1));
4617            }
4618            wi.started_ok.store(false, Ordering::Release);
4619        })
4620        .unwrap();
4621        assert_ne!(l_id, w_id);
4622        while inner.start_state.load(Ordering::Acquire) != ST_RUNNING {
4623            std::thread::yield_now();
4624        }
4625
4626        join_failed_start(&slot, &inner, l_id);
4627        assert!(
4628            slot.lock().unwrap().is_some(),
4629            "winner's handle must survive"
4630        );
4631        assert_eq!(inner.stop_state.load(Ordering::Acquire), STOP_NONE);
4632        assert!(inner.worker_alive(), "winner must still be running");
4633
4634        {
4635            let mut g = slot.lock().unwrap();
4636            let h = g.take().expect("winner handle present");
4637            inner.request_external_stop();
4638            h.join().unwrap();
4639        }
4640        assert!(!inner.worker_alive());
4641    }
4642
4643    /// Two threads race the full start sequence (`spawn_worker` + handshake poll +
4644    /// conditional failed-start cleanup), 200 times.
4645    ///
4646    /// `spawn_worker` joins the prior worker before spawning, so the bodies run in spawn
4647    /// order: the first fails, the second serves until stopped. Whatever the interleaving,
4648    /// the second run must end `RUNNING` with no stray stop — a loser cleanup that killed the
4649    /// winner would leave an empty slot and a `STOP_EXTERNAL` behind.
4650    #[test]
4651    fn concurrent_start_loser_never_kills_winner() {
4652        for _ in 0..200 {
4653            let inner = Arc::new(Inner::new());
4654            let slot = Arc::new(Mutex::new(None::<JoinHandle<()>>));
4655            let runs = Arc::new(AtomicUsize::new(0));
4656
4657            let starter = |name: &'static str| {
4658                let inner = inner.clone();
4659                let slot = slot.clone();
4660                let runs = runs.clone();
4661                std::thread::spawn(move || {
4662                    let bi = inner.clone();
4663                    let bruns = runs.clone();
4664                    let id = spawn_worker(&slot, &inner, name, move || {
4665                        if bruns.fetch_add(1, Ordering::AcqRel) == 0 {
4666                            bi.started_ok.store(false, Ordering::Release);
4667                            bi.start_state.store(ST_FAILED, Ordering::Release);
4668                        } else {
4669                            bi.started_ok.store(true, Ordering::Release);
4670                            bi.start_state.store(ST_RUNNING, Ordering::Release);
4671                            let mut spins: u64 = 0;
4672                            while !bi.stop_pending() {
4673                                std::thread::sleep(Duration::from_millis(1));
4674                                spins += 1;
4675                                assert!(spins < 10_000, "external stop was lost");
4676                            }
4677                            bi.started_ok.store(false, Ordering::Release);
4678                        }
4679                    })
4680                    .unwrap();
4681                    let mut state = inner.start_state.load(Ordering::Acquire);
4682                    let mut tries = 0;
4683                    while state == ST_STARTING && tries < 2000 {
4684                        std::thread::sleep(Duration::from_millis(1));
4685                        state = inner.start_state.load(Ordering::Acquire);
4686                        tries += 1;
4687                    }
4688                    if state == ST_FAILED {
4689                        join_failed_start(&slot, &inner, id);
4690                    }
4691                })
4692            };
4693            let a = starter("start-a");
4694            let b = starter("start-b");
4695            a.join().unwrap();
4696            b.join().unwrap();
4697
4698            assert_eq!(runs.load(Ordering::Acquire), 2);
4699            assert!(
4700                slot.lock().unwrap().is_some(),
4701                "winner's thread must survive"
4702            );
4703            assert_eq!(inner.start_state.load(Ordering::Acquire), ST_RUNNING);
4704            assert!(
4705                inner.worker_alive(),
4706                "winner must still be alive after both starts"
4707            );
4708
4709            {
4710                let mut g = slot.lock().unwrap();
4711                let h = g.take().expect("winner handle present");
4712                inner.request_external_stop();
4713                h.join().unwrap();
4714            }
4715            assert!(!inner.worker_alive());
4716        }
4717    }
4718
4719    /// The in-place `write_ws_prefix_into` + appended primary is byte-identical to the
4720    /// copy-based `build_ws_body` reference across the full matrix — empty vs mixed history
4721    /// (including one block aged out of the 14-bit offset at high pts and one oversized block
4722    /// that is always skipped), every `red_distance`, header on/off, and several pts values.
4723    #[test]
4724    fn prefix_writer_matches_build_ws_body() {
4725        let primary: Vec<u8> = (0u8..200).collect();
4726        let mut mixed: VecDeque<(Vec<u8>, u64)> = VecDeque::new();
4727        mixed.push_back((vec![1u8; 100], 0));
4728        mixed.push_back((vec![2u8; RED_MAX_LEN + 1], 960));
4729        mixed.push_back((vec![3u8; 50], 1440));
4730        mixed.push_back((vec![4u8; 900], 1900));
4731        for hist in [VecDeque::new(), mixed] {
4732            for red in [0usize, 1, 2, 4] {
4733                for hdr in [true, false] {
4734                    for pts in [960u64, 3840, 20000] {
4735                        let reference = build_ws_body(&primary, pts, &hist, red, hdr);
4736                        let mut buf = vec![0u8; RED_PREFIX_MAX + MAX_OPUS_PACKET];
4737                        let prefix = write_ws_prefix_into(&mut buf, pts, &hist, red, hdr);
4738                        buf[prefix..prefix + primary.len()].copy_from_slice(&primary);
4739                        buf.truncate(prefix + primary.len());
4740                        assert_eq!(buf, reference, "red={red} hdr={hdr} pts={pts}");
4741                    }
4742                }
4743            }
4744        }
4745    }
4746
4747    /// A truncated buffer returned to the pool comes back as the same allocation
4748    /// (recycled) restored to full `buf_size` length, and an undersized foreign buffer is
4749    /// rejected rather than pooled.
4750    #[test]
4751    fn buffer_pool_recycles_and_restores_length() {
4752        let pool = BufferPool::new(64);
4753        let mut a = pool.take();
4754        assert_eq!(a.len(), 64);
4755        let ptr = a.as_ptr() as usize;
4756        a.truncate(7);
4757        pool.put(a);
4758        let b = pool.take();
4759        assert_eq!(b.as_ptr() as usize, ptr, "buffer must be recycled");
4760        assert_eq!(b.len(), 64, "length must be restored");
4761        pool.put(Vec::new());
4762        assert_eq!(pool.take().len(), 64);
4763    }
4764
4765    /// Dropping an `AudioFrame` whose `pool` is set returns its buffer to that pool,
4766    /// so the next `take` hands back the same allocation.
4767    #[test]
4768    fn audio_frame_drop_refills_pool() {
4769        let pool = Arc::new(BufferPool::new(32));
4770        let buf = pool.take();
4771        let ptr = buf.as_ptr() as usize;
4772        drop(AudioFrame {
4773            data: buf,
4774            pts: 0,
4775            channels: 2,
4776            pool: Some(Arc::clone(&pool)),
4777        });
4778        let recycled = pool.take();
4779        assert_eq!(recycled.as_ptr() as usize, ptr);
4780    }
4781
4782    /// Micro-benchmark (not a correctness test) isolating the emit-path assembly cost:
4783    /// the copy-based `build_ws_body` + per-frame alloc versus the pooled in-place prefix,
4784    /// with the encoder stubbed to a memcpy.
4785    ///
4786    /// `#[ignore]`d by default; run with
4787    /// `cargo test --release bench_emit_assembly -- --ignored --nocapture`. The pooled arm
4788    /// circulates buffers at the delivery-ring depth (>8 in flight) so refill batching engages
4789    /// as it does live — several returns per drain, not one.
4790    #[test]
4791    #[ignore]
4792    fn bench_emit_assembly() {
4793        use std::hint::black_box;
4794        use std::time::Instant;
4795
4796        const ITERS: u32 = 500_000;
4797        const PAYLOAD: usize = 200;
4798        let src = vec![0xA5u8; PAYLOAD];
4799        let mut hist: VecDeque<(Vec<u8>, u64)> = VecDeque::new();
4800        hist.push_back((vec![6u8; 180], 0));
4801        hist.push_back((vec![7u8; 180], 480));
4802
4803        for (red, label) in [(0usize, "red=0"), (2usize, "red=2")] {
4804            let old = Instant::now();
4805            let mut out = vec![0u8; MAX_OPUS_PACKET];
4806            for i in 0..ITERS {
4807                out[..PAYLOAD].copy_from_slice(&src);
4808                let data = build_ws_body(&out[..PAYLOAD], 960 + i as u64, &hist, red, true);
4809                black_box(&data);
4810            }
4811            let old_ns = old.elapsed().as_nanos() / ITERS as u128;
4812
4813            let pool = Arc::new(BufferPool::new(RED_PREFIX_MAX + MAX_OPUS_PACKET));
4814            let mut taker = PoolTaker::new(Arc::clone(&pool));
4815            let mut inflight: VecDeque<Vec<u8>> = VecDeque::new();
4816            let new = Instant::now();
4817            for i in 0..ITERS {
4818                let mut data = taker.take();
4819                let prefix = write_ws_prefix_into(&mut data, 960 + i as u64, &hist, red, true);
4820                data[prefix..prefix + PAYLOAD].copy_from_slice(&src);
4821                data.truncate(prefix + PAYLOAD);
4822                black_box(&data);
4823                inflight.push_back(data);
4824                if inflight.len() > 8 {
4825                    pool.put(inflight.pop_front().unwrap());
4826                }
4827            }
4828            let new_ns = new.elapsed().as_nanos() / ITERS as u128;
4829            println!("{label}: old {old_ns} ns/frame -> pooled in-place {new_ns} ns/frame");
4830        }
4831    }
4832}
4833
4834/// PyO3 module init: register the capture/playback classes and the atexit sweep.
4835///
4836/// Exposes `AudioCapture`, `AudioCaptureSettings`, `AudioFrame`, `AudioPlayback`, and
4837/// `AudioPlaybackSettings`, then registers `_stop_all_captures` on Python's `atexit` so no
4838/// still-running capture thread is calling into Python during interpreter finalization.
4839#[pymodule]
4840fn pcmflux(m: &Bound<'_, PyModule>) -> PyResult<()> {
4841    m.add_class::<AudioCapture>()?;
4842    m.add_class::<AudioCaptureSettings>()?;
4843    m.add_class::<AudioFrame>()?;
4844    m.add_class::<AudioPlayback>()?;
4845    m.add_class::<AudioPlaybackSettings>()?;
4846    m.add_function(wrap_pyfunction!(_stop_all_captures, m)?)?;
4847    if let Ok(atexit) = m.py().import("atexit") {
4848        let _ = atexit.call_method1("register", (m.getattr("_stop_all_captures")?,));
4849    }
4850    Ok(())
4851}