"""Duck (lower) media volume during recording via PulseAudio/PipeWire.""" import re import shutil import subprocess _saved_volumes: dict[int, str] = {} def _pactl_available() -> bool: return shutil.which("pactl") is not None def _get_sink_inputs() -> list[tuple[int, str]]: """Return list of (sink_input_index, current_volume_string).""" try: out = subprocess.run( ["pactl", "list", "sink-inputs"], capture_output=True, text=True, timeout=3, ).stdout except (subprocess.TimeoutExpired, FileNotFoundError): return [] results = [] current_idx = None for line in out.splitlines(): m = re.match(r"Sink Input #(\d+)", line) if m: current_idx = int(m.group(1)) continue if current_idx is not None and "Volume:" in line: results.append((current_idx, line.strip())) current_idx = None return results def _parse_percent(vol_line: str) -> int | None: """Extract first percentage value from a Volume: line.""" m = re.search(r"(\d+)%", vol_line) return int(m.group(1)) if m else None def duck(duck_percent: int = 20) -> None: """Lower all sink inputs to duck_percent of their current volume.""" _saved_volumes.clear() if not _pactl_available(): return for idx, vol_line in _get_sink_inputs(): pct = _parse_percent(vol_line) if pct is not None: _saved_volumes[idx] = f"{pct}%" ducked = max(1, int(pct * duck_percent / 100)) try: subprocess.run( ["pactl", "set-sink-input-volume", str(idx), f"{ducked}%"], check=False, timeout=2, ) except (subprocess.TimeoutExpired, FileNotFoundError): pass def unduck() -> None: """Restore all sink inputs to their saved volumes.""" if not _pactl_available(): return for idx, vol in _saved_volumes.items(): try: subprocess.run( ["pactl", "set-sink-input-volume", str(idx), vol], check=False, timeout=2, ) except (subprocess.TimeoutExpired, FileNotFoundError): pass _saved_volumes.clear()