pub(crate) struct Inner {
pub(crate) stop_state: AtomicI64,
pub(crate) started_ok: AtomicBool,
pub(crate) start_state: AtomicU8,
pub(crate) last_error: Mutex<Option<String>>,
pub(crate) capture_tid: AtomicI64,
pub(crate) deliver_tid: AtomicI64,
pub(crate) opus_bitrate: AtomicI32,
pub(crate) use_silence_gate: AtomicBool,
pub(crate) debug_logging: AtomicBool,
pub(crate) emit_audio_header: AtomicBool,
}Expand description
Shared state for one capture (or playback) run: the lifecycle state machine plus the
per-frame settings mirrors the worker reads on the hot path — all lock-free except the
rarely touched last_error.
The lifecycle is driven by two atomics — stop_state (the single source of truth for
“should this run stop”) and start_state (IDLE → STARTING → RUNNING, then FAILED with a
last_error or back to IDLE on a clean stop) — plus capture_tid and deliver_tid, the
worker and delivery threads’ OS tids used to detect a re-entrant stop/start issued from
inside the Python callback (which runs on the delivery thread). The remaining atomics
mirror settings the worker consults each frame without re-snapshotting Settings, so
update_audio_bitrate can retune the encoder mid-run without locking; the silence and
header flags are published once at start and only read per frame.
Fields§
§stop_state: AtomicI64Single lifecycle source of truth: STOP_NONE (running), STOP_EXTERNAL, or a
positive tid meaning the run self-stopped from inside its own callback (recorded
under the issuing thread’s tid — the delivery thread’s). A re-entrant start clears
only its own self-stop via compare-exchange, so it can never clobber an external
stop that raced in mid-join (which would strand it).
started_ok: AtomicBool§start_state: AtomicU8§last_error: Mutex<Option<String>>Why the last run failed (start_state == ST_FAILED), for Python’s last_error.
Written before ST_FAILED is published and cleared when a new run is spawned, so
a failure that lands after the start handshake returned — the retry ladder giving
up, a mid-run reconnect budget spent, a worker panic — is still observable.
capture_tid: AtomicI64OS tid of the running capture thread; 0 when no worker is live.
deliver_tid: AtomicI64OS tid of the running delivery thread (the one that invokes the Python callback);
0 when none is live. Checked by the re-entrancy guards alongside capture_tid,
because a stop/start issued from inside the callback executes on THIS thread — a
join from it would cycle (stopper joins capture, capture joins delivery).
opus_bitrate: AtomicI32Lock-free per-frame settings mirrors, re-read by the worker each frame.
opus_bitrate is republished by update_audio_bitrate; the rest are published
once, by the run that starts.
use_silence_gate: AtomicBool§debug_logging: AtomicBool§emit_audio_header: AtomicBoolImplementations§
Source§impl Inner
impl Inner
pub(crate) fn new() -> Self
Sourcepub(crate) fn request_external_stop(&self)
pub(crate) fn request_external_stop(&self)
Request an authoritative external stop, stored unconditionally.
The single source of truth for stopping a run. External stops win every race: they are published (inside the lifecycle lock, immediately before join) with a plain store, so a concurrent self-stop/self-start — which only ever compare-exchanges state it owns — can never clobber one and strand the join forever.
Sourcepub(crate) fn request_self_stop(&self, me: i64)
pub(crate) fn request_self_stop(&self, me: i64)
Record a re-entrant self-stop from inside the run’s own callback.
Only transitions STOP_NONE -> me via compare-exchange, so it never overwrites a
pending external stop (which must win the join).
Sourcepub(crate) fn undo_self_stop(&self, me: i64)
pub(crate) fn undo_self_stop(&self, me: i64)
Undo a re-entrant self-stop (self-start from inside the callback).
Compare-exchanges me -> STOP_NONE, clearing the stop only if this same thread
still owns it. If an external stop landed in between, the CAS fails and that stop
stands.
Sourcepub(crate) fn clear_stop(&self)
pub(crate) fn clear_stop(&self)
Clear the stop state back to running. Only ever called under the lifecycle lock (after join, before spawn), where no external stop can be in flight — the lost-stop invariant that lets this be an unconditional store.
Sourcepub(crate) fn stop_pending(&self) -> bool
pub(crate) fn stop_pending(&self) -> bool
True once any stop (external or self) is pending; the hot loops poll this.
Sourcepub(crate) fn worker_alive(&self) -> bool
pub(crate) fn worker_alive(&self) -> bool
True while a worker is (or is still becoming) live: the startup handshake is in flight, or the hot loop is running with no stop pending.
Goes false the moment the worker fails, is stopped, or dies mid-run — the hot loop
clears started_ok before breaking on error, even with no stop pending and
start_state still RUNNING. Producers (e.g. AudioPlayback::write) gate on this
so they surface a dead stream instead of feeding state nothing services.
Sourcepub(crate) fn running(&self) -> bool
pub(crate) fn running(&self) -> bool
True while the worker is connected and running with no stop pending — the
Python is_capturing / is_running getters.
Sourcepub(crate) fn state_name(&self) -> &'static str
pub(crate) fn state_name(&self) -> &'static str
Lifecycle phase for Python’s state: "idle" (no run yet, or the last run stopped
cleanly), "starting" (worker spawned, first PulseAudio session not yet up),
"running" (connected, or reconnecting mid-run, with no stop pending), or
"failed" (the last run ended in error; last_error says why). A run that was
stopped reads "idle" again even while its thread is still winding down.
Sourcepub(crate) fn fail(&self, msg: String)
pub(crate) fn fail(&self, msg: String)
Mark the run dead with a reason: logs msg to stderr, records it for last_error,
and only then publishes ST_FAILED, so a reader that observes the failed state also
finds its message. Every terminal worker error goes through here.
Sourcepub(crate) fn last_error(&self) -> Option<String>
pub(crate) fn last_error(&self) -> Option<String>
The message of the last run’s failure, or None while no run has failed since the
last (re)start.
Sourcepub(crate) fn clear_error(&self)
pub(crate) fn clear_error(&self)
Forget the previous run’s failure; called by spawn_worker as a new run is armed.
Sourcepub(crate) fn is_own_thread(&self, me: i64) -> bool
pub(crate) fn is_own_thread(&self, me: i64) -> bool
True when the calling thread is one of this run’s own threads — the capture worker or the delivery thread that runs the Python callback — i.e. the call is a re-entrant stop/start/drop from inside the callback. Such a caller must never join: teardown has the capture thread join the delivery thread, so a join from either one closes a cycle and deadlocks.