Files
remote-volume-monitor/src/volume_control.py
Agent e4a53c6064 重构:将 Code 文件夹内容移至仓库根目录
- 移除 Code 文件夹层级
- 源代码、文档、配置直接放在根目录
- 更新 .gitignore 排除 Releases/Test
2026-03-20 07:08:05 +08:00

282 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)