audio_control
PulseAudio/PipeWire control plane shared by the WebSocket and WebRTC transports.
Both transports need the same few one-shot control operations around the
pcmflux data planes: make sure the null sink whose monitor the server captures
exists, resolve the capture source, bring up the SelkiesVirtualMic (the
input null sink plus module-virtual-source) for microphone forwarding, set
the system defaults, and move a strayed pcmflux source-output back onto its
source. AudioControl does all of it in-process over pulsectl_asyncio; the
pactl subprocess backend is engaged only when the bindings are missing or
the server connection cannot be made, and says so in one log line.
Safety contract with the ctypes bindings: libpulse keeps raw function pointers
to per-operation callback trampolines that live exactly as long as the
awaiting coroutine frame, so cancelling a pending operation (a timeout, a peer
teardown) lets the server's late reply jump into freed memory. Every operation
therefore runs in a task of its own that is never cancelled: a timeout
abandons the connection instead (libpulse drops the pending operations'
callbacks on disconnect) and the task is left to finish by itself, and every
client is closed through aclose rather than garbage-collected with its state
callback still registered. A client binds to the event loop it first runs on
and refuses use from any other loop or thread.
attributePULSE_AVAILABLE= Trueattributelogger= logging.getLogger('audio_control')attributeT= TypeVar('T')attributeDEFAULT_CAPTURE_SINK= 'output'attributeVIRTUAL_MIC_SOURCE= 'SelkiesVirtualMic'attributeVIRTUAL_MIC_SINK= 'input'attributeVIRTUAL_MIC_MASTER= f'{VIRTUAL_MIC_SINK}.monitor'attributePIPEWIRE_NULL_MONITOR= 'auto_null.monitor'attributeVIRTUAL_MIC_SOURCE_NAMESTuple[str, ...]= (VIRTUAL_MIC_SOURCE, f'output.{VIRTUAL_MIC_SOURCE}')attributePCMFLUX_APP_NAME= 'pcmflux'