994 lines
37 KiB
Plaintext
994 lines
37 KiB
Plaintext
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();
|
||
}
|