重构:将 Code 文件夹内容移至仓库根目录
- 移除 Code 文件夹层级 - 源代码、文档、配置直接放在根目录 - 更新 .gitignore 排除 Releases/Test
This commit is contained in:
281
src/volume_control.py
Normal file
281
src/volume_control.py
Normal file
@@ -0,0 +1,281 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user