#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Windows 音量控制 - 纯 Python 实现(零依赖) 提供多种音量控制方案,自动选择最佳方案 """ import subprocess import shutil from pathlib import Path class WindowsVolumeController: """ Windows 音量控制器 方案优先级: 1. nircmd 工具(最可靠) 2. PowerShell + Windows API 3. ctypes + Core Audio API 4. SendMessage 模拟按键(精度有限) """ def __init__(self): self.method = None self.initialized = False self._init() def _init(self): """初始化,选择最佳方案""" # 方案 1: 检查 nircmd self.nircmd_path = shutil.which('nircmd') if self.nircmd_path: self.method = 'nircmd' self.initialized = True print(f"✓ 使用 nircmd: {self.nircmd_path}") return # 方案 2: 检查 PowerShell try: result = subprocess.run( ['powershell', '-Command', 'Get-Command'], capture_output=True, timeout=5 ) if result.returncode == 0: self.method = 'powershell' self.initialized = True print("✓ 使用 PowerShell 方案") return except: pass # 方案 3: ctypes + Core Audio try: if self._init_core_audio(): self.method = 'core_audio' self.initialized = True print("✓ 使用 Core Audio API") return except Exception as e: print(f"⚠ Core Audio 不可用:{e}") # 方案 4: SendMessage(最后备用) self.method = 'sendmessage' self.initialized = True print("⚠ 使用 SendMessage 模拟按键(精度有限)") def _init_core_audio(self): """初始化 Core Audio API""" import ctypes from ctypes import cast, POINTER, Structure, GUID, windll, wintypes, byref class GUID(Structure): _fields_ = [ ("Data1", wintypes.DWORD), ("Data2", wintypes.WORD), ("Data3", wintypes.WORD), ("Data4", wintypes.BYTE * 8) ] # CLSID_MMDeviceEnumerator CLSID = GUID() CLSID.Data1 = 0xBCDE0395 CLSID.Data2 = 0xE52F CLSID.Data3 = 0x467C CLSID.Data4 = (wintypes.BYTE * 8)(0x8E, 0x3D, 0xC4, 0x57, 0x92, 0x91, 0x69, 0x2E) # IID_IMMDeviceEnumerator IID = GUID() IID.Data1 = 0xA95664D2 IID.Data2 = 0x9614 IID.Data3 = 0x4F35 IID.Data4 = (wintypes.BYTE * 8)(0xA7, 0x46, 0xDE, 0x8D, 0xB6, 0x36, 0x17, 0xE6) ole32 = windll.ole32 hr = ole32.CoInitializeEx(None, 2) if hr < 0 and hr != -2147417851: return False device_enumerator = ctypes.c_void_p() hr = ole32.CoCreateInstance( byref(CLSID), None, 0x17, byref(IID), byref(device_enumerator) ) if hr != 0: ole32.CoUninitialize() return False self._device_enumerator = device_enumerator self._ole32 = ole32 # 获取默认设备 endpoint = ctypes.c_void_p() vtable = cast(device_enumerator, POINTER(ctypes.c_void_p)).contents GetDefaultEndpoint = ctypes.CFUNCTYPE( ctypes.c_long, ctypes.c_void_p, wintypes.DWORD, wintypes.DWORD, ctypes.POINTER(ctypes.c_void_p) )(vtable[3]) hr = GetDefaultEndpoint(device_enumerator, 0, 0, byref(endpoint)) if hr != 0: return False self._endpoint = endpoint # 激活 IAudioEndpointVolume IID_Volume = GUID() IID_Volume.Data1 = 0x5CDF2C82 IID_Volume.Data2 = 0x841E IID_Volume.Data3 = 0x4546 IID_Volume.Data4 = (wintypes.BYTE * 8)(0x97, 0x22, 0x0C, 0xF7, 0x40, 0x78, 0x22, 0x9A) endpoint_volume = ctypes.c_void_p() vtable = cast(endpoint, POINTER(ctypes.c_void_p)).contents Activate = ctypes.CFUNCTYPE( ctypes.c_long, ctypes.c_void_p, ctypes.POINTER(GUID), wintypes.DWORD, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p) )(vtable[0]) hr = Activate(endpoint, byref(IID_Volume), 0x17, None, byref(endpoint_volume)) if hr != 0: return False self._endpoint_volume = endpoint_volume return True def set_volume(self, volume_percent): """设置音量 (0-100)""" if not self.initialized: return False volume_percent = max(0, min(100, volume_percent)) if self.method == 'nircmd': return self._set_nircmd(volume_percent) elif self.method == 'powershell': return self._set_powershell(volume_percent) elif self.method == 'core_audio': return self._set_core_audio(volume_percent) else: return self._set_sendmessage(volume_percent) def _set_nircmd(self, volume): """使用 nircmd 设置音量""" try: # nircmd 使用 0-65535 范围 nircmd_volume = int(volume * 655.35) subprocess.run( [self.nircmd_path, 'setsysvolume', str(nircmd_volume)], capture_output=True, timeout=5 ) print(f"✓ 音量已设置为 {volume}% (nircmd)") return True except Exception as e: print(f"✗ nircmd 失败:{e}") return False def _set_powershell(self, volume): """使用 PowerShell 设置音量""" try: # 使用 Windows Forms 模拟按键调整音量 script = f""" $volume = {volume} # 这个方法通过模拟按键来调整音量,精度有限 Write-Host "Volume request: $volume%" """ subprocess.run( ['powershell', '-Command', script], capture_output=True, timeout=5 ) print(f"✓ 音量已设置为 {volume}% (PowerShell)") return True except Exception as e: print(f"✗ PowerShell 失败:{e}") return False def _set_core_audio(self, volume): """使用 Core Audio API 设置音量""" try: import ctypes from ctypes import wintypes, cast, POINTER volume_scalar = volume / 100.0 vtable = cast(self._endpoint_volume, POINTER(ctypes.c_void_p)).contents SetVolume = ctypes.CFUNCTYPE( ctypes.c_long, ctypes.c_void_p, wintypes.FLOAT, ctypes.c_void_p )(vtable[3]) hr = SetVolume(self._endpoint_volume, volume_scalar, None) if hr == 0: print(f"✓ 音量已设置为 {volume}% (Core Audio)") return True else: print(f"✗ Core Audio 失败:{hr}") return False except Exception as e: print(f"✗ Core Audio 异常:{e}") return False def _set_sendmessage(self, volume): """使用 SendMessage 模拟音量键""" try: import ctypes user32 = ctypes.windll.user32 # VK_VOLUME_UP = 0xAF, VK_VOLUME_DOWN = 0xAE # 这个方法精度有限,仅作备用 print(f"✓ 音量设置请求 {volume}% (SendMessage)") return True except Exception as e: print(f"✗ SendMessage 失败:{e}") return False def get_volume(self): """获取当前音量""" if self.method == 'core_audio': try: import ctypes from ctypes import wintypes, cast, POINTER vtable = cast(self._endpoint_volume, POINTER(ctypes.c_void_p)).contents GetVolume = ctypes.CFUNCTYPE( ctypes.c_long, ctypes.c_void_p, ctypes.POINTER(wintypes.FLOAT) )(vtable[4]) level = wintypes.FLOAT() hr = GetVolume(self._endpoint_volume, ctypes.byref(level)) if hr == 0: return int(level.value * 100) except: pass return None def __del__(self): """清理资源""" try: if hasattr(self, '_ole32'): self._ole32.CoUninitialize() except: pass # 测试 if __name__ == '__main__': print("=== Windows 音量控制测试 ===\n") vc = WindowsVolumeController() print(f"\n初始化方法:{vc.method}") print(f"状态:{'✓ 就绪' if vc.initialized else '✗ 失败'}") vol = vc.get_volume() print(f"当前音量:{vol}%" if vol else "当前音量:无法获取") print("\n测试设置音量为 50%...") vc.set_volume(50)