7.0 KiB
7.0 KiB
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/.sotrongvst_dir_scan_soundfonts(): quét.sf2/.sf3trongsf_dirload_vst(name, preset_data=None): load VST3 bằngpedalboard.VST3Pluginload_soundfont(path): load SF2 bằngpyfluidsynth(in-memory buffer)list_available(): trả về{ vst_instruments: [...], soundfonts: [...] }
- Import
pedalboard+pyfluidsynthvớitry/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
pyfluidsynthC-bindings thay vì lệnhfluidsynthCLI qua subprocess - Luồng render:
fl = pyfluidsynth.FluidSynth(sample_rate=sr, gain=0.5)fl.sfload(path_to_sf2)→fl.program_select(track_id, font_id, bank, preset)- Render từng block MIDI event → numpy array bằng
fl.render_midi(midi_events, sr) - 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.MidiMessagephải kèmsample_offsetchí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
- Mỗi
- 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
- Gọi
- Thêm
render_soundfont_track(midi_events, sf_path, sr, bpm):- Dùng
pyfluidsynth.FluidSynthin-memory fl.render_midi()→ numpy array
- Dùng
- Sửa
render_session_container()nhậntrack["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→ scanapp/static/soundfonts/ - POST
/api/v1/projects/render→ nhận ProjectSchema → render → return URL WAV - Mount router trong
main.py: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
.sf2khô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:
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/availablelistDefaultSoundfonts()→GET /v1/plugins/default-soundfontsrenderProject(data)→POST /v1/projects/renderuploadSoundFont(file)→POST /v1/audio/upload-soundfont(FormData)
2.2 SoundFont Player (app/static/js/services/soundfontPlayer.js — MỚI)
loadSoundFont(url)→ fetch.sf2→ ArrayBufferinitFluidSynth()→ 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/.syxcho 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 | – |