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