From 014b9ac4602980ccb4cff4e66346970f25e98b6b Mon Sep 17 00:00:00 2001 From: locphamtran Date: Fri, 4 Sep 2026 11:49:42 +0700 Subject: [PATCH] =?UTF-8?q?M5:=20capture=20editor=20state=20->=20preset=5F?= =?UTF-8?q?b64=20khi=20=C4=91=C3=B3ng=20GUI=20native=20(JuceFxLoop=20ghi?= =?UTF-8?q?=20.preset.json=20+=20/fx-gui/close=20poll)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/plugins.py | 7 +++- app/core/fx_realtime.py | 33 +++++++++++++++++++ native_bridge/juce_fx/JuceFxLoop.cpp | 49 ++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index fc2dab2..836b86f 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -834,8 +834,13 @@ async def close_fx_gui(req: FxGuiCloseRequest, current_user: dict = Depends(get_ fx_realtime.editor_unregister(plugin_path) _sess = fx_realtime.get_session(_sid) if _sess is not None and _sess.alive(): + # M5: capture state editor TRƯỚC khi đóng (preset user chỉnh trong + # GUI → lưu về project như WM_FXGUI_CAPTURE cũ; preset_b64 dùng để + # mở lại GUI / nạp chain với đúng cài đặt). Không lỗi nếu rỗng. + _preset = _sess.capture_editor_preset(_slot) _sess.send_editor_cmd("close", slot=_slot) - return {"success": True, "closed": True, "native": True} + return {"success": True, "closed": True, "native": True, + "preset_b64": _preset} # session đã chết — rơi xuống spawn fallback (đóng bridge fx-gui cũ nếu có) for proc in _FX_GUI_PROCESSES: if proc is not None and proc.poll() is None and getattr(proc, "_sf_plugin_path", None) == plugin_path: diff --git a/app/core/fx_realtime.py b/app/core/fx_realtime.py index ff959c8..3d50817 100644 --- a/app/core/fx_realtime.py +++ b/app/core/fx_realtime.py @@ -484,6 +484,39 @@ class FxRealtimeSession: return i return None + def capture_editor_preset(self, slot, timeout=3.0): + """M5: capture state editor của CHÍNH instance DSP (ed.capture) → C++ + ghi file JSON cạnh job: .preset.json {slot, preset_b64} (xem + JuceFxLoop.cpp) → đọc + xoá file → trả preset_b64 ("" nếu plugin không + có state / bridge chết / timeout). Không cần editor đang mở — state + đọc từ processor sống trong graph; gọi TRƯỚC ed.close khi đóng GUI.""" + cap_path = (self._job_path or "") + ".preset.json" + if cap_path and os.path.exists(cap_path): + try: + os.remove(cap_path) + except Exception: + pass + if not self.send_editor_cmd("capture", slot=slot): + return "" + deadline = time.time() + timeout + while time.time() < deadline: + if not self.alive(): + return "" + if cap_path and os.path.exists(cap_path): + try: + with open(cap_path, "r", encoding="utf-8") as f: + data = json.load(f) + os.remove(cap_path) + return data.get("preset_b64", "") + except Exception: + try: + os.remove(cap_path) + except Exception: + pass + return "" + time.sleep(0.05) + return "" + def _watch_loop(self): """M4.5 watchdog — thread riêng (ngoài audio). Proc chết ngoài ý muốn (crash view/plugin/loop) → AUTO restart tối đa FXRT_MAX_RESTARTS lần, diff --git a/native_bridge/juce_fx/JuceFxLoop.cpp b/native_bridge/juce_fx/JuceFxLoop.cpp index 79bd005..4b1260e 100644 --- a/native_bridge/juce_fx/JuceFxLoop.cpp +++ b/native_bridge/juce_fx/JuceFxLoop.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -79,6 +80,23 @@ std::string extractChainJson(const std::string& s) { return s.substr(p, i - p + 1); } +// M5: base64 encode (RFC 4648) cho editor state capture -> file preset. +static std::string base64Encode(const std::string& in) { + static const char* tbl = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string out; + out.reserve(((in.size() + 2) / 3) * 4); + for (size_t i = 0; i < in.size(); i += 3) { + unsigned a = (unsigned char)in[i]; + unsigned b = i + 1 < in.size() ? (unsigned char)in[i + 1] : 0; + unsigned c = i + 2 < in.size() ? (unsigned char)in[i + 2] : 0; + out += tbl[a >> 2]; + out += tbl[((a & 3) << 4) | (b >> 4)]; + out += i + 1 < in.size() ? tbl[((b & 15) << 2) | (c >> 6)] : '='; + out += i + 2 < in.size() ? tbl[c & 63] : '='; + } + return out; +} } // namespace int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName, @@ -232,6 +250,37 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName, engine.requestEditor(slot, act, w, h); std::cerr << "[JuceFxLoop] ctrl " << key << " slot=" << slot << " w=" << w << " h=" << h << std::endl; + // M5: capture state editor (GUI đóng → preset về project như + // WM_FXGUI_CAPTURE cũ). PumpEditorQueue (GUI tick 30ms) thực thi + // Capture → captured[slot] (raw). Ctrl thread đợi tối đa 3s rồi + // base64 + ghi file JSON cạnh job: .preset.json + // {slot, preset_b64}. Python /fx-gui/close poll file này (xoá file + // cũ trước — chỉ đọc file MỚI cho capture lần này). + if (act == JuceFxEngine::EditorAction::Capture) { + const std::string capFile = jobPath + ".preset.json"; + std::remove(capFile.c_str()); + std::string raw; + for (int i = 0; i < 300 && raw.empty(); ++i) { + sleepMs(10); + if (!ipc->h.running || ipc->h.state != FXRT_STATE_READY) + break; + raw = engine.takeCapturedState(slot); + } + if (!raw.empty()) { + const std::string b64 = base64Encode(raw); + std::ofstream f(capFile, std::ios::binary); + f << "{\"slot\":" << slot << ",\"preset_b64\":\"" + << b64 << "\"}" << std::endl; + std::cerr << "[JuceFxLoop] capture slot=" << slot + << " -> " << capFile << " (" << raw.size() + << " bytes, " << b64.size() << " b64)" + << std::endl; + } else { + std::cerr << "[JuceFxLoop] capture slot=" << slot + << " rỗng — plugin không có state" << std::endl; + } + } + } sleepMs(2); }