whisper-dictation/whisper_app/media_duck.py

75 lines
2.2 KiB
Python

"""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()