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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user