Files
SonicForgeStudio/plans/PLAN_VSTI.md

172 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | |