G2.1: save full instrument state to disk (path/type/preset/transport)
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user