T11: IPC param sync 2 chieu native<->JS (setParamNormalized/setParameter, vst_param_changed event, guard chong loop)
This commit is contained in:
@@ -75,8 +75,9 @@ pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.invoke_handler(tauri::generate_handler![vst_gui::open_vst_gui])
|
||||
.invoke_handler(tauri::generate_handler![vst_gui::open_vst_gui, vst_gui::set_vst_param, vst_gui::get_vst_params, vst_gui::close_vst_editor])
|
||||
.setup(|app| {
|
||||
vst_gui::init(app.handle());
|
||||
let res_dir = app
|
||||
.path()
|
||||
.resource_dir()
|
||||
|
||||
+340
-28
@@ -1,44 +1,356 @@
|
||||
// VST GUI windows (spec vsti_gui): floating child window per (track, plugin).
|
||||
//
|
||||
// T8: command `open_vst_gui(plugin_id, track_id)` tao WebviewWindow noi
|
||||
// label `vst_gui_{track}_{plugin}`, 800x600, always-on-top; mo lai thi focus.
|
||||
// raw_window_handle() lay HWND (Windows) — T9 se attach native VST3/VST2
|
||||
// editor vao HWND cua window nay. Chua load plugin GUI (T9), chi placeholder.
|
||||
// T8: command `open_vst_gui(plugin_id, track_id)` tao WebviewWindow noi label
|
||||
// `vst_gui_{track}_{plugin}`, 800x600, always-on-top; mo lai thi focus.
|
||||
// raw_window_handle() lay HWND (Windows).
|
||||
// T9/T10: native_host/*.dll attach VST3/VST2 editor vao HWND cua window.
|
||||
// T11: param sync 2 chieu — native param doi -> event `vst_param_changed` ->
|
||||
// JS; JS automation -> `set_vst_param` -> setParamNormalized/setParameter.
|
||||
// Guard chong loop nam o JS side (bo qua event echo cua chinh minh).
|
||||
use raw_window_handle::HasWindowHandle;
|
||||
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{c_char, c_void, CString};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
/// Mo (hoac focus neu da mo) cua so VST GUI cho 1 (track, plugin).
|
||||
/// Tra ve chuoi trang thai cho JS log — khong throw.
|
||||
#[cfg(target_os = "windows")]
|
||||
mod native {
|
||||
use super::*;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
// Callback signature trung voi SF_ParamChangedCallback trong C++ bridge.
|
||||
pub type ParamCb = unsafe extern "C" fn(i32, i32, f64, *mut c_void);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct Bridge {
|
||||
pub module: *mut c_void,
|
||||
pub attach: Option<unsafe extern "C" fn(*const c_char, *const c_char, *mut c_void, *mut i32, *mut i32, *mut c_char, i32) -> i32>,
|
||||
pub load: Option<unsafe extern "C" fn(*const c_char, *mut c_void, *mut i32, *mut i32, *mut c_char, i32) -> i32>,
|
||||
pub close: Option<unsafe extern "C" fn(i32, *mut c_char, i32) -> i32>,
|
||||
pub set_param_cb: Option<unsafe extern "C" fn(i32, ParamCb, *mut c_void) -> i32>,
|
||||
pub set_param: Option<unsafe extern "C" fn(i32, i32, f64) -> i32>,
|
||||
pub get_param_count: Option<unsafe extern "C" fn(i32) -> i32>,
|
||||
pub get_param: Option<unsafe extern "C" fn(i32, i32, *mut f64) -> i32>,
|
||||
pub get_param_info: Option<unsafe extern "C" fn(i32, i32, *mut i32, *mut c_char, i32, *mut f64) -> i32>,
|
||||
}
|
||||
// Raw pointer `module` khong Send; bridge chi dung tu main thread, an toan.
|
||||
unsafe impl Send for Bridge {}
|
||||
|
||||
static BRIDGES: LazyLock<Mutex<HashMap<String, Bridge>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
pub static APP: Mutex<Option<AppHandle>> = Mutex::new(None);
|
||||
// handle native -> (track_id, plugin_id, kind)
|
||||
pub static HANDLE_MAP: LazyLock<Mutex<HashMap<i32, (String, String, String)>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
// "{track}|{plugin}" -> handle native
|
||||
pub static EDITOR_MAP: LazyLock<Mutex<HashMap<String, i32>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
unsafe fn load_bridge(kind: &str, app: &AppHandle) -> Option<Bridge> {
|
||||
let name = if kind == "vst2" { "vst2_host_bridge.dll" } else { "vst3_host_bridge.dll" };
|
||||
let path = find_bridge_path(name, app)?;
|
||||
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||
let module = unsafe { LoadLibraryW(wide.as_ptr()) };
|
||||
if module.is_null() {
|
||||
return None;
|
||||
}
|
||||
let get = |sym: &str| -> Option<*mut c_void> {
|
||||
let mut buf: Vec<u8> = sym.bytes().collect();
|
||||
buf.push(0);
|
||||
let p = unsafe { GetProcAddress(module, buf.as_ptr()) };
|
||||
if p.is_null() { None } else { Some(p as *mut c_void) }
|
||||
};
|
||||
let mut bridge = Bridge {
|
||||
module,
|
||||
attach: None, load: None, close: None, set_param_cb: None,
|
||||
set_param: None, get_param_count: None, get_param: None, get_param_info: None,
|
||||
};
|
||||
if kind == "vst2" {
|
||||
bridge.load = get("SF_VST2_Load").map(|p| unsafe { std::mem::transmute(p) });
|
||||
bridge.close = get("SF_VST2_Close").map(|p| unsafe { std::mem::transmute(p) });
|
||||
bridge.set_param_cb = get("SF_VST2_SetParamCallback").map(|p| unsafe { std::mem::transmute(p) });
|
||||
bridge.set_param = get("SF_VST2_SetParam").map(|p| unsafe { std::mem::transmute(p) });
|
||||
bridge.get_param_count = get("SF_VST2_GetParamCount").map(|p| unsafe { std::mem::transmute(p) });
|
||||
bridge.get_param = get("SF_VST2_GetParam").map(|p| unsafe { std::mem::transmute(p) });
|
||||
} else {
|
||||
bridge.attach = get("SF_VST3_Attach").map(|p| unsafe { std::mem::transmute(p) });
|
||||
bridge.close = get("SF_VST3_Close").map(|p| unsafe { std::mem::transmute(p) });
|
||||
bridge.set_param_cb = get("SF_VST3_SetParamCallback").map(|p| unsafe { std::mem::transmute(p) });
|
||||
bridge.set_param = get("SF_VST3_SetParam").map(|p| unsafe { std::mem::transmute(p) });
|
||||
bridge.get_param_count = get("SF_VST3_GetParamCount").map(|p| unsafe { std::mem::transmute(p) });
|
||||
bridge.get_param_info = get("SF_VST3_GetParamInfo").map(|p| unsafe { std::mem::transmute(p) });
|
||||
}
|
||||
Some(bridge)
|
||||
}
|
||||
|
||||
fn find_bridge_path(name: &str, app: &AppHandle) -> Option<PathBuf> {
|
||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||
if let Ok(dir) = std::env::var("SF_NATIVE_HOST_DIR") {
|
||||
candidates.push(Path::new(&dir).join(name));
|
||||
}
|
||||
if let Ok(res) = app.path().resource_dir() {
|
||||
candidates.push(res.join(name));
|
||||
candidates.push(res.join("native").join(name));
|
||||
}
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
candidates.push(dir.join(name));
|
||||
}
|
||||
}
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
candidates.push(cwd.join(name));
|
||||
candidates.push(cwd.join("native_host").join("build").join("Release").join(name));
|
||||
}
|
||||
candidates.into_iter().find(|p| p.exists())
|
||||
}
|
||||
|
||||
// Callback native -> Rust: plugin doi param trong editor.
|
||||
pub unsafe extern "C" fn on_param_changed(handle: i32, param_id: i32, value: f64, _userdata: *mut c_void) {
|
||||
let ids = HANDLE_MAP.lock().unwrap().get(&handle).cloned();
|
||||
let app = APP.lock().unwrap().clone();
|
||||
if let (Some((track, plugin, _kind)), Some(app)) = (ids, app) {
|
||||
let _ = app.emit_to(
|
||||
&format!("vst_gui_{}_{}", track, plugin),
|
||||
"vst_param_changed",
|
||||
serde_json::json!({
|
||||
"track_id": track,
|
||||
"plugin_id": plugin,
|
||||
"param_id": param_id,
|
||||
"value": value,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bridge(kind: &str, app: &AppHandle) -> Option<Bridge> {
|
||||
let mut bridges = BRIDGES.lock().unwrap();
|
||||
if let Some(b) = bridges.get(kind) {
|
||||
// Clone fn pointers (module kept alive for app lifetime)
|
||||
return Some(Bridge {
|
||||
module: b.module,
|
||||
attach: b.attach, load: b.load, close: b.close, set_param_cb: b.set_param_cb,
|
||||
set_param: b.set_param, get_param_count: b.get_param_count,
|
||||
get_param: b.get_param, get_param_info: b.get_param_info,
|
||||
});
|
||||
}
|
||||
let b = unsafe { load_bridge(kind, app) }?;
|
||||
bridges.insert(kind.to_string(), b);
|
||||
Some(b)
|
||||
}
|
||||
|
||||
#[link(name = "kernel32")]
|
||||
extern "system" {
|
||||
fn LoadLibraryW(name: *const u16) -> *mut c_void;
|
||||
fn GetProcAddress(module: *mut c_void, name: *const u8) -> *mut c_void;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ParamInfo {
|
||||
pub param_id: i32,
|
||||
pub title: String,
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
fn urlencode(s: &str) -> String {
|
||||
let mut out = String::new();
|
||||
for b in s.bytes() {
|
||||
match b {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(b as char),
|
||||
_ => out.push_str(&format!("%{:02X}", b)),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn init(app: &AppHandle) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
*native::APP.lock().unwrap() = Some(app.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Mo (hoac focus neu da mo) cua so VST GUI cho 1 (track, plugin) va attach
|
||||
/// native editor (VST3 .vst3 hoac VST2 .dll) vao HWND cua window.
|
||||
#[tauri::command]
|
||||
pub fn open_vst_gui(
|
||||
app: AppHandle,
|
||||
plugin_id: String,
|
||||
track_id: String,
|
||||
plugin_path: String,
|
||||
plugin_kind: String,
|
||||
) -> Result<String, String> {
|
||||
let label = format!("vst_gui_{}_{}", track_id, plugin_id);
|
||||
if let Some(win) = app.get_webview_window(&label) {
|
||||
let win = if let Some(win) = app.get_webview_window(&label) {
|
||||
win.set_focus().map_err(|e| e.to_string())?;
|
||||
return Ok(format!("focused: {}", label));
|
||||
}
|
||||
let win = WebviewWindowBuilder::new(&app, &label, WebviewUrl::App("vst_gui.html".into()))
|
||||
.title(format!("VST GUI - {}", plugin_id))
|
||||
.inner_size(800.0, 600.0)
|
||||
.always_on_top(true)
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let _ = win.set_focus();
|
||||
// HWND cho native editor attach (T9). Log de verify — chua dung.
|
||||
match win.window_handle() {
|
||||
Ok(raw) => {
|
||||
let raw = raw.as_raw();
|
||||
#[cfg(target_os = "windows")]
|
||||
if let raw_window_handle::RawWindowHandle::Win32(h) = raw {
|
||||
println!("[vst_gui] {} hwnd={:?}", label, h.hwnd);
|
||||
win
|
||||
} else {
|
||||
let url = WebviewUrl::App(format!("vst_gui.html?track={}&plugin={}", urlencode(&track_id), urlencode(&plugin_id)).into());
|
||||
let win = WebviewWindowBuilder::new(&app, &label, url)
|
||||
.title(format!("VST GUI - {}", plugin_id))
|
||||
.inner_size(800.0, 600.0)
|
||||
.always_on_top(true)
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let _ = win.set_focus();
|
||||
win
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let hwnd = match win.window_handle() {
|
||||
Ok(raw) => match raw.as_raw() {
|
||||
raw_window_handle::RawWindowHandle::Win32(h) => h.hwnd.get() as *mut c_void,
|
||||
_ => {
|
||||
return Err("not a Win32 window".into());
|
||||
}
|
||||
},
|
||||
Err(e) => return Err(format!("raw_window_handle error: {}", e)),
|
||||
};
|
||||
|
||||
let kind = if plugin_kind.eq_ignore_ascii_case("vst2") { "vst2" } else { "vst3" };
|
||||
let b = native::bridge(kind, &app).ok_or("bridge DLL not found (set SF_NATIVE_HOST_DIR or bundle it)")?;
|
||||
let mut err = [0i8; 512];
|
||||
let mut w: i32 = 0;
|
||||
let mut h: i32 = 0;
|
||||
let c_path = CString::new(plugin_path.as_str()).map_err(|e| e.to_string())?;
|
||||
let c_plugin = CString::new(plugin_id.as_str()).map_err(|e| e.to_string())?;
|
||||
|
||||
let handle = unsafe {
|
||||
if kind == "vst2" {
|
||||
let f = b.load.ok_or("vst2 bridge missing SF_VST2_Load")?;
|
||||
f(c_path.as_ptr(), hwnd, &mut w, &mut h, err.as_mut_ptr(), err.len() as i32)
|
||||
} else {
|
||||
let f = b.attach.ok_or("vst3 bridge missing SF_VST3_Attach")?;
|
||||
f(c_path.as_ptr(), c_plugin.as_ptr(), hwnd, &mut w, &mut h, err.as_mut_ptr(), err.len() as i32)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let _ = raw;
|
||||
};
|
||||
if handle <= 0 {
|
||||
let msg = unsafe { std::ffi::CStr::from_ptr(err.as_ptr()) }.to_string_lossy().into_owned();
|
||||
return Err(format!("attach failed ({}): {}", kind, msg));
|
||||
}
|
||||
Err(e) => println!("[vst_gui] {} raw_window_handle error: {}", label, e),
|
||||
|
||||
let cb = b.set_param_cb.ok_or("bridge missing SetParamCallback")?;
|
||||
let rc = unsafe { cb(handle, native::on_param_changed, std::ptr::null_mut()) };
|
||||
if rc != 0 {
|
||||
return Err(format!("set param callback failed: {}", rc));
|
||||
}
|
||||
|
||||
native::HANDLE_MAP.lock().unwrap().insert(handle, (track_id.clone(), plugin_id.clone(), kind.to_string()));
|
||||
native::EDITOR_MAP.lock().unwrap().insert(format!("{}|{}", track_id, plugin_id), handle);
|
||||
Ok(format!("opened: {} (handle={}, {}x{})", label, handle, w, h))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Ok(format!("opened (no native attach on this OS): {}", label))
|
||||
}
|
||||
}
|
||||
|
||||
/// JS automation -> setParamNormalized/setParameter.
|
||||
#[tauri::command]
|
||||
pub fn set_vst_param(
|
||||
app: AppHandle,
|
||||
track_id: String,
|
||||
plugin_id: String,
|
||||
param_id: i32,
|
||||
value: f64,
|
||||
) -> Result<i32, String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let key = format!("{}|{}", track_id, plugin_id);
|
||||
let handle = *native::EDITOR_MAP.lock().unwrap().get(&key).ok_or("editor not open")?;
|
||||
let kind = {
|
||||
let m = native::HANDLE_MAP.lock().unwrap();
|
||||
m.get(&handle).map(|t| t.2.clone()).unwrap_or_else(|| "vst2".to_string())
|
||||
};
|
||||
let b = native::bridge(&kind, &app).ok_or("bridge DLL not found")?;
|
||||
let f = b.set_param.ok_or("bridge missing SetParam")?;
|
||||
let rc = unsafe { f(handle, param_id, value) };
|
||||
if rc != 0 {
|
||||
return Err(format!("set param failed: {}", rc));
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = (app, track_id, plugin_id, param_id, value);
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Lay danh sach param (id, title, value) de JS dung UI.
|
||||
#[tauri::command]
|
||||
pub fn get_vst_params(
|
||||
app: AppHandle,
|
||||
track_id: String,
|
||||
plugin_id: String,
|
||||
) -> Result<Vec<ParamInfo>, String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let key = format!("{}|{}", track_id, plugin_id);
|
||||
let handle = *native::EDITOR_MAP.lock().unwrap().get(&key).ok_or("editor not open")?;
|
||||
let kind = {
|
||||
let m = native::HANDLE_MAP.lock().unwrap();
|
||||
m.get(&handle).map(|t| t.2.clone()).unwrap_or_else(|| "vst2".to_string())
|
||||
};
|
||||
let b = native::bridge(&kind, &app).ok_or("bridge DLL not found")?;
|
||||
let count = {
|
||||
let f = b.get_param_count.ok_or("bridge missing GetParamCount")?;
|
||||
unsafe { f(handle) }
|
||||
};
|
||||
if count < 0 {
|
||||
return Err(format!("get param count failed: {}", count));
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for i in 0..count {
|
||||
if kind == "vst2" {
|
||||
let f = b.get_param.ok_or("bridge missing GetParam")?;
|
||||
let mut v: f64 = 0.0;
|
||||
let rc = unsafe { f(handle, i, &mut v) };
|
||||
if rc == 0 {
|
||||
out.push(ParamInfo { param_id: i, title: format!("Param {}", i), value: v });
|
||||
}
|
||||
} else {
|
||||
let f = b.get_param_info.ok_or("bridge missing GetParamInfo")?;
|
||||
let mut id: i32 = 0;
|
||||
let mut title = [0i8; 128];
|
||||
let mut v: f64 = 0.0;
|
||||
let rc = unsafe { f(handle, i, &mut id, title.as_mut_ptr(), title.len() as i32, &mut v) };
|
||||
if rc == 0 {
|
||||
let title_str = unsafe { std::ffi::CStr::from_ptr(title.as_ptr()) }.to_string_lossy().into_owned();
|
||||
out.push(ParamInfo { param_id: id, title: title_str, value: v });
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = (app, track_id, plugin_id);
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Dong editor native + xoa map (goi khi cua so VST GUI dong).
|
||||
#[tauri::command]
|
||||
pub fn close_vst_editor(app: AppHandle, track_id: String, plugin_id: String) -> Result<i32, String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let key = format!("{}|{}", track_id, plugin_id);
|
||||
let handle = native::EDITOR_MAP.lock().unwrap().remove(&key).ok_or("editor not open")?;
|
||||
let kind = native::HANDLE_MAP.lock().unwrap().remove(&handle).map(|t| t.2).unwrap_or_else(|| "vst2".to_string());
|
||||
let b = native::bridge(&kind, &app).ok_or("bridge DLL not found")?;
|
||||
let f = b.close.ok_or("bridge missing Close")?;
|
||||
let mut err = [0i8; 256];
|
||||
let rc = unsafe { f(handle, err.as_mut_ptr(), err.len() as i32) };
|
||||
Ok(rc)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = (app, track_id, plugin_id);
|
||||
Ok(0)
|
||||
}
|
||||
Ok(format!("opened: {}", label))
|
||||
}
|
||||
|
||||
@@ -5,16 +5,97 @@
|
||||
<title>VST GUI</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; background: #1a1d24; color: #c8ccd4;
|
||||
display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
||||
#msg { text-align: center; max-width: 520px; line-height: 1.6; }
|
||||
height: 100vh; margin: 0; display: flex; }
|
||||
#editor { flex: 1; position: relative; }
|
||||
#panel { width: 260px; border-left: 1px solid #2c313b; padding: 10px; overflow-y: auto;
|
||||
background: #171a20; }
|
||||
#panel h3 { margin: 4px 0 10px; font-size: 13px; color: #9aa3b2; font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: .04em; }
|
||||
.row { margin-bottom: 10px; }
|
||||
.row label { display: block; font-size: 12px; color: #c8ccd4; margin-bottom: 2px;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.row .val { float: right; color: #6ea8ff; font-variant-numeric: tabular-nums; }
|
||||
.row input[type=range] { width: 100%; accent-color: #3b82f6; }
|
||||
#msg { padding: 20px; text-align: center; color: #9aa3b2; line-height: 1.6; }
|
||||
code { background: #262b34; padding: 2px 6px; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="msg">Cửa sổ nổi VST GUI (T8).<br>Plugin editor sẽ được attach vào HWND cửa sổ này ở T9.</div>
|
||||
<div id="editor"><div id="msg">Plugin editor attach vào HWND (vùng này).<br>Nếu không thấy editor: cửa sổ này mở qua DAW, native DLL phải tìm thấy (SF_NATIVE_HOST_DIR).</div></div>
|
||||
<div id="panel">
|
||||
<h3>Parameters</h3>
|
||||
<div id="params"><div id="msg">Đang tải params…</div></div>
|
||||
</div>
|
||||
<script>
|
||||
// T8: placeholder — chưa có native editor. JS của DAW gọi invoke('open_vst_gui', ...)
|
||||
// để mở/focus cửa sổ này. Thông tin (track, plugin) nằm ở title + label window.
|
||||
// T11: param sync 2 chiều — native đổi param -> event vst_param_changed -> cập nhật UI;
|
||||
// JS kéo slider -> invoke set_vst_param -> setParamNormalized/setParameter.
|
||||
// Guard chống loop: pendingSet chứa param_id JS vừa set; event echo của chính mình bị bỏ qua.
|
||||
const T = window.__TAURI__;
|
||||
if (!T) { document.getElementById('msg').textContent = 'Không có __TAURI__ (chạy ngoài Tauri).'; throw new Error('no tauri'); }
|
||||
const { invoke } = T.core;
|
||||
const { listen } = T.event;
|
||||
|
||||
const qs = new URLSearchParams(location.search);
|
||||
const trackId = qs.get('track') || '';
|
||||
const pluginId = qs.get('plugin') || '';
|
||||
const pendingSet = new Set();
|
||||
const sliders = new Map(); // param_id -> {slider, val}
|
||||
|
||||
function setValue(paramId, value) {
|
||||
const el = sliders.get(paramId);
|
||||
if (!el) return;
|
||||
el.slider.value = value;
|
||||
el.val.textContent = Number(value).toFixed(3);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const box = document.getElementById('params');
|
||||
let list;
|
||||
try {
|
||||
list = await invoke('get_vst_params', { trackId, pluginId });
|
||||
} catch (e) {
|
||||
box.innerHTML = '<div id="msg">Lỗi: ' + String(e) + '</div>';
|
||||
return;
|
||||
}
|
||||
if (!list || list.length === 0) {
|
||||
box.innerHTML = '<div id="msg">Plugin không có param nào.</div>';
|
||||
return;
|
||||
}
|
||||
box.innerHTML = '';
|
||||
for (const p of list) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row';
|
||||
const lbl = document.createElement('label');
|
||||
lbl.textContent = p.title;
|
||||
const val = document.createElement('span');
|
||||
val.className = 'val';
|
||||
val.textContent = Number(p.value).toFixed(3);
|
||||
const slider = document.createElement('input');
|
||||
slider.type = 'range'; slider.min = 0; slider.max = 1; slider.step = 0.001;
|
||||
slider.value = p.value;
|
||||
slider.addEventListener('input', () => { val.textContent = parseFloat(slider.value).toFixed(3); });
|
||||
slider.addEventListener('change', () => {
|
||||
const v = parseFloat(slider.value);
|
||||
pendingSet.add(p.param_id);
|
||||
invoke('set_vst_param', { trackId, pluginId, paramId: p.param_id, value: v })
|
||||
.catch(err => { console.error('set_vst_param', err); })
|
||||
.finally(() => setTimeout(() => pendingSet.delete(p.param_id), 500));
|
||||
});
|
||||
row.appendChild(lbl); row.appendChild(val); row.appendChild(slider);
|
||||
box.appendChild(row);
|
||||
sliders.set(p.param_id, { slider, val });
|
||||
}
|
||||
}
|
||||
|
||||
// native đổi param (từ editor/automation) -> cập nhật UI
|
||||
listen('vst_param_changed', (e) => {
|
||||
const d = e.payload || {};
|
||||
if (d.track_id !== trackId || d.plugin_id !== pluginId) return;
|
||||
if (pendingSet.has(d.param_id)) { pendingSet.delete(d.param_id); return; } // echo của chính mình
|
||||
setValue(d.param_id, d.value);
|
||||
}).catch(err => console.error('listen vst_param_changed', err));
|
||||
|
||||
refresh().catch(err => console.error('refresh', err));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user