jarvis-cloud / modules /audio_control.py
Jarvis2345's picture
Squash history — remove all prior commits (secret hygiene, S4)
a31f556
Raw
History Blame
3.66 kB
"""
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")
@dataclass(frozen=True, slots=True)
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}%.")