# 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 | – |
