Expand description
pcmflux: PulseAudio/PipeWire audio capture with Opus encoding, plus mic-uplink playback, exposed as a pure-Rust PyO3 extension (the audio sibling of pixelflux).
The capture thread pulls S16LE PCM fragments from a PulseAudio record stream,
reassembles them into fixed-size Opus frames, encodes them (mono/stereo via the
opus crate, 5.1/7.1 surround via the multistream API), optionally wraps them in
RFC 2198 RED framing — redundant copies of recent frames, so a client on a lossy
transport rebuilds a dropped packet from the next one it receives instead of stalling
for a retransmit — and hands each frame to a delivery thread that runs the Python
callback off the audio path, so a slow or GIL-blocked callback can never stall the
PulseAudio pump. The playback path mirrors this in reverse: it decodes the Opus mic
uplink and writes PCM into a virtual sink.
Concurrency design (the invariants below are load-bearing):
- A lifecycle mutex serializes joining/reassigning the capture thread.
- A single stop_state atomic is the one source of truth (0 = running, -1 = external stop, a positive value = self-stop recorded under the issuing thread’s tid — the delivery thread’s for a callback-issued stop). The external -1 is stored INSIDE that lock immediately before join, so a stop can never be lost between observing a live thread and asking it to stop. A re-entrant self-start undoes only its own self-stop via one compare-exchange, so a racing external stop is never clobbered.
- The PulseAudio mainloop is pumped with a bounded ~20ms timeout, so a stop is observed within ~20ms even if the audio source delivers no data (is wedged).
- The GIL is released around join, because joining the capture thread transitively joins the delivery thread, whose in-flight Python callback needs the GIL; holding it while joining would deadlock.
- A callback may itself call stop/start; the callback runs on the delivery thread, so that re-entrant case is detected via the delivery thread’s OS tid (the capture thread’s tid is checked too) and short-circuits without joining — a join from inside the callback would cycle (stopper joins capture, capture joins delivery, delivery is the stopper).
Macros§
- elog 🔒
- Non-panicking
eprintln!replacement — the stderr sibling ofplog!. - plog 🔒
- Non-panicking
println!replacement that swallows write errors (e.g. EPIPE) instead of panicking, so a broken output pipe can’t unwind the capture thread or a callback.
Structs§
- Audio
Capture 🔒 - Python-facing capture handle. Owns the
Sharedlifecycle state and exposesstart_capture/stop_capture/update_audio_bitrate/is_capturingto Python. - Audio
Capture 🔒Settings - Python-facing capture/encode configuration read by
start_capture. - Audio
Frame 🔒 - Zero-copy buffer-protocol result type handed to the Python callback.
- Audio
Playback 🔒 - Python-facing mic-playback handle, symmetric to
AudioCapture. - Audio
Playback 🔒Settings - Python-facing mic-playback configuration read by
AudioPlayback.start. - Buffer
Pool 🔒 - Recycles outgoing frame buffers from dropped
AudioFrames back to the capture thread, so the steady-state emit path allocates nothing. - Delivery
Ring 🔒 - Bounded, drop-oldest hand-off from the capture thread to the Python delivery thread, so a slow or GIL-blocked callback can never stall the PulseAudio pump.
- Inner 🔒
- Lock-free 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.
- Multi
Opus 🔒 - Owning wrapper over a raw
OpusMSEncoder(the surround multistream encoder). - Opus
Playback 🔒Decoder - Turns the mic uplink back into PCM: the client always sends the mic as Opus, so
every inbound packet must be decoded before it can be queued for the virtual sink. It
decodes one packet to interleaved S16LE PCM, reusing a scratch buffer across calls to
stay off the per-decode allocation path. Lives behind a
MutexonPbSharedand is driven fromwrite/write_red. - PbSettings 🔒
- Playback settings, snapshotted from the Python
AudioPlaybackSettingsat start so the playback thread owns an immutable copy for the run’s lifetime. - PbShared 🔒
- Per-
AudioPlaybackshared handle — the mirror ofSharedfor the playback path. - Play
Queue 🔒 - Bounded, drop-oldest byte queue for the mic-PCM handoff into the virtual “input” sink — the whole of the playback path’s buffering in a single bound.
- Pool
Taker 🔒 - Sole-consumer view over the shared
BufferPoolfor the capture thread. - RunState 🔒
- Per-run encode/deliver state, living on the capture thread’s stack for the lifetime of one capture. Holds the encoder, the frame-reassembly buffers, the outgoing buffer recycler, the RED redundancy history, and the running debug-log counters.
- Settings 🔒
- Capture/encode settings, snapshotted from
AudioCaptureSettingsat start. - Shared 🔒
- Per-
AudioCaptureshared handle: the lock-freeInnerstate plus the lifecycle-locked join handle for the capture thread.
Enums§
- PcmEncoder 🔒
- One encode surface over both Opus APIs: the
opuscrate for mono/stereo, and the raw multistream C API for 6/8-channel surround.
Constants§
- MAX_
OPUS_ 🔒PACKET - Max Opus output packet size in bytes per stream; sizes the emit buffer pool.
- PUMP_
TIMEOUT_ 🔒US - PulseAudio mainloop pump timeout (~20 ms) — upper bound on how long a pending stop can go unobserved even when the audio source delivers nothing.
- RED_
BLOCK_ 🔒PT - RED block payload type.
- RED_
MAX_ 🔒DISTANCE - Max redundant copies per frame and RED history depth.
- RED_
MAX_ 🔒LEN - Max RED block length — 10-bit ceiling in bytes.
- RED_
MAX_ 🔒OFFSET - Max RED timestamp offset — 14-bit ceiling in 48 kHz samples from the primary.
- RED_
PREFIX_ 🔒MAX - Worst-case byte length of the WS frame prefix (tag + n_red + pts + headers + payloads).
- STOP_
EXTERNAL 🔒 stop_statesentinel: authoritative external stop (-1). Any positive value instead is the OS tid of a capture thread that self-stopped from its callback.- STOP_
NONE 🔒 stop_statesentinel: no stop pending (running).- ST_
FAILED 🔒 start_statehandshake: startup failed.- ST_
RUNNING 🔒 start_statehandshake: the hot loop is running.- ST_
STARTING 🔒 start_statehandshake: startup in progress.
Statics§
- PLAYBACK_
REGISTRY 🔒 - Process-wide registry of live playbacks, swept by the same atexit sweep as
captures. Holds
Weakreferences so it keeps nothing alive on its own. - REGISTRY 🔒
- Process-wide registry of live captures, swept at interpreter exit. Holds
Weakreferences so it keeps nothing alive on its own.
Functions§
- __
pyfunction__ 🔒 ⚠stop_ all_ captures - _stop_
all_ 🔒captures - atexit sweep: stop and join every live capture and playback before interpreter shutdown, so no worker thread is still calling into Python during finalization.
- capture_
run 🔒 - Own one whole capture run end to end on a dedicated thread — connect PulseAudio,
encode, and deliver — until stopped. It runs on its own thread because the PulseAudio
mainloop must be pumped continuously and independently of Python: sharing the caller’s
thread would tie capture cadence to the GIL and let any Python stall starve the audio.
The body handed to
spawn_worker. - extract_
pb_ 🔒settings - Read a Python
AudioPlaybackSettingsinto a RustPbSettingsby attribute name. - extract_
settings 🔒 - Read a Python
AudioCaptureSettingsinto a RustSettingsby attribute name. - gettid 🔒
- Returns the calling thread’s OS tid (
gettidsyscall) so a stop/start issued from inside the Python callback can detect it is on the capture thread and avoid self-joining. - join_
failed_ 🔒start - Failed-start teardown: stop and join the thread
spawnedby THIS start attempt, but only if it still owns the slot. - multiopus_
layout 🔒 - Chromium’s multistream-Opus surround layout for a channel count, as
(streams, coupled, mapping);Nonefor anything but 5.1 (6) or 7.1 (8). - parse_
device_ 🔒name - Normalize a Python
device_name(str | bytes | None) intoOption<String>, mapping bothNoneand the empty string toNone(meaning the system default). Shared by the capture and playback settings extractors. - pcmflux 🔒
- PyO3 module init: register the capture/playback classes and the atexit sweep.
- playback_
registry 🔒 - Lazily initialize and return the playback registry.
- playback_
run 🔒 - Drive one whole mic-playback run on the playback thread. The body handed to
spawn_worker; the mirror ofcapture_runfor the uplink. - pump 🔒
- Run one bounded iteration of a PulseAudio standard mainloop:
prepare(timeout_us)→poll→dispatch. Returnsfalseif any stage errors. - registry 🔒
- Lazily initialize and return the capture registry.
- spawn_
worker 🔒 - Locked takeover + spawn of a worker thread, shared by the capture and playback
starts. Returns the new thread’s id, or
Noneif the spawn failed. - valid_
opus_ 🔒duration - True if
msis a valid Opus frame duration (2.5, 5, 10, 20, 40, or 60 ms). - write_
ws_ 🔒prefix_ into - Emit the RFC 2198 RED framing prefix into
bufin-place and return its length, so the encoder can serialize the Opus packet directly after it with no scratch buffer and no per-frame allocation. The runtime counterpart ofbuild_ws_body.
Type Aliases§
- Frame
Queue 🔒 - Backing store for the delivery ring:
Some(queue)while open,Noneonce closed sopopwakes and returnsNonefor a clean shutdown.