From 57fa2e78fef03560a1b49930bec81208202b67f6 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Tue, 25 Aug 2026 21:14:48 +0700 Subject: [PATCH] fix(export): carry bridge-captured VSTi preset into offline render Offline render tao bridge moi khong co preset -> plugin load init patch -> am khac realtime (user tuong fallback). Gio doc presetBase64 tu bridge_state.json (channel == track.id, plugin_path khop basename) va truyen vao render_offline khi track khong nhung preset_data. --- app/core/native_render.py | 37 +++++++++++++++++++++++++++++++++++++ app/core/render_engine.py | 14 +++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/app/core/native_render.py b/app/core/native_render.py index 5cacbc4..77359dc 100644 --- a/app/core/native_render.py +++ b/app/core/native_render.py @@ -137,6 +137,43 @@ def _preset_b64(preset_id, preset_path, preset_data_b64) -> str: return "" +def bridge_state_preset_b64(channel=None, plugin_path=None) -> str: + """presetBase64 từ bridge_state.json — bridge C++ đang giữ preset user + chỉnh realtime (capture mỗi ~2s). Export tạo bridge mới không có preset → + plugin load init patch → âm khác realtime (user tưởng "fallback"). Fallback + này nối: dùng preset mà bridge đã capture cho channel đó. + + Match: channel == int(track.id) (scheduleMidiNoteDispatch pushEvent + channel: trkId) + path khớp plugin (basename, case-insensitive). Không tìm + thấy → "" (giữ hành vi cũ — không đoán bừa preset channel khác).""" + try: + st_path = os.path.join(os.environ.get("APPDATA", ""), "SonicForgeDAW", + "state", "bridge_state.json") + if not os.path.isfile(st_path): + return "" + with open(st_path, encoding="utf-8", errors="replace") as fh: + st = json.load(fh) + try: + ch = int(channel) if channel is not None else None + except Exception: + ch = None + want = None + if plugin_path: + want = os.path.basename(os.fspath(plugin_path)).lower() + for ins in st.get("instruments") or []: + if ch is not None and ins.get("channel") != ch: + continue + ip = ins.get("path") or "" + if want and os.path.basename(ip).lower() != want: + continue + pb = ins.get("presetBase64") or "" + if pb: + return pb + except Exception: + pass + return "" + + def _run_notes_job(job: dict, label: str) -> tuple: """Chạy job MIDI render qua native_bridge --render → (out_path, duration_sec). diff --git a/app/core/render_engine.py b/app/core/render_engine.py index e7dde06..20f5f85 100644 --- a/app/core/render_engine.py +++ b/app/core/render_engine.py @@ -709,6 +709,18 @@ class PythonRenderEngine: "start_beat": ev["start_beat"], "duration_beats": ev["duration_beats"], } for ev in midi_events] + # preset_data rỗng → fallback preset + # bridge đang giữ (bridge_state.json) — + # export tạo bridge mới init patch ≠ âm + # realtime user đang nghe. + preset_b64 = (se2.get("preset_data") + or track.get("preset_data")) + if not preset_b64: + preset_b64 = native_render.bridge_state_preset_b64( + channel=track.get("id"), + plugin_path=(se2.get("plugin_path") + or track.get("plugin_path")), + ) out_path, _dur = native_render.render_offline( instrument_id=instrument_id, notes=bridge_notes, @@ -716,7 +728,7 @@ class PythonRenderEngine: sample_rate=self.sample_rate, preset_id=se2.get("preset_id") or track.get("preset_id"), preset_path=se2.get("preset_path") or track.get("preset_path"), - preset_data_b64=se2.get("preset_data") or track.get("preset_data"), + preset_data_b64=preset_b64, soundfont_bank=soundfont_bank, soundfont_program=soundfont_program, plugin_path=se2.get("plugin_path") or track.get("plugin_path"),