The null sink and its two loopbacks were loaded at startup and stayed
loaded forever. While loaded they link the microphone and the speaker
device into a single PipeWire driver group: the microphone drives the
graph and the speaker device runs as a clock follower, so every playback
stream pays for continuous cross-device resampling.
Nothing needs the chain while idle — recording is triggered manually.
Build it in toggle_recording() and drop it as soon as the audio is
captured, before transcription runs for minutes.
Module ids are now derived from pactl instead of tracked in a state file,
so a crashed process cannot leave stale ids behind. The state file only
holds the device selection. Startup tears down any leftover chain.
Loopbacks get latency_msec=200; transcription is offline, and a small
buffer would put these nodes on the graph's realtime deadline.
- /audio/devices now returns sounddevice device names (not pactl source names)
so the stored device name works directly with sd.InputStream
- /audio/combined maps sounddevice names back to pactl source names via
description matching for the loopback commands
- Combined sink description set to 'transkriptor-combined' (no spaces) so
sounddevice name matches the value stored in config
- Add _pactl_source_for_sd_name() helper for the mapping