From dc331f39d8c3bd73403db99ff1a2bfe3a3d8c08c Mon Sep 17 00:00:00 2001 From: locphamtran Date: Mon, 17 Aug 2026 12:58:51 +0700 Subject: [PATCH] =?UTF-8?q?phase5:=20m=E1=BA=B7c=20=C4=91=E1=BB=8Bnh=20REN?= =?UTF-8?q?DER=5FENGINE=3Dbridge,=20g=E1=BB=A1=20pedalboard=20kh=E1=BB=8Fi?= =?UTF-8?q?=20requirements/source/LICENSE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- LICENSE.md | 10 +--- PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md | 2 +- THIRD_PARTY_LICENSES.md | 10 ++-- app/api/v1/plugins.py | 76 ++++++------------------ app/config.py | 9 ++- app/core/native_render.py | 2 +- app/core/render_engine.py | 43 +------------- app/core/runtime.py | 21 +++---- app/core/vst_engine.py | 45 +++----------- requirements.txt | 1 - tests/test_native_render.py | 24 -------- tests/test_plugin_api.py | 15 ----- tests/test_render_engine.py | 8 +-- tests/test_vst_engine.py | 4 +- 14 files changed, 55 insertions(+), 215 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index d6a112a..6e1ac2a 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -58,7 +58,6 @@ license gốc; chi tiết + attribution: `THIRD_PARTY_LICENSES.md`. | mido | MIT | | 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+") | -| pedalboard 0.9.19 | **GPL-3.0** ⚠️ — xem mục 3 | ### 2.3. Native bridge (`native_bridge/`, C++) | 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 -1. **pedalboard (GPL-3.0)** — dùng runtime ở server (`app/core/render_engine.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`). +1. **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 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 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. diff --git a/PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md b/PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md index f8f9fef..e72eafb 100644 --- a/PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md +++ b/PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md @@ -1,7 +1,7 @@ # PLAN: thay pedalboard (GPL-3.0) bằng native_bridge — render VSTi offline 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 Xóa dependency GPL-3.0 (`pedalboard`) khỏi cả 2 môi trường, chuyển toàn bộ diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md index e0667a8..ceb33a9 100644 --- a/THIRD_PARTY_LICENSES.md +++ b/THIRD_PARTY_LICENSES.md @@ -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 | | 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 | -| 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++) | 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 - **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:** - 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ế. - 2. **sf2utils (LGPL-3.0+)** — giữ notice + relinkable object khi phân phối binary. - 3. Bổ sung `LICENSE` text (MIT + LGPL-2.1) + attribution cho SpessaSynth (MIT notice) và Tailwind. - 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õ. - 5. Không nhúng SGM-V2.01 vào bản phân phối (hạn chế redistribution). + 1. **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. 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). diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index e7c53e7..63c01ff 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -6,7 +6,7 @@ from fastapi.responses import FileResponse from pydantic import BaseModel from typing import Optional, Any 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.soundfont_inspector import SoundFontInspector 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): """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 - if settings.RENDER_ENGINE != "bridge" and not HAS_PEDALBOARD: - raise HTTPException(status_code=501, detail="pedalboard không khả dụng trên máy này") + if settings.RENDER_ENGINE != "bridge": + raise HTTPException(status_code=501, detail="Chỉ hỗ trợ native_bridge render — set SF_RENDER_ENGINE=bridge") if not req.notes: raise HTTPException(status_code=400, detail="Chưa có nốt nhạc để preview") try: @@ -969,8 +969,8 @@ async def midi_render(req: MidiRenderRequest, current_user: dict = Depends(get_c pass raise HTTPException( status_code=500, - detail=f"Render MIDI thất bại: {e}. Nếu plugin là VST2 hoặc pedalboard " - "không load được, hãy mở Carla Bridge (chọn VSTi, chỉnh âm, " + detail=f"Render MIDI thất bại: {e}. Nếu plugin VSTi không load được " + "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 " "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, soundfont_program=None) -> tuple: """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.""" - if settings.RENDER_ENGINE == "bridge": - from app.core import native_render - return native_render.render_offline( - instrument_id=instrument_id, - notes=notes, - bpm=bpm, - sample_rate=sample_rate, - preset_id=preset_id, - preset_path=preset_path, - 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, + if settings.RENDER_ENGINE != "bridge": + raise HTTPException(status_code=501, + detail="Chỉ hỗ trợ native_bridge render — set SF_RENDER_ENGINE=bridge") + from app.core import native_render + return native_render.render_offline( + instrument_id=instrument_id, + notes=notes, + bpm=bpm, + sample_rate=sample_rate, preset_id=preset_id, preset_path=preset_path, 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): diff --git a/app/config.py b/app/config.py index 49c75ae..adf0836 100644 --- a/app/config.py +++ b/app/config.py @@ -67,10 +67,9 @@ class Settings: # storage (Windows desktop). PRESET_DIR: str = os.path.join(STORAGE_DIR, "presets") - # Render engine offline (preview/export VSTi): 'pedalboard' (cũ, GPL-3.0) - # hoặc 'bridge' (native_bridge --render, MIT). Mặc định pedalboard trong - # giai đoạn chuyển; đảo sang 'bridge' ở phase 5 - # (PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md). - RENDER_ENGINE: str = os.getenv("SF_RENDER_ENGINE", "pedalboard") + # Render engine offline (preview/export VSTi): 'bridge' (native_bridge + # --render, MIT). Giá trị khác → fallback synth (pedalboard GPL-3.0 đã gỡ + # phase 5 — PLAN_REPLACE_PEDALBOARD_NATIVE_BRIDGE.md). + RENDER_ENGINE: str = os.getenv("SF_RENDER_ENGINE", "bridge") settings = Settings() diff --git a/app/core/native_render.py b/app/core/native_render.py index add5f71..e000dd9 100644 --- a/app/core/native_render.py +++ b/app/core/native_render.py @@ -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 diff --git a/app/core/render_engine.py b/app/core/render_engine.py index 2c5cded..5cdb0cf 100644 --- a/app/core/render_engine.py +++ b/app/core/render_engine.py @@ -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' diff --git a/app/core/runtime.py b/app/core/runtime.py index 384a47a..2d7ca87 100644 --- a/app/core/runtime.py +++ b/app/core/runtime.py @@ -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", } diff --git a/app/core/vst_engine.py b/app/core/vst_engine.py index cd33757..0778243 100644 --- a/app/core/vst_engine.py +++ b/app/core/vst_engine.py @@ -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: diff --git a/requirements.txt b/requirements.txt index 89d99ec..9c0463b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,6 @@ soundfile>=0.12.1 jinja2>=3.1.2 httpx>=0.24.0 jsonschema>=4.18.0 -pedalboard==0.9.19 mido>=1.3.0 pyfluidsynth>=1.3.0 sf2utils>=0.9.0 diff --git a/tests/test_native_render.py b/tests/test_native_render.py index ecd82d1..88e3574 100644 --- a/tests/test_native_render.py +++ b/tests/test_native_render.py @@ -66,27 +66,3 @@ def test_plugins_bridge_branch_dispatch(monkeypatch): with pytest.raises(HTTPException) as ei: pl._render_midi_notes_pedalboard("__no_such__", [], bpm=120.0, sample_rate=44100) 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}" diff --git a/tests/test_plugin_api.py b/tests/test_plugin_api.py index c7c4de4..c31b347 100644 --- a/tests/test_plugin_api.py +++ b/tests/test_plugin_api.py @@ -5,7 +5,6 @@ from fastapi.testclient import TestClient from unittest.mock import patch, MagicMock from app.main import app -from app.core.vst_engine import HAS_PEDALBOARD, HAS_PYFLUIDSYNTH client = TestClient(app) @@ -176,20 +175,6 @@ class TestMidiRenderVSTi: assert resp.status_code == 404 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): client.cookies.clear() # TestClient giữ cookie login từ test trước resp = client.post("/api/v1/plugins/carla-play-notes", diff --git a/tests/test_render_engine.py b/tests/test_render_engine.py index d6fb074..4a6c784 100644 --- a/tests/test_render_engine.py +++ b/tests/test_render_engine.py @@ -56,8 +56,8 @@ def test_render_session_vsti_bridge_unknown_instrument_falls_back(monkeypatch): assert (buf != 0.0).any() -def test_render_session_vsti_pedalboard_default_falls_back(monkeypatch): - """Mặc định pedalboard: không có plugin load được → oscillator (không crash).""" +def test_render_session_vsti_non_bridge_engine_falls_back(monkeypatch): + """Engine không phải bridge (vd 'pedalboard' cũ): VSTi → fallback oscillator (không crash).""" from app.api.v1 import plugins as pl monkeypatch.setattr(pl.settings, "RENDER_ENGINE", "pedalboard") engine = PythonRenderEngine() @@ -69,7 +69,7 @@ def test_render_session_vsti_pedalboard_default_falls_back(monkeypatch): def test_render_session_fx_chorus_scipy_fallback(): """FX chorus: fallback scipy (không pedalboard) không crash, thay đổi âm.""" from app.api.v1 import plugins as pl - pl.settings.RENDER_ENGINE = "pedalboard" + pl.settings.RENDER_ENGINE = "bridge" engine = PythonRenderEngine() session = {"tracks": [_midi_track("__no_such_plugin__", fx_type="chorus")]} 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(): """FX reverb: fallback scipy (không pedalboard) không crash, thay đổi âm.""" from app.api.v1 import plugins as pl - pl.settings.RENDER_ENGINE = "pedalboard" + pl.settings.RENDER_ENGINE = "bridge" engine = PythonRenderEngine() session = {"tracks": [_midi_track("__no_such_plugin__", fx_type="reverb")]} buf = engine.render_session_container(session, {}, 120.0, 4, 44100) diff --git a/tests/test_vst_engine.py b/tests/test_vst_engine.py index c786d76..1614014 100644 --- a/tests/test_vst_engine.py +++ b/tests/test_vst_engine.py @@ -4,7 +4,7 @@ import pytest import numpy as np 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: @@ -20,8 +20,6 @@ class TestPluginManager: assert pm.sf_dir == "/opt/daw_engine/soundfonts" 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}] bpm = 120 sr = 44100