75 lines
2.2 KiB
Python
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()
|