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
This commit is contained in:
2026-08-17 12:58:51 +07:00
parent e79dae5d5c
commit dc331f39d8
14 changed files with 55 additions and 215 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
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.
(settings.RENDER_ENGINE); mặc định bridge từ phase 5.
"""
import base64
import json
+3 -40
View File
@@ -6,10 +6,7 @@ import soundfile as sf
from app.config import settings
from app.core.vst_engine import (
render_midi_events_to_audio,
PluginManager,
HAS_PEDALBOARD,
HAS_PYFLUIDSYNTH,
apply_preset_to_plugin,
RENDER_LOCK,
)
@@ -199,9 +196,6 @@ class PythonRenderEngine:
if midi_events:
try:
plugin_mgr = PluginManager()
vst = plugin_mgr.load_vst(instrument_id) if instrument_id else None
if instrument_source == "pianobook":
# Phase 4 (§3.4): .dspreset không hỗ trợ nữa (pedalboard
# GPL-3.0 gỡ; spike load .dspreset fail). Chặn + fallback synth.
@@ -214,8 +208,8 @@ class PythonRenderEngine:
midi_events=midi_events, sr=self.sample_rate, bpm=bpm, instrument='synth'
)
elif instrument_id and not instrument_id.startswith("sf_"):
# VSTi track: native_bridge --render (SF_RENDER_ENGINE=bridge)
# thay pedalboard; ngược lại giữ pedalboard path cũ.
# VSTi track: native_bridge --render (SF_RENDER_ENGINE=bridge).
# Giá trị engine khác → fallback synth (pedalboard GPL-3.0 đã gỡ).
if settings.RENDER_ENGINE == "bridge":
try:
from app.core import native_render
@@ -251,38 +245,7 @@ class PythonRenderEngine:
synth_buffer = render_midi_events_to_audio(
midi_events=midi_events, sr=self.sample_rate, bpm=bpm, instrument='synth'
)
elif vst and HAS_PEDALBOARD:
from pedalboard import Pedalboard
# Preset bridge Carla → pedalboard: preset_id
# (thư viện storage/presets), preset_path (file)
# hoặc preset_data (base64 .vstpreset nhúng trong
# project). Cùng sample rate → âm render = âm đã
# chỉnh trong Carla.
try:
se2 = track.get("synth_engine", {}) or {}
apply_preset_to_plugin(
vst,
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"),
)
except Exception as e:
logger.warning("[RenderEngine] Preset apply failed: %s", e)
midi_messages = PluginManager.midi_events_to_messages(
midi_events, bpm, self.sample_rate,
bank=soundfont_bank, program=soundfont_program
)
total_needed = 0
for ev in midi_events:
end_sec = (ev.get("start_beat", 0) + ev.get("duration_beats", 1)) * (60.0 / bpm)
dur_samples = int(end_sec * self.sample_rate)
if dur_samples > total_needed:
total_needed = dur_samples
total_needed = max(total_needed, 1024)
silent = np.zeros((2, total_needed), dtype=np.float32)
# pedalboard >= 0.9: instrument KHÔNG vào Pedalboard container
synth_buffer = vst(midi_messages, sample_rate=self.sample_rate,
duration=total_needed / float(self.sample_rate), num_channels=2)
else:
synth_buffer = render_midi_events_to_audio(
midi_events=midi_events, sr=self.sample_rate, bpm=bpm, instrument='synth'
+11 -10
View File
@@ -1,12 +1,12 @@
# SonicForge Runtime Profile — phát hiện môi trường chạy (desktop Windows /
# docker headless) và khả năng của máy, để app TỰ CHỌN cách xử lý:
# - Windows desktop (server + client cùng 1 máy): Carla local để preview +
# chỉnh preset → pedalboard render (Hướng A)
# chỉnh preset → native_bridge render (Hướng A)
# - Docker / Linux headless (server + browser UI): soundfont + VSTi mở được
# từ storage mount; preset upload qua browser; không có GUI local.
#
# Ưu tiên: SF_RUNTIME env override > heuristic (platform + display + docker).
# Module CHỈ dùng stdlib + app.config (không kéo pedalboard/numpy) để import
# Module CHỈ dùng stdlib + app.config (không kéo numpy) để import
# nhẹ và hoạt động trong mọi tiến trình (web/worker/celery).
import os
import sys
@@ -374,13 +374,14 @@ def detect() -> dict:
def _vst_render_available() -> bool:
"""pedalboard có sẵn không (dùng find_spec — KHÔNG import pedalboard để
tránh kéo JUCE lib; giống pattern _module_available trong vst_engine)."""
import importlib.util
try:
return importlib.util.find_spec("pedalboard") is not None
except (ImportError, AttributeError, ValueError):
return False
"""native_bridge có sẵn không: SF_BRIDGE_PATH trỏ file, hoặc exe trong
install/ (không import app.core.native_render để tránh kéo numpy — mirror
logic find_bridge_exe)."""
env = os.getenv("SF_BRIDGE_PATH", "").strip()
if env and os.path.isfile(env):
return True
exe = "daw_vst_bridge.exe" if os.name == "nt" else "daw_vst_bridge"
return os.path.isfile(os.path.join(settings.BASE_DIR, "install", exe))
def _lan_info() -> dict:
@@ -406,7 +407,7 @@ def capabilities() -> dict:
"preset_upload": True, # cả 2 mode đều upload preset được
"vst_render": d["vst_render"],
"tauri_bridge": d["tauri_bridge"],
# preview VSTi: quick_render (pedalboard render clip ngắn — âm thật,
# preview VSTi: quick_render (native_bridge render clip ngắn — âm thật,
# giống export) là chuẩn cho cả 2 mode; không có realtime trong web UI.
"preview_mode": "quick_render" if d["vst_render"] else "wasm",
}
+7 -38
View File
@@ -6,7 +6,7 @@ import functools
import threading
from ctypes import c_char_p
# pedalboard VST3 + pyfluidsynth KHÔNG thread-safe: tuần tự hóa mọi native render
# Native render (bridge/pyfluidsynth) không thread-safe: tuần tự hóa
RENDER_LOCK = threading.RLock()
@@ -166,14 +166,9 @@ def _module_available(name: str) -> bool:
return False
def check_pedalboard_safe():
return _module_available("pedalboard")
def check_pyfluidsynth_safe():
return _module_available("fluidsynth")
HAS_PEDALBOARD = check_pedalboard_safe()
HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe()
def ensure_pyfluidsynth():
@@ -182,12 +177,6 @@ def ensure_pyfluidsynth():
HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe()
return HAS_PYFLUIDSYNTH
if HAS_PEDALBOARD:
try:
from pedalboard import VST3Plugin, Pedalboard, Gain
except Exception:
HAS_PEDALBOARD = False
if HAS_PYFLUIDSYNTH:
try:
import fluidsynth
@@ -221,7 +210,7 @@ def get_plugin_manager(vst_dir=None, sf_dir=None, upload_sf_dir=None) -> "Plugin
"""Singleton: reuse PluginManager when args match, else create new.
Default dirs từ settings (env/.env/docker-compose hoặc user override).
VST scan gộp thêm plugin_dirs user (Plugin Manager) — nút Synth phải liệt
kê được VSTi đã scan và load_vst phải tìm thấy chúng khi render."""
kê được VSTi đã scan."""
from app.config import settings as _st
vst_dir = vst_dir or _st.VST_DIR
sf_dir = sf_dir or _st.SOUNDFONT_DIR
@@ -287,7 +276,7 @@ class PluginManager:
self.sf_dir = sf_dir or _st.SOUNDFONT_DIR
self.upload_sf_dir = upload_sf_dir
# Thư mục VST thêm (plugin_dirs user scan trong Plugin Manager) —
# list_available()/load_vst phải thấy VSTi user đã scan (bug: nút
# list_available() phải thấy VSTi user đã scan (bug: nút
# Synth chỉ quét vst_dir env mặc định /opt/daw_engine/vst3).
self.extra_vst_dirs = [d for d in (extra_vst_dirs or []) if d]
self._sf_scan_cache = None # cache for _scan_soundfonts()
@@ -351,23 +340,6 @@ class PluginManager:
}
return list(sf_map.values())
@_render_locked
def load_vst(self, plugin_name: str, preset_data: dict = None):
if not HAS_PEDALBOARD:
return None
plugins = self._scan_plugins()
if plugin_name not in plugins:
return None
path = plugins[plugin_name]
vst = VST3Plugin(path)
if preset_data:
for k, v in preset_data.items():
try:
setattr(vst, k, v)
except Exception:
pass
return vst
def _scan_soundfonts_cached(self):
if self._sf_scan_cache is not None:
return self._sf_scan_cache
@@ -475,7 +447,7 @@ class PluginManager:
"vst_instruments": [
{"id": k, "name": k,
"type": "VST3" if p.lower().endswith(".vst3") else "VST2",
"has_native_support": HAS_PEDALBOARD}
"has_native_support": True}
for k, p in plugins.items()
],
"soundfonts": self._scan_soundfonts()
@@ -483,12 +455,9 @@ class PluginManager:
@staticmethod
def midi_events_to_messages(midi_events: list, bpm: float, sr: int, bank: int = None, program: int = None) -> list:
if not HAS_PEDALBOARD:
return []
beat_duration_sec = 60.0 / max(30.0, bpm)
# pedalboard >= 0.9: MIDI messages là tuple (bytes, timestamp_seconds)
# (MidiMessage class đã bị bỏ). bytes là raw MIDI event:
# 0x9n note_on, 0x8n note_off, 0xB0 control_change, 0xC0 program_change
# MIDI message = (bytes raw MIDI, timestamp_seconds). bytes là raw MIDI
# event: 0x9n note_on, 0x8n note_off, 0xB0 control_change, 0xC0 program_change
messages = []
if bank is not None:
messages.append((bytes([0xB0, 0, int(bank) & 0x7F]), 0.0))
@@ -607,7 +576,7 @@ def resolve_plugin_path(name_or_path: str, ext: str = "") -> str:
return ""
def apply_preset_to_plugin(plugin, preset_id=None, preset_path=None, preset_data_b64=None) -> bool:
"""Gán preset lên plugin pedalboard: bytes nhúng (base64 .vstpreset) ưu
"""Gán preset lên plugin VSTi: bytes nhúng (base64 .vstpreset) ưu
tiên, sau đó preset_id (thư viện), sau preset_path (file). Trả True nếu
áp dụng được; KHÔNG raise (render engine chỉ log warning)."""
if plugin is None: