diff --git a/20260815-tasks.md b/20260815-tasks.md index 47a8f00..139c186 100644 --- a/20260815-tasks.md +++ b/20260815-tasks.md @@ -152,7 +152,7 @@ Lý do: G2.2 phụ thuộc G2.1 nhưng giá trị thấp hơn G2.3; làm G2.3 s | G2.0 | StateStore.h/.cpp (JSON thủ công, atomic tmp+rename, UTF-8, API save/load/clear, defaultPath=%APPDATA%/SonicForgeDAW/state/bridge_state.json); them vao CMakeLists target daw_vst_bridge; test_state_store.cpp (round-trip self-check) | Build PASS (daw_vst_bridge.exe), round-trip PASS (save/load 2 inst: path Windows co space+backslash, presetBase64 co newline/quote/backslash; khong con .tmp; clear xoa file) | — (module moi, chua hook vao bridge) | — | ✅ (Win xong) | | G2.1 | main.cpp: globals g_stateDirty/g_transportPlaying/g_lastSnapMutex/g_lastSnap, snapshotsEqual (bo playhead); stateFile = SF_STATE_FILE fallback defaultPath; playheadSamples -> std::atomic; saveStateJob (UiThread, skip khi isReloading, chi save khi snapshot doi); TRANSPORT set g_transportPlaying + dirty; cuoi LOAD dirty=true; periodic 2s dirty + debounce 300ms. Vst3Instrument.h: khai bao bankOf/programOf. StateStore.cpp: ensureParentDir() (CreateDirectoryA chain / mkdir) goi dau save(). | Build PASS (daw_vst_bridge.exe). Deploy PASS (PID moi: sonicforge-daw 9876, daw_engine 24632, daw_vst_bridge 7352). Verify live PASS: LOAD Nexus ch5 + PLAY -> bridge_state.json co 7 instrument (ch0-6 frontend da load), playing=true, presetBase64 non-empty (Nexus 3468/3488, EZkeys 24764); STOP -> playing=false. State round-trip test_state_store.cpp PASS. | test script ban dau ky vong dung 1 instrument (frontend da load 7) -> FAIL gia, chuc nang PASS | assert dung so instrument | ✅ (Win xong) | | G2.2 | | | | | ⬜ | -| G2.3 | | | | | ⬜ | +| G2.3 | main.cpp: startup restore block (doc SF_STATE_FILE, guard channel trong, post LOAD job tung channel qua uiWorker, assign + loadSerializedState preset / selectProgram bank-program, g_loadMutex serialize, restore transport PLAY + playhead neu state playing). Vst3Instrument: base64Decode + loadSerializedState (parse [4B BE compLen][comp][4B BE ctrlLen][ctrl], goi restoreState). INativeInstrument.h: virtual loadSerializedState no-op. lib.rs: spawn_bridge set env SF_STATE_FILE = %APPDATA%/SonicForgeDAW/state/bridge_state.json. | Build PASS (bridge + cargo check). Deploy PASS. Verify live PASS: LOAD ch5 + PLAY (playhead 5000) -> kill bridge -> respawn -> log "restoring 7 instruments" + "restored ch=0..6" (Nexus + EZkeys) + "restoring transport PLAY playhead=5000" + ch5 preset 3488B giu nguyen; state file playing=true playhead=5000 inst=7; writeIndex advancing (14859->15234). | - | - | ✅ (Win xong) | | G2.4 | | | | | ⬜ | | G2.5 | | | | | ⬜ | | G2.6 | | | | | ⬜ | @@ -161,6 +161,7 @@ Lý do: G2.2 phụ thuộc G2.1 nhưng giá trị thấp hơn G2.3; làm G2.3 s - **G2.0** (2026-08-16, Windows): tao `native_bridge/include/StateStore.h` + `native_bridge/src/StateStore.cpp` (CRLF, JSON viet tay, atomic write .tmp→rename, escape \ " , load parser an toan). Them StateStore.cpp vao CMakeLists target daw_vst_bridge. Build bridge PASS. Test round-trip `native_bridge/debug/test_state_store.cpp` PASS (compile cl vcvars64 + chay). Commit `G2.0: StateStore (JSON state file + atomic write)`. - **G2.1** (2026-08-16, Windows): save day du state ra disk. main.cpp: globals + snapshotsEqual + stateFile env fallback + playheadSamples atomic + saveStateJob (UiThread, skip isReloading, chi ghi khi snapshot doi) + hook TRANSPORT/LOAD + periodic 2s dirty debounce 300ms. Them khai bao bankOf/programOf vao Vst3Instrument.h (thieu declaration -> build fail). StateStore.cpp: ensureParentDir() (CreateDirectoryA chain, Linux mkdir) goi dau save() (fix C2001 do heredoc backslash mangle qua script fix_bs.py). Build PASS. Deploy PASS (PID 9876/24632/7352). Verify live PASS: LOAD Nexus ch5 + PLAY -> state file 7 inst + playing=true + presetBase64 day du; STOP -> playing=false. StateStore round-trip test PASS. Commit `G2.1: save full instrument state to disk (path/type/preset/transport)`. +- **G2.3** (2026-08-16, Windows): auto-reload sau respawn. main.cpp: block restore truoc audio loop - doc state file, chi restore channel trong (guard instruments.get(ch)==nullptr), post LOAD job (assign + loadSerializedState / selectProgram) serialize bang g_loadMutex, neu state playing thi set playhead + g_transportPlaying=true + g_stateDirty. Vst3Instrument.cpp: base64Decode (b64val lookup) + loadSerializedState (parse blob, goi restoreState); Vst3Instrument.h + INativeInstrument.h: khai bao. lib.rs: spawn_bridge them env SF_STATE_FILE (fallback StateStore::defaultPath khi khong set). Build PASS (daw_vst_bridge.exe + cargo check). Deploy + verify live PASS: LOAD ch5 + PLAY playhead 5000 -> taskkill bridge -> respawn tu dong -> log restore 7 inst (Nexus ch0-5, EZkeys ch6) + restore transport PLAY playhead=5000 + ch5 preset 3488B giu nguyen; state file playing=true inst=7; writeIndex advancing. Commit `G2.3: auto-reload instruments from state file after bridge respawn`. --- diff --git a/install/daw_vst_bridge.exe b/install/daw_vst_bridge.exe index 15b33fd..4990e7a 100644 Binary files a/install/daw_vst_bridge.exe and b/install/daw_vst_bridge.exe differ diff --git a/native_bridge/include/INativeInstrument.h b/native_bridge/include/INativeInstrument.h index 65f1072..1d576dc 100644 --- a/native_bridge/include/INativeInstrument.h +++ b/native_bridge/include/INativeInstrument.h @@ -65,6 +65,10 @@ public: // call captureState() first (VST3 buffers are refreshed there). Non-VST // engines: empty. virtual std::string serializeState() const { return std::string(); } + // G2.3: replace the preset with a previously serialized one (VST3: + // base64 of [4B BE compLen][component][4B BE ctrlLen][controller]). + // Must be called after the instrument is loaded. Non-VST: no-op. + virtual void loadSerializedState(const std::string& /*base64*/) {} // G2.1: last bank/program for a MIDI channel (SF2/SF3). Others: 0. virtual uint32_t bankOf(uint32_t channel) const { return 0; } virtual uint32_t programOf(uint32_t channel) const { return 0; } diff --git a/native_bridge/include/Vst3Instrument.h b/native_bridge/include/Vst3Instrument.h index 05e9bb9..4c8f7b8 100644 --- a/native_bridge/include/Vst3Instrument.h +++ b/native_bridge/include/Vst3Instrument.h @@ -35,6 +35,7 @@ public: // G2.1: base64 of [4B BE componentLen][component][4B BE controllerLen][controller] // from the last captureState(). Empty when nothing captured. std::string serializeState() const override; + void loadSerializedState(const std::string& base64) override; void resizeView(int w, int h) override; bool hasAttachedView() const override; void closeGUI() override; diff --git a/native_bridge/src/Vst3Instrument.cpp b/native_bridge/src/Vst3Instrument.cpp index e357fcd..003f831 100644 --- a/native_bridge/src/Vst3Instrument.cpp +++ b/native_bridge/src/Vst3Instrument.cpp @@ -587,6 +587,67 @@ std::string Vst3Instrument::serializeState() const { #endif } +// G2.3: base64 decode (RFC 4648). Empty / invalid -> false. +static int b64val(unsigned char c) { + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '+') return 62; + if (c == '/') return 63; + return -1; +} +static bool base64Decode(const std::string& in, std::vector& out) { + out.clear(); + out.reserve((in.size() / 4) * 3); + uint32_t acc = 0; + int bits = 0; + for (unsigned char ch : in) { + if (ch == '=' || ch == '\n' || ch == '\r' || ch == ' ') continue; + int v = b64val(ch); + if (v < 0) return false; + acc = (acc << 6) | (uint32_t)v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out.push_back((uint8_t)((acc >> bits) & 0xFF)); + } + } + return true; +} + +// G2.3: restore a preset previously saved by serializeState() (base64 of +// [4B BE componentLen][component][4B BE controllerLen][controller]). +// Decodes into the saved-state buffers and applies them to the live instance. +void Vst3Instrument::loadSerializedState(const std::string& base64) { +#ifndef HAVE_VST3SDK + (void)base64; + return; +#else + std::vector blob; + if (!base64Decode(base64, blob) || blob.size() < 8) { + std::cerr << "[Vst3Instrument] loadSerializedState: bad base64 ch=" << channel_ << std::endl; + return; + } + auto read32 = [&blob](size_t off) -> uint32_t { + return ((uint32_t)blob[off] << 24) | ((uint32_t)blob[off + 1] << 16) | + ((uint32_t)blob[off + 2] << 8) | (uint32_t)blob[off + 3]; + }; + size_t off = 0; + uint32_t clen = read32(off); off += 4; + if (off + clen > blob.size()) { std::cerr << "[Vst3Instrument] loadSerializedState: bad compLen" << std::endl; return; } + savedComponentState_.assign(blob.begin() + off, blob.begin() + off + clen); + off += clen; + if (off + 4 > blob.size()) { std::cerr << "[Vst3Instrument] loadSerializedState: bad ctrlLen" << std::endl; return; } + uint32_t klen = read32(off); off += 4; + if (off + klen > blob.size()) { std::cerr << "[Vst3Instrument] loadSerializedState: bad ctrlLen" << std::endl; return; } + savedControllerState_.assign(blob.begin() + off, blob.begin() + off + klen); + std::cout << "[Vst3Instrument] loaded serialized state ch=" << channel_ + << " component=" << savedComponentState_.size() + << " controller=" << savedControllerState_.size() << std::endl; + restoreState(); +#endif +} + // G0.4: restore the captured state onto the freshly loaded instance. void Vst3Instrument::restoreState() { #ifndef HAVE_VST3SDK diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index 2a08b24..0a4740a 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -1045,6 +1045,63 @@ int main(int argc, char* argv[]) { std::cerr << "[NativeBridge] state save FAILED: " << stateFile << std::endl; }; + // G2.3: auto-reload instruments from the state file after a respawn. + // Runs before the audio loop: read the file once, post one LOAD job per + // saved channel to the UiThread (assign + preset restore), then resume + // transport if the session was PLAYing. Only restores channels that are + // empty (the frontend may already have loaded some by the time the state + // file becomes visible). All jobs serialize on g_loadMutex; file IO here + // is startup-only, never in the audio loop. + { + BridgeStateSnapshot snap; + if (StateStore::load(snap, stateFile)) { + std::cout << "[NativeBridge] restoring " << snap.instruments.size() + << " instruments from " << stateFile << std::endl; + for (const auto& e : snap.instruments) { + uint32_t ch = e.channel & 0xF; + if (e.path.empty() || instruments.get(ch)) continue; // guard: empty channel only + InstrumentType t = (InstrumentType)e.type; + std::string path = e.path; + std::string preset = e.presetBase64; + uint32_t bank = e.bank, program = e.program; + uiWorker.post([&instruments, t, ch, path, preset, bank, program, sampleRate, block]() { + std::lock_guard lg(g_loadMutex); + bool ok = false; + try { + ok = instruments.assign(ch, t, path, sampleRate, block); + } catch (...) { + std::cerr << "[NativeBridge] restore assign EXCEPTION ch=" << ch + << " type=" << (int)t << " path=" << path << std::endl; + ok = false; + } + if (ok) { + if (auto* inst = instruments.get(ch)) { + if (t == InstrumentType::VST3 && !preset.empty()) + inst->loadSerializedState(preset); + else + inst->selectProgram(ch, bank, program); + } + std::cout << "[NativeBridge] restored ch=" << ch + << " type=" << (int)t << " " << path << std::endl; + } else { + std::cerr << "[NativeBridge] restore FAILED ch=" << ch + << " type=" << (int)t << " " << path << std::endl; + } + }); + } + if (snap.playing) { + playheadSamples = (uint64_t)snap.playhead; + transportStopped = false; + g_transportPlaying = true; + g_stateDirty = true; + std::cout << "[NativeBridge] restoring transport PLAY playhead=" + << playheadSamples.load() << std::endl; + } + } else { + std::cout << "[NativeBridge] no state file to restore (" << stateFile << ")" << std::endl; + } + } + // 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP while (true) { // CRASH FIX (0xc000041d): last-resort net — any C++ exception that diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f2f7829..73183c2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -173,6 +173,11 @@ fn spawn_bridge( .env("SF_PARENT_PID", std::process::id().to_string()) .env("SF_SAMPLE_RATE", sample_rate.to_string()) .env("SF_BLOCK_SIZE", "256") + // G2.3: point the bridge at the persisted state file so a + // respawn can auto-reload instruments (StateStore::defaultPath). + .env("SF_STATE_FILE", std::env::var("APPDATA") + .unwrap_or_else(|_| ".".into()) + + "/SonicForgeDAW/state/bridge_state.json") .spawn() { Ok((mut rx, child)) => {