96 lines
7.0 KiB
Markdown
96 lines
7.0 KiB
Markdown
# 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)
|
||
```
|