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:
+2
-8
@@ -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.
|
||||
|
||||
@@ -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ộ
|
||||
|
||||
@@ -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).
|
||||
|
||||
+17
-59
@@ -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):
|
||||
|
||||
+4
-5
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user