feat: thêm soundfont và VSTi cho MIDI

This commit is contained in:
2026-07-23 17:47:46 +07:00
parent 0b2382573f
commit 225f23516f
16 changed files with 1232 additions and 60 deletions
+171
View File
@@ -0,0 +1,171 @@
# Kế hoạch cài đặt SoundFont / VSTi (32_SF_VSTi.md)
> Dựa trên hiện trạng codebase: `app/core/render_engine.py`, `app/core/vst_engine.py`, `tests/test_render_engine.py`, `app/api/v1/audio.py`, `requirements.txt` (có pedalboard, numpy, soundfile).
---
## 1. Server Backend (Python / FastAPI)
### 1.1 PluginManager (app/core/vst_engine.py — SỬA)
**Hiện trạng:** `vst_engine.py``render_midi_events_to_audio()` synth sine cơ bản. Cần class quản lý VST3.
**Công việc:**
- Thêm class `PluginManager`:
- `__init__(vst_dir="/opt/daw_engine/vst3", sf_dir="/opt/daw_engine/soundfonts")`
- `_scan_plugins()`: quét `.vst3`/`.so` trong `vst_dir`
- `_scan_soundfonts()`: quét `.sf2`/`.sf3` trong `sf_dir`
- `load_vst(name, preset_data=None)`: load VST3 bằng `pedalboard.VST3Plugin`
- `load_soundfont(path)`: load SF2 bằng `pyfluidsynth` (in-memory buffer)
- `list_available()`: trả về `{ vst_instruments: [...], soundfonts: [...] }`
- Import `pedalboard` + `pyfluidsynth` với `try/except`
- **requirements.txt:** thêm `mido>=1.3.0`, `pyfluidsynth>=1.3.0`
**File:** `app/core/vst_engine.py`
---
### 1.2 Render Engine (app/core/render_engine.py — SỬA)
**Hiện trạng:** `PythonRenderEngine` render audio clips + MIDI events. Cần VST3 + SoundFont path.
**Optimization 1 — In-memory SoundFont render (thay subprocess CLI):**
- Dùng `pyfluidsynth` C-bindings thay vì lệnh `fluidsynth` CLI qua subprocess
- Luồng render:
1. `fl = pyfluidsynth.FluidSynth(sample_rate=sr, gain=0.5)`
2. `fl.sfload(path_to_sf2)``fl.program_select(track_id, font_id, bank, preset)`
3. Render từng block MIDI event → numpy array bằng `fl.render_midi(midi_events, sr)`
4. Trả về in-memory buffer, không ghi file tạm
- Loại bỏ hoàn toàn subprocess CLI (tránh I/O disk, race condition, temp file collision)
**Optimization 2 — Sample Offset chính xác cho Pedalboard VST3:**
- Công thức quy đổi beat → sample offset:
```
SampleOffset = BeatPosition × (60 / BPM) × SampleRate
```
- Khi gọi `vst_plugin(array, sample_rate=sr, midi_messages=midi_list)`:
- Mỗi `pedalboard.MidiMessage` phải kèm `sample_offset` chính xác
- Chuyển từng MIDI event: `midi_events[].start_beat` → `sample_offset`
- Nếu không quy đổi, VSTi dồn toàn bộ nốt vào sample đầu ($0\text{ms}$) → sai nhịp
- Thêm hàm `_midi_events_to_messages(midi_events, bpm, sr) → list[MidiMessage]`
**Công việc:**
- Thêm `render_midi_track_with_vst(midi_events, vst_plugin, sr, bpm)`:
- Gọi `_midi_events_to_messages()` → quy đổi sang sample offset
- Khởi tạo VST3 từ `PluginManager.load_vst()`, process buffer
- Fallback về basic synth nếu VST không available
- Thêm `render_soundfont_track(midi_events, sf_path, sr, bpm)`:
- Dùng `pyfluidsynth.FluidSynth` in-memory
- `fl.render_midi()` → numpy array
- Sửa `render_session_container()` nhận `track["instrument"]` (VST/SF id) mỗi track
**File:** `app/core/render_engine.py`
---
### 1.3 API Endpoints (app/api/v1/plugins.py — MỚI + main.py — SỬA)
- **GET** `/api/v1/plugins/available` → `PluginManager().list_available()`
- **GET** `/api/v1/plugins/default-soundfonts` → scan `app/static/soundfonts/`
- **POST** `/api/v1/projects/render` → nhận ProjectSchema → render → return URL WAV
- Mount router trong `main.py`:
```python
from app.api.v1.plugins import router
app.include_router(router, prefix="/api/v1/plugins", tags=["plugins"])
```
**Optimization 3 — Validation Magic Bytes cho Upload SoundFont:**
- Endpoint upload `.sf2` không chỉ kiểm tra đuôi file
- Thêm FastAPI Dependency kiểm tra Header/Magic Bytes:
```
RIFF header (4 bytes: 0x52 0x49 0x46 0x46) + sfbk (4 bytes: 0x73 0x66 0x62 0x6B)
```
- Logic validate:
```python
def validate_sf2_header(data: bytes):
if len(data) < 12: return False
if data[0:4] != b'RIFF': return False
if data[8:12] != b'sfbk': return False
return True
```
- Từ chối upload nếu magic bytes không khớp → chặn file độc hại ngay từ API layer
**File mới:** `app/api/v1/plugins.py`
---
### 1.4 Dockerfile / Dependencies
- **requirements.txt:** thêm `mido>=1.3.0`, `pyfluidsynth>=1.3.0`
- **Dockerfile:** cài `fluidsynth` (shared lib cho pyfluidsynth), `libfluidsynth-dev`, `build-essential`
- Tạo `/opt/daw_engine/vst3/` và `/opt/daw_engine/soundfonts/`
- Tạo `app/static/soundfonts/` cho default SF
---
## 2. Client Frontend (JavaScript)
### 2.1 API Client (app/static/js/services/api.js — SỬA)
Thêm methods:
- `listPlugins()` → `GET /v1/plugins/available`
- `listDefaultSoundfonts()` → `GET /v1/plugins/default-soundfonts`
- `renderProject(data)` → `POST /v1/projects/render`
- `uploadSoundFont(file)` → `POST /v1/audio/upload-soundfont` (FormData)
---
### 2.2 SoundFont Player (app/static/js/services/soundfontPlayer.js — MỚI)
- `loadSoundFont(url)` → fetch `.sf2` → ArrayBuffer
- `initFluidSynth()` → khởi tạo Was m engine (fallback Web Audio API basic synth)
- `playNote(note, velocity, duration)` → play MIDI note
- Lưu user SF vào IndexedDB qua `window.SonicStorage`
- Global: `window.SonicSF`
---
### 2.3 Synth / Instrument Panel UI (index.html)
- **Instrument Selector:** dropdown chọn VST/SoundFont từ `listPlugins()`
- **SoundFont Upload:** drag & drop `.sf2` → IndexedDB
- **Preset Browser:** load `.vital`/`.fxp`/`.syx` cho VST đang chọn
- **Render to WAV button:** gọi `renderProject()` → progress → play
---
### 2.4 MIDI Track Support (index.html)
- Track model thêm: `type: 'audio' | 'midi'`, `midiEvents: []`, `instrumentId`
- WaveformLane vẽ MIDI notes (rects) thay waveform
- Double-click → piano roll editor (tham khảo `md/Piano_roll_UX.md`)
---
## 3. Tests
| Test | File | Mới/Sửa |
|---|---|---|
| PluginManager init + scan + load | `tests/test_vst_engine.py` | MỚI |
| Render MIDI track with VST (sample offset chính xác) | `tests/test_render_engine.py` | SỬA |
| Render MIDI track with SoundFont (pyfluidsynth in-memory) | `tests/test_render_engine.py` | SỬA |
| Render project mixed audio + MIDI | `tests/test_render_engine.py` | SỬA |
| API plugins/available/render | `tests/test_plugin_api.py` | MỚI |
| SF2 Magic Bytes validation (RIFF+sfbk) | `tests/test_plugin_api.py` | MỚI |
---
## 4. Thứ tự thực hiện
| Bước | Mô tả | Phụ thuộc |
|---|---|---|
| **B1** | Sửa `vst_engine.py`: PluginManager class | |
| **B2** | Thêm `mido`, `pyfluidsynth` vào `requirements.txt` | |
| **B3** | Tạo `app/api/v1/plugins.py`: 3 endpoints + SF2 magic bytes validate | B1 |
| **B4** | Mount router trong `main.py` | B3 |
| **B5** | Sửa `render_engine.py`: VST3 (sample offset) + SF (pyfluidsynth in-memory) | B1 |
| **B6** | Tạo `soundfontPlayer.js` | |
| **B7** | Sửa `api.js`: plugin/SF methods | |
| **B8** | UI Synth Panel + MIDI track (index.html) | B5-B7 |
| **B9** | Tests | B1-B5 |
| **B10** | Dockerfile: fluidsynth lib + VST dirs | |