158 lines
5.7 KiB
Python
158 lines
5.7 KiB
Python
# 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
|