G2.1: save full instrument state to disk (path/type/preset/transport)
This commit is contained in:
+2
-1
@@ -150,7 +150,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
|
||||
| ID | Cài đặt | Kiểm tra | Bugs bắt được | Fix | Trạng thái |
|
||||
|---|---|---|---|---|---|
|
||||
| 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 | | | | | ⬜ |
|
||||
| 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<uint64_t>; 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.4 | | | | | ⬜ |
|
||||
@@ -160,6 +160,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
|
||||
### Nhật ký thực hiện
|
||||
- **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)`.
|
||||
|
||||
---
|
||||
|
||||
Binary file not shown.
@@ -60,6 +60,14 @@ public:
|
||||
// (GUI close/reopen). Non-VST engines: no-op (nothing to save).
|
||||
virtual void captureState() {}
|
||||
virtual void restoreState() {}
|
||||
// G2.1: serialize the current preset to a portable string for the state
|
||||
// file (VST3: base64 of captured component+controller state). Caller must
|
||||
// call captureState() first (VST3 buffers are refreshed there). Non-VST
|
||||
// engines: empty.
|
||||
virtual std::string serializeState() const { return std::string(); }
|
||||
// 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; }
|
||||
|
||||
// Real-time Audio PCM Float32 rendering loop
|
||||
virtual void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) = 0;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#define NATIVE_INSTRUMENT_ENGINE_H
|
||||
|
||||
#include "INativeInstrument.h"
|
||||
#include "StateStore.h"
|
||||
|
||||
#include <sfizz.hpp>
|
||||
|
||||
@@ -31,6 +32,9 @@ public:
|
||||
void pitchBend(uint32_t channel, uint32_t bend14) override;
|
||||
bool openGUI(void* parentWindowHandle) override;
|
||||
void closeGUI() override;
|
||||
// G2.1: expose last bank/program for the state snapshot.
|
||||
uint32_t bankOf(uint32_t channel) const override;
|
||||
uint32_t programOf(uint32_t channel) const override;
|
||||
void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) override;
|
||||
|
||||
private:
|
||||
@@ -40,6 +44,7 @@ private:
|
||||
// Per-channel bank select (CC0<<7 | CC32) — programChange phai dung bank
|
||||
// that (truoc day hardcode bank 0 -> preset o bank != 0 khong chon duoc).
|
||||
uint32_t bank_[16];
|
||||
uint32_t program_[16];
|
||||
};
|
||||
|
||||
// sfizz (.sfz)
|
||||
@@ -97,6 +102,14 @@ public:
|
||||
// case-insensitive — caller must lowercase before comparing.
|
||||
std::string pathOf(uint32_t channel);
|
||||
|
||||
// G2.1: copy every assigned channel into a state snapshot (channel,
|
||||
// type, path, VST3 preset blob -> base64, SF2/SF3 bank/program).
|
||||
// MUST run on the channel worker (captureState enters the plugin);
|
||||
// takes mu_ for the whole call so the audio loop cannot be inside
|
||||
// process() on the same instance while its state is captured.
|
||||
// Channels mid-reload are skipped (their old instance is dying).
|
||||
bool snapshot(BridgeStateSnapshot& out);
|
||||
|
||||
// Mark a channel as rebuilding its VST instance (GUI reopen): the
|
||||
// real-time loop then skips processAudioBlock for that channel so VST
|
||||
// teardown never races the audio thread.
|
||||
@@ -128,6 +141,7 @@ private:
|
||||
mutable std::mutex mu_;
|
||||
std::map<uint32_t, std::unique_ptr<INativeInstrument>> channels_;
|
||||
std::map<uint32_t, std::string> paths_; // last assigned path per channel
|
||||
std::map<uint32_t, InstrumentType> types_; // last assigned type per channel (G2.1)
|
||||
// Per-instrument scratch so engines that overwrite (not mix) stay additive.
|
||||
std::vector<float> scratchL_, scratchR_;
|
||||
// Channel-level reload flag (see isReloading) — indexed by MIDI channel.
|
||||
|
||||
@@ -32,6 +32,9 @@ public:
|
||||
// instance from scratch — without save/restore the plugin resets).
|
||||
void captureState();
|
||||
void restoreState();
|
||||
// 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 resizeView(int w, int h) override;
|
||||
bool hasAttachedView() const override;
|
||||
void closeGUI() override;
|
||||
|
||||
@@ -43,6 +43,7 @@ bool FluidSynthInstrument::loadSoundFontFile(const std::string& path, double sam
|
||||
// Reset all channels to font preset 0 (spec §VII: bank0/prog0 piano)
|
||||
for (uint32_t ch = 0; ch < 16; ++ch) {
|
||||
bank_[ch] = 0;
|
||||
program_[ch] = 0;
|
||||
fluid_synth_program_select(FS_SYNTH, ch, sfontId, 0, 0);
|
||||
}
|
||||
return true;
|
||||
@@ -55,6 +56,7 @@ bool FluidSynthInstrument::init(double sampleRate, uint32_t maxBlockSize) {
|
||||
void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) {
|
||||
if (!synth || channel >= 16) return;
|
||||
bank_[channel] = bank;
|
||||
program_[channel] = program;
|
||||
fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, program);
|
||||
}
|
||||
|
||||
@@ -81,6 +83,7 @@ void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) {
|
||||
if (!synth || channel >= 16) return;
|
||||
// Dung bank da nhan tu CC0/CC32 — bank hardcode 0 lam preset o bank != 0
|
||||
// khong duoc chon (fluid giu preset cu -> ra piano sai).
|
||||
program_[channel] = program;
|
||||
fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank_[channel], program);
|
||||
}
|
||||
|
||||
@@ -96,6 +99,15 @@ bool FluidSynthInstrument::openGUI(void* parentWindowHandle) {
|
||||
|
||||
void FluidSynthInstrument::closeGUI() {}
|
||||
|
||||
// G2.1: expose last bank/program for the state snapshot.
|
||||
uint32_t FluidSynthInstrument::bankOf(uint32_t channel) const {
|
||||
return channel < 16 ? bank_[channel] : 0;
|
||||
}
|
||||
|
||||
uint32_t FluidSynthInstrument::programOf(uint32_t channel) const {
|
||||
return channel < 16 ? program_[channel] : 0;
|
||||
}
|
||||
|
||||
void FluidSynthInstrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
|
||||
if (!synth) return;
|
||||
fluid_synth_write_float(FS_SYNTH, numSamples, outputL, 0, 1, outputR, 0, 1);
|
||||
@@ -191,6 +203,7 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
|
||||
if (it != channels_.end()) oldInst = std::move(it->second);
|
||||
channels_[channel] = std::move(inst);
|
||||
paths_[channel] = path;
|
||||
types_[channel] = type;
|
||||
// Fresh instance is by construction not reloading — clear the channel
|
||||
// flag so real-time MIDI dispatch (which drops reloading channels)
|
||||
// flows to it again.
|
||||
@@ -221,6 +234,7 @@ void InstrumentEngineManager::unload(uint32_t channel) {
|
||||
if (it != channels_.end()) oldInst = std::move(it->second);
|
||||
channels_.erase(channel);
|
||||
paths_.erase(channel);
|
||||
types_.erase(channel);
|
||||
reloadingCh_[channel] = false;
|
||||
}
|
||||
if (oldInst) {
|
||||
@@ -242,6 +256,32 @@ std::string InstrumentEngineManager::pathOf(uint32_t channel) {
|
||||
return it == paths_.end() ? std::string() : it->second;
|
||||
}
|
||||
|
||||
bool InstrumentEngineManager::snapshot(BridgeStateSnapshot& out) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
out.instruments.clear();
|
||||
for (auto& kv : channels_) {
|
||||
uint32_t ch = kv.first;
|
||||
if (ch < 16 && reloadingCh_[ch]) continue; // old instance dying
|
||||
InstrumentStateEntry e;
|
||||
e.channel = ch;
|
||||
auto tp = types_.find(ch);
|
||||
e.type = tp == types_.end() ? 0 : (int)tp->second;
|
||||
auto pp = paths_.find(ch);
|
||||
e.path = pp == paths_.end() ? std::string() : pp->second;
|
||||
if (INativeInstrument* i = kv.second.get()) {
|
||||
// VST3: refresh the saved buffers then serialize them. Safe
|
||||
// here: mu_ is held, so the audio loop cannot be inside
|
||||
// process() on this instance while getState runs.
|
||||
i->captureState();
|
||||
e.presetBase64 = i->serializeState();
|
||||
e.bank = i->bankOf(ch);
|
||||
e.program = i->programOf(ch);
|
||||
}
|
||||
out.instruments.push_back(std::move(e));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
INativeInstrument* InstrumentEngineManager::get(uint32_t channel) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
auto it = channels_.find(channel);
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cerrno>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -37,6 +39,30 @@ std::string jsonEscape(const std::string& s) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Create the parent directory of `path` (all missing levels). Returns false
|
||||
// only on a hard error (permission, invalid path).
|
||||
bool ensureParentDir(const std::string& path) {
|
||||
const size_t slash = path.find_last_of("/\\");
|
||||
if (slash == std::string::npos) return true;
|
||||
const std::string dir = path.substr(0, slash);
|
||||
if (dir.empty()) return true;
|
||||
#ifdef _WIN32
|
||||
// CreateDirectoryA creates one level at a time - walk the chain so
|
||||
// %APPDATA%\SonicForgeDAW\state (or a custom SF_STATE_FILE dir) is
|
||||
// created even when none of it exists yet.
|
||||
for (size_t i = 0; i < dir.size(); ++i) {
|
||||
if ((dir[i] == '/' || dir[i] == '\\') && i > 0)
|
||||
CreateDirectoryA(dir.substr(0, i).c_str(), nullptr);
|
||||
}
|
||||
if (CreateDirectoryA(dir.c_str(), nullptr)) return true;
|
||||
return GetLastError() == ERROR_ALREADY_EXISTS;
|
||||
#else
|
||||
for (size_t i = 1; i < dir.size(); ++i)
|
||||
if (dir[i] == '/') mkdir(dir.substr(0, i).c_str(), 0755);
|
||||
return mkdir(dir.c_str(), 0755) == 0 || errno == EEXIST;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Skip whitespace; returns pointer at first non-ws char.
|
||||
const char* skipWs(const char* p) {
|
||||
while (p && *p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) ++p;
|
||||
@@ -144,6 +170,7 @@ std::string StateStore::defaultPath() {
|
||||
}
|
||||
|
||||
bool StateStore::save(const BridgeStateSnapshot& snap, const std::string& path) {
|
||||
if (!ensureParentDir(path)) return false;
|
||||
std::string json;
|
||||
json.reserve(256 + snap.instruments.size() * 192);
|
||||
json += "{\n";
|
||||
|
||||
@@ -549,6 +549,44 @@ void Vst3Instrument::captureState() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// G2.1: base64 encode (RFC 4648). Small, dependency-free.
|
||||
static std::string base64Encode(const uint8_t* data, size_t len) {
|
||||
static const char* tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
std::string out;
|
||||
out.reserve(((len + 2) / 3) * 4);
|
||||
for (size_t i = 0; i < len; i += 3) {
|
||||
uint32_t n = (uint32_t)data[i] << 16;
|
||||
if (i + 1 < len) n |= (uint32_t)data[i + 1] << 8;
|
||||
if (i + 2 < len) n |= (uint32_t)data[i + 2];
|
||||
out += tbl[(n >> 18) & 63];
|
||||
out += tbl[(n >> 12) & 63];
|
||||
out += (i + 1 < len) ? tbl[(n >> 6) & 63] : '=';
|
||||
out += (i + 2 < len) ? tbl[n & 63] : '=';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string Vst3Instrument::serializeState() const {
|
||||
#ifndef HAVE_VST3SDK
|
||||
return std::string();
|
||||
#else
|
||||
if (savedComponentState_.empty() && savedControllerState_.empty())
|
||||
return std::string();
|
||||
std::vector<uint8_t> blob;
|
||||
auto append32 = [&blob](uint32_t v) {
|
||||
blob.push_back((uint8_t)(v >> 24));
|
||||
blob.push_back((uint8_t)(v >> 16));
|
||||
blob.push_back((uint8_t)(v >> 8));
|
||||
blob.push_back((uint8_t)v);
|
||||
};
|
||||
append32((uint32_t)savedComponentState_.size());
|
||||
blob.insert(blob.end(), savedComponentState_.begin(), savedComponentState_.end());
|
||||
append32((uint32_t)savedControllerState_.size());
|
||||
blob.insert(blob.end(), savedControllerState_.begin(), savedControllerState_.end());
|
||||
return base64Encode(blob.data(), blob.size());
|
||||
#endif
|
||||
}
|
||||
|
||||
// G0.4: restore the captured state onto the freshly loaded instance.
|
||||
void Vst3Instrument::restoreState() {
|
||||
#ifndef HAVE_VST3SDK
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "INativeInstrument.h"
|
||||
#include "SharedMemoryIPC.h"
|
||||
#include "NativeInstrumentEngine.h"
|
||||
#include "StateStore.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -200,6 +201,27 @@ static std::mutex g_loadMutex;
|
||||
// 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;
|
||||
// G2.1: state persistence. g_stateDirty is set by load/unload/transport/periodic
|
||||
// marks; the main loop debounces and posts one save job to the UiThread, so disk
|
||||
// IO never runs on the audio loop. Playing is tracked separately from
|
||||
// transportStopped because the audio loop must not write files.
|
||||
static std::atomic<bool> g_stateDirty{false};
|
||||
static std::atomic<bool> g_transportPlaying{false};
|
||||
static std::mutex g_lastSnapMutex;
|
||||
static BridgeStateSnapshot g_lastSnap;
|
||||
// Same instrument set and transport -> nothing to write. Playhead is excluded:
|
||||
// comparing it would rewrite the file every ~2s while playing.
|
||||
static bool snapshotsEqual(const BridgeStateSnapshot& a, const BridgeStateSnapshot& b) {
|
||||
if (a.playing != b.playing || a.instruments.size() != b.instruments.size()) return false;
|
||||
for (size_t i = 0; i < a.instruments.size(); ++i) {
|
||||
const auto& x = a.instruments[i];
|
||||
const auto& y = b.instruments[i];
|
||||
if (x.channel != y.channel || x.type != y.type || x.path != y.path ||
|
||||
x.presetBase64 != y.presetBase64 || x.bank != y.bank || x.program != y.program)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -663,8 +685,12 @@ int main(int argc, char* argv[]) {
|
||||
std::cerr << "[NativeBridge] SHM block size fixed at " << AUDIO_BLOCK_SIZE
|
||||
<< " (SF_BLOCK_SIZE=" << b << " ignored)" << std::endl;
|
||||
}
|
||||
// G2.1: state file. The Rust app sets SF_STATE_FILE; fall back to the default.
|
||||
std::string stateFile;
|
||||
if (const char* e = std::getenv("SF_STATE_FILE")) stateFile = e;
|
||||
if (stateFile.empty()) stateFile = StateStore::defaultPath();
|
||||
const uint32_t block = AUDIO_BLOCK_SIZE;
|
||||
uint64_t playheadSamples = 0;
|
||||
std::atomic<uint64_t> playheadSamples{0};
|
||||
// V8 bug 3: khi STOP da xu ly, bo qua NOTE_ON (velocity>0) den sau - JS
|
||||
// note-on timer co the bay toi sau STOP (guardPlay tre do React re-render)
|
||||
// -> retrigger note -> VST loop am. Chi PLAY moi nhan note-on lai.
|
||||
@@ -995,6 +1021,30 @@ int main(int argc, char* argv[]) {
|
||||
});
|
||||
};
|
||||
|
||||
// G2.1: persist instrument state to disk. Runs on the UiThread (posted via
|
||||
// uiWorker.post when g_stateDirty flips) so file IO never blocks the audio
|
||||
// loop. Deferred while any channel is mid-reload (half-dead instance).
|
||||
auto saveStateJob = [&]() {
|
||||
for (uint32_t ch = 0; ch < 16; ++ch) {
|
||||
if (instruments.isReloading(ch)) { g_stateDirty = true; return; }
|
||||
}
|
||||
BridgeStateSnapshot snap;
|
||||
if (!instruments.snapshot(snap)) return;
|
||||
snap.playing = g_transportPlaying.load();
|
||||
snap.playhead = (double)playheadSamples.load();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_lastSnapMutex);
|
||||
if (snapshotsEqual(snap, g_lastSnap)) return;
|
||||
g_lastSnap = snap;
|
||||
}
|
||||
if (StateStore::save(snap, stateFile))
|
||||
std::cout << "[NativeBridge] state saved: " << snap.instruments.size()
|
||||
<< " instruments, playing=" << (snap.playing ? "true" : "false")
|
||||
<< std::endl;
|
||||
else
|
||||
std::cerr << "[NativeBridge] state save FAILED: " << stateFile << std::endl;
|
||||
};
|
||||
|
||||
// 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
|
||||
while (true) {
|
||||
// CRASH FIX (0xc000041d): last-resort net — any C++ exception that
|
||||
@@ -1140,6 +1190,7 @@ int main(int argc, char* argv[]) {
|
||||
if (!ok) g_engine->setReloading(ch, false);
|
||||
for (uint32_t y : spOld) unmute_if_not_closing(y, "load");
|
||||
for (uint32_t y : spNew) unmute_if_not_closing(y, "load");
|
||||
g_stateDirty = true; // G2.1: instrument changed
|
||||
});
|
||||
} else if (c.type == 1) { // PANIC
|
||||
instruments.allNotesOff();
|
||||
@@ -1154,13 +1205,18 @@ int main(int argc, char* argv[]) {
|
||||
if (auto* inst = instruments.get(ch)) inst->controlChange(ch, 64, 0);
|
||||
}
|
||||
instruments.allNotesOff();
|
||||
g_transportPlaying = false;
|
||||
g_stateDirty = true; // G2.1: transport changed
|
||||
std::cout << "[NativeBridge] transport STOP — all notes off" << std::endl;
|
||||
} else if (c.arg0 == 1) { // PLAY
|
||||
transportStopped = false;
|
||||
g_transportPlaying = true;
|
||||
g_stateDirty = true; // G2.1: transport changed
|
||||
playheadSamples = c.arg1;
|
||||
std::cout << "[NativeBridge] transport PLAY playhead=" << playheadSamples << std::endl;
|
||||
} else if (c.arg0 == 2) { // SET_POSITION (seek while stopped)
|
||||
playheadSamples = c.arg1;
|
||||
g_stateDirty = true; // G2.1: transport changed
|
||||
}
|
||||
} 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;
|
||||
@@ -1188,6 +1244,29 @@ int main(int argc, char* argv[]) {
|
||||
handleOpenGui(g.first, (uintptr_t)g.second.first, g.second.second);
|
||||
}
|
||||
|
||||
// G2.1: state persistence. The VST GUI can change the preset with no
|
||||
// control/MIDI traffic, so mark dirty every ~2s while instruments are
|
||||
// loaded; a 300ms debounce posts one save job to the UiThread.
|
||||
{
|
||||
static auto lastMark = std::chrono::steady_clock::now();
|
||||
auto nowMark = std::chrono::steady_clock::now();
|
||||
if (std::chrono::duration_cast<std::chrono::milliseconds>(nowMark - lastMark).count() >= 2000) {
|
||||
lastMark = nowMark;
|
||||
for (uint32_t ch = 0; ch < 16; ++ch) {
|
||||
if (instruments.get(ch)) { g_stateDirty = true; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (g_stateDirty.load()) {
|
||||
static auto lastStatePost = std::chrono::steady_clock::now();
|
||||
auto nowPost = std::chrono::steady_clock::now();
|
||||
if (std::chrono::duration_cast<std::chrono::milliseconds>(nowPost - lastStatePost).count() >= 300) {
|
||||
lastStatePost = nowPost;
|
||||
g_stateDirty = false;
|
||||
uiWorker.post([&]() { saveStateJob(); });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// B. Snapshot queued MIDI events (bounded copy, queue reset immediately)
|
||||
uint32_t nEvents = shmIPC->midiQueueCount > 64 ? 64 : shmIPC->midiQueueCount;
|
||||
|
||||
Reference in New Issue
Block a user