feat: thêm soundfont và VSTi cho MIDI
This commit is contained in:
@@ -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` có `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 | – |
|
||||
Reference in New Issue
Block a user