|
import logging |
|
import platform |
|
import subprocess |
|
import re |
|
import shutil |
|
|
|
|
|
class VolumeController: |
|
"""跨平台音量控制器""" |
|
|
|
def __init__(self): |
|
self.logger = logging.getLogger("VolumeController") |
|
self.system = platform.system() |
|
self.is_arm = platform.machine().startswith(('arm', 'aarch')) |
|
|
|
|
|
if self.system == "Windows": |
|
self._init_windows() |
|
elif self.system == "Darwin": |
|
self._init_macos() |
|
elif self.system == "Linux": |
|
self._init_linux() |
|
else: |
|
self.logger.warning(f"不支持的操作系统: {self.system}") |
|
raise NotImplementedError(f"不支持的操作系统: {self.system}") |
|
|
|
def _init_windows(self): |
|
"""初始化Windows音量控制""" |
|
try: |
|
from ctypes import cast, POINTER |
|
from comtypes import CLSCTX_ALL |
|
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume |
|
|
|
self.devices = AudioUtilities.GetSpeakers() |
|
interface = self.devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None) |
|
self.volume_control = cast(interface, POINTER(IAudioEndpointVolume)) |
|
self.logger.debug("Windows音量控制初始化成功") |
|
except Exception as e: |
|
self.logger.error(f"Windows音量控制初始化失败: {e}") |
|
raise |
|
|
|
def _init_macos(self): |
|
"""初始化macOS音量控制""" |
|
try: |
|
import applescript |
|
|
|
result = applescript.run('get volume settings') |
|
if not result or result.code != 0: |
|
raise Exception("无法访问macOS音量控制") |
|
self.logger.debug("macOS音量控制初始化成功") |
|
except Exception as e: |
|
self.logger.error(f"macOS音量控制初始化失败: {e}") |
|
raise |
|
|
|
def _init_linux(self): |
|
"""初始化Linux音量控制""" |
|
|
|
self.linux_tool = None |
|
|
|
def cmd_exists(cmd): |
|
return shutil.which(cmd) is not None |
|
|
|
|
|
if cmd_exists("pactl"): |
|
self.linux_tool = "pactl" |
|
elif cmd_exists("wpctl"): |
|
self.linux_tool = "wpctl" |
|
elif cmd_exists("amixer"): |
|
self.linux_tool = "amixer" |
|
elif cmd_exists("alsamixer") and cmd_exists("expect"): |
|
self.linux_tool = "alsamixer" |
|
|
|
if not self.linux_tool: |
|
self.logger.error("未找到可用的Linux音量控制工具") |
|
raise Exception("未找到可用的Linux音量控制工具") |
|
|
|
self.logger.debug(f"Linux音量控制初始化成功,使用: {self.linux_tool}") |
|
|
|
def get_volume(self): |
|
"""获取当前音量 (0-100)""" |
|
if self.system == "Windows": |
|
return self._get_windows_volume() |
|
elif self.system == "Darwin": |
|
return self._get_macos_volume() |
|
elif self.system == "Linux": |
|
return self._get_linux_volume() |
|
return 70 |
|
|
|
def set_volume(self, volume): |
|
"""设置音量 (0-100)""" |
|
|
|
volume = max(0, min(100, volume)) |
|
|
|
if self.system == "Windows": |
|
self._set_windows_volume(volume) |
|
elif self.system == "Darwin": |
|
self._set_macos_volume(volume) |
|
elif self.system == "Linux": |
|
self._set_linux_volume(volume) |
|
|
|
def _get_windows_volume(self): |
|
"""获取Windows音量""" |
|
try: |
|
|
|
volume_scalar = self.volume_control.GetMasterVolumeLevelScalar() |
|
return int(volume_scalar * 100) |
|
except Exception as e: |
|
self.logger.warning(f"获取Windows音量失败: {e}") |
|
return 70 |
|
|
|
def _set_windows_volume(self, volume): |
|
"""设置Windows音量""" |
|
try: |
|
|
|
self.volume_control.SetMasterVolumeLevelScalar(volume / 100.0, None) |
|
except Exception as e: |
|
self.logger.warning(f"设置Windows音量失败: {e}") |
|
|
|
def _get_macos_volume(self): |
|
"""获取macOS音量""" |
|
try: |
|
import applescript |
|
result = applescript.run('output volume of (get volume settings)') |
|
if result and result.out: |
|
return int(result.out.strip()) |
|
return 70 |
|
except Exception as e: |
|
self.logger.warning(f"获取macOS音量失败: {e}") |
|
return 70 |
|
|
|
def _set_macos_volume(self, volume): |
|
"""设置macOS音量""" |
|
try: |
|
import applescript |
|
applescript.run(f'set volume output volume {volume}') |
|
except Exception as e: |
|
self.logger.warning(f"设置macOS音量失败: {e}") |
|
|
|
def _get_linux_volume(self): |
|
"""获取Linux音量""" |
|
if self.linux_tool == "pactl": |
|
return self._get_pactl_volume() |
|
elif self.linux_tool == "wpctl": |
|
return self._get_wpctl_volume() |
|
elif self.linux_tool == "amixer": |
|
return self._get_amixer_volume() |
|
return 70 |
|
|
|
def _set_linux_volume(self, volume): |
|
"""设置Linux音量""" |
|
if self.linux_tool == "pactl": |
|
self._set_pactl_volume(volume) |
|
elif self.linux_tool == "wpctl": |
|
self._set_wpctl_volume(volume) |
|
elif self.linux_tool == "amixer": |
|
self._set_amixer_volume(volume) |
|
elif self.linux_tool == "alsamixer": |
|
self._set_alsamixer_volume(volume) |
|
|
|
def _get_pactl_volume(self): |
|
"""使用pactl获取音量""" |
|
try: |
|
result = subprocess.run( |
|
["pactl", "list", "sinks"], |
|
capture_output=True, |
|
text=True |
|
) |
|
if result.returncode == 0: |
|
for line in result.stdout.split('\n'): |
|
if 'Volume:' in line and 'front-left:' in line: |
|
match = re.search(r'(\d+)%', line) |
|
if match: |
|
return int(match.group(1)) |
|
except Exception as e: |
|
self.logger.debug(f"通过pactl获取音量失败: {e}") |
|
return 70 |
|
|
|
def _set_pactl_volume(self, volume): |
|
"""使用pactl设置音量""" |
|
try: |
|
subprocess.run( |
|
["pactl", "set-sink-volume", "@DEFAULT_SINK@", f"{volume}%"], |
|
capture_output=True, |
|
text=True |
|
) |
|
except Exception as e: |
|
self.logger.warning(f"通过pactl设置音量失败: {e}") |
|
|
|
def _get_wpctl_volume(self): |
|
"""使用wpctl获取音量""" |
|
try: |
|
result = subprocess.run( |
|
["wpctl", "get-volume", "@DEFAULT_AUDIO_SINK@"], |
|
capture_output=True, |
|
text=True, |
|
check=True |
|
) |
|
return int(float(result.stdout.split(' ')[1]) * 100) |
|
except Exception as e: |
|
self.logger.debug(f"通过wpctl获取音量失败: {e}") |
|
return 70 |
|
|
|
def _set_wpctl_volume(self, volume): |
|
"""使用wpctl设置音量""" |
|
try: |
|
subprocess.run( |
|
["wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", f"{volume}%"], |
|
capture_output=True, |
|
text=True, |
|
check=True |
|
) |
|
except Exception as e: |
|
self.logger.warning(f"通过wpctl设置音量失败: {e}") |
|
|
|
def _get_amixer_volume(self): |
|
"""使用amixer获取音量""" |
|
try: |
|
result = subprocess.run( |
|
["amixer", "get", "Master"], |
|
capture_output=True, |
|
text=True |
|
) |
|
if result.returncode == 0: |
|
match = re.search(r'(\d+)%', result.stdout) |
|
if match: |
|
return int(match.group(1)) |
|
except Exception as e: |
|
self.logger.debug(f"通过amixer获取音量失败: {e}") |
|
return 70 |
|
|
|
def _set_amixer_volume(self, volume): |
|
"""使用amixer设置音量""" |
|
try: |
|
subprocess.run( |
|
["amixer", "sset", "Master", f"{volume}%"], |
|
capture_output=True, |
|
text=True |
|
) |
|
except Exception as e: |
|
self.logger.warning(f"通过amixer设置音量失败: {e}") |
|
|
|
def _set_alsamixer_volume(self, volume): |
|
"""使用alsamixer设置音量""" |
|
try: |
|
script = f""" |
|
spawn alsamixer |
|
send "m" |
|
send "{volume}" |
|
send "%" |
|
send "q" |
|
expect eof |
|
""" |
|
subprocess.run( |
|
["expect", "-c", script], |
|
capture_output=True, |
|
text=True |
|
) |
|
except Exception as e: |
|
self.logger.warning(f"通过alsamixer设置音量失败: {e}") |
|
|
|
@staticmethod |
|
def check_dependencies(): |
|
"""检查并报告缺少的依赖""" |
|
import platform |
|
system = platform.system() |
|
missing = [] |
|
|
|
if system == "Windows": |
|
try: |
|
import pycaw |
|
except ImportError: |
|
missing.append("pycaw") |
|
try: |
|
import comtypes |
|
except ImportError: |
|
missing.append("comtypes") |
|
|
|
elif system == "Darwin": |
|
try: |
|
import applescript |
|
except ImportError: |
|
missing.append("applescript") |
|
|
|
elif system == "Linux": |
|
import shutil |
|
tools = ["pactl", "wpctl", "amixer", "alsamixer"] |
|
found = False |
|
for tool in tools: |
|
if shutil.which(tool): |
|
found = True |
|
break |
|
if not found: |
|
missing.append("pulseaudio-utils、wireplumber 或 alsa-utils") |
|
|
|
if missing: |
|
print(f"警告: 音量控制需要以下依赖,但未找到: {', '.join(missing)}") |
|
print("请使用以下命令安装缺少的依赖:") |
|
if system == "Windows": |
|
print("pip install " + " ".join(missing)) |
|
elif system == "Darwin": |
|
print("pip install " + " ".join(missing)) |
|
elif system == "Linux": |
|
print("sudo apt-get install " + " ".join(missing)) |
|
return False |
|
|
|
return True |