G2.1: save full instrument state to disk (path/type/preset/transport)

This commit is contained in:
2026-08-16 07:28:17 +07:00
parent 8d9011222a
commit 28441566c2
9 changed files with 212 additions and 2 deletions
+80 -1
View File
@@ -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) {
@@ -1140,6 +1190,7 @@ int main(int argc, char* argv[]) {
// 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) unmute_if_not_closing(y, "load");
g_stateDirty = true; // G2.1: instrument changed
});
} else if (c.type == 1) { // PANIC
@@ -1154,13 +1205,18 @@ int main(int argc, char* argv[]) {
for (uint32_t ch = 0; ch < 16; ++ch) {
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
@@ -1188,6 +1244,29 @@ int main(int argc, char* argv[]) {
for (auto& g : readyGui)
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)