feat: VSTi autosample -> SF2/SF3 (tools/autosample_vsti.py) + bit_depth 16/24/32 render/export WAV (render_engine, plugins API, audio_editor, app.jsx encoders) + tests + task/walkthrough docs (53)

This commit is contained in:
2026-08-11 12:09:02 +07:00
parent f8cb2c6a5e
commit 7293d7ac7e
12 changed files with 2266 additions and 18 deletions
+82
View File
@@ -0,0 +1,82 @@
# TASK 53: ALTERNATIVE SOLUTIONS FOR DIRECT VSTi USAGE IN STANDALONE DAW APPLICATIONS
> Spec: "ALTERNATIVE SOLUTIONS FOR DIRECT VSTi USAGE IN STANDALONE DAW APPLICATIONS"
> Thực hiện trên `C:/Users/locpham/SonicForgeStudio` (branch `standalone`).
> Tài liệu đi kèm: `md/53_VSTI_AUTOSAMPLE_RENDER_WALKTHROUGH.md`.
---
## 1. Mục tiêu (từ spec)
| Option | Yêu cầu spec | Hiện thực |
|---|---|---|
| **Option 1** | Client-side WASM live preview với **0% IPC** — cần "a custom Python script" tự động sample các preset VSTi thành `.sf3` (~3-8MB) | `tools/autosample_vsti.py` — render từng nốt qua pedalboard → SF2 (16-bit mono), tùy chọn convert sang SF3 |
| **Option 2** | Offline render/export qua Python backend (pedalboard VST3 + FluidSynth) — WAV chất lượng **24-bit / 32-bit** | Thêm `bit_depth` (16/24/32) vào toàn bộ đường render/export backend + frontend |
---
## 2. Thay đổi
### 2.1 Auto-sampling tool (mới)
**File:** `tools/autosample_vsti.py` (mới, untracked)
- CLI: `--instrument <plugin_id> --out out.sf2 [--preset] [--low --high --step] [--sf3] [--duration --release --velocity --sample-rate]`
- Reuse `PluginManager().load_vst`, `apply_preset_to_plugin`, `HAS_PEDALBOARD` từ `app/core/vst_engine.py`
- Render từng nốt bằng pedalboard, MIDI dạng **raw tuple** `(bytes([0x90,note,vel]), 0.0)` / `(bytes([0x80,note,0]), dur)` — pedalboard 0.9.19 build này **không có** `NoteOn` classes; khớp `PluginManager.midi_events_to_messages` (trả `(bytes, seconds)` tuples)
- Trim leading silence + normalize peak 0.9 trước khi cast int16
- `write_sf2()`: SF2 tối giản hợp lệ — 16-bit mono PCM, 1 zone/nốt (`keyRange lo=hi`), `sampleID`, `overridingRootKey`, `end=exclusive`; preset 0/bank 0
- `--sf3`: convert qua `SoundFontConverter` (`app/core/soundfont_converter.py`); fallback giữ SF2 nếu thiếu ffmpeg libvorbis
- Toàn bộ text ASCII-only (an toàn cp1252 console Windows)
**SF2 writer — chi tiết đúng chuẩn** (đã verify bằng sf2utils):
- INFO text chunk (INAM) **phải chẵn** — pad `\x00` bên trong data (strict parsers không skip RIFF pad byte của odd chunk)
- Records: `phdr` 38B (name20 + `<HHHIII`), `inst` 22B, `pmod`/`imod` 10B, `pgen`/`igen` 4B (`<HH`), `ibag`/`pbag` 4B, `shdr` 46B; bắt buộc terminator records cho từng bảng
**Kết quả verify:** Nexus.vst3 → 3 nốt (60/62/64) → SF2 207KB, sf2utils parse sạch, audio peak 29490. Kontakt load được nhưng silent (không có .nki — đúng dự kiến).
### 2.2 bit_depth 16/24/32 WAV export (Option 2)
| File | Thay đổi |
|---|---|
| `app/core/render_engine.py` | `render_project(self, project_json, output_filepath, bit_depth=16)` (L453); `subtype_map={16:"PCM_16",24:"PCM_24",32:"PCM_32"}``sf.write` (L479-481) |
| `app/api/v1/plugins.py` | `bit_depth: int = 16` thêm vào `RenderRequest` (L493), `MidiRenderRequest` (L937), `SoundfontRenderRequest` (L1063); truyền xuống `engine.render_project(..., bit_depth=req.bit_depth)` (L1200), `_render_midi_notes_pedalboard(..., bit_depth=16)` (signature L1001, sf.write L1048), `soundfont_render` sf.write L1100 |
| `app/core/audio_editor.py` | Cả 2 subtype map `{8:"PCM_S8",16:"PCM_16",24:"PCM_24",32:"PCM_32"}` (L196, L257) — mix multitrack + export |
| `app/static/js/app.jsx` | Option `32` thêm vào **cả 2** dropdown export (L11355 JSX + compiled); encoder 32-bit (`view.setInt32(... s*0x80000000 ...)`) thêm vào **cả 2** client WAV encoder — realtime bounce (L25044-25045) và offline-session (L25363-25368). Đồng thời sửa bug có sẵn: trước đây chọn 24-bit rơi vào nhánh fallback 8-bit |
### 2.3 Tests
- `tests/test_autosampler.py` (mới): `test_write_sf2_valid_structure` (RIFF/sfbk, 3 samples, preset 0/bank 0, 8820 frames), `test_write_sf2_rejects_empty`, `test_write_sf2_odd_name_no_corruption` (regression: tên lẻ "Nexus" không làm sf2utils báo corrupted)
- `tests/test_render_engine.py`: thêm `test_render_project_bit_depth` (16/24/32 → PCM_16/24/32)
---
## 3. Kết quả test
```
python -m pytest tests/ -q
→ 108 passed, 1 skipped, 1 failed
```
1 fail: `tests/test_vst_engine.py::TestPluginManager::test_init`**lỗi môi trường có sẵn** (machine VST3 override `C:\Program Files\Common Files\VST3` vs expected `/opt/daw_engine/vst3`; đã xác nhận fail cả trên code sạch bằng git stash). Không thuộc task này.
> Lưu ý: không chạy `pytest` bare từ root repo (collect torch resources dưới `src-tauri/target/release/resources/` → 52 collection errors). Luôn nhắm `tests/`.
---
## 4. Cách dùng
```bash
# Option 1 — auto-sample VSTi preset sang SF2 (client WASM preview, 0% IPC)
python tools/autosample_vsti.py --instrument Nexus.vst3 --out app/storage/soundfonts/nexus.sf2 --preset <path.vstpreset> --low 36 --high 96 --step 2
# ... hoặc sang SF3 (cần ffmpeg + libvorbis)
python tools/autosample_vsti.py --instrument Nexus.vst3 --out app/storage/soundfonts/nexus.sf3 --sf3
# Option 2 — render/export 24-bit/32-bit (API)
POST /api/v1/plugins/render { "project_json": {...}, "bit_depth": 24 }
POST /api/v1/plugins/midi-render { ..., "bit_depth": 32 }
POST /api/v1/plugins/soundfont-render { ..., "bit_depth": 24 }
```
SF2/SF3 đặt vào `app/storage/soundfonts/` để client tải qua `/soundfonts/download`.
@@ -0,0 +1,95 @@
# WALKTHROUGH 53: VSTi AUTOSAMPLE + 24/32-BIT RENDER/EXPORT
> Ghi lại các hành động đã thực hiện cho Task 53 (`md/53_VSTI_AUTOSAMPLE_RENDER.md`).
> Môi trường: Windows 11, bash, Python 3.13.7, pedalboard==0.9.19, repo `C:/Users/locpham/SonicForgeStudio` branch `standalone`.
---
## Bước 0 — Đọc spec & khảo sát hiện trạng
1. Đọc spec "ALTERNATIVE SOLUTIONS FOR DIRECT VSTi USAGE IN STANDALONE DAW APPLICATIONS" → 2 option cần hiện thực:
- Option 1: custom Python script auto-sample VSTi preset → `.sf3` (~3-8MB) cho client WASM live preview 0% IPC
- Option 2: offline render/export qua Python backend, WAV 24-bit/32-bit
2. Khảo sát codebase: `app/core/vst_engine.py` (PluginManager, apply_preset_to_plugin, HAS_PEDALBOARD, midi_events_to_messages), `app/core/render_engine.py`, `app/core/audio_editor.py`, `app/api/v1/plugins.py` (RenderRequest, MidiRenderRequest, SoundfontRenderRequest), `app/static/js/app.jsx` (2 WAV encoder), `app/core/soundfont_converter.py` (SF2→SF3).
## Bước 1 — Khảo sát API pedalboard trên máy
3. `dir(pedalboard)`**không có** NoteOn/MIDI classes trong build này. Xác định MIDI phải truyền dạng **raw tuple** `(bytes, seconds)`:
- `dir(PluginManager)` và đọc `midi_events_to_messages` trong `vst_engine.py` → trả `(bytes, seconds)` tuples.
- Quyết định: render MIDI qua `vst(messages, sample_rate=..., duration=...)` với `(bytes([0x90,note,vel]), 0.0)` (note-on) và `(bytes([0x80,note,0]), dur)` (note-off).
## Bước 2 — Viết `tools/autosample_vsti.py`
4. Viết script lần 1 bằng heredoc với nội dung có ký tự Unicode (dấu tiếng Việt) → **crash**: `open(p,'w')` trên cp1252 console gây `UnicodeEncodeError` giữa chừng → **file bị truncate/hỏng**.
5. **Viết lại từ đầu** với toàn bộ text ASCII-only (chuẩn style repo `tools/gen_icons.py`): dùng "duong dan", "khong", "not" thay vì dấu tiếng Việt; mọi in/help đều ASCII.
6. Cấu trúc script:
- `_render_note(vst, note, sr, dur, release, velocity)`: build raw MIDI tuples → `vst(...)` → mean về mono → trim leading silence (ngưỡng `peak*0.001`) → normalize peak 0.9 → int16; trả `None` nếu silent.
- `write_sf2(out_path, samples, sample_rate, name)`: tự sinh SF2 tối giản (16-bit mono PCM, 1 zone/nốt, preset 0/bank 0).
- `main()`: argparse `--instrument --out --preset --low --high --step --duration --release --velocity --sample-rate --sf3`.
7. Verify từng record size SF2 bằng chuẩn spec + sf2utils:
- `phdr` 38B, `inst` 22B, `pmod`/`imod` 10B, `pgen`/`igen` 4B, `ibag`/`pbag` 4B, `shdr` 46B + terminator records.
- **Bug phát hiện**: INAM lẻ ("Nexus") → sf2utils báo "corrupted but salvageable" vì strict parser không skip RIFF pad byte của odd chunk. Fix: pad `\x00` **bên trong data** cho INFO text chunk chẵn.
## Bước 3 — Verify end-to-end auto-sample
8. Chạy thử với VSTi thật:
```
python tools/autosample_vsti.py --instrument Nexus.vst3 --out nexus_test.sf2 --low 60 --high 64 --step 2
```
→ 3 nốt (60/62/64) → SF2 **207KB**, sf2utils parse sạch, audio peak 29490. ✅
9. Thử Kontakt.vst3 → load được nhưng silent (không có .nki — đúng dự kiến, không phải lỗi script).
## Bước 4 — Thêm bit_depth 16/24/32 backend
10. `app/core/render_engine.py` L453: `render_project(self, project_json, output_filepath, bit_depth=16)`; L479-481: `subtype_map={16:"PCM_16",24:"PCM_24",32:"PCM_32"}` → `sf.write(..., subtype=subtype_map.get(int(bit_depth), "PCM_16"))`.
11. `app/api/v1/plugins.py`:
- `bit_depth: int = 16` vào `RenderRequest` (L493), `MidiRenderRequest` (L937), `SoundfontRenderRequest` (L1063).
- `_render_midi_notes_pedalboard(..., bit_depth=16)` (L1001) → sf.write L1048.
- `soundfont_render` sf.write L1100.
- `render_project` API truyền `bit_depth=req.bit_depth` (L1200).
12. `app/core/audio_editor.py`: cả 2 subtype map (L196, L257) thêm `32:"PCM_32"` — mix multitrack + export.
13. Verify bằng `sf.info(path).subtype` (soundfile): 16→PCM_16, 24→PCM_24, 32→PCM_32. ✅
## Bước 5 — Thêm bit_depth 32 frontend (`app/static/js/app.jsx` — CRLF)
14. **Chú ý CRLF**: `edit_file` multi-line fail nếu old_string không có `\r`. Dùng python heredoc: đọc `open(p, encoding='utf-8', newline='').read()`, assert count==1, replace bằng chuỗi chứa `\r\n`, ghi lại với `newline=''`. Không bao giờ `open(p,'w')` trước khi encode.
15. Thêm `<option value="32">32</option>` vào **cả 2** dropdown export (JSX L11355 + compiled bản ~L28700).
16. Thêm nhánh encoder 32-bit vào **cả 2** client WAV encoder:
- realtime bounce (L25044-25045): `else if (bitDepth === 32) view.setInt32(o, Math.floor(s < 0 ? s * 0x80000000 : s * 0x7FFFFFFF), true);`
- offline-session (L25363-25368): tương tự `setInt32`.
17. **Bug cũ được sửa ngầm**: trước đây chọn 24-bit bị rơi vào nhánh fallback 8-bit (thiếu nhánh `===24`); thêm nhánh 24-bit + 32-bit đúng thứ tự.
## Bước 6 — Tests
18. Viết `tests/test_autosampler.py` (mới, LF):
- `test_write_sf2_valid_structure`: RIFF/sfbk, 3 samples, preset 0/bank 0, 8820 frames, `sample_rate==44100`.
- `test_write_sf2_rejects_empty`: `write_sf2([], ...)` → ValueError.
- `test_write_sf2_odd_name_no_corruption`: tên "Nexus" (5 ký tự lẻ) → sf2utils không log "corrupted".
19. Thêm `test_render_project_bit_depth` vào `tests/test_render_engine.py`: loop (16,24,32) → assert `sf.info(out).subtype == PCM_16/24/32`.
20. Chạy:
```
python -m pytest tests/ -q
→ 108 passed, 1 skipped, 1 failed
```
- 1 skipped: pyfluidsynth không có native lib trên máy (tests không yêu cầu).
- 1 failed: `TestPluginManager::test_init` — **pre-existing** (machine VST3 override `C:\Program Files\Common Files\VST3` vs expected `/opt/daw_engine/vst3`).
21. Xác nhận fail đó không do mình: `git stash` → chạy test → fail y hệt trên code sạch → `git stash pop`. ✅
## Bước 7 — Kiểm tra cuối & tài liệu
22. `git status` → các file modified/untracked đúng như dự kiến (không đụng file ngoài phạm vi; giữ nguyên các thay đổi pre-existing của user: `app.jsx`, `services/api.js`, `services/fluidsynthLoader.js`, `app.precompiled.js`).
23. Viết `md/53_VSTI_AUTOSAMPLE_RENDER.md` (task) + `md/53_VSTI_AUTOSAMPLE_RENDER_WALKTHROUGH.md` (file này).
---
## Tổng kết diff
```
M app/api/v1/plugins.py (bit_depth 3 requests + 3 sf.write + pass-through)
M app/core/audio_editor.py (+32:"PCM_32" ở 2 subtype map)
M app/core/render_engine.py (render_project bit_depth + subtype_map)
M app/static/js/app.jsx (option 32 ×2 dropdown, encoder 32-bit ×2, fix 24-bit fallback)
M tests/test_render_engine.py (+test_render_project_bit_depth)
?? tools/autosample_vsti.py (mới — auto-sample VSTi → SF2/SF3)
?? tests/test_autosampler.py (mới — 3 tests SF2 writer)
```