Files
YMhut-box-C-/参考/NexBox-main/src-tauri/src/audio_eq_head.txt
T

994 lines
37 KiB
Plaintext
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.
use serde::{Deserialize, Serialize};
use std::fs;
use std::os::windows::process::CommandExt;
use std::path::PathBuf;
use std::process::Command;
use tauri::{AppHandle, Manager};
use sysinfo::System;
mod audio_device;
const CREATE_NO_WINDOW: u32 = 0x08000000;
const WUJIEQ_EXE: &str = "wujieq.exe";
const FXVAD_SERVICE: &str = "FXVAD";
/// 驱动状态
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DriverStatus {
pub installed: bool,
pub service_exists: bool,
pub service_running: bool,
pub device_name: String,
pub needs_reboot: bool,
}
/// EQ 频段信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EqBand {
pub freq: f64,
pub gain: f64,
}
/// EQ 预设
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EqPreset {
pub id: String,
pub name: String,
pub bands: Vec<EqBand>,
pub enabled: bool,
}
/// EQ 引擎状态
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EngineStatus {
pub running: bool,
pub pid: Option<u32>,
}
/// 获取 fxvad 资源目录
fn get_fxvad_resource_dir(app: &AppHandle) -> Option<PathBuf> {
// 1. 通过 Tauri resource_dir 查找(生产环境 + 部分开发现境)
if let Ok(resource_dir) = app.path().resource_dir() {
let candidates = [
resource_dir.join("binaries").join("fxvad"),
resource_dir.join("resources").join("binaries").join("fxvad"),
resource_dir.join("_up_").join("resources").join("binaries").join("fxvad"),
resource_dir.join("_up_").join("_up_").join("src-tauri").join("resources").join("binaries").join("fxvad"),
];
for path in &candidates {
if path.exists() {
return Some(path.clone());
}
}
}
// 2. 通过 exe 路径查找(开发环境:exe 在 src-tauri/target/debug/ 下)
if let Ok(exe_path) = std::env::current_exe() {
if let Some(parent) = exe_path.parent() {
let candidates = [
parent.join("binaries").join("fxvad"),
parent.join("resources").join("binaries").join("fxvad"),
// dev: target/debug -> src-tauri/resources/binaries/fxvad
parent.join("..").join("..").join("resources").join("binaries").join("fxvad"),
// dev: target/debug -> NexBox/src-tauri/resources/binaries/fxvad
parent.join("..").join("..").join("..").join("src-tauri").join("resources").join("binaries").join("fxvad"),
];
for path in &candidates {
if path.exists() {
if let Ok(canon) = path.canonicalize() {
return Some(canon);
}
return Some(path.clone());
}
}
}
}
// 3. 编译时路径(开发环境最可靠:CARGO_MANIFEST_DIR = src-tauri 目录)
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let dev_path = manifest_dir.join("resources").join("binaries").join("fxvad");
if dev_path.exists() {
return Some(dev_path);
}
None
}
/// 获取 wujieq 工作目录(%LOCALAPPDATA%/NexBox/EQEngine
fn get_eq_work_dir() -> PathBuf {
let mut path = dirs::data_local_dir().unwrap_or_else(|| PathBuf::from("."));
path.push("NexBox");
path.push("EQEngine");
if !path.exists() {
let _ = fs::create_dir_all(&path);
}
path
}
/// 解析 .fac 预设文件
fn parse_fac_file(content: &str, file_id: &str) -> Option<EqPreset> {
let mut name = String::new();
let mut bands: Vec<EqBand> = Vec::new();
let mut enabled = true;
let mut in_bands = false;
let mut current_freq: Option<f64> = None;
for (line_idx, line) in content.lines().enumerate() {
let trimmed = line.trim();
// 第3行是预设名称(索引2)
if line_idx == 2 {
name = trimmed.to_string();
continue;
}
// 检测 On/Off Flag
if trimmed.contains("On/Off Flag") {
if let Some(val_str) = trimmed.split(':').next() {
if let Ok(val) = val_str.trim().parse::<i32>() {
enabled = val != 0;
}
}
in_bands = true;
continue;
}
if in_bands {
// 匹配 " 62.5: CF" 格式
if trimmed.ends_with(": CF") || trimmed.ends_with(":CF") {
let freq_str = trimmed.split(':').next().unwrap_or("").trim();
if let Ok(freq) = freq_str.parse::<f64>() {
current_freq = Some(freq);
}
}
// 匹配 " 0: Boost/Cut" 格式
else if trimmed.contains("Boost/Cut") {
if let Some(freq) = current_freq {
let gain_str = trimmed.split(':').next().unwrap_or("").trim();
if let Ok(gain) = gain_str.parse::<f64>() {
bands.push(EqBand { freq, gain });
}
current_freq = None;
}
}
}
}
if name.is_empty() {
name = format!("Preset {}", file_id);
}
Some(EqPreset {
id: file_id.to_string(),
name,
bands,
enabled,
})
}
#[tauri::command]
pub fn check_virtual_audio_driver() -> Result<DriverStatus, String> {
let mut service_exists = false;
let mut service_running = false;
// 使用 sc query 检查服务
if let Ok(output) = Command::new("sc")
.args(["query", FXVAD_SERVICE])
.creation_flags(CREATE_NO_WINDOW)
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{}\n{}", stdout, stderr);
// sc query 成功 (exit code 0) 才说明服务存在
// 服务不存在时 exit code 非零,输出包含错误码 1060
let cmd_success = output.status.success();
let is_error_1060 = combined.contains("1060")
&& (combined.contains("does not exist")
|| combined.contains("不存在")
|| combined.contains("未安装")
|| combined.contains("FAILED")
|| combined.contains("失败"));
if cmd_success || (combined.to_lowercase().contains("fxvad") && !is_error_1060) {
service_exists = true;
if combined.contains("RUNNING") {
service_running = true;
}
}
}
let mut needs_reboot = false;
if service_exists {
if let Ok(output) = Command::new("sc")
.args(["qc", FXVAD_SERVICE])
.creation_flags(CREATE_NO_WINDOW)
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("DISABLED") || stdout.contains("禁用") {
needs_reboot = true;
}
}
}
// 使用 pnputil 检查驱动是否已安装
let mut driver_installed = service_exists;
if !driver_installed {
if let Ok(output) = Command::new("pnputil")
.args(["/enum-drivers"])
.creation_flags(CREATE_NO_WINDOW)
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.to_lowercase().contains("fxsound") || stdout.to_lowercase().contains("fxvad") {
driver_installed = true;
}
}
}
Ok(DriverStatus {
installed: driver_installed,
service_exists,
service_running,
device_name: "FxSound Audio Enhancer".to_string(),
needs_reboot,
})
}
#[tauri::command]
pub async fn install_virtual_audio_driver(app: AppHandle) -> Result<String, String> {
let fxvad_dir = get_fxvad_resource_dir(&app)
.ok_or_else(|| "无法找到驱动资源目录".to_string())?;
let inf_path = fxvad_dir.join("fxvad.inf");
let devcon_path = fxvad_dir.join("fxdevcon64.exe");
let sys_path = fxvad_dir.join("fxvad.sys");
if !inf_path.exists() {
return Err("驱动 INF 文件不存在".to_string());
}
if !devcon_path.exists() {
return Err("devcon 工具不存在".to_string());
}
let inf_str = inf_path.to_string_lossy().replace('\'', "''");
let devcon_str = devcon_path.to_string_lossy().replace('\'', "''");
let sys_str = sys_path.to_string_lossy().replace('\'', "''");
// 构建 PowerShell 安装脚本(以管理员权限运行)
let ps_script = format!(
r#"
$ErrorActionPreference = 'SilentlyContinue'
$logFile = Join-Path $env:TEMP 'nexbox_eq_install.log'
"=== Install Start ===" | Out-File $logFile -Encoding UTF8
# Step 1: 复制驱动文件到 system32\drivers
"Step 1: Copying fxvad.sys" | Out-File $logFile -Append -Encoding UTF8
$srcSys = '{sys_str}'
$dstSys = 'C:\Windows\System32\drivers\fxvad.sys'
if (-not (Test-Path $dstSys)) {{
Copy-Item $srcSys $dstSys -Force 2>&1 | Out-File $logFile -Append -Encoding UTF8
"Copied fxvad.sys" | Out-File $logFile -Append -Encoding UTF8
}} else {{
"fxvad.sys already exists" | Out-File $logFile -Append -Encoding UTF8
}}
# Step 2: 使用 pnputil 安装驱动包
"Step 2: pnputil /add-driver" | Out-File $logFile -Append -Encoding UTF8
$infPath = '{inf_str}'
$pnputilResult = pnputil /add-driver $infPath /install 2>&1
"pnputil output: $pnputilResult" | Out-File $logFile -Append -Encoding UTF8
# Step 3: 使用 devcon 创建设备实例
"Step 3: devcon install" | Out-File $logFile -Append -Encoding UTF8
$devconExe = '{devcon_str}'
$devconResult = & $devconExe install $infPath 'Root\FXVAD' 2>&1
"devcon output: $devconResult" | Out-File $logFile -Append -Encoding UTF8
Start-Sleep -Seconds 3
# 验证
"Step 4: Verification" | Out-File $logFile -Append -Encoding UTF8
$scQuery = sc.exe query FXVAD 2>&1
"sc query: $scQuery" | Out-File $logFile -Append -Encoding UTF8
if ($scQuery -match 'FXVAD|FxSound') {{
"RESULT:SUCCESS" | Out-File $logFile -Append -Encoding UTF8
Write-Output 'SUCCESS'
}} else {{
# 检查 pnputil 中是否有驱动
$enumCheck = pnputil /enum-drivers 2>&1
$found = $enumCheck | Where-Object {{ $_ -match 'fxsound|fxvad' }}
if ($found) {{
"RESULT:SUCCESS" | Out-File $logFile -Append -Encoding UTF8
Write-Output 'SUCCESS'
}} else {{
"RESULT:FAILED" | Out-File $logFile -Append -Encoding UTF8
Write-Output 'FAILED'
}}
}}
"#,
sys_str = sys_str,
inf_str = inf_str,
devcon_str = devcon_str
);
let temp_dir = std::env::temp_dir();
let script_path = temp_dir.join("nexbox_eq_install.ps1");
fs::write(&script_path, &ps_script)
.map_err(|e| format!("写入安装脚本失败: {}", e))?;
let script_str = script_path.to_string_lossy().replace('\'', "''");
log::info!("[install] Script written to {:?}", script_path);
let ps_command = format!(
"Start-Process -FilePath 'powershell' -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','\"{}\"' -Verb RunAs -Wait -WindowStyle Hidden",
script_str
);
let output = Command::new("powershell")
.args(["-WindowStyle", "Hidden", "-NoProfile", "-Command", &ps_command])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| format!("执行安装脚本失败: {}", e))?;
// 读取日志
let log_path = temp_dir.join("nexbox_eq_install.log");
if let Ok(log_content) = fs::read_to_string(&log_path) {
log::info!("[install] Log:\n{}", log_content);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
log::info!("[install] PowerShell stdout: {}", stdout);
if !stderr.is_empty() {
log::warn!("[install] PowerShell stderr: {}", stderr);
}
// 清理临时脚本
let _ = fs::remove_file(&script_path);
// 检查用户是否取消了 UAC
if !output.status.success() {
let stderr_lower = stderr.to_lowercase();
if stderr_lower.contains("cancel") || stderr_lower.contains("denied") || stderr_lower.contains("取消") {
return Err("管理员授权被取消,安装未执行".to_string());
}
}
// 验证安装结果
std::thread::sleep(std::time::Duration::from_secs(1));
let status = check_virtual_audio_driver()?;
if status.installed {
let _ = fs::remove_file(&log_path);
Ok("虚拟声卡驱动安装成功".to_string())
} else {
// 读取日志中的详细信息
let log_detail = fs::read_to_string(&log_path).unwrap_or_default();
let log_summary = log_detail
.lines()
.filter(|l| l.contains("output:") || l.contains("RESULT:") || l.contains("Failed"))
.collect::<Vec<_>>()
.join("\n");
Err(format!(
log_summary
))
}
}
#[tauri::command]
pub async fn uninstall_virtual_audio_driver(app: AppHandle) -> Result<String, String> {
// 先停止 EQ 引擎
let _ = stop_eq_engine();
let fxvad_dir = get_fxvad_resource_dir(&app)
.ok_or_else(|| "无法找到驱动资源目录".to_string())?;
let devcon_path = fxvad_dir.join("fxdevcon64.exe");
let devcon_str = devcon_path.to_string_lossy().replace('\'', "''");
// 构建精简 PowerShell 卸载脚本(极简:核心就是一条正则找 oem.inf → pnputil 删除)
let ps_script = format!(
r#"
$ErrorActionPreference = 'Continue'
$log = "$env:TEMP\nexbox_eq_uninstall.log"
'START' | Out-File $log
# 1. 禁用服务
sc.exe config FXVAD start= disabled *>> $log
# 2. 尝试停止(NOT_STOPPABLE 会失败,忽略)
sc.exe stop FXVAD *>> $log
# 3. devcon 移除设备
$d = '{devcon_str}'
if (Test-Path $d) {{
& $d remove '@ROOT\FXVAD\*' *>> $log
& $d remove '=MEDIA' 'ROOT\FXVAD*' *>> $log
}}
$delOut = sc.exe delete FXVAD 2>&1
"SC_DELETE: $delOut" | Out-File $log -Append
$isMarked = $delOut -match '1072|marked|标记'
"MARKED: $isMarked" | Out-File $log -Append
# 5. 删除驱动包 —— 核心:一条正则找 oemXX.inf 的 Published Name
$raw = pnputil /enum-drivers 2>&1 | Out-String
# 匹配 "发布名称: oem6.inf ... 原始名称: fxvad.inf"(中英文均兼容)
$re = '(?:Published Name|发布名称):\s+(oem\d+\.inf).*?\n.*?(?:Original Name|原始名称):\s+fxvad\.inf'
if ($raw -match $re) {{
$oem = $Matches[1]
"DELETE_DRIVER: $oem" | Out-File $log -Append
pnputil /delete-driver $oem /uninstall /force *>> $log
}} else {{
"NO_FXVAD_OEM" | Out-File $log -Append
}}
# 6. 安排重启后删除 fxvad.sys
$sys = 'C:\Windows\System32\drivers\fxvad.sys'
if (Test-Path $sys) {{
try {{ Remove-Item $sys -Force -ErrorAction Stop; "FILE_DELETED" | Out-File $log -Append }}
catch {{
"FILE_REBOOT" | Out-File $log -Append
Add-Type -Name MF -Namespace W -EA SilentlyContinue -MemberDef '[DllImport("kernel32.dll")]public static extern bool MoveFileEx(string a,string b,int f);'
[W.MF]::MoveFileEx($sys, $null, 4) | Out-Null
}}
}}
# 7. 验证 + 报告
$ck = sc.exe query FXVAD 2>&1
"CHECK: $ck" | Out-File $log -Append
$gone = $ck -match '1060|不存在|未安装'
# 再次检查 pnputil 是否还有残留
$check2 = pnputil /enum-drivers 2>&1 | Out-String
$pnpLeft = $check2 -match 'fxvad|fxsound'
"PNP_LEFT: $pnpLeft" | Out-File $log -Append
if ($gone -and -not $pnpLeft) {{ Write-Output 'SUCCESS' }}
elseif ($pnpLeft -and ($gone -or $isMarked)) {{
if ($check2 -match '(?:Published Name|发布名称):\s+(oem\d+\.inf)[\s\S]*?fxvad') {{
$oem2 = $Matches[1]
pnputil /delete-driver $oem2 /uninstall /force *>> $log
$finalCheck = pnputil /enum-drivers 2>&1 | Out-String
if ($finalCheck -match 'fxvad|fxsound') {{
Write-Output 'REBOOT_REQUIRED'
}} else {{
Write-Output 'SUCCESS'
}}
}} else {{
Write-Output 'REBOOT_REQUIRED'
}}
}}
elseif ($isMarked -and ($ck -match 'FXVAD')) {{ Write-Output 'REBOOT_REQUIRED' }}
else {{ Write-Output 'PARTIAL' }}
"#,
devcon_str = devcon_str
);
let temp_dir = std::env::temp_dir();
let script_path = temp_dir.join("nexbox_eq_uninstall.ps1");
fs::write(&script_path, &ps_script)
.map_err(|e| format!("写入卸载脚本失败: {}", e))?;
let script_str = script_path.to_string_lossy().replace('\'', "''");
log::info!("[uninstall] Script written to {:?}", script_path);
let ps_command = format!(
"Start-Process -FilePath 'powershell' -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','\"{}\"' -Verb RunAs -Wait -WindowStyle Hidden",
script_str
);
let output = Command::new("powershell")
.args(["-WindowStyle", "Hidden", "-NoProfile", "-Command", &ps_command])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| format!("执行卸载脚本失败: {}", e))?;
let log_path = temp_dir.join("nexbox_eq_uninstall.log");
if let Ok(log_content) = fs::read_to_string(&log_path) {
log::info!("[uninstall] Log:\n{}", log_content);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
log::info!("[uninstall] stdout: {}", stdout);
if !stderr.is_empty() {
log::warn!("[uninstall] stderr: {}", stderr);
}
let _ = fs::remove_file(&script_path);
// UAC 取消检测
if !output.status.success() {
let se = stderr.to_lowercase();
if se.contains("cancel") || se.contains("denied") || se.contains("取消") {
return Err("管理员授权被取消,卸载未执行".to_string());
}
}
// 解析结果
let out = stdout.trim();
std::thread::sleep(std::time::Duration::from_secs(1));
if out.contains("SUCCESS") {
log::info!("[uninstall] Fully removed");
let _ = fs::remove_file(&log_path);
Ok("虚拟声卡驱动已卸载".to_string())
} else if out.contains("REBOOT_REQUIRED") {
log::info!("[uninstall] Pending reboot");
let _ = fs::remove_file(&log_path);
Ok("驱动已标记卸载,需要重启电脑完成卸载".to_string())
} else {
let detail = fs::read_to_string(&log_path).unwrap_or_default();
let summary: Vec<&str> = detail
.lines()
.filter(|l| l.contains("DELETE_DRIVER:") || l.contains("NO_FXVAD_OEM") || l.contains("SC_DELETE:") || l.contains("MARKED:") || l.contains("FILE_") || l.contains("CHECK:"))
.collect();
log::error!("[uninstall] Incomplete.\n{}", summary.join("\n"));
Err(format!(
summary.join("\n")
))
}
}
/// 切换 Windows 默认音频播放设备到 FxSound Audio Enhancer
fn switch_default_audio_to_fxsound() -> Result<String, String> {
audio_device::switch_to_fxsound()
}
// switch function ends above, old body removed
fn restore_default_audio_device(device_name: &str) {
audio_device::restore_device(device_name);
}
#[tauri::command]
pub fn get_default_audio_device() -> Result<String, String> {
audio_device::get_current_device()
}
/// 启动 EQ 引擎(wujieq.exe),并自动切换默认音频设备到 FxSound
#[tauri::command]
pub async fn start_eq_engine(app: AppHandle) -> Result<String, String> {
var imm = (IMMDeviceEnumerator)e;
imm.EnumAudioEndpoints(0,1,out IntPtr pC);
var col = (IMMDeviceCollection)Marshal.GetObjectForIUnknown(pC);
col.GetCount(out uint n);
for (uint i=0;i<n;i++) { col.Item(i,out IntPtr pD);
var dev = (IMMDevice)Marshal.GetObjectForIUnknown(pD);
dev.OpenPropertyStore(0,out IntPtr pS);
var s = (IPropertyStore)Marshal.GetObjectForIUnknown(pS);
var k = new PROPERTYKEY { fmtid=Guid.Parse("{A45C254E-DF1C-4EFD-8020-67D146A850E0}"), pid=14 };
s.GetValue(ref k, out PROPVARIANT v);
string nm = Marshal.PtrToStringUni(v.ptrVal) ?? "";
if (nm.IndexOf(name,StringComparison.OrdinalIgnoreCase)>=0) {
dev.GetId(out string did);
var cfg = Activator.CreateInstance(Type.GetTypeFromCLSID(Guid.Parse("{870AF99C-171D-4F9E-AF0D-E63DF40C2BC9}")));
((IPolicyConfig)cfg).SetDefaultEndpoint(did,0);
return nm;
}
}
return null;
}
"#;
// COM 接口定义
let com_defs = r#"
[ComImport,Guid("870AF99C-171D-4F9E-AF0D-E63DF40C2BC9")] public class _PC {}
[ComImport,Guid("F8679F50-850A-41CF-9C72-430F290290C8"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IPolicyConfig {
[PreserveSig] int a();[PreserveSig] int b();[PreserveSig] int c();[PreserveSig] int d();[PreserveSig] int e();[PreserveSig] int f();
[PreserveSig] int g();[PreserveSig] int h();[PreserveSig] int i();[PreserveSig] int j();
[PreserveSig] int SetDefaultEndpoint([MarshalAs(UnmanagedType.LPWStr)] string id, int role);
[PreserveSig] int k();
}
[ComImport,Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] public class _MMDE {}
[ComImport,Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IMMDeviceEnumerator {
int a();int b();int c();int d();
int EnumAudioEndpoints(int df, int sm, out IntPtr p);
int GetDefaultAudioEndpoint(int df, int r, out IntPtr p);
}
[ComImport,Guid("D666063F-1587-4E43-81F1-B948E807363F"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IMMDevice {
int a(); int OpenPropertyStore(int s, out IntPtr p);
int GetId([MarshalAs(UnmanagedType.LPWStr)] out string id);
}
[ComImport,Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IPropertyStore {
int a();int b();int c();int d(); int GetValue(ref PROPERTYKEY k, out PROPVARIANT v);
}
[ComImport,Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IMMDeviceCollection { int GetCount(out uint c); int Item(uint i, out IntPtr p); }
[StructLayout(LayoutKind.Sequential)] public struct PROPERTYKEY { public Guid fmtid; public uint pid; }
[StructLayout(LayoutKind.Explicit)] public struct PROPVARIANT { [FieldOffset(0)] public ushort vt; [FieldOffset(8)] public IntPtr ptrVal; }
"#;
// 构建完整 PowerShell 脚本
let ps_script = format!(
"$ErrorActionPreference = 'Stop'\r\n\
$src = @'\r\n\
using System;\r\n\
using System.Runtime.InteropServices;\r\n\
{com_defs}\r\n\
public static class AudioSwitch {{\r\n\
{cs_code}\r\n\
}}\r\n\
'@\r\n\
\r\n\
$type = Add-Type -TypeDefinition $src -PassThru -ErrorAction Stop\r\n\
$prev = [AudioSwitch]::GetCur()\r\n\
$ok = [AudioSwitch]::SetDef('FxSound')\r\n\
if ($ok) {{ Write-Output \"PREV:$prev\" }} else {{ Write-Output \"NOT_FOUND\" }}\r\n",
com_defs = com_defs,
cs_code = cs_code,
);
let temp_dir = std::env::temp_dir();
let script_path = temp_dir.join("nexbox_audio_switch.ps1");
// 写入 UTF-8 with BOM (PowerShell 需要 BOM 才能正确识别 UTF-8)
let mut bom = vec![0xEF, 0xBB, 0xBF];
bom.extend_from_slice(ps_script.as_bytes());
fs::write(&script_path, &bom).map_err(|e| format!("写入脚本失败: {}", e))?;
let output = Command::new("powershell")
.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File"])
.arg(&script_path)
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| format!("执行失败: {}", e))?;
let _ = fs::remove_file(&script_path);
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
log::info!("[audio_switch] stdout: {}", stdout);
if !stderr.is_empty() {
log::warn!("[audio_switch] stderr: {}", stderr);
}
if stdout.starts_with("PREV:") {
Ok(stdout)
} else {
Err(format!("找不到 FxSound Audio Enhancer 设备,请确认驱动已安装并在声音设置中可见。\nstderr: {}", stderr))
}
}
/// 恢复 Windows 默认音频设备
fn restore_default_audio_device(device_name: &str) {
let escaped = device_name.replace('\'', "''");
let com_defs = r#"[ComImport,Guid("870AF99C-171D-4F9E-AF0D-E63DF40C2BC9")] public class _PC {}
[ComImport,Guid("F8679F50-850A-41CF-9C72-430F290290C8"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IPolicyConfig { [PreserveSig] int a();[PreserveSig] int b();[PreserveSig] int c();[PreserveSig] int d();[PreserveSig] int e();[PreserveSig] int f();[PreserveSig] int g();[PreserveSig] int h();[PreserveSig] int i();[PreserveSig] int j();[PreserveSig] int SetDefaultEndpoint([MarshalAs(UnmanagedType.LPWStr)] string id, int role);[PreserveSig] int k(); }
[ComImport,Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] public class _MMDE {}
[ComImport,Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IMMDeviceEnumerator { int a();int b();int c();int d();int EnumAudioEndpoints(int df, int sm, out IntPtr p);int GetDefaultAudioEndpoint(int df, int r, out IntPtr p); }
[ComImport,Guid("D666063F-1587-4E43-81F1-B948E807363F"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IMMDevice { int a();int OpenPropertyStore(int s, out IntPtr p);int GetId([MarshalAs(UnmanagedType.LPWStr)] out string id); }
[ComImport,Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IPropertyStore { int a();int b();int c();int d();int GetValue(ref PROPERTYKEY k, out PROPVARIANT v); }
[ComImport,Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IMMDeviceCollection { int GetCount(out uint c); int Item(uint i, out IntPtr p); }
[StructLayout(LayoutKind.Sequential)] public struct PROPERTYKEY { public Guid fmtid; public uint pid; }
[StructLayout(LayoutKind.Explicit)] public struct PROPVARIANT { [FieldOffset(0)] public ushort vt;[FieldOffset(8)] public IntPtr ptrVal; }
"#;
let cs_code = format!(r#"
public static string Restore(string name) {{
var e = Activator.CreateInstance(Type.GetTypeFromProgID("MMDeviceEnumerator"));
var imm = (IMMDeviceEnumerator)e;
imm.EnumAudioEndpoints(0,1,out IntPtr pC);
var col = (IMMDeviceCollection)Marshal.GetObjectForIUnknown(pC);
col.GetCount(out uint n);
for (uint i=0;i<n;i++) {{ col.Item(i,out IntPtr pD);
var dev = (IMMDevice)Marshal.GetObjectForIUnknown(pD);
dev.OpenPropertyStore(0,out IntPtr pS);
var s = (IPropertyStore)Marshal.GetObjectForIUnknown(pS);
var k = new PROPERTYKEY {{ fmtid=Guid.Parse("{{A45C254E-DF1C-4EFD-8020-67D146A850E0}}"), pid=14 }};
s.GetValue(ref k, out PROPVARIANT v);
string nm = Marshal.PtrToStringUni(v.ptrVal) ?? "";
if (nm.IndexOf(name,StringComparison.OrdinalIgnoreCase)>=0) {{
dev.GetId(out string did);
var cfg = Activator.CreateInstance(Type.GetTypeFromCLSID(Guid.Parse("{{870AF99C-171D-4F9E-AF0D-E63DF40C2BC9}}")));
((IPolicyConfig)cfg).SetDefaultEndpoint(did,0);
return "OK";
}}
}}
return null;
}}
"#);
let ps_script = format!(
"$ErrorActionPreference = 'Stop'\r\n\
$src = @'\r\n\
using System;\r\n\
using System.Runtime.InteropServices;\r\n\
{com_defs}\r\n\
public static class AudioRestore {{\r\n\
{cs_code}\r\n\
}}\r\n\
'@\r\n\
Add-Type -TypeDefinition $src -ErrorAction SilentlyContinue | Out-Null\r\n\
if ([AudioRestore]::Restore('{escaped}')) {{ }} else {{ }}\r\n",
com_defs = com_defs,
cs_code = cs_code,
escaped = escaped,
);
let temp_dir = std::env::temp_dir();
let script_path = temp_dir.join("nexbox_audio_restore.ps1");
let mut bom = vec![0xEF, 0xBB, 0xBF];
bom.extend_from_slice(ps_script.as_bytes());
let _ = fs::write(&script_path, &bom);
let output = Command::new("powershell")
.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File"])
.arg(&script_path)
.creation_flags(CREATE_NO_WINDOW)
.output();
let _ = fs::remove_file(&script_path);
match output {
Ok(o) => {
let s = String::from_utf8_lossy(&o.stdout);
if !s.trim().is_empty() {
log::info!("[audio_restore] stdout: {}", s.trim());
}
}
Err(e) => log::warn!("[audio_restore] failed: {}", e),
}
}
#[tauri::command]
pub fn get_default_audio_device() -> Result<String, String> {
let com_defs = r#"[ComImport,Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] public class _MMDE {}
[ComImport,Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IMMDeviceEnumerator { int a();int b();int c();int d();int e();int GetDefaultAudioEndpoint(int df, int r, out IntPtr p); }
[ComImport,Guid("D666063F-1587-4E43-81F1-B948E807363F"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IMMDevice { int a();int OpenPropertyStore(int s, out IntPtr p);int GetId(out string id); }
[ComImport,Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IPropertyStore { int a();int b();int c();int d();int GetValue(ref PROPERTYKEY k, out PROPVARIANT v); }
[StructLayout(LayoutKind.Sequential)] public struct PROPERTYKEY { public Guid fmtid; public uint pid; }
[StructLayout(LayoutKind.Explicit)] public struct PROPVARIANT { [FieldOffset(0)] public ushort vt;[FieldOffset(8)] public IntPtr ptrVal; }
"#;
let ps_script = format!(
"$ErrorActionPreference = 'Stop'\r\n\
$src = @'\r\n\
using System;\r\n\
using System.Runtime.InteropServices;\r\n\
{com_defs}\r\n\
public static class AudioQ {{\r\n\
public static string Get() {{\r\n\
var e = Activator.CreateInstance(Type.GetTypeFromProgID(\"MMDeviceEnumerator\"));\r\n\
var imm = (IMMDeviceEnumerator)e;\r\n\
imm.GetDefaultAudioEndpoint(0,0,out IntPtr pD);\r\n\
var dev = (IMMDevice)Marshal.GetObjectForIUnknown(pD);\r\n\
dev.OpenPropertyStore(0,out IntPtr pS);\r\n\
var s = (IPropertyStore)Marshal.GetObjectForIUnknown(pS);\r\n\
var k = new PROPERTYKEY {{ fmtid=Guid.Parse(\"{{A45C254E-DF1C-4EFD-8020-67D146A850E0}}\"), pid=14 }};\r\n\
s.GetValue(ref k, out PROPVARIANT v);\r\n\
return Marshal.PtrToStringUni(v.ptrVal) ?? \"\";\r\n\
}}}}\r\n\
'@\r\n\
Add-Type -TypeDefinition $src -ErrorAction Stop | Out-Null\r\n\
Write-Output ([AudioQ]::Get())\r\n",
com_defs = com_defs
);
let temp_dir = std::env::temp_dir();
let script_path = temp_dir.join("nexbox_audio_query.ps1");
let mut bom = vec![0xEF, 0xBB, 0xBF];
bom.extend_from_slice(ps_script.as_bytes());
fs::write(&script_path, &bom).map_err(|e| format!("{}", e))?;
let output = Command::new("powershell")
.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File"])
.arg(&script_path)
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| format!("{}", e))?;
let _ = fs::remove_file(&script_path);
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
/// 启动 EQ 引擎(wujieq.exe),并自动切换默认音频设备到 FxSound
#[tauri::command]
pub async fn start_eq_engine(app: AppHandle) -> Result<String, String> {
let status = get_eq_engine_status()?;
if status.running {
return Err("EQ 引擎已在运行".to_string());
}
let driver_status = check_virtual_audio_driver()?;
if !driver_status.installed {
return Err("请先安装虚拟声卡驱动".to_string());
}
let fxvad_dir = get_fxvad_resource_dir(&app)
.ok_or_else(|| "无法找到 EQ 引擎资源".to_string())?;
let wujieq_path = fxvad_dir.join(WUJIEQ_EXE);
if !wujieq_path.exists() {
return Err("EQ 引擎程序不存在".to_string());
}
// 将 wujieq.exe 复制到工作目录
let work_dir = get_eq_work_dir();
let work_exe = work_dir.join(WUJIEQ_EXE);
if !work_exe.exists() || fs::metadata(&wujieq_path).map(|m| m.len()).unwrap_or(0)
!= fs::metadata(&work_exe).map(|m| m.len()).unwrap_or(0)
{
fs::copy(&wujieq_path, &work_exe)
.map_err(|e| format!("复制 EQ 引擎失败: {}", e))?;
}
let presets_src = fxvad_dir.join("presets");
let presets_dst = work_dir.join("presets");
if presets_src.exists() {
let _ = fs::create_dir_all(&presets_dst);
if let Ok(entries) = fs::read_dir(&presets_src) {
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
let _ = fs::copy(entry.path(), presets_dst.join(name));
}
}
}
}
// 保存当前默认设备,切换到 FxSound
let prev_device = switch_default_audio_to_fxsound()
.unwrap_or_else(|e| { log::warn!("[eq] Audio switch warning: {}", e); String::new() });
// 保存前一个设备名,stop 时恢复
if prev_device.starts_with("PREV:") {
let prev_name = &prev_device[5..];
let _ = fs::write(std::env::temp_dir().join("nexbox_eq_prev_device.txt"), prev_name);
}
// 启动 wujieq.exe
Command::new(&work_exe)
.current_dir(&work_dir)
.creation_flags(CREATE_NO_WINDOW)
.spawn()
.map_err(|e| format!("启动 EQ 引擎失败: {}", e))?;
// 等待启动
std::thread::sleep(std::time::Duration::from_millis(500));
if prev_device.starts_with("PREV:") {
Ok(format!("PREV:{}", &prev_device[5..]))
} else {
Ok("SWITCHED".to_string())
}
}
/// 停止 EQ 引擎
#[tauri::command]
pub fn stop_eq_engine() -> Result<(), String> {
let mut sys = System::new();
sys.refresh_processes();
let mut killed = 0;
for (_pid, process) in sys.processes() {
let name = process.name().to_lowercase();
if name == "wujieq.exe" {
if process.kill() {
killed += 1;
}
}
}
if killed > 0 {
log::info!("EQ engine stopped, killed {} process(es)", killed);
}
let prev_file = std::env::temp_dir().join("nexbox_eq_prev_device.txt");
if let Ok(prev_name) = fs::read_to_string(&prev_file) {
let prev_name = prev_name.trim().to_string();
if !prev_name.is_empty() && prev_name.to_lowercase().contains("fxsound") == false {
log::info!("[eq] Restoring audio device to: {}", prev_name);
restore_default_audio_device(&prev_name);
}
let _ = fs::remove_file(&prev_file);
}
Ok(())
}
/// 获取 EQ 引擎运行状态
#[tauri::command]
pub fn get_eq_engine_status() -> Result<EngineStatus, String> {
let mut sys = System::new();
sys.refresh_processes();
for (pid, process) in sys.processes() {
let name = process.name().to_lowercase();
if name == "wujieq.exe" {
return Ok(EngineStatus {
running: true,
pid: Some(pid.as_u32()),
});
}
}
Ok(EngineStatus {
running: false,
pid: None,
})
}
/// 获取所有内置 EQ 预设
#[tauri::command]
pub fn get_eq_presets(app: AppHandle) -> Result<Vec<EqPreset>, String> {
let fxvad_dir = get_fxvad_resource_dir(&app)
.ok_or_else(|| "无法找到资源目录".to_string())?;
let presets_dir = fxvad_dir.join("presets");
if !presets_dir.exists() {
return Ok(Vec::new());
}
let mut presets: Vec<EqPreset> = Vec::new();
let mut entries: Vec<_> = fs::read_dir(&presets_dir)
.map_err(|e| format!("读取预设目录失败: {}", e))?
.filter_map(|e| e.ok())
.collect();
// 按文件名排序
entries.sort_by_key(|e| {
e.file_name()
.to_string_lossy()
.trim_end_matches(".fac")
.parse::<i32>()
.unwrap_or(999)
});
for entry in entries {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("fac") {
continue;
}
let file_id = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
if let Ok(content) = fs::read_to_string(&path) {
if let Some(preset) = parse_fac_file(&content, &file_id) {
presets.push(preset);
}
}
}
Ok(presets)
}
#[tauri::command]
pub fn apply_eq_preset(app: AppHandle, preset_id: String) -> Result<(), String> {
let fxvad_dir = get_fxvad_resource_dir(&app)
.ok_or_else(|| "无法找到资源目录".to_string())?;
let preset_file = fxvad_dir.join("presets").join(format!("{}.fac", preset_id));
if !preset_file.exists() {
return Err(format!("预设 {} 不存在", preset_id));
}
let work_dir = get_eq_work_dir();
let _ = fs::create_dir_all(&work_dir);
// 复制到工作目录
let target = work_dir.join(format!("{}.fac", preset_id));
fs::copy(&preset_file, &target)
.map_err(|e| format!("复制预设文件失败: {}", e))?;
Ok(())
}
pub fn cleanup() {
let _ = stop_eq_engine();
}