From 9eec33cc79549b645bfacfe3cd50bdd3124a1fdd Mon Sep 17 00:00:00 2001 From: locphamtran Date: Tue, 11 Aug 2026 16:27:30 +0700 Subject: [PATCH] T11: IPC param sync 2 chieu native<->JS (setParamNormalized/setParameter, vst_param_changed event, guard chong loop) --- .gitignore | 6 + native_host/VST2AudioEngine.cpp | 96 +++++++- native_host/tests/fake_vst2.cpp | 32 ++- native_host/vst3_host_bridge.cpp | 95 +++++++- src-tauri/src/lib.rs | 3 +- src-tauri/src/vst_gui.rs | 368 ++++++++++++++++++++++++++++--- src-tauri/ui/vst_gui.html | 91 +++++++- 7 files changed, 650 insertions(+), 41 deletions(-) diff --git a/.gitignore b/.gitignore index f59eb7b..de61f00 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,9 @@ src-tauri/vc_redist.x64.exe app/storage/plugin_dirs.json app/storage/sf_scan_state.json.bak-root app/storage/soundfonts/ + +# Native host build artifacts +native_host/tests/*.dll +native_host/tests/*.exp +native_host/tests/*.lib +native_host/tests/*.obj diff --git a/native_host/VST2AudioEngine.cpp b/native_host/VST2AudioEngine.cpp index 8d75471..da0e3d0 100644 --- a/native_host/VST2AudioEngine.cpp +++ b/native_host/VST2AudioEngine.cpp @@ -26,6 +26,10 @@ namespace { +// Callback param (T11): plugin đổi param (audioMasterAutomate) → host. +typedef void (VSTCALLBACK* SF_ParamChangedCallback)(int32 handle, int32 paramIndex, + double valueNormalized, void* userdata); + struct Instance { HMODULE module = nullptr; @@ -38,9 +42,14 @@ struct Instance ERect* editorRect = nullptr; bool editorOpen = false; bool active = false; + int32 handle = 0; + SF_ParamChangedCallback paramCb = nullptr; + void* paramCbUserdata = nullptr; }; std::mutex g_mutex; +std::mutex g_effect_map_mutex; // riêng: audioMasterAutomate gọi từ audio thread +std::map g_effect_map; std::map> g_instances; int32 g_next_handle = 1; @@ -59,10 +68,9 @@ thread_local Instance* tls_loading = nullptr; VstIntPtr VSTCALLBACK hostAudioMasterImpl (AEffect* effect, int32 opcode, int32 index, VstIntPtr value, void* ptr, float opt) { - (void) effect; (void) index; (void) value; - (void) opt; + (void) ptr; Instance* inst = tls_loading; switch (opcode) { @@ -72,6 +80,22 @@ VstIntPtr VSTCALLBACK hostAudioMasterImpl (AEffect* effect, int32 opcode, int32 return inst ? inst->sampleRate : 44100; case audioMasterGetBlockSize: return inst ? inst->blockSize : 512; + case audioMasterAutomate: + { + // Plugin đổi param (user kéo knob trong editor) → callback lên host. + // Effect → instance qua map riêng (audio thread, không giữ g_mutex). + Instance* owner = nullptr; + { + std::lock_guard lock (g_effect_map_mutex); + auto it = g_effect_map.find (effect); + if (it != g_effect_map.end ()) + owner = it->second; + } + if (owner && owner->paramCb) + owner->paramCb (owner->handle, index, static_cast (opt), + owner->paramCbUserdata); + return 1; + } case audioMasterCurrentId: case audioMasterGetLanguage: return 0; @@ -200,6 +224,11 @@ __declspec (dllexport) int32 SF_VST2_Load (const char* module_path_utf8, } int32 handle = g_next_handle++; + inst->handle = handle; + { + std::lock_guard lock (g_effect_map_mutex); + g_effect_map[inst->effect] = inst.get (); + } g_instances[handle] = std::move (inst); return handle; } @@ -217,6 +246,10 @@ __declspec (dllexport) int32 SF_VST2_Close (int32 handle, char* err, int32 err_c Instance* inst = it->second.get (); if (inst->effect) { + { + std::lock_guard lock (g_effect_map_mutex); + g_effect_map.erase (inst->effect); + } if (inst->active) { inst->effect->dispatcher (inst->effect, effStopProcess, 0, 0, nullptr, 0.0f); @@ -372,4 +405,63 @@ __declspec (dllexport) int32 SF_VST2_Process (int32 handle, float* buffers, int3 return -4; } +// Đăng ký callback param (T11): plugin đổi param (audioMasterAutomate) → +// cb(handle, paramIndex, valueNormalized, userdata). +__declspec (dllexport) int32 SF_VST2_SetParamCallback (int32 handle, + SF_ParamChangedCallback cb, + void* userdata) +{ + std::lock_guard lock (g_mutex); + auto it = g_instances.find (handle); + if (it == g_instances.end ()) + return -1; + it->second->paramCb = cb; + it->second->paramCbUserdata = userdata; + return 0; +} + +// JS automation → setParameter (T11). +__declspec (dllexport) int32 SF_VST2_SetParam (int32 handle, int32 paramIndex, + double valueNormalized) +{ + std::lock_guard lock (g_mutex); + auto it = g_instances.find (handle); + if (it == g_instances.end ()) + return -1; + Instance* inst = it->second.get (); + if (!inst->effect || !inst->effect->setParameter) + return -2; + inst->effect->setParameter (inst->effect, paramIndex, + static_cast (valueNormalized)); + return 0; +} + +// Số tham số plugin. +__declspec (dllexport) int32 SF_VST2_GetParamCount (int32 handle) +{ + std::lock_guard lock (g_mutex); + auto it = g_instances.find (handle); + if (it == g_instances.end ()) + return -1; + Instance* inst = it->second.get (); + if (!inst->effect || !inst->effect->numParams) + return -2; + return inst->effect->numParams (inst->effect); +} + +// Giá trị param (0..1) — JS đọc để dựng UI. +__declspec (dllexport) int32 SF_VST2_GetParam (int32 handle, int32 paramIndex, double* out_value) +{ + std::lock_guard lock (g_mutex); + auto it = g_instances.find (handle); + if (it == g_instances.end ()) + return -1; + Instance* inst = it->second.get (); + if (!inst->effect || !inst->effect->getParameter) + return -2; + if (out_value) + *out_value = static_cast (inst->effect->getParameter (inst->effect, paramIndex)); + return 0; +} + } // extern "C" diff --git a/native_host/tests/fake_vst2.cpp b/native_host/tests/fake_vst2.cpp index 00df9e6..1b066f5 100644 --- a/native_host/tests/fake_vst2.cpp +++ b/native_host/tests/fake_vst2.cpp @@ -13,9 +13,33 @@ struct FakeState { float phase = 0.0f; int32 sampleRate = 44100; + float gain = 0.5f; // param 0 — "Gain" (0..1) }; FakeState g_state; +void* g_audioMaster = nullptr; + +void VSTCALLBACK fakeSetParameter (AEffect* effect, int32 index, float value) +{ + if (index == 0) + { + g_state.gain = value; + // Báo host: param đổi từ editor/plugin (T11) — chống loop host-side. + if (g_audioMaster) + { + typedef VstIntPtr (VSTCALLBACK* AMFn) (AEffect*, int32, int32, VstIntPtr, void*, float); + reinterpret_cast (g_audioMaster) (effect, audioMasterAutomate, index, 0, nullptr, + value); + } + } +} + +float VSTCALLBACK fakeGetParameter (AEffect*, int32 index) +{ + if (index == 0) + return g_state.gain; + return 0.0f; +} int32 VSTCALLBACK fakeDispatcher (AEffect* effect, int32 opcode, int32 index, VstIntPtr value, void* ptr, float opt) @@ -91,15 +115,18 @@ void VSTCALLBACK fakeProcessReplacing (AEffect*, float** inputs, float** outputs AEffect g_effect; -AEffect* VSTCALLBACK createInstance (void*) +AEffect* VSTCALLBACK createInstance (void* audioMaster) { + g_audioMaster = audioMaster; std::memset (&g_effect, 0, sizeof (g_effect)); g_effect.magic = CCONST ('V', 's', 't', 'P'); g_effect.dispatcher = &fakeDispatcher; g_effect.processReplacing = &fakeProcessReplacing; + g_effect.setParameter = &fakeSetParameter; + g_effect.getParameter = &fakeGetParameter; g_effect.numInputs = [] (AEffect*) -> int32 { return 0; }; g_effect.numOutputs = [] (AEffect*) -> int32 { return 2; }; - g_effect.numParams = [] (AEffect*) -> int32 { return 0; }; + g_effect.numParams = [] (AEffect*) -> int32 { return 1; }; g_effect.numPrograms = [] (AEffect*) -> int32 { return 0; }; g_effect.flags = [] (AEffect*) -> int32 { return effFlagsCanReplacing | effFlagsIsSynth; }; g_effect.uniqueID = CCONST ('F', 'k', '2', 'V'); @@ -111,6 +138,5 @@ AEffect* VSTCALLBACK createInstance (void*) extern "C" __declspec (dllexport) AEffect* VSTPluginMain (void* audioMaster) { - (void) audioMaster; return createInstance (audioMaster); } diff --git a/native_host/vst3_host_bridge.cpp b/native_host/vst3_host_bridge.cpp index 9865b8a..67eb151 100644 --- a/native_host/vst3_host_bridge.cpp +++ b/native_host/vst3_host_bridge.cpp @@ -24,6 +24,7 @@ #include "public.sdk/source/vst/hosting/module.h" #include "public.sdk/source/vst/hosting/plugprovider.h" +#include "public.sdk/source/common/commonstringconvert.h" #include "pluginterfaces/base/funknown.h" #include "pluginterfaces/gui/iplugview.h" #include "pluginterfaces/vst/ivstaudioprocessor.h" @@ -35,7 +36,12 @@ using namespace Steinberg::Vst; namespace { -// ComponentHandler tối thiểu — T11: performEdit → JS param sync. +// Callback param: plugin đổi param trong editor → đẩy lên host (T11). +// handle = instance handle; paramId = ParamID; valueNormalized 0..1. +typedef void (__cdecl* SF_ParamChangedCallback)(int32 handle, int32 paramId, + double valueNormalized, void* userdata); + +// ComponentHandler tối thiểu — T11: performEdit → callback param sync. class ComponentHandler : public IComponentHandler { public: @@ -55,12 +61,26 @@ public: uint32 PLUGIN_API addRef () override { return 1; } uint32 PLUGIN_API release () override { return 1; } tresult PLUGIN_API beginEdit (ParamID /*id*/) override { return kResultOk; } - tresult PLUGIN_API performEdit (ParamID /*id*/, ParamValue /*valueNormalized*/) override + tresult PLUGIN_API performEdit (ParamID id, ParamValue valueNormalized) override { + if (cb) + cb (handle, static_cast (id), static_cast (valueNormalized), userdata); return kResultOk; } tresult PLUGIN_API endEdit (ParamID /*id*/) override { return kResultOk; } tresult PLUGIN_API restartComponent (int32 /*flags*/) override { return kResultOk; } + + void setCallback (int32 h, SF_ParamChangedCallback c, void* u) + { + handle = h; + cb = c; + userdata = u; + } + +private: + int32 handle = 0; + SF_ParamChangedCallback cb = nullptr; + void* userdata = nullptr; }; struct Instance @@ -233,4 +253,75 @@ __declspec (dllexport) int32 SF_VST3_Resize (int32 handle, int32 w, int32 h) return -2; } +// Đăng ký callback param (T11): plugin đổi param trong editor → cb(handle, +// paramId, valueNormalized, userdata). userdata là con trỏ do host giữ. +__declspec (dllexport) int32 SF_VST3_SetParamCallback (int32 handle, + SF_ParamChangedCallback cb, + void* userdata) +{ + std::lock_guard lock (g_mutex); + auto it = g_instances.find (handle); + if (it == g_instances.end ()) + return -1; + it->second->handler.setCallback (handle, cb, userdata); + return 0; +} + +// JS automation → setParamNormalized (T11). +__declspec (dllexport) int32 SF_VST3_SetParam (int32 handle, int32 paramId, double valueNormalized) +{ + std::lock_guard lock (g_mutex); + auto it = g_instances.find (handle); + if (it == g_instances.end ()) + return -1; + if (!it->second->controller) + return -2; + it->second->controller->setParamNormalized (static_cast (paramId), + static_cast (valueNormalized)); + return 0; +} + +// Số tham số plugin (để JS dựng UI param list). +__declspec (dllexport) int32 SF_VST3_GetParamCount (int32 handle) +{ + std::lock_guard lock (g_mutex); + auto it = g_instances.find (handle); + if (it == g_instances.end ()) + return -1; + if (!it->second->controller) + return -2; + return static_cast (it->second->controller->getParameterCount ()); +} + +// Thông tin param thứ index (0-based): id + tên (title) + giá trị normalized. +// Trả 0 nếu OK; -1 bad handle; -2 no controller; -3 index ngoài phạm vi. +__declspec (dllexport) int32 SF_VST3_GetParamInfo (int32 handle, int32 index, + int32* out_id, char* out_title, + int32 title_cap, double* out_value) +{ + std::lock_guard lock (g_mutex); + auto it = g_instances.find (handle); + if (it == g_instances.end ()) + return -1; + IEditController* ctrl = it->second->controller; + if (!ctrl) + return -2; + if (index < 0 || static_cast (index) >= ctrl->getParameterCount ()) + return -3; + ParameterInfo info = {}; + if (ctrl->getParameterInfo (static_cast (index), info) != kResultTrue) + return -3; + if (out_id) + *out_id = static_cast (info.id); + if (out_title && title_cap > 0) + { + std::string titleUtf8 = StringConvert::convert (std::u16string (info.title)); + std::strncpy (out_title, titleUtf8.c_str (), static_cast (title_cap - 1)); + out_title[title_cap - 1] = '\0'; + } + if (out_value) + *out_value = static_cast (ctrl->getParamNormalized (info.id)); + return 0; +} + } // extern "C" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 397c3b7..95e496c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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() diff --git a/src-tauri/src/vst_gui.rs b/src-tauri/src/vst_gui.rs index 3a0f531..1409e5e 100644 --- a/src-tauri/src/vst_gui.rs +++ b/src-tauri/src/vst_gui.rs @@ -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 i32>, + pub load: Option i32>, + pub close: Option i32>, + pub set_param_cb: Option i32>, + pub set_param: Option i32>, + pub get_param_count: Option i32>, + pub get_param: Option i32>, + pub get_param_info: Option i32>, + } + // Raw pointer `module` khong Send; bridge chi dung tu main thread, an toan. + unsafe impl Send for Bridge {} + + static BRIDGES: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); + + pub static APP: Mutex> = Mutex::new(None); + // handle native -> (track_id, plugin_id, kind) + pub static HANDLE_MAP: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + // "{track}|{plugin}" -> handle native + pub static EDITOR_MAP: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); + + unsafe fn load_bridge(kind: &str, app: &AppHandle) -> Option { + let name = if kind == "vst2" { "vst2_host_bridge.dll" } else { "vst3_host_bridge.dll" }; + let path = find_bridge_path(name, app)?; + let wide: Vec = 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 = 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 { + let mut candidates: Vec = 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 { + 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 { 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 { + #[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, 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 { + #[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)) } diff --git a/src-tauri/ui/vst_gui.html b/src-tauri/ui/vst_gui.html index 8b19af7..b7671d7 100644 --- a/src-tauri/ui/vst_gui.html +++ b/src-tauri/ui/vst_gui.html @@ -5,16 +5,97 @@ VST GUI -
Cửa sổ nổi VST GUI (T8).
Plugin editor sẽ được attach vào HWND cửa sổ này ở T9.
+
Plugin editor attach vào HWND (vùng này).
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).
+
+

Parameters

+
Đang tải params…
+