# app/core/native_render.py """Render MIDI notes → WAV qua native_bridge CLI (daw_vst_bridge --render). Thay thế pedalboard (GPL-3.0) bằng bridge (MIT) cho preview/export offline: cùng chữ ký (out_path, duration_sec) nên plugins.py/render_engine thay trực tiếp. KHÔNG import pedalboard. Kích hoạt bằng SF_RENDER_ENGINE=bridge (settings.RENDER_ENGINE); mặc định pedalboard trong giai đoạn chuyển. """ import base64 import json import os import subprocess import sys import uuid import soundfile as sf from fastapi import HTTPException from app.config import settings BRIDGE_EXE = "daw_vst_bridge.exe" if os.name == "nt" else "daw_vst_bridge" def find_bridge_exe() -> str: """Đường dẫn bridge exe: env SF_BRIDGE_PATH → bundle (frozen) → install/ (dev).""" env = os.getenv("SF_BRIDGE_PATH", "").strip() if env and os.path.isfile(env): return env cands = [] if getattr(sys, "frozen", False): root = getattr(sys, "_MEIPASS", "") if root: cands += [os.path.join(root, "binaries", BRIDGE_EXE), os.path.join(root, BRIDGE_EXE)] cands.append(os.path.join(settings.BASE_DIR, "install", BRIDGE_EXE)) for p in cands: if p and os.path.isfile(p): return p return "" def classify_instrument(path: str) -> int: """InstrumentType enum của bridge: 0=VST3, 1=VST2, 2=SF2/SF3, 3=SFZ.""" low = path.lower() if low.endswith(".vst3"): return 0 if low.endswith((".dll", ".so")): return 1 if low.endswith((".sf2", ".sf3")): return 2 if low.endswith(".sfz"): return 3 return 0 def _resolve_instrument(instrument_id: str): from app.core.vst_engine import PluginManager path = PluginManager()._scan_plugins().get(instrument_id) if not path: return None, None return classify_instrument(path), path def _preset_b64(preset_id, preset_path, preset_data_b64) -> str: """Base64 .vstpreset cho job: bytes nhúng ưu tiên, sau đó id/path file.""" if preset_data_b64: return preset_data_b64 from app.core.vst_engine import resolve_preset_path p = resolve_preset_path(preset_id or "") or resolve_preset_path(preset_path or "") if p: try: with open(p, "rb") as fh: return base64.b64encode(fh.read()).decode("ascii") except Exception: return "" return "" def render_offline(instrument_id, notes, bpm, sample_rate, preset_id=None, preset_path=None, preset_data_b64=None) -> tuple: """Render MIDI notes qua native_bridge --render → (out_path, duration_sec). Giữ response shape của _render_midi_notes_pedalboard. Lỗi → HTTPException (404 instrument lạ, 501 thiếu bridge, 500 lỗi render — file rác bị xóa). """ itype, path = _resolve_instrument(instrument_id) if not path: raise HTTPException( status_code=404, detail=f"Không tìm thấy VSTi: {instrument_id} — chưa scan thấy plugin này. " "Kiểm tra Plugins Manager → Scan.", ) exe = find_bridge_exe() if not exe: raise HTTPException( status_code=501, detail="native_bridge không khả dụng — đặt SF_BRIDGE_PATH hoặc cài daw_vst_bridge", ) job = { "instrument_type": itype, "plugin_path": path, "sample_rate": int(sample_rate), "bpm": float(bpm), "notes": [{ "pitch": int(n.get("pitch", 60)), "velocity": int(float(n.get("velocity", 0.8)) * 127), "start_beat": float(n.get("start_beat", 0)), "duration_beats": float(n.get("duration_beats", 1)), } for n in notes], } if itype == 0: # preset chỉ áp dụng cho VST3 (loadSerializedState) preset = _preset_b64(preset_id, preset_path, preset_data_b64) if preset: job["preset"] = preset os.makedirs(settings.PROCESSED_DIR, exist_ok=True) out_path = os.path.join(settings.PROCESSED_DIR, f"preview_{uuid.uuid4().hex[:10]}.wav") job_path = out_path + ".job.json" try: with open(job_path, "w", encoding="utf-8") as fh: json.dump(job, fh) r = subprocess.run([exe, "--render", job_path, "--out", out_path], capture_output=True, text=True, timeout=120) log = ((r.stdout or "") + (r.stderr or "")).strip() if r.returncode != 0 or not os.path.isfile(out_path): if os.path.exists(out_path): try: os.remove(out_path) except Exception: pass if r.returncode == 2: raise HTTPException(status_code=500, detail=f"Bridge không load được plugin {instrument_id}. {log[-500:]}") raise HTTPException(status_code=500, detail=f"Render thất bại (rc={r.returncode}). {log[-500:]}") return out_path, sf.info(out_path).frames / float(sample_rate) except HTTPException: raise except subprocess.TimeoutExpired: if os.path.exists(out_path): try: os.remove(out_path) except Exception: pass raise HTTPException(status_code=500, detail="Render quá lâu (timeout 120s)") except Exception as e: if os.path.exists(out_path): try: os.remove(out_path) except Exception: pass raise HTTPException(status_code=500, detail=f"Render thất bại: {e}") finally: if os.path.exists(job_path): try: os.remove(job_path) except Exception: pass