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
+2 -8
View File
@@ -58,7 +58,6 @@ license gốc; chi tiết + attribution: `THIRD_PARTY_LICENSES.md`.
| mido | MIT | | mido | MIT |
| pyfluidsynth | MIT (wrapper — link FluidSynth **LGPL-2.1+**) | | pyfluidsynth | MIT (wrapper — link FluidSynth **LGPL-2.1+**) |
| sf2utils 1.0.0 | **LGPL-3.0+** (LICENSE.txt trong wheel; metadata PyPI ghi nhầm "GPLv3+") | | sf2utils 1.0.0 | **LGPL-3.0+** (LICENSE.txt trong wheel; metadata PyPI ghi nhầm "GPLv3+") |
| pedalboard 0.9.19 | **GPL-3.0** ⚠️ — xem mục 3 |
### 2.3. Native bridge (`native_bridge/`, C++) ### 2.3. Native bridge (`native_bridge/`, C++)
| Thành phần | License | | Thành phần | License |
@@ -83,15 +82,10 @@ license gốc; chi tiết + attribution: `THIRD_PARTY_LICENSES.md`.
## 3. Lưu ý pháp lý khi phân phối ## 3. Lưu ý pháp lý khi phân phối
1. **pedalboard (GPL-3.0)** — dùng runtime ở server (`app/core/render_engine.py`, 1. **sf2utils (LGPL-3.0+)** — dùng runtime (`app/core/soundfont_inspector.py`).
`app/core/vst_engine.py`, `app/api/v1/plugins.py`) cho render VST3.
GPL-3.0 copyleft → phân phối bản server gộp pedalboard có thể kéo toàn bộ
phần combined theo GPL. Cần: tách pedalboard ra subprocess/plugin riêng
(không import trong process MIT chính) hoặc thay thế bằng host khác.
2. **sf2utils (LGPL-3.0+)** — dùng runtime (`app/core/soundfont_inspector.py`).
LGPL cho phép app MIT dùng nếu giữ notice + (khi phân phối binary) cung cấp LGPL cho phép app MIT dùng nếu giữ notice + (khi phân phối binary) cung cấp
khả năng relink/object tương ứng. khả năng relink/object tương ứng.
3. **FluidSynth (LGPL-2.1+)** — vừa nhúng Emscripten (`app/static`) vừa link 2. **FluidSynth (LGPL-2.1+)** — vừa nhúng Emscripten (`app/static`) vừa link
tĩnh qua vcpkg (`native_bridge`). Bắt buộc: giữ license notice, cung cấp tĩnh qua vcpkg (`native_bridge`). Bắt buộc: giữ license notice, cung cấp
link/object relinkable khi phân phối bản build. link/object relinkable khi phân phối bản build.
4. **vst3sdk (MIT)** — tương thích hoàn toàn với dự án MIT. 4. **vst3sdk (MIT)** — tương thích hoàn toàn với dự án MIT.
+1 -1
View File
@@ -1,7 +1,7 @@
# PLAN: thay pedalboard (GPL-3.0) bằng native_bridge — render VSTi offline # PLAN: thay pedalboard (GPL-3.0) bằng native_bridge — render VSTi offline
Ngày: 2026-08-15 Ngày: 2026-08-15
Trạng thái: Phase 4 xong (engine.spec bỏ pedalboard; DecentSampler → hướng dẫn convert .dspreset→.vstpreset; Dockerfile build native_bridge Linux — sfizz build từ nguồn + sed fix sfizz.pc + bridge --render SF2/SFZ; Linux port: CMakeLists nhánh non-WIN32, main_linux.cpp, guard Windows-only NativeInstrumentEngine.cpp/RenderJob.cpp; verified WSL Ubuntu 24.04 — bridge Linux render SF2 ra WAV 44100 stereo peak 1575; test Python 115 pass 1 skip). Phase 5 còn lại: đảo flag mặc định bridge, gỡ pedalboard khỏi requirements/source, LICENSE. Trạng thái: HOÀN THÀNH — Phase 0-4 xong; Phase 5 xong (RENDER_ENGINE mặc định bridge; gỡ pedalboard khỏi requirements.txt + source (vst_engine/render_engine/plugins/runtime); midi_events_to_messages giữ làm utility thuần; LICENSE.md + THIRD_PARTY_LICENSES.md bỏ cảnh báo GPL-3.0; tests 114 pass, 0 skip). Docker image build chưa test được (máy không có docker) — recipe verify qua WSL.
## 1. Mục tiêu ## 1. Mục tiêu
Xóa dependency GPL-3.0 (`pedalboard`) khỏi cả 2 môi trường, chuyển toàn bộ Xóa dependency GPL-3.0 (`pedalboard`) khỏi cả 2 môi trường, chuyển toàn bộ
+4 -6
View File
@@ -30,7 +30,6 @@ Danh sách thành phần bên thứ ba + license. Bản tổng hợp: `LICENSE.m
| mido | MIT | OK | | mido | MIT | OK |
| pyfluidsynth | MIT (wrapper) | OK — link FluidSynth LGPL-2.1+ | | pyfluidsynth | MIT (wrapper) | OK — link FluidSynth LGPL-2.1+ |
| sf2utils 1.0.0 | **LGPL-3.0+** | ⚠️ LICENSE.txt trong wheel là LGPLv3 (metadata PyPI ghi nhầm "GPLv3+"). Dùng runtime `soundfont_inspector.py` — giữ notice + relinkable khi phân phối binary | | sf2utils 1.0.0 | **LGPL-3.0+** | ⚠️ LICENSE.txt trong wheel là LGPLv3 (metadata PyPI ghi nhầm "GPLv3+"). Dùng runtime `soundfont_inspector.py` — giữ notice + relinkable khi phân phối binary |
| pedalboard 0.9.19 | **GPL-3.0** | ⚠️ RỦI RO — dùng runtime render VST3 (render_engine/vst_engine/plugins). Copyleft mạnh → tách subprocess hoặc thay thế trước khi phân phối server |
## 1c. Native bridge (native_bridge/, C++) ## 1c. Native bridge (native_bridge/, C++)
| Thành phần | License | Ghi chú | | Thành phần | License | Ghi chú |
@@ -62,8 +61,7 @@ Danh sách thành phần bên thứ ba + license. Bản tổng hợp: `LICENSE.m
## KẾT LUẬN ## KẾT LUẬN
- **Không phát hiện vi phạm bản quyền rõ ràng** (không có code GPL bị nhúng vào project MIT; LGPL của FluidSynth tương thích nếu giữ notice). - **Không phát hiện vi phạm bản quyền rõ ràng** (không có code GPL bị nhúng vào project MIT; LGPL của FluidSynth tương thích nếu giữ notice).
- **Cần xử lý trước khi phân phối công khai:** - **Cần xử lý trước khi phân phối công khai:**
1. **pedalboard (GPL-3.0)** — dependency runtime server render VST3 → rủi ro copyleft cao nhất. Tách subprocess/plugin hoặc thay thế. 1. **sf2utils (LGPL-3.0+)** — giữ notice + relinkable object khi phân phối binary.
2. **sf2utils (LGPL-3.0+)** — giữ notice + relinkable object khi phân phối binary. 2. Bổ sung `LICENSE` text (MIT + LGPL-2.1) + attribution cho SpessaSynth (MIT notice) và Tailwind.
3. Bổ sung `LICENSE` text (MIT + LGPL-2.1) + attribution cho SpessaSynth (MIT notice) và Tailwind. 3. Ghi attribution SF2 Rich Nagel ("General MIDI SoundFont v3.0" — © 2006-2010 Rich 'Weeds' Nagel — Some rights reserved) hoặc thay soundfont khác license rõ.
4. Ghi attribution SF2 Rich Nagel ("General MIDI SoundFont v3.0 — © 2006-2010 Rich 'Weeds' Nagel — Some rights reserved") hoặc thay soundfont khác license rõ. 4. Không nhúng SGM-V2.01 vào bản phân phối (hạn chế redistribution).
5. Không nhúng SGM-V2.01 vào bản phân phối (hạn chế redistribution).
+17 -59
View File
@@ -6,7 +6,7 @@ from fastapi.responses import FileResponse
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, Any from typing import Optional, Any
from app.config import settings from app.config import settings
from app.core.vst_engine import PluginManager, HAS_PEDALBOARD, HAS_PYFLUIDSYNTH, apply_preset_to_plugin, render_soundfont_midi_to_audio, RENDER_LOCK, _render_locked from app.core.vst_engine import PluginManager, HAS_PYFLUIDSYNTH, render_soundfont_midi_to_audio, RENDER_LOCK, _render_locked
from app.core.render_engine import PythonRenderEngine, _find_sf2_path, _find_default_sf2 from app.core.render_engine import PythonRenderEngine, _find_sf2_path, _find_default_sf2
from app.core.soundfont_inspector import SoundFontInspector from app.core.soundfont_inspector import SoundFontInspector
from app.core.soundfont_converter import SoundFontConverter from app.core.soundfont_converter import SoundFontConverter
@@ -875,8 +875,8 @@ def _write_carla_project(plugin_name: str, plugin_path: str) -> str:
async def preview_instrument(req: PreviewRequest): async def preview_instrument(req: PreviewRequest):
"""Quick-render preview VSTi (âm thật, cùng code path với export).""" """Quick-render preview VSTi (âm thật, cùng code path với export)."""
# SF_RENDER_ENGINE=bridge: preview qua native_bridge, không cần pedalboard # SF_RENDER_ENGINE=bridge: preview qua native_bridge, không cần pedalboard
if settings.RENDER_ENGINE != "bridge" and not HAS_PEDALBOARD: if settings.RENDER_ENGINE != "bridge":
raise HTTPException(status_code=501, detail="pedalboard không khả dụng trên máy này") raise HTTPException(status_code=501, detail="Chỉ hỗ trợ native_bridge render — set SF_RENDER_ENGINE=bridge")
if not req.notes: if not req.notes:
raise HTTPException(status_code=400, detail="Chưa có nốt nhạc để preview") raise HTTPException(status_code=400, detail="Chưa có nốt nhạc để preview")
try: try:
@@ -969,8 +969,8 @@ async def midi_render(req: MidiRenderRequest, current_user: dict = Depends(get_c
pass pass
raise HTTPException( raise HTTPException(
status_code=500, status_code=500,
detail=f"Render MIDI thất bại: {e}. Nếu plugin là VST2 hoặc pedalboard " detail=f"Render MIDI thất bại: {e}. Nếu plugin VSTi không load được "
"không load được, hãy mở Carla Bridge (chọn VSTi, chỉnh âm, " "qua native_bridge, hãy mở Carla Bridge (chọn VSTi, chỉnh âm, "
"Save preset .vstpreset) rồi Upload preset vào track — render " "Save preset .vstpreset) rồi Upload preset vào track — render "
"sẽ dùng đúng âm đã chỉnh.", "sẽ dùng đúng âm đã chỉnh.",
) )
@@ -982,66 +982,24 @@ def _render_midi_notes_pedalboard(instrument_id: str, notes: list, bpm: float,
preset_data_b64=None, soundfont_bank=None, preset_data_b64=None, soundfont_bank=None,
soundfont_program=None) -> tuple: soundfont_program=None) -> tuple:
"""Render MIDI notes (VSTi + preset) → WAV trong PROCESSED_DIR — qua """Render MIDI notes (VSTi + preset) → WAV trong PROCESSED_DIR — qua
pedalboard (mặc định) hoặc native_bridge --render (SF_RENDER_ENGINE=bridge). native_bridge --render (SF_RENDER_ENGINE=bridge; pedalboard GPL-3.0 đã gỡ).
Trả (out_path, duration_sec). Ném HTTPException khi plugin không load được.""" Trả (out_path, duration_sec). Ném HTTPException khi plugin không load được."""
if settings.RENDER_ENGINE == "bridge": if settings.RENDER_ENGINE != "bridge":
from app.core import native_render raise HTTPException(status_code=501,
return native_render.render_offline( detail="Chỉ hỗ trợ native_bridge render — set SF_RENDER_ENGINE=bridge")
instrument_id=instrument_id, from app.core import native_render
notes=notes, return native_render.render_offline(
bpm=bpm, instrument_id=instrument_id,
sample_rate=sample_rate, notes=notes,
preset_id=preset_id, bpm=bpm,
preset_path=preset_path, sample_rate=sample_rate,
preset_data_b64=preset_data_b64,
soundfont_bank=soundfont_bank,
soundfont_program=soundfont_program,
)
if not HAS_PEDALBOARD:
raise HTTPException(status_code=501, detail="pedalboard không khả dụng trên máy này")
pm = PluginManager()
vst = pm.load_vst(instrument_id)
if vst is None:
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, hoặc mở Carla để load VST2 "
"(pedalboard chỉ render được VST3).",
)
apply_preset_to_plugin(
vst,
preset_id=preset_id, preset_id=preset_id,
preset_path=preset_path, preset_path=preset_path,
preset_data_b64=preset_data_b64, preset_data_b64=preset_data_b64,
soundfont_bank=soundfont_bank,
soundfont_program=soundfont_program,
) )
from pedalboard import Pedalboard
midi_events = []
for n in notes:
midi_events.append({
"note": int(n.get("pitch", 60)),
"start_beat": float(n.get("start_beat", 0)),
"duration_beats": float(n.get("duration_beats", 1)),
"velocity": int(float(n.get("velocity", 0.8)) * 127),
})
midi_messages = PluginManager.midi_events_to_messages(
midi_events, bpm, sample_rate,
bank=soundfont_bank, program=soundfont_program,
)
total_needed = 0
beat_sec = 60.0 / max(30.0, bpm)
for ev in midi_events:
end_sec = (ev["start_beat"] + ev["duration_beats"]) * beat_sec
if int(end_sec * sample_rate) > total_needed:
total_needed = int(end_sec * sample_rate)
total_needed = max(total_needed, 1024)
# pedalboard >= 0.9: Pedalboard container KHÔNG chứa instrument — gọi
# thẳng overload MIDI của plugin (overload 2: midi_messages + duration).
buf = vst(midi_messages, sample_rate=sample_rate,
duration=total_needed / float(sample_rate), num_channels=2)
out_path = os.path.join(settings.PROCESSED_DIR, f"preview_{uuid.uuid4().hex[:10]}.wav")
sf.write(out_path, buf.T, sample_rate)
return out_path, buf.shape[1] / float(sample_rate)
class SoundfontRenderRequest(BaseModel): class SoundfontRenderRequest(BaseModel):
+4 -5
View File
@@ -67,10 +67,9 @@ class Settings:
# storage (Windows desktop). # storage (Windows desktop).
PRESET_DIR: str = os.path.join(STORAGE_DIR, "presets") PRESET_DIR: str = os.path.join(STORAGE_DIR, "presets")
# Render engine offline (preview/export VSTi): 'pedalboard' (cũ, GPL-3.0) # Render engine offline (preview/export VSTi): 'bridge' (native_bridge
# hoặc 'bridge' (native_bridge --render, MIT). Mặc định pedalboard trong # --render, MIT). Giá trị khác → fallback synth (pedalboard GPL-3.0 đã gỡ
# giai đoạn chuyển; đảo sang 'bridge' ở phase 5 # phase 5 — PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md).
# (PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md). RENDER_ENGINE: str = os.getenv("SF_RENDER_ENGINE", "bridge")
RENDER_ENGINE: str = os.getenv("SF_RENDER_ENGINE", "pedalboard")
settings = Settings() settings = Settings()
+1 -1
View File
@@ -4,7 +4,7 @@
Thay thế pedalboard (GPL-3.0) bằng bridge (MIT) cho preview/export offline: 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 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 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 base64
import json import json
+3 -40
View File
@@ -6,10 +6,7 @@ import soundfile as sf
from app.config import settings from app.config import settings
from app.core.vst_engine import ( from app.core.vst_engine import (
render_midi_events_to_audio, render_midi_events_to_audio,
PluginManager,
HAS_PEDALBOARD,
HAS_PYFLUIDSYNTH, HAS_PYFLUIDSYNTH,
apply_preset_to_plugin,
RENDER_LOCK, RENDER_LOCK,
) )
@@ -199,9 +196,6 @@ class PythonRenderEngine:
if midi_events: if midi_events:
try: try:
plugin_mgr = PluginManager()
vst = plugin_mgr.load_vst(instrument_id) if instrument_id else None
if instrument_source == "pianobook": if instrument_source == "pianobook":
# Phase 4 (§3.4): .dspreset không hỗ trợ nữa (pedalboard # Phase 4 (§3.4): .dspreset không hỗ trợ nữa (pedalboard
# GPL-3.0 gỡ; spike load .dspreset fail). Chặn + fallback synth. # 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' midi_events=midi_events, sr=self.sample_rate, bpm=bpm, instrument='synth'
) )
elif instrument_id and not instrument_id.startswith("sf_"): elif instrument_id and not instrument_id.startswith("sf_"):
# VSTi track: native_bridge --render (SF_RENDER_ENGINE=bridge) # VSTi track: native_bridge --render (SF_RENDER_ENGINE=bridge).
# thay pedalboard; ngược lại giữ pedalboard path cũ. # Giá trị engine khác → fallback synth (pedalboard GPL-3.0 đã gỡ).
if settings.RENDER_ENGINE == "bridge": if settings.RENDER_ENGINE == "bridge":
try: try:
from app.core import native_render from app.core import native_render
@@ -251,38 +245,7 @@ class PythonRenderEngine:
synth_buffer = render_midi_events_to_audio( synth_buffer = render_midi_events_to_audio(
midi_events=midi_events, sr=self.sample_rate, bpm=bpm, instrument='synth' 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: else:
synth_buffer = render_midi_events_to_audio( synth_buffer = render_midi_events_to_audio(
midi_events=midi_events, sr=self.sample_rate, bpm=bpm, instrument='synth' 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 / # 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ý: # 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 + # - 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 # - Docker / Linux headless (server + browser UI): soundfont + VSTi mở được
# từ storage mount; preset upload qua browser; không có GUI local. # từ storage mount; preset upload qua browser; không có GUI local.
# #
# Ưu tiên: SF_RUNTIME env override > heuristic (platform + display + docker). # Ư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). # nhẹ và hoạt động trong mọi tiến trình (web/worker/celery).
import os import os
import sys import sys
@@ -374,13 +374,14 @@ def detect() -> dict:
def _vst_render_available() -> bool: def _vst_render_available() -> bool:
"""pedalboard có sẵn không (dùng find_spec — KHÔNG import pedalboard để """native_bridge có sẵn không: SF_BRIDGE_PATH trỏ file, hoặc exe trong
tránh kéo JUCE lib; giống pattern _module_available trong vst_engine).""" install/ (không import app.core.native_render để tránh kéo numpy — mirror
import importlib.util logic find_bridge_exe)."""
try: env = os.getenv("SF_BRIDGE_PATH", "").strip()
return importlib.util.find_spec("pedalboard") is not None if env and os.path.isfile(env):
except (ImportError, AttributeError, ValueError): return True
return False 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: def _lan_info() -> dict:
@@ -406,7 +407,7 @@ def capabilities() -> dict:
"preset_upload": True, # cả 2 mode đều upload preset được "preset_upload": True, # cả 2 mode đều upload preset được
"vst_render": d["vst_render"], "vst_render": d["vst_render"],
"tauri_bridge": d["tauri_bridge"], "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. # 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", "preview_mode": "quick_render" if d["vst_render"] else "wasm",
} }
+7 -38
View File
@@ -6,7 +6,7 @@ import functools
import threading import threading
from ctypes import c_char_p 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() RENDER_LOCK = threading.RLock()
@@ -166,14 +166,9 @@ def _module_available(name: str) -> bool:
return False return False
def check_pedalboard_safe():
return _module_available("pedalboard")
def check_pyfluidsynth_safe(): def check_pyfluidsynth_safe():
return _module_available("fluidsynth") return _module_available("fluidsynth")
HAS_PEDALBOARD = check_pedalboard_safe()
HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe() HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe()
def ensure_pyfluidsynth(): def ensure_pyfluidsynth():
@@ -182,12 +177,6 @@ def ensure_pyfluidsynth():
HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe() HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe()
return HAS_PYFLUIDSYNTH return HAS_PYFLUIDSYNTH
if HAS_PEDALBOARD:
try:
from pedalboard import VST3Plugin, Pedalboard, Gain
except Exception:
HAS_PEDALBOARD = False
if HAS_PYFLUIDSYNTH: if HAS_PYFLUIDSYNTH:
try: try:
import fluidsynth 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. """Singleton: reuse PluginManager when args match, else create new.
Default dirs từ settings (env/.env/docker-compose hoặc user override). 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 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 from app.config import settings as _st
vst_dir = vst_dir or _st.VST_DIR vst_dir = vst_dir or _st.VST_DIR
sf_dir = sf_dir or _st.SOUNDFONT_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.sf_dir = sf_dir or _st.SOUNDFONT_DIR
self.upload_sf_dir = upload_sf_dir self.upload_sf_dir = upload_sf_dir
# Thư mục VST thêm (plugin_dirs user scan trong Plugin Manager) — # 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). # 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.extra_vst_dirs = [d for d in (extra_vst_dirs or []) if d]
self._sf_scan_cache = None # cache for _scan_soundfonts() self._sf_scan_cache = None # cache for _scan_soundfonts()
@@ -351,23 +340,6 @@ class PluginManager:
} }
return list(sf_map.values()) 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): def _scan_soundfonts_cached(self):
if self._sf_scan_cache is not None: if self._sf_scan_cache is not None:
return self._sf_scan_cache return self._sf_scan_cache
@@ -475,7 +447,7 @@ class PluginManager:
"vst_instruments": [ "vst_instruments": [
{"id": k, "name": k, {"id": k, "name": k,
"type": "VST3" if p.lower().endswith(".vst3") else "VST2", "type": "VST3" if p.lower().endswith(".vst3") else "VST2",
"has_native_support": HAS_PEDALBOARD} "has_native_support": True}
for k, p in plugins.items() for k, p in plugins.items()
], ],
"soundfonts": self._scan_soundfonts() "soundfonts": self._scan_soundfonts()
@@ -483,12 +455,9 @@ class PluginManager:
@staticmethod @staticmethod
def midi_events_to_messages(midi_events: list, bpm: float, sr: int, bank: int = None, program: int = None) -> list: 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) beat_duration_sec = 60.0 / max(30.0, bpm)
# pedalboard >= 0.9: MIDI messages là tuple (bytes, timestamp_seconds) # MIDI message = (bytes raw MIDI, timestamp_seconds). bytes là raw MIDI
# (MidiMessage class đã bị bỏ). bytes là raw MIDI event: # event: 0x9n note_on, 0x8n note_off, 0xB0 control_change, 0xC0 program_change
# 0x9n note_on, 0x8n note_off, 0xB0 control_change, 0xC0 program_change
messages = [] messages = []
if bank is not None: if bank is not None:
messages.append((bytes([0xB0, 0, int(bank) & 0x7F]), 0.0)) 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 "" return ""
def apply_preset_to_plugin(plugin, preset_id=None, preset_path=None, preset_data_b64=None) -> bool: 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 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).""" áp dụng được; KHÔNG raise (render engine chỉ log warning)."""
if plugin is None: if plugin is None:
-1
View File
@@ -11,7 +11,6 @@ soundfile>=0.12.1
jinja2>=3.1.2 jinja2>=3.1.2
httpx>=0.24.0 httpx>=0.24.0
jsonschema>=4.18.0 jsonschema>=4.18.0
pedalboard==0.9.19
mido>=1.3.0 mido>=1.3.0
pyfluidsynth>=1.3.0 pyfluidsynth>=1.3.0
sf2utils>=0.9.0 sf2utils>=0.9.0
-24
View File
@@ -66,27 +66,3 @@ def test_plugins_bridge_branch_dispatch(monkeypatch):
with pytest.raises(HTTPException) as ei: with pytest.raises(HTTPException) as ei:
pl._render_midi_notes_pedalboard("__no_such__", [], bpm=120.0, sample_rate=44100) pl._render_midi_notes_pedalboard("__no_such__", [], bpm=120.0, sample_rate=44100)
assert ei.value.status_code == 404 assert ei.value.status_code == 404
def test_bridge_matches_pedalboard_rms():
"""Golden so sánh (plan §6): bridge vs pedalboard cùng job Nexus → RMS
trong tolerance. Skip nếu máy thiếu pedalboard hoặc Nexus VST3."""
from app.core.vst_engine import HAS_PEDALBOARD, PluginManager
if not HAS_PEDALBOARD:
pytest.skip("pedalboard not installed")
itype, path = native_render._resolve_instrument("Nexus")
if not path:
pytest.skip("Nexus VST3 not installed")
notes = [{"pitch": 60, "velocity": 0.8, "start_beat": 0.0, "duration_beats": 1.0}]
out_b, dur_b = native_render.render_offline("Nexus", notes, bpm=120.0, sample_rate=44100)
vst = PluginManager().load_vst("Nexus")
if vst is None:
pytest.skip("pedalboard cannot load Nexus")
ev = [{"note": 60, "start_beat": 0.0, "duration_beats": 1.0, "velocity": 101}]
msgs = PluginManager.midi_events_to_messages(ev, 120.0, 44100)
buf = vst(msgs, sample_rate=44100, duration=dur_b, num_channels=2)
import numpy as np
audio = sf.read(out_b)[0]
rms_b = float(np.sqrt((audio ** 2).mean()))
rms_p = float(np.sqrt((np.asarray(buf.T) ** 2).mean()))
assert abs(rms_b - rms_p) < 0.01, f"bridge={rms_b:.5f} pedalboard={rms_p:.5f}"
-15
View File
@@ -5,7 +5,6 @@ from fastapi.testclient import TestClient
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
from app.main import app from app.main import app
from app.core.vst_engine import HAS_PEDALBOARD, HAS_PYFLUIDSYNTH
client = TestClient(app) client = TestClient(app)
@@ -176,20 +175,6 @@ class TestMidiRenderVSTi:
assert resp.status_code == 404 assert resp.status_code == 404
assert "VSTi" in resp.json()["detail"] assert "VSTi" in resp.json()["detail"]
def test_midi_render_requires_pedalboard(self):
# Nếu pedalboard thiếu → 501 (không crash)
import app.api.v1.plugins as plugins_mod
token = get_admin_token()
if not token:
pytest.skip("Cannot get admin token")
if not plugins_mod.HAS_PEDALBOARD:
resp = client.post("/api/v1/plugins/midi-render", headers={"Authorization": f"Bearer {token}"},
json={"instrument_id": "x",
"notes": [{"pitch": 60, "start_beat": 0, "duration_beats": 1}]})
assert resp.status_code == 501
else:
pytest.skip("pedalboard present — render path covered by unknown-instrument test")
def test_carla_play_notes_requires_auth(self): def test_carla_play_notes_requires_auth(self):
client.cookies.clear() # TestClient giữ cookie login từ test trước client.cookies.clear() # TestClient giữ cookie login từ test trước
resp = client.post("/api/v1/plugins/carla-play-notes", resp = client.post("/api/v1/plugins/carla-play-notes",
+4 -4
View File
@@ -56,8 +56,8 @@ def test_render_session_vsti_bridge_unknown_instrument_falls_back(monkeypatch):
assert (buf != 0.0).any() assert (buf != 0.0).any()
def test_render_session_vsti_pedalboard_default_falls_back(monkeypatch): def test_render_session_vsti_non_bridge_engine_falls_back(monkeypatch):
"""Mặc định pedalboard: không có plugin load được → oscillator (không crash).""" """Engine không phải bridge (vd 'pedalboard' cũ): VSTi → fallback oscillator (không crash)."""
from app.api.v1 import plugins as pl from app.api.v1 import plugins as pl
monkeypatch.setattr(pl.settings, "RENDER_ENGINE", "pedalboard") monkeypatch.setattr(pl.settings, "RENDER_ENGINE", "pedalboard")
engine = PythonRenderEngine() engine = PythonRenderEngine()
@@ -69,7 +69,7 @@ def test_render_session_vsti_pedalboard_default_falls_back(monkeypatch):
def test_render_session_fx_chorus_scipy_fallback(): def test_render_session_fx_chorus_scipy_fallback():
"""FX chorus: fallback scipy (không pedalboard) không crash, thay đổi âm.""" """FX chorus: fallback scipy (không pedalboard) không crash, thay đổi âm."""
from app.api.v1 import plugins as pl from app.api.v1 import plugins as pl
pl.settings.RENDER_ENGINE = "pedalboard" pl.settings.RENDER_ENGINE = "bridge"
engine = PythonRenderEngine() engine = PythonRenderEngine()
session = {"tracks": [_midi_track("__no_such_plugin__", fx_type="chorus")]} session = {"tracks": [_midi_track("__no_such_plugin__", fx_type="chorus")]}
buf = engine.render_session_container(session, {}, 120.0, 4, 44100) buf = engine.render_session_container(session, {}, 120.0, 4, 44100)
@@ -80,7 +80,7 @@ def test_render_session_fx_chorus_scipy_fallback():
def test_render_session_fx_reverb_scipy_fallback(): def test_render_session_fx_reverb_scipy_fallback():
"""FX reverb: fallback scipy (không pedalboard) không crash, thay đổi âm.""" """FX reverb: fallback scipy (không pedalboard) không crash, thay đổi âm."""
from app.api.v1 import plugins as pl from app.api.v1 import plugins as pl
pl.settings.RENDER_ENGINE = "pedalboard" pl.settings.RENDER_ENGINE = "bridge"
engine = PythonRenderEngine() engine = PythonRenderEngine()
session = {"tracks": [_midi_track("__no_such_plugin__", fx_type="reverb")]} session = {"tracks": [_midi_track("__no_such_plugin__", fx_type="reverb")]}
buf = engine.render_session_container(session, {}, 120.0, 4, 44100) buf = engine.render_session_container(session, {}, 120.0, 4, 44100)
+1 -3
View File
@@ -4,7 +4,7 @@ import pytest
import numpy as np import numpy as np
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
from app.core.vst_engine import PluginManager, midi_note_to_freq, render_midi_events_to_audio, HAS_PEDALBOARD from app.core.vst_engine import PluginManager, midi_note_to_freq, render_midi_events_to_audio
class TestPluginManager: class TestPluginManager:
@@ -20,8 +20,6 @@ class TestPluginManager:
assert pm.sf_dir == "/opt/daw_engine/soundfonts" assert pm.sf_dir == "/opt/daw_engine/soundfonts"
def test_midi_events_to_messages(self): def test_midi_events_to_messages(self):
if not HAS_PEDALBOARD:
pytest.skip("pedalboard not available")
events = [{"note": 60, "start_beat": 0, "duration_beats": 1, "velocity": 100}] events = [{"note": 60, "start_beat": 0, "duration_beats": 1, "velocity": 100}]
bpm = 120 bpm = 120
sr = 44100 sr = 44100