fix(ui): GUI button inline same row + same height as Synth/File/FX in TCP
This commit is contained in:
+16
-11
@@ -5768,7 +5768,7 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
|
||||
// Requirement 2: VST3 load → mở native GUI ngay (C++ attach editor vào
|
||||
// cửa sổ bridge tự tạo — control type=4, hwnd=0).
|
||||
if (ok && type === 'VST3' && window.NativeBridgeService.openNativeGUI) {
|
||||
try { await openVstGuiRetry(name, 0, 8); } catch (e) {}
|
||||
try { await openVstGuiRetry(name, 0, 20); } catch (e) {}
|
||||
}
|
||||
return ok;
|
||||
} catch (err) {
|
||||
@@ -6210,14 +6210,14 @@ const ensureAndOpenVstGui = async (trackId, instrumentId) => {
|
||||
const ok = await loadVstToBridge({ id: trackId, instrumentId });
|
||||
if (!ok) return;
|
||||
}
|
||||
try { await openVstGuiRetry(instrumentId, bch, 8); } catch (e) { console.warn('[Bridge] openNativeGUI fail:', e); }
|
||||
try { await openVstGuiRetry(instrumentId, bch, 20); } catch (e) { console.warn('[Bridge] openNativeGUI fail:', e); }
|
||||
};
|
||||
// V8 bug 4 + V9 bug 3/6/7: C++ load instrument ASYNC (worker thread) — OPEN_GUI
|
||||
// som → C++ defer (chi log stderr, invoke van tra Ok) → retry PHAI chay DU so
|
||||
// lan, khong `return true` som. Mo GUI co the can cho den khi assign() xong
|
||||
// (Nexus ~1-2s) — 8 lan x 500ms, window reuse khi load xong.
|
||||
// (Nexus ~1-2s) — 20 lan x 500ms, window reuse khi load xong.
|
||||
const openVstGuiRetry = async (instrumentId, bch, attempts) => {
|
||||
const n = (attempts && attempts > 0) ? attempts : 8;
|
||||
const n = (attempts && attempts > 0) ? attempts : 20;
|
||||
for (let i = 0; i < n; i++) {
|
||||
try { await window.NativeBridgeService.openNativeGUI(instrumentId, bch); }
|
||||
catch (e) { console.warn('[Bridge] openNativeGUI retry', i + 1, e); }
|
||||
@@ -9919,8 +9919,8 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
||||
ensureAndOpenVstGui(st.trackId, st.instrumentId);
|
||||
},
|
||||
title: "Mở lại GUI VSTi",
|
||||
className: "px-1 py-0.5 text-[9px] rounded font-mono font-bold border transition flex items-center gap-0.5 shrink-0 bg-zinc-800 text-violet-300 border-violet-800 hover:bg-violet-900/40"
|
||||
}, React.createElement("i", { "data-lucide": "sliders-horizontal", className: "w-3 h-3 shrink-0" }), React.createElement("span", { className: "truncate text-[9px]" }, "GUI")) : null)), React.createElement("div", {
|
||||
className: "px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 shrink-0 bg-zinc-800 text-violet-300 border-violet-800 hover:bg-violet-900/40"
|
||||
}, React.createElement("i", { "data-lucide": "sliders-horizontal", className: "w-3 h-3 shrink-0" }), React.createElement("span", { className: "truncate text-[10px]" }, "GUI")) : null)), React.createElement("div", {
|
||||
className: "flex items-center gap-1 ml-1 text-xs"
|
||||
}, React.createElement("span", { className: "text-zinc-500" }, "AI:"), React.createElement("input", {
|
||||
type: "number", value: aiBarStart, onChange: e => setAiBarStart(parseInt(e.target.value) || 0),
|
||||
@@ -15474,6 +15474,11 @@ const App = () => {
|
||||
if (!window.SonicMidiRouter || !window.SonicMidiRouter.isBridgeActive() || !instrumentId) return;
|
||||
const isSf = typeof instrumentId === 'string' && instrumentId.startsWith('sf_');
|
||||
const bch = window.SonicMidiRouter.allocateChannel(trackId, bank === 128);
|
||||
// V9 GUI-button bug: switch VSTi → SF2 phải xóa flag "đã load VST" của
|
||||
// channel, nếu không ensureAndOpenVstGui sẽ bỏ qua load khi chọn lại VST
|
||||
// (flag cũ còn true) → OPEN_GUI đến bridge khi channel chưa có instrument
|
||||
// → deferred mãi, nút GUI không mở gì.
|
||||
if (isSf && window.__bridgeLoadedChannels) delete window.__bridgeLoadedChannels[bch];
|
||||
const btype = isSf ? 'SF2' : 'VST3';
|
||||
let bpath = null;
|
||||
if (instrumentSelectorData) {
|
||||
@@ -15513,7 +15518,7 @@ const App = () => {
|
||||
// Requirement 2: chọn VST3 qua nút Synth → load xong mở native GUI
|
||||
// (C++ attach editor vào cửa sổ bridge tự tạo — control type=4, hwnd=0).
|
||||
if (ok && btype === 'VST3' && window.NativeBridgeService.openNativeGUI) {
|
||||
try { await openVstGuiRetry(instrumentId, bch, 8); } catch (e) {}
|
||||
try { await openVstGuiRetry(instrumentId, bch, 20); } catch (e) {}
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -29843,20 +29848,20 @@ STRICT CONSTRAINTS:
|
||||
className: "text-zinc-500 font-normal"
|
||||
}, track.fxType || "None")), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: (e) => { e.stopPropagation(); openInstrumentSelector(track.id); },
|
||||
className: "px-1.5 py-0.5 bg-zinc-800 hover:bg-zinc-700 text-amber-400 border border-amber-800 rounded text-[10px] font-bold flex items-center gap-1 max-w-[90px] shrink-0"
|
||||
className: "px-1.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-amber-400 border border-amber-800 rounded text-xs font-bold flex items-center gap-1 max-w-[90px] shrink-0"
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: "inline-flex items-center shrink-0"
|
||||
}, /*#__PURE__*/React.createElement("i", {
|
||||
"data-lucide": "music",
|
||||
className: "w-2.5 h-2.5 shrink-0"
|
||||
})), /*#__PURE__*/React.createElement("span", { className: "truncate text-[10px] min-w-0" }, track.instrumentName || track.instrumentId || "Synth"), /*#__PURE__*/React.createElement("i", { "data-lucide": "chevron-down", className: "w-3 h-3 shrink-0" }))), /*#__PURE__*/React.createElement("div", { className: "flex items-center gap-0.5 shrink-0 min-w-0" }, (track.instrumentId && typeof track.instrumentId === 'string' && !track.instrumentId.startsWith('sf_') ? /*#__PURE__*/React.createElement("button", {
|
||||
})), /*#__PURE__*/React.createElement("span", { className: "truncate text-xs min-w-0" }, track.instrumentName || track.instrumentId || "Synth"), /*#__PURE__*/React.createElement("i", { "data-lucide": "chevron-down", className: "w-3 h-3 shrink-0" })), (track.instrumentId && typeof track.instrumentId === 'string' && !track.instrumentId.startsWith('sf_') ? /*#__PURE__*/React.createElement("button", {
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
ensureAndOpenVstGui(track.id, track.instrumentId);
|
||||
},
|
||||
title: "Mở lại GUI VSTi",
|
||||
className: "px-1 py-0.5 bg-zinc-800 hover:bg-zinc-700 text-violet-300 border border-violet-800 rounded text-[9px] font-bold flex items-center gap-0.5 shrink-0"
|
||||
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "sliders-horizontal", className: "w-2.5 h-2.5 shrink-0" }), /*#__PURE__*/React.createElement("span", { className: "truncate text-[8px] min-w-0" }, "GUI")) : null)), /*#__PURE__*/React.createElement("div", {
|
||||
className: "px-1.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-violet-300 border border-violet-800 rounded text-xs font-bold flex items-center gap-1 shrink-0"
|
||||
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "sliders-horizontal", className: "w-3 h-3 shrink-0" }), /*#__PURE__*/React.createElement("span", { className: "truncate text-xs min-w-0" }, "GUI")) : null)), /*#__PURE__*/React.createElement("div", {
|
||||
onMouseDown: e => handleTrackResizeMouseDown(e, track.id),
|
||||
className: "absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors",
|
||||
onClick: e => e.stopPropagation()
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -50,7 +50,7 @@
|
||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608132205" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608141030" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
+162
-122
@@ -77,6 +77,17 @@ static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARA
|
||||
}
|
||||
#endif
|
||||
|
||||
// Serialize plugin loads across channels: concurrent VST3 createInstance on
|
||||
// separate worker threads (Nexus) deadlocks inside the SDK factory - all
|
||||
// stuck forever (observed: 3 concurrent Nexus loads, zero assign returned).
|
||||
// One load at a time; SF2/SFZ loads are fast, contention negligible.
|
||||
static std::mutex g_loadMutex;
|
||||
// Pending OPEN_GUI requests whose channel instrument was not loaded yet.
|
||||
// The main loop fulfils them as soon as the load job completes (assign ok),
|
||||
// so the GUI button works regardless of JS retry timing.
|
||||
static std::mutex g_pendingGuiMutex;
|
||||
static std::map<uint32_t, std::pair<void*, std::string>> g_pendingGui;
|
||||
|
||||
// B9: native Win32 window for the VST editor (replaces the WebView2 surface —
|
||||
// the HTML window was drawn ON TOP of the plugin GUI). MUST be created on the
|
||||
// ChannelWorker thread so the worker's idle message pump services its messages.
|
||||
@@ -333,6 +344,137 @@ int main(int argc, char* argv[]) {
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
uint64_t blockCount = 0;
|
||||
|
||||
// OPEN_GUI handling - shared by the control queue and the pending-GUI
|
||||
// sweep. Gate on the MAIN thread before creating a window: instrument
|
||||
// load is ASYNC (worker thread); OPEN_GUI too early would attach to an
|
||||
// empty channel and leave a blank window. Not loaded yet => remember the
|
||||
// request; the sweep retries once the load job completes. Window MUST
|
||||
// belong to the MAIN thread (audio loop pump dispatches its messages);
|
||||
// a window on a worker whose pump is idle during jobs hangs
|
||||
// view->attached() (gui_probe: two_workers_close TIMEOUT). The attach
|
||||
// job runs on the channel worker (COM STA apartment stays alive).
|
||||
auto handleOpenGui = [&](uint32_t guiCh, uintptr_t arg1, const std::string& pluginId) {
|
||||
if (!instruments.get(guiCh)) {
|
||||
std::cerr << "[NativeBridge] GUI deferred ch=" << guiCh
|
||||
<< " plugin=" << pluginId << " (no instrument yet) - queued" << std::endl;
|
||||
std::lock_guard<std::mutex> lock(g_pendingGuiMutex);
|
||||
g_pendingGui[guiCh] = { (void*)arg1, pluginId };
|
||||
return;
|
||||
}
|
||||
if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
|
||||
void* hwnd = (void*)arg1;
|
||||
#ifdef _WIN32
|
||||
// Window PHAI thuoc MAIN thread (audio loop pump nay dispatch
|
||||
// messages cua no moi vong lap). Tao/cap nhat window ngay tai day.
|
||||
HWND nativeHwnd = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_guiMutex);
|
||||
auto it = g_guiWindows.find(guiCh);
|
||||
if (it != g_guiWindows.end()) nativeHwnd = (HWND)it->second;
|
||||
}
|
||||
if (hwnd == 0) {
|
||||
if (nativeHwnd && IsWindow(nativeHwnd)) {
|
||||
hwnd = (void*)nativeHwnd;
|
||||
SetWindowTextA(nativeHwnd, pluginId.c_str());
|
||||
ShowWindow(nativeHwnd, SW_SHOW);
|
||||
SetForegroundWindow(nativeHwnd);
|
||||
// Reuse: cap nhat USERDATA (channel+1) - inst CU da bi thay
|
||||
// the boi assign() -> WM_DESTROY sau nay lookup inst MOI.
|
||||
SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
|
||||
} else {
|
||||
nativeHwnd = (HWND)create_native_vst_window(pluginId.c_str());
|
||||
if (!nativeHwnd) {
|
||||
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << pluginId << std::endl;
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_guiMutex);
|
||||
g_guiWindows[guiCh] = nativeHwnd; // keep window alive
|
||||
g_hwndToCh[nativeHwnd] = guiCh; // WM_DESTROY cleanup
|
||||
}
|
||||
SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
|
||||
hwnd = (void*)nativeHwnd;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
workers[guiCh]->post([&instruments, guiCh, hwnd, arg2 = pluginId]() {
|
||||
if (!instruments.get(guiCh)) {
|
||||
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
|
||||
<< " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl;
|
||||
return;
|
||||
}
|
||||
std::cerr << "[dbg] openGUI thread start hwnd=" << hwnd
|
||||
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
|
||||
#ifdef _WIN32
|
||||
// Option B: chi 1 editor VST mo tai 1 thoi diem toan
|
||||
// bridge. Instance thu 2 cua CUNG plugin (Nexus) attach
|
||||
// view o apartment/worker khac -> treo. Dong editor cua
|
||||
// channel khac TRUOC khi attach: WM_CLOSE -> main pump
|
||||
// (audio loop) destroy window -> WM_DESTROY -> closeGUI()
|
||||
// + xoa registry. Chay tren worker job de khong stall
|
||||
// writeIndex cua real-time loop.
|
||||
{
|
||||
std::vector<uint32_t> others;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_guiMutex);
|
||||
for (const auto& kv : g_guiWindows)
|
||||
if (kv.first != guiCh) others.push_back(kv.first);
|
||||
}
|
||||
for (uint32_t y : others) {
|
||||
HWND yHwnd = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_guiMutex);
|
||||
auto it = g_guiWindows.find(y);
|
||||
if (it != g_guiWindows.end()) yHwnd = (HWND)it->second;
|
||||
}
|
||||
if (!yHwnd || !IsWindow(yHwnd)) continue;
|
||||
PostMessage(yHwnd, WM_CLOSE, 0, 0);
|
||||
std::cerr << "[dbg] openGUI: closing editor ch=" << y
|
||||
<< " before attach ch=" << guiCh << std::endl;
|
||||
bool closed = false;
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_guiMutex);
|
||||
if (g_guiWindows.find(y) == g_guiWindows.end()) { closed = true; break; }
|
||||
}
|
||||
Sleep(10);
|
||||
}
|
||||
if (!closed)
|
||||
std::cerr << "[dbg] openGUI: editor ch=" << y
|
||||
<< " not closed in 5s, proceeding" << std::endl;
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void)0;
|
||||
#endif
|
||||
if (auto* inst = instruments.get(guiCh)) {
|
||||
// Reopen sau khi dong: reload() (terminate + loadPlugin)
|
||||
// PHAI chay tren worker thread nay - COM STA apartment
|
||||
// cua channel song o day. view->attached() cung chay o
|
||||
// day; window thuoc main thread nen main pump (audio
|
||||
// loop) dispatch messages cua no - khong can pump worker.
|
||||
// Chan UAF bang flag reloading_ (set/clear duoi engine
|
||||
// mutex; renderAll giu mutex khi process).
|
||||
bool guard = inst->needsReload();
|
||||
if (guard) instruments.setReloading(guiCh, true);
|
||||
bool ok = false;
|
||||
{
|
||||
// Serialize createInstance (reload) with other loads.
|
||||
std::lock_guard<std::mutex> lg(g_loadMutex);
|
||||
ok = inst->reloadForGUI();
|
||||
}
|
||||
if (ok) ok = inst->attachView(hwnd);
|
||||
if (guard) instruments.setReloading(guiCh, false);
|
||||
if (ok)
|
||||
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
|
||||
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
|
||||
else
|
||||
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
|
||||
<< " plugin=" << arg2 << std::endl;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
|
||||
while (true) {
|
||||
#ifdef _WIN32
|
||||
@@ -389,6 +531,7 @@ int main(int argc, char* argv[]) {
|
||||
#endif
|
||||
std::cerr << "[dbg] load thread start ch=" << ch
|
||||
<< " type=" << (int)t << " path=" << path << std::endl;
|
||||
std::lock_guard<std::mutex> lg(g_loadMutex);
|
||||
bool ok = instruments.assign(ch, t, path, sampleRate, block);
|
||||
std::cerr << "[dbg] assign returned ch=" << ch << " ok=" << (ok ? 1 : 0) << std::endl;
|
||||
if (ok) {
|
||||
@@ -423,132 +566,29 @@ int main(int argc, char* argv[]) {
|
||||
} else if (c.type == 4) { // OPEN_GUI (A7): arg1 = parent HWND (0 → bridge tự tạo native window), arg2 = plugin id
|
||||
uint32_t guiCh = c.channel;
|
||||
if (guiCh >= 16) guiCh = 0;
|
||||
// V9 bug 3/6/7: gate tren MAIN thread TRUOC khi tao window —
|
||||
// instrument load ASYNC (worker thread); OPEN_GUI som -> attach
|
||||
// job fail -> cua so trang van con song. Chua load xong =>
|
||||
// defer: KHONG tao window, KHONG post job. JS retry
|
||||
// openNativeGUI 8x500ms; khi assign xong nhay lai day, tao
|
||||
// window 1 lan va attach OK.
|
||||
if (!instruments.get(guiCh)) {
|
||||
std::cerr << "[NativeBridge] GUI deferred ch=" << guiCh
|
||||
<< " plugin=" << c.arg2 << " (no instrument yet)" << std::endl;
|
||||
continue;
|
||||
}
|
||||
if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
|
||||
void* hwnd = (void*)(uintptr_t)c.arg1;
|
||||
#ifdef _WIN32
|
||||
// Window PHAI thuoc MAIN thread (audio loop pump nay dispatch
|
||||
// messages cua no moi vong lap). Window tren worker + worker
|
||||
// khong pump trong luc job chay -> view->attached() treo
|
||||
// (gui_probe: two_workers_close TIMEOUT; same_thread /
|
||||
// two_instances_close — window tren main — OK). Tao/cap nhat
|
||||
// window ngay tai day tren main thread.
|
||||
HWND nativeHwnd = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_guiMutex);
|
||||
auto it = g_guiWindows.find(guiCh);
|
||||
if (it != g_guiWindows.end()) nativeHwnd = (HWND)it->second;
|
||||
}
|
||||
if (hwnd == 0) {
|
||||
if (nativeHwnd && IsWindow(nativeHwnd)) {
|
||||
hwnd = (void*)nativeHwnd;
|
||||
SetWindowTextA(nativeHwnd, std::string(c.arg2).c_str());
|
||||
ShowWindow(nativeHwnd, SW_SHOW);
|
||||
SetForegroundWindow(nativeHwnd);
|
||||
// Reuse: cap nhat USERDATA (channel+1) — inst CU da bi
|
||||
// thay the boi assign() -> WM_DESTROY sau nay lookup
|
||||
// inst MOI, khong dung con tro dangling.
|
||||
SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
|
||||
} else {
|
||||
nativeHwnd = (HWND)create_native_vst_window(std::string(c.arg2).c_str());
|
||||
if (!nativeHwnd) {
|
||||
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << c.arg2 << std::endl;
|
||||
continue;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_guiMutex);
|
||||
g_guiWindows[guiCh] = nativeHwnd; // keep window alive
|
||||
g_hwndToCh[nativeHwnd] = guiCh; // WM_DESTROY cleanup
|
||||
}
|
||||
SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
|
||||
hwnd = (void*)nativeHwnd;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
workers[guiCh]->post([&instruments, guiCh, hwnd, arg2 = std::string(c.arg2)]() {
|
||||
if (!instruments.get(guiCh)) {
|
||||
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
|
||||
<< " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl;
|
||||
return;
|
||||
}
|
||||
std::cerr << "[dbg] openGUI thread start hwnd=" << hwnd
|
||||
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
|
||||
#ifdef _WIN32
|
||||
// Option B: chi 1 editor VST mo tai 1 thoi diem toan
|
||||
// bridge. Instance thu 2 cua CUNG plugin (Nexus) attach
|
||||
// view o apartment/worker khac -> treo. Dong editor cua
|
||||
// channel khac TRUOC khi attach: WM_CLOSE -> main pump
|
||||
// (audio loop) destroy window -> WM_DESTROY -> closeGUI()
|
||||
// + xoa registry. Chay tren worker job de khong stall
|
||||
// writeIndex cua real-time loop.
|
||||
{
|
||||
std::vector<uint32_t> others;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_guiMutex);
|
||||
for (const auto& kv : g_guiWindows)
|
||||
if (kv.first != guiCh) others.push_back(kv.first);
|
||||
}
|
||||
for (uint32_t y : others) {
|
||||
HWND yHwnd = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_guiMutex);
|
||||
auto it = g_guiWindows.find(y);
|
||||
if (it != g_guiWindows.end()) yHwnd = (HWND)it->second;
|
||||
}
|
||||
if (!yHwnd || !IsWindow(yHwnd)) continue;
|
||||
PostMessage(yHwnd, WM_CLOSE, 0, 0);
|
||||
std::cerr << "[dbg] openGUI: closing editor ch=" << y
|
||||
<< " before attach ch=" << guiCh << std::endl;
|
||||
bool closed = false;
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_guiMutex);
|
||||
if (g_guiWindows.find(y) == g_guiWindows.end()) { closed = true; break; }
|
||||
}
|
||||
Sleep(10);
|
||||
}
|
||||
if (!closed)
|
||||
std::cerr << "[dbg] openGUI: editor ch=" << y
|
||||
<< " not closed in 5s, proceeding" << std::endl;
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void)0;
|
||||
#endif
|
||||
if (auto* inst = instruments.get(guiCh)) {
|
||||
// Reopen sau khi dong: reload() (terminate + loadPlugin)
|
||||
// PHAI chay tren worker thread nay — COM STA apartment
|
||||
// cua channel song o day. view->attached() cung chay o
|
||||
// day; window thuoc main thread nen main pump (audio
|
||||
// loop) dispatch messages cua no — khong can pump worker.
|
||||
// Chan UAF bang flag reloading_ (set/clear duoi engine
|
||||
// mutex; renderAll giu mutex khi process).
|
||||
bool guard = inst->needsReload();
|
||||
if (guard) instruments.setReloading(guiCh, true);
|
||||
bool ok = inst->reloadForGUI() && inst->attachView(hwnd);
|
||||
if (guard) instruments.setReloading(guiCh, false);
|
||||
if (ok)
|
||||
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
|
||||
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl;
|
||||
else
|
||||
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
|
||||
<< " plugin=" << arg2 << std::endl;
|
||||
}
|
||||
});
|
||||
handleOpenGui(guiCh, (uintptr_t)c.arg1, std::string(c.arg2));
|
||||
}
|
||||
}
|
||||
shmIPC->controlQueueCount = 0;
|
||||
|
||||
// Pending OPEN_GUI requests: fulfilled as soon as the channel's
|
||||
// instrument becomes available (load job completed on its worker).
|
||||
{
|
||||
std::vector<std::pair<uint32_t, std::pair<void*, std::string>>> readyGui;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_pendingGuiMutex);
|
||||
for (auto it = g_pendingGui.begin(); it != g_pendingGui.end(); ) {
|
||||
if (instruments.get(it->first)) {
|
||||
readyGui.push_back({ it->first, it->second });
|
||||
it = g_pendingGui.erase(it);
|
||||
} else ++it;
|
||||
}
|
||||
}
|
||||
for (auto& g : readyGui)
|
||||
handleOpenGui(g.first, (uintptr_t)g.second.first, g.second.second);
|
||||
}
|
||||
|
||||
|
||||
// B. Snapshot queued MIDI events (bounded copy, queue reset immediately)
|
||||
uint32_t nEvents = shmIPC->midiQueueCount > 64 ? 64 : shmIPC->midiQueueCount;
|
||||
SharedAudioBufferIPC::MidiEventIPC evts[64];
|
||||
|
||||
@@ -15,9 +15,15 @@ use tauri_plugin_dialog::{DialogExt, FilePath};
|
||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
mod shm;
|
||||
|
||||
/// App đang tắt (cửa sổ chính đã Destroyed) — pump thread phải dừng ngay,
|
||||
/// không respawn bridge mới sau khi sidecar đã bị giết (fix: app còn sống
|
||||
/// sau khi đóng window → pump thấy stall 3s → spawn bridge mới → orphan).
|
||||
static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false);
|
||||
use shm::{Shm, ShmState};
|
||||
|
||||
struct EngineProcess(Mutex<Option<CommandChild>>);
|
||||
@@ -356,6 +362,10 @@ pub fn run() {
|
||||
let mut down_emitted = false;
|
||||
let mut restart_attempts = 0u32;
|
||||
loop {
|
||||
// App đang tắt (window Destroyed) — dừng pump, không respawn bridge.
|
||||
if SHUTTING_DOWN.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let state = pump_handle.state::<ShmState>();
|
||||
let guard = match state.0.lock() {
|
||||
Ok(g) => g,
|
||||
@@ -385,6 +395,10 @@ pub fn run() {
|
||||
AudioFrame { l: l.to_vec(), r: r.to_vec() },
|
||||
);
|
||||
} else if last_change.elapsed() >= std::time::Duration::from_secs(3) && !down_emitted {
|
||||
// Check lai truoc khi respawn — Destroyed co the vua set flag.
|
||||
if SHUTTING_DOWN.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
down_emitted = true;
|
||||
if restart_attempts == 0 {
|
||||
restart_attempts += 1;
|
||||
@@ -483,6 +497,8 @@ pub fn run() {
|
||||
.on_window_event(|window, event| {
|
||||
// Terminate sidecar khi DAW window dong — tranh orphan process
|
||||
if let tauri::WindowEvent::Destroyed = event {
|
||||
// Dung pump thread: khong respawn bridge sau khi sidecar da bi giet.
|
||||
SHUTTING_DOWN.store(true, Ordering::Relaxed);
|
||||
let child = window
|
||||
.state::<EngineProcess>()
|
||||
.0
|
||||
@@ -506,6 +522,12 @@ pub fn run() {
|
||||
let _ = child.kill();
|
||||
println!("daw_vst_bridge sidecar terminated.");
|
||||
}
|
||||
// Cua so chinh da dong — ep app thoat han. Neu con window
|
||||
// phu/an giu event loop song, Tauri khong tu exit; exit(0)
|
||||
// ket thuc dut diem (pump da dung, khong con respawn bridge).
|
||||
if window.label() == "main" {
|
||||
window.app_handle().exit(0);
|
||||
}
|
||||
}
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@@ -10,8 +10,14 @@ from app.core.vst_engine import PluginManager, midi_note_to_freq, render_midi_ev
|
||||
class TestPluginManager:
|
||||
def test_init(self):
|
||||
pm = PluginManager()
|
||||
assert pm.vst_dir == "/opt/daw_engine/vst3"
|
||||
assert pm.sf_dir == "/opt/daw_engine/soundfonts"
|
||||
if os.name == "nt":
|
||||
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
|
||||
assert pm.vst_dir == os.path.join(pf, "Common Files", "VST3")
|
||||
root = os.environ.get("APPDATA") or os.path.expanduser("~")
|
||||
assert pm.sf_dir == os.path.join(root, "SonicForgeDAW", "soundfonts")
|
||||
else:
|
||||
assert pm.vst_dir == "/opt/daw_engine/vst3"
|
||||
assert pm.sf_dir == "/opt/daw_engine/soundfonts"
|
||||
|
||||
def test_midi_events_to_messages(self):
|
||||
if not HAS_PEDALBOARD:
|
||||
|
||||
Reference in New Issue
Block a user