Files
SonicForgeStudio/app/core/native_render.py
T
admin dc331f39d8 phase5: mặc định RENDER_ENGINE=bridge, gỡ pedalboard khỏi requirements/source/LICENSE
- config.py: SF_RENDER_ENGINE default 'bridge' (pedalboard GPL-3.0 đã gỡ)
- requirements.txt: bỏ pedalboard==0.9.19
- vst_engine.py: bỏ HAS_PEDALBOARD/check_pedalboard_safe/load_vst/import pedalboard;
  midi_events_to_messages giữ làm utility thuần (không gate pedalboard)
- render_engine.py: bỏ nhánh elif vst and HAS_PEDALBOARD + import thừa
- plugins.py: preview/midi-render chỉ qua native_bridge; guard SF_RENDER_ENGINE=bridge
- runtime.py: _vst_render_available() check bridge exe (SF_BRIDGE_PATH/install/) thay find_spec('pedalboard')
- LICENSE.md/THIRD_PARTY_LICENSES.md: bỏ cảnh báo GPL-3.0 pedalboard, đánh số lại
- tests: bỏ test golden so pedalboard + test requires_pedalboard; 114 passed 0 skip
2026-08-17 12:58:51 +07:00

165 lines
6.2 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 bridge từ phase 5.
"""
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,
soundfont_bank=None, soundfont_program=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": float(n.get("velocity", 0.8)), # bridge job: float 0..1 (RenderJob.cpp *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
# bank/program (giữ parity với midi_events_to_messages cũ: key có mặt
# kể cả giá trị 0 — bridge xử lý theo presence, không theo truthiness)
if soundfont_bank is not None:
job["soundfont_bank"] = int(soundfont_bank) & 0x7F
if soundfont_program is not None:
job["soundfont_program"] = int(soundfont_program) & 0x7F
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