Spaces:
Running
Running
| """ | |
| modules/audio_control.py — Phase 4 (System Control): Audio controls (Windows). | |
| Master volume: | |
| Uses nircmd.exe (already bundled) for reliable behavior. | |
| Per-app volume: | |
| Optional (requires pycaw). If pycaw isn't installed, we return a clear message. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import re | |
| import subprocess | |
| from dataclasses import dataclass | |
| from config import BASE_DIR | |
| NIRCMD = os.path.join(BASE_DIR, "nircmd.exe") | |
| class AudioResult: | |
| ok: bool | |
| message: str | |
| def _run(cmd: list[str]) -> subprocess.CompletedProcess[str]: | |
| return subprocess.run(cmd, capture_output=True, text=True, shell=False) | |
| def set_master_volume(percent: int) -> AudioResult: | |
| p = max(0, min(100, int(percent))) | |
| if not os.path.exists(NIRCMD): | |
| return AudioResult(False, "nircmd.exe missing; master volume unavailable.") | |
| # nircmd setsysvolume expects 0..65535 | |
| val = int((p / 100.0) * 65535) | |
| r = _run([NIRCMD, "setsysvolume", str(val)]) | |
| if r.returncode != 0: | |
| err = (r.stderr or r.stdout or "").strip() | |
| return AudioResult(False, f"Master volume failed: {err[:160]}") | |
| return AudioResult(True, f"Master volume set to {p}%.") | |
| def change_master_volume(delta_percent: int) -> AudioResult: | |
| d = int(delta_percent) | |
| if not os.path.exists(NIRCMD): | |
| return AudioResult(False, "nircmd.exe missing; master volume unavailable.") | |
| val = int((d / 100.0) * 65535) | |
| r = _run([NIRCMD, "changesysvolume", str(val)]) | |
| if r.returncode != 0: | |
| err = (r.stderr or r.stdout or "").strip() | |
| return AudioResult(False, f"Master volume change failed: {err[:160]}") | |
| return AudioResult(True, "Master volume changed.") | |
| def mute(muted: bool = True) -> AudioResult: | |
| if not os.path.exists(NIRCMD): | |
| return AudioResult(False, "nircmd.exe missing; mute unavailable.") | |
| r = _run([NIRCMD, "mutesysvolume", "1" if muted else "0"]) | |
| if r.returncode != 0: | |
| err = (r.stderr or r.stdout or "").strip() | |
| return AudioResult(False, f"Mute failed: {err[:160]}") | |
| return AudioResult(True, "Muted." if muted else "Unmuted.") | |
| def set_app_volume(process_name: str, percent: int) -> AudioResult: | |
| """ | |
| Set volume for an app by process name, e.g. 'chrome.exe' or 'discord.exe'. | |
| Requires pycaw; otherwise returns a clear message. | |
| """ | |
| p = max(0, min(100, int(percent))) | |
| name = process_name.strip() | |
| if not name: | |
| return AudioResult(False, "Missing process name.") | |
| if not name.lower().endswith(".exe"): | |
| name += ".exe" | |
| try: | |
| from pycaw.pycaw import AudioUtilities # type: ignore | |
| from comtypes import CLSCTX_ALL # type: ignore | |
| from ctypes import POINTER, cast # type: ignore | |
| from pycaw.pycaw import ISimpleAudioVolume # type: ignore | |
| except Exception: | |
| return AudioResult(False, "Per-app volume requires pycaw (not installed).") | |
| sessions = AudioUtilities.GetAllSessions() | |
| found = False | |
| for s in sessions: | |
| try: | |
| if not s.Process: | |
| continue | |
| if s.Process.name().lower() != name.lower(): | |
| continue | |
| found = True | |
| vol = s._ctl.QueryInterface(ISimpleAudioVolume) # pylint: disable=protected-access | |
| vol.SetMasterVolume(p / 100.0, None) | |
| except Exception: | |
| continue | |
| if not found: | |
| return AudioResult(False, f"No active audio session for {name}.") | |
| return AudioResult(True, f"{name} volume set to {p}%.") | |