diff --git a/TEST_NOTES.md b/TEST_NOTES.md index 59ffa16..77721bd 100644 --- a/TEST_NOTES.md +++ b/TEST_NOTES.md @@ -86,3 +86,10 @@ chạy từng mục trên máy Windows (hoặc OS native có đủ toolchain), - ⚠️ Nexus VST3 silent (license). Dùng ACDD demo để chứng minh âm. - ⚠️ daw_engine.exe serve index.html từ memory lúc start (v=202608121600) dù file trên đĩa đã bump v=202608121700 → restart app mới nhận build mới. - Ghi chú deploy: copy `app/static/js/app.precompiled.js` + `app/templates/index.html` → `dist/`, `src-tauri/resources/`, `src-tauri/target/release/` (cùng dưới `daw_engine/_internal/app/`), bump `?v=` chống cache. +## Kết quả test THẬT Windows 2026-08-13 (build af59aaf + hotfix SF→VSTi khi play) +- 🐛 **BUG (user báo): auto-restore project → track SF2 đang play → nút Synth đổi SF→VSTi → crash bridge, tắt âm, không play lại được.** +- Nguyên nhân C++: LOAD job (type=2) silence same-plugin channels CHỈ SAU `assign()` — mà `assign()` chứa `createInstance` (loadPlugin) chạy trên worker. Nếu track KHÁC (từ restore) đang giữ CÙNG DLL VST3 với instrument mới → trong lúc createInstance, audio loop vẫn `process()` inst của track đó → 2 threads trong 1 DLL (Nexus) → crash thoát im lặng. +- Fix C++ (`main.cpp` LOAD job): tính `spNew` (channels giữ path mới, lowercase so sánh) TRƯỚC `assign()`, silence chúng cùng `spOld`; restore sau assign dùng `unmute_if_not_closing`. Tách `lower_path_str()` khỏi `lower_plugin_path()`. +- Nguyên nhân "tắt âm + không tải lại được": sau load VST3 OK, JS tự mở native GUI (`openVstGuiRetry`) dù đang play → C++ mute channel tới khi đóng GUI (reloading_=true, note-on drop) → track câm vĩnh viễn giữa lúc play. +- Fix JS (`app.jsx` loadTrackInstrumentToBridge): chỉ tự mở GUI khi `!isPlayingRef.current`; đang play thì chỉ load instrument im lặng, GUI mở qua nút GUI riêng (`ensureAndOpenVstGui`). +- ✅ **Windows verify**: mở project cũ (track SF2 + track VST3), play, đổi track SF2 → VSTi cùng DLL track kia → không crash, âm VSTi mới phát; đổi lại SF2 → play tiếp được. Mở GUI lúc đang play → track mute (trade-off có chủ đích, âm lại sau khi đóng GUI). diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 63fb710..860585f 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -15548,7 +15548,11 @@ 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) { + // SF→VSTi BUG 2026-08-13: GUI mở → C++ mute channel tới khi đóng GUI → + // đang play thì âm tắt ngay (reloading_=true, note-on drop). Khi đang + // play chỉ load instrument im lặng; GUI mở qua nút GUI riêng + // (ensureAndOpenVstGui) — không tự mở giữa lúc phát. + if (ok && btype === 'VST3' && !isPlayingRef.current && window.NativeBridgeService.openNativeGUI) { try { await openVstGuiRetry(instrumentId, bch, 20); } catch (e) {} } } diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 3177881..7da25d8 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -809,7 +809,11 @@ if(isSf&&window.__bridgeLoadedChannels)delete window.__bridgeLoadedChannels[bch] // phải gửi lại program change để ra ĐÚNG instrument. if(ok&&window.__bridgeLastProgram){const _pfx=trackId+':';Object.keys(window.__bridgeLastProgram).forEach(k=>{if(k.indexOf(_pfx)===0)delete window.__bridgeLastProgram[k];});}if(ok){if(!window.__bridgeLoadedChannels)window.__bridgeLoadedChannels={};window.__bridgeLoadedChannels[bch]=true;}if(ok&&btype==='VST3'&&p){try{if(!window.__bridgePluginPaths)window.__bridgePluginPaths={};window.__bridgePluginPaths[instrumentId]=p;updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,synth_engine:{...(t.synth_engine||{}),plugin_path:p}}:t));}catch(e){}}// 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,20);}catch(e){}}}})();};const setTrackInstrumentWithProgram=(trackId,instrumentId,programNumber,displayName,bankNumber)=>{const isSfInstrument=instrumentId&&typeof instrumentId==='string'&&instrumentId.startsWith('sf_');const sfBank=bankNumber!==undefined?bankNumber:isSfInstrument?0:undefined;const sfProg=programNumber!==undefined?programNumber:undefined;var mt=activeTracksRef.current||tracks;var curTrk=null;for(var ci=0;ci{const isSfInstrument=instrumentId&&typeof instrumentId==='string'&&instrumentId.startsWith('sf_');const sfBank=bankNumber!==undefined?bankNumber:isSfInstrument?0:undefined;const sfProg=programNumber!==undefined?programNumber:undefined;var mt=activeTracksRef.current||tracks;var curTrk=null;for(var ci=0;ciprev.map(t=>{if(t.id!==trackId)return t;const hasInstrument=!!instrumentId;const instrType=isSfInstrument?'soundfont':hasInstrument?'vst3':'default';const synthEngine=hasInstrument?{type:instrType,plugin_id:instrumentId,soundfont_bank:sfBank!==undefined?sfBank:0,soundfont_program:sfProg!==undefined?sfProg:0,soundfont_id:isSfInstrument?instrumentId.replace('sf_',''):''}:undefined;return{...t,midiChannel:mch,instrumentId,instrumentProgram:sfProg,instrumentName:displayName,soundfont_bank:sfBank,soundfont_program:sfProg,synth_engine:synthEngine,type:hasInstrument?'MIDI':t.type==='MIDI'?'audio':t.type};}));setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setSynthCategory(null);// Trigger FluidSynth load + program change when soundfont instrument selected diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index 330a258..a82ac3d 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -227,13 +227,16 @@ private: // Two threads inside one VST3 DLL (Nexus) crash/deadlock: the audio loop's // process() on one instance must not race createInstance/createView/terminate // on another. Windows plugin paths are case-insensitive — compare lowercased. -static std::string lower_plugin_path(uint32_t ch) { - std::string p = g_engine ? g_engine->pathOf(ch) : std::string(); +static std::string lower_path_str(std::string p) { std::transform(p.begin(), p.end(), p.begin(), [](unsigned char c) { return (char)::tolower(c); }); return p; } +static std::string lower_plugin_path(uint32_t ch) { + return g_engine ? lower_path_str(g_engine->pathOf(ch)) : std::string(); +} + // Channels (other than ch) currently assigned the same plugin DLL path. static std::vector same_plugin_channels(uint32_t ch) { std::vector out; @@ -820,8 +823,14 @@ int main(int argc, char* argv[]) { // session-restore instances). Silence this channel (its old // instance is being torn down / replaced) and every // same-plugin channel for the whole job. Old path covers - // the old-instance teardown; the new path (after assign) - // covers the freshly created instance. + // the old-instance teardown; the new path (computed BEFORE + // assign — see spNew below) covers the freshly created + // instance. CRASH FIX (SF→VSTi while playing): the new + // path must be silenced BEFORE assign() — assign() itself + // runs createInstance (loadPlugin) on this worker, and a + // restored channel on ANOTHER track holding the SAME DLL + // kept process()ing on the audio loop during that window + // (old code silenced spNew only AFTER assign → crash). instruments.setReloading(ch, true); std::vector spOld = same_plugin_channels(ch); for (uint32_t y : spOld) { @@ -829,6 +838,24 @@ int main(int argc, char* argv[]) { std::cerr << "[dbg] load: silenced same-plugin ch=" << y << " during load ch=" << ch << std::endl; } + // Channels (other than ch) already assigned the NEW path — + // silence BEFORE createInstance enters that DLL. + std::vector spNew; + if (t == InstrumentType::VST3) { + std::string np = lower_path_str(path); + if (!np.empty()) { + for (uint32_t y = 0; y < 16; ++y) { + if (y == ch) continue; + if (!g_engine->get(y)) continue; + if (std::find(spOld.begin(), spOld.end(), y) != spOld.end()) continue; + if (lower_plugin_path(y) != np) continue; + g_engine->setReloading(y, true); + spNew.push_back(y); + std::cerr << "[dbg] load: silenced same-plugin (new path) ch=" << y + << " during load ch=" << ch << std::endl; + } + } + } #ifdef _WIN32 // DONG cua so editor dang mo cua channel TRUOC khi assign(): // thay the inst (VST3 -> SF2/inst khac) ma editor con song -> @@ -879,25 +906,12 @@ int main(int argc, char* argv[]) { std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch << " type=" << (int)t << " " << path << std::endl; } - // New-path channels (the freshly created instance's DLL) — - // silence any not already covered by the old path. - std::vector spNew; - if (ok) spNew = same_plugin_channels(ch); - for (uint32_t y : spNew) { - if (std::find(spOld.begin(), spOld.end(), y) == spOld.end()) { - g_engine->setReloading(y, true); - std::cerr << "[dbg] load: silenced same-plugin ch=" << y - << " (new path) during load ch=" << ch << std::endl; - } - } // Restore. Failed load keeps the OLD instance — unmute it // back (silenced at job start); a successful assign leaves // the fresh instance unmuted by construction. if (!ok) g_engine->setReloading(ch, false); for (uint32_t y : spOld) unmute_if_not_closing(y, "load"); - for (uint32_t y : spNew) - if (std::find(spOld.begin(), spOld.end(), y) == spOld.end()) - unmute_if_not_closing(y, "load"); + for (uint32_t y : spNew) unmute_if_not_closing(y, "load"); }); } else if (c.type == 1) { // PANIC instruments.allNotesOff();