diff --git a/BIG_PHASE_3.md b/BIG_PHASE_3.md new file mode 100644 index 0000000..9497b2d --- /dev/null +++ b/BIG_PHASE_3.md @@ -0,0 +1,277 @@ +# BIG PHASE 3 — Export MIDI/Mix, FX CHAIN, Virtual MIDI Keyboard & Chords Panel + +--- + +## ⚠️ GHI NHỚ — QUY TẮC BẮT BUỘC KHI THỰC HIỆN (đọc trước, tuân thủ mọi phase) + +1. **KHÔNG ĐƯỢC LÀM HỎNG code đã chạy ổn định.** 4 lỗi nghiêm trọng đã fix ở phase trước, tuyệt đối không tái phạm (verify regression sau MỖI thay đổi): + - Âm lượng track không đồng đều (jbridge/SandboxVst2Host — calibrate makeup phải ≈2.59–2.71 cho Qin, KHÔNG được quay lại makeup=1.0). + - VST GUI đã load không hiện / mất GUI sau khi đóng-mở (PluginHost z-order top, embed child #32770 jBridge + Qin_VST_Window). + - Bị câm khi đã load instrument (VSTi/SF2 qua bridge — load xong phải nghe được ngay). + - Bị câm khi preview MIDI (preview VSTi/midi-render phải ra âm). + - Trước khi commit bất kỳ phase nào: chạy lại bộ verify âm thanh cũ (note_quick / open_gui / silence_all) xác nhận KHÔNG regression. +2. **PHẢI OPTIMIZE sau mỗi lần sửa code** (xóa biến chết, branch chết, code trùng; gọn nhất có thể — YAGNI). +3. **LUÔN LUÔN tuân thủ đường dẫn của các lần compile/build trước:** + - Frontend: sửa trong `app/static/js/app.jsx` → `node build.mjs` → `app/static/js/app.precompiled.js`. + - Engine: `python -m PyInstaller engine.spec --clean --noconfirm` từ repo root → output `dist/daw_engine/` → **copy toàn folder** sang `src-tauri/target/release/daw_engine/`. + - C++ bridge: build trong `native_bridge/` bằng `cmake --build build --config Release --target plugin_host daw_vst_bridge` → copy `plugin_host.exe` + `daw_vst_bridge.exe` sang `src-tauri/binaries/` (gồm bản `-x86_64-pc-windows-msvc.exe`) + `src-tauri/target/release/`. +4. **KHÔNG TỰ Ý thay đổi các đường dẫn cũ** (SHM layout, storage paths `%APPDATA%/SonicForgeDAW`, plugin dirs, soundfont dir, port 8000–8010, tên exe, cấu trúc `src-tauri/`). Chỉ đổi khi có lý do bắt buộc và ghi rõ trong commit. +5. **SAU KHI BUILD HOÀN THÀNH phải dọn dẹp tập tin tạm / tập tin rác:** xóa `build/`, `dist/` (sau khi đã copy xong), `__pycache__/`, `.pytest_cache/`, file `.bak`/`.new`/`.recovered` không dùng, file log rác — rồi `git status` phải sạch (chỉ còn thay đổi chủ đích). + +--- + +> Repo: `C:/Users/locpham/SonicForgeStudio` · branch: `standalone-shm-bridge` +> Frontend là 1 file monolith: `app/static/js/app.jsx` (JSX) → build bằng `node build.mjs` → `app/static/js/app.precompiled.js` (file được template nạp: `app/templates/index.html` load `app.precompiled.js?v=...`). +> Backend engine (FastAPI) đóng băng bằng PyInstaller → **MỌI thay đổi frontend/backend đều phải rebuild engine** rồi copy sang folder chạy thật (xem §0). + +--- + +## 0. QUY TRÌNH BUILD & CHẠY THỬ (bắt buộc cho mọi phase) + +```bash +# 1. Sửa app/static/js/app.jsx (hoặc file backend .py) +# 2. Recompile frontend: +node build.mjs +# 3. Rebuild engine đóng băng (nếu đổi app.jsx HOẶC app/**.py): +cd /c/Users/locpham/SonicForgeStudio +python -m PyInstaller engine.spec --clean --noconfirm +# 4. Copy engine mới sang nơi app thật dùng: +cp -r dist/daw_engine/* src-tauri/target/release/daw_engine/ +# 5. Kill tiến trình cũ rồi chạy app thật: +taskkill //IM sonicforge-daw.exe //F 2>/dev/null; taskkill //IM daw_engine.exe //F 2>/dev/null +src-tauri/target/release/sonicforge-daw.exe & +# 6. Login API (backend port 8000): +# POST http://127.0.0.1:8000/api/v1/auth/login {"username":"admin","password":"admin123"} +# → token → header `Authorization: Bearer ` cho các endpoint cần auth. +``` + +Ghi chú: +- `app/static/js/*.js` (services) và `app/static/js/app.jsx` là LF; `native_bridge/` C++ là CRLF — kiểm tra bằng `file` trước khi patch binary. +- Chạy thật = `src-tauri/target/release/sonicforge-daw.exe` (Tauri WebView2). Không test bằng browser Chrome thuần — lỗi "sau khi chuyển standalone" chỉ tái hiện trong WebView2/engine đóng băng. + +--- + +## PHASE 1 — FIX EXPORT MIDI (đã có feature, lỗi sau khi chuyển standalone) + +### 1.1 Hiện trạng +- Code xuất MIDI nằm ở `triggerMidiExport()` trong `app/static/js/app.jsx` (anchor ~dòng 26059). +- Hoàn toàn client-side: gom note từ `track.midiItems[].notes` → build bytes MIDI format 1 (header `MThd`, từng track `MTrk`, delta time bằng `writeVLQ`) → `Blob` → download bằng `a.href = URL.createObjectURL(blob); a.click()`. +- Menu: File → Export MIDI... (anchor ~29344). Còn 1 nút "Export MIDI" trong piano roll toolbar (~10534) chỉ gọi handler khác (midi-render → WAV), không phải export .mid. + +### 1.2 Nghi vấn root cause (chưa xác nhận — chẩn đoán trước) +1. **WebView2 chặn blob download**: `URL.createObjectURL` + `a.click()` với thuộc tính `download` không hoạt động trong WebView2 (không có hộp thoại lưu, không có sự kiện download). → Nghi ngờ cao nhất. +2. Bug phụ trong code hiện tại (sửa luôn): + - `velocity = Math.round((note.velocity || 0.8) * 100)` → scale 0..100, **sai chuẩn MIDI 0..127** (đáng lẽ `* 127`). + - Track audio không có MIDI được xuất thành "rest track" với note pitch 0 velocity 1 — gây tiếng "bíp" nếu DAW khác phát file này; nên bỏ hẳn track không có note MIDI. + +### 1.3 Giải pháp +- **Bước A — Chẩn đoán xác nhận (làm trước khi code):** + 1. Mở app thật, tạo project có MIDI item (vài nốt), bấm File → Export MIDI... → quan sát: có hộp thoại lưu không? console WebView2 có lỗi không? (dùng `--remote-debugging-port` nếu cần). + 2. Nếu không có download → xác nhận giả thuyết WebView2 chặn blob. +- **Bước B — Fix (2 lựa chọn, ưu tiên B1):** + - **B1 (khuyên dùng): Server-side export endpoint.** Thêm `POST /api/v1/export/midi` vào `app/api/v1/audio.py` (hoặc file mới `app/api/v1/midi.py`): + - Request: `{ bpm, ppq, tracks: [{ name, notes: [{ pitch, start_beat, duration_beats, velocity }] }] }`. + - Backend dùng **stdlib thuần** (không thêm dependency) viết file .mid (format 1, `writeVLQ` giống frontend, velocity scale chuẩn `*127`, bỏ track không có note). + - Trả `file_id` (lưu vào `app/storage/processed/`) + `download_url` → frontend tải qua `GET /api/v1/audio/download/{file_id}` (pattern đã có sẵn, chạy tốt trong WebView2 vì là network download http://127.0.0.1, không phải blob). + - **B2 (fallback nếu B1 không khả thi):** giữ generation client-side nhưng đổi cơ chế tải: thử `a.click()` → nếu không có download, dùng Tauri dialog: `window.__TAURI__.dialog.save()` (capability `dialog:default` đã có) + ghi file qua... lưu ý **thiếu permission fs** — cần thêm `fs:default` vào `src-tauri/capabilities/default.json` nếu đi hướng này. → B1 đơn giản hơn, không đụng Rust/capabilities. +- **Bước C — Cập nhật UI:** `triggerMidiExport()` gọi API mới, hiện spinner `setIsExporting`, toast kết quả; giữ nguyên menu/nút cũ. + +### 1.4 Verify +- Tạo project MIDI (2 track, nhiều note, velocity khác nhau) → Export MIDI → file .mid tải về được. +- Mở file bằng DAW khác (hoặc script đọc bytes): số track đúng, tempo đúng, pitch/duration đúng, velocity nằm trong 1..127 (không còn giới hạn 100). +- Export project rỗng → toast "Không có dữ liệu MIDI" (không crash). + +--- + +## PHASE 2 — FIX EXPORT MIX (xuất âm thanh) + TÙY CHỌN LOOP (không fade in/out) + +### 2.1 Hiện trạng +- Menu: File → Export Mix... (anchor ~29340) → `triggerWavExport()` (anchor ~26293). +- `ExportModal` (anchor ~11881): option Nguồn (`project`/`track_mix`/`active_clip`/`clip_selection`), Định dạng (wav/mp3/ogg), SR, Bit, Kênh. **Chưa có option loop, chưa có option fade.** +- 3 đường xuất âm: + 1. **Server-side** (`allOnServer && serverStatus === 'connected'`): POST `/api/v1/multitrack/mix` → task → poll → download file_id. Điều kiện: mọi track có `serverFileIdMap[t.id]` (đã upload file lên server) và `serverStatus === 'connected'`. + 2. **Client-side** `clientSideExport()` (anchor ~26734): `OfflineAudioContext` build lại full graph (track FX Rack + mastering chain) → encode WAV bằng tay (RIFF header + PCM). Chỉ hỗ trợ WAV; mp3/ogg bắt buộc server (`serverStatus === 'connected'`, anchor ~26849). + 3. **Bounce realtime** `triggerBounceExport()` (anchor ~26450): khi project có MIDI items mà MIDI cache chưa đủ → chạy lại project thật, capture PCM master bus → WAV. +- Server path gửi `fade_in_ms: 0, fade_out_ms: 0` rồi nhưng `ClipConfig` backend mặc định 150/150 — kiểm tra worker có tôn trọng 0 không (xem `app/tasks/worker.py` `mix_multitrack_task`). + +### 2.2 Nghi vấn root cause (chẩn đoán trước) +1. Sau standalone, `serverStatus` thường = `checking...`/`offline` (không có server riêng) → nhánh server-side không chạy; nếu user chọn mp3/ogg → báo lỗi/không xuất. Kiểm tra `serverStatus` khi chạy app thật. +2. Client-side path dùng `OfflineAudioContext` — trong WebView2 có thể fail nếu audio context chưa từng được unlock (autoplay policy) → export ra file rỗng/crash. Kiểm tra log. +3. File tải về qua blob download — cùng vấn đề WebView2 như Phase 1. +4. MIDI bounce: nếu master bus chưa init → toast "Hãy phát thử một lần" (đã có guard) — nhưng dễ bị kẹt ở trạng thái này khi user chưa Play. + +### 2.3 Giải pháp +- **Bước A — Chẩn đoán:** chạy app thật, tạo project (audio clip + 1 MIDI track), thử Export Mix cả 3 nguồn; ghi lại: toast nào hiện, file có tải về không, file có đúng độ dài/âm thanh không. +- **Bước B — Sửa đường xuất âm cho standalone:** + 1. `clientSideExport`: bọc `OfflineAudioContext` bằng try/catch; trước khi render gọi `await ensureAudioUnlocked()` (unlock context bằng `resume()`/silent buffer nếu chưa unlock). + 2. Chuyển hết đường tải file sang pattern download qua server (giống Phase 1 B1): hoặc dùng helper `downloadBlobViaServer(url)`; đơn giản nhất: mọi export (client/bounce) lưu ra `app/storage/processed/` qua 1 endpoint upload hoặc tự backend sinh → trả file_id → `a.href = /api/v1/audio/download/{file_id}` (network download, hoạt động trong WebView2). + 3. mp3/ogg khi server offline: thêm fallback — nếu không server, chuyển tự động về WAV kèm toast thông báo (không im lặng fail). +- **Bước C — Tùy chọn LOOP (yêu cầu chính):** + 1. Thêm state `exportSettings.loop` (boolean) + `exportSettings.loopBars` (số ô nhịp, default 4) vào `useState` (anchor ~17249). + 2. `ExportModal`: thêm checkbox **"Xuất loop (không fade in / không fade out)"** + input số ô nhịp (hoặc dùng vùng chọn hiện tại nếu có selection). + 3. Khi `loop = true`: + - Nguồn buộc = vùng loop: `duration = loopBars * (60/bpm) * 4` giây (hoặc selection bounds), bắt đầu từ playhead/bar đầu selection. + - **Mọi path đều zero-fade:** server path `fade_in_ms: 0, fade_out_ms: 0` (đã gửi, kiểm tra worker tôn trọng); client path `clientSideExport` không áp fade (clip buffer thô — xác nhận `buildOfflineTrackNode` không chèn fade); bounce path capture PCM không fade. + - Không kéo đuôi: cắt chính xác tại `start + duration` (không để tail reverb/decay thừa — nếu muốn giữ decay thì là option riêng, ngoài phạm vi; note `ponytail:`). + 4. Đảm bảo file loop không bị "click" ở biên: zero-fade nghĩa là giữ nguyên dạng sóng; yêu cầu người dùng tự chọn loop point sạch (không tự động crossfade — đúng spec "không fade"). +- **Bước D — Cập nhật nút/UI:** giữ menu Export Mix..., thêm shortcut nếu tiện. + +### 2.4 Verify +- Export WAV loop 4 bars (project có audio + MIDI qua FX CHAIN): file đúng độ dài `4*4*60/bpm` giây (±1 sample), **mẫu đầu và mẫu cuối ≠ 0** nếu nội dung phát tại biên (chứng minh không fade in/out), loop nối vòng không click rõ rệt. +- Export project không loop (bình thường) vẫn ra đúng như trước (không regression fade 150ms mặc định nếu có). +- mp3/ogg khi server offline → tự về WAV + toast. + +--- + +## PHASE 3 — ĐỔI MASTERING PANEL / FX RACK PANEL → FX CHAIN (modal dùng chung: plugin tự viết + VST FX) + +### 3.1 Hiện trạng +- 2 modal riêng biệt: + - `FXRackModal` (anchor ~12160): cho 1 track. Header `{track.name} — FX RACK PANEL` (anchor ~12492). Chain built-in = `track.fxChain` (module tự viết: EQ/comp/limiter/reverb/delay..., meta `TRACK_FX_META` anchor ~1331); VST FX riêng = `track.vstFxChain` (danh sách từ `window.SonicAPI.listFx()` = `fx_vst_bridge --scan`, áp khi render/export). + - `MasteringModal` (anchor ~12743): cho master. Header `MASTERING SUITE ... WEB MASTERING V10.5` (anchor ~13503). State `masteringSettings` (ozState): built-in mastering (EQ 4 băng/comp/limiter) + `ozState.vstFxChain`. +- Nút mở: + - Master: nút "MASTERING PANEL" trong `MasterStripConsole` (anchor ~2393), nút PWR cạnh master fader (~2526), menu File → "Mastering Suite" (anchor ~29348, Ctrl+Shift+M). + - Track: nút `FX` trên TCP (anchor ~2740) → `window.__openFxRack(track.id, track.name)` (định nghĩa ~18065) → set `fxRackTarget` → render `FXRackModal` (anchor ~32645). + +### 3.2 Mục tiêu (yêu cầu) +1. Bỏ tên "MASTERING PANEL"/"FX RACK PANEL" → thống nhất **"FX CHAIN"**. +2. Một modal chung chứa **cả plugin xử lí âm thanh tự viết (built-in DSP) lẫn VST FX** trong **một chain duy nhất**, dùng được cho **master** và cho **track**. +3. Chain áp realtime khi Play và áp đúng khi Export (cả client offline lẫn bounce). + +### 3.3 Giải pháp +- **Bước A — Tách component chung:** tạo `FxChainModal({ target, onClose })` với `target = { kind: 'master' } | { kind: 'track', trackId }` (thay 2 component cũ, hoặc giữ wrapper cũ gọi chung — chọn cách ít đụng nhất: giữ `FXRackModal`/`MasteringModal` làm wrapper mỏng, body dùng chung component `FxChainBody`). +- **Bước B — Hợp nhất chain trong UI:** + - Hiển thị 1 hàng chain duy nhất: mỗi slot = `{ type: 'builtin' | 'vst', id, name, icon, bypass, preset_b64? }`. + - Backing state giữ nguyên để không phá lưu project cũ: track → `track.fxChain` (builtin) + `track.vstFxChain` (vst); master → `masteringSettings` builtin + `masteringSettings.vstFxChain`. Chỉ hợp nhất ở tầng render + thao tác (thêm/xóa/kéo sắp xếp/bypass ghi vào đúng mảng gốc). + - Picker thêm slot: 2 tab — "Module tự viết" (TRACK_FX_META + mastering built-ins) và "VST FX" (`listFx` → `vstFxChain`), chèn vào đúng vị trí kéo thả. +- **Bước C — Đổi tên toàn bộ hiển thị:** + - `FXRackModal` header → `{track.name} — FX CHAIN`. + - `MasteringModal` header → `MASTER FX CHAIN` (bỏ "MASTERING SUITE"/"WEB MASTERING V10.5" hoặc chuyển thành subtitle nhỏ). + - Nút `MasterStripConsole` → "FX CHAIN". + - Menu File "Mastering Suite" → "Master FX Chain" (giữ Ctrl+Shift+M). + - Cập nhật tooltip "nút A/♪" mô tả ("Mastering FX Chain" → "Master FX Chain"). +- **Bước D — Đảm bảo chain áp dụng đúng chỗ (không đổi logic DSP):** + - Track: giữ graph hiện có — `buildOfflineTrackNode`/`__rebuildTrackFxGraph`/`fxActive` PWR (đừng đụng nếu đang chạy đúng). + - Master: giữ `toggleMasteringOnMaster` + `NativeBridgeService.setFxChain` (anchor ~12799) cho VST master realtime. + - Export: clientSideExport/bounce đã đi qua cả 2 chain — verify không regression. + +### 3.4 Verify +- Mở FX CHAIN master: thêm 1 module tự viết (EQ) + 1 VST FX → header "MASTER FX CHAIN", cả 2 hiện trong 1 chain, kéo thay đổi thứ tự, bypass từng slot, nghe thay đổi realtime, export ra đúng chain. +- Mở project cũ (lưu trước phase): chain cũ vẫn hiện đúng (không mất cấu hình) — test migration state. +- Không còn chuỗi "MASTERING PANEL"/"FX RACK PANEL" trong UI (grep app.jsx). + +--- + +## PHASE 4 — NÚT FX TRÊN TCP MỞ FX CHAIN CỦA TRACK + +### 4.1 Hiện trạng +- Nút `FX` trên TCP track đã gọi `window.__openFxRack(track.id, track.name)` (anchor ~2740) → `fxRackTarget` → `FXRackModal` (anchor ~32645). **Phần wiring có vẻ đã có** — cần xác nhận nó còn hoạt động sau Phase 3 refactor. + +### 4.2 Giải pháp +- **Bước A — Kiểm tra hiện trạng:** mở app thật, chọn track → bấm nút FX → modal có mở không? Chain có hiện đúng `track.fxChain` + `track.vstFxChain` không? +- **Bước B — Fix nếu hỏng:** các nguyên nhân có thể: `tracks.find(t => t.id === fxRackTarget.trackId)` trả null (id mismatch giữa `tracks` và `activeTracks`/sessionTabs), hoặc `FXRackModal` trả `null` sớm khi `track` undefined. Sửa lookup: dùng `allTracks` gồm main + session tabs (`(activeTracksRef.current || tracks)` như triggerWavExport làm). +- **Bước C — Sau Phase 3:** nút FX mở `FxChainModal` với `target = { kind: 'track', trackId }` → header "FX CHAIN" hiển thị chain track. + +### 4.3 Verify +- Bấm nút FX trên track A → modal FX CHAIN của track A (đúng tên, đúng chain); bấm trên track B → chain track B; đóng/mở nhiều lần không lỗi; VST GUI embed (nếu có) vẫn mở được từ chain. + +--- + +## PHASE 5 — VIRTUAL MIDI KEYBOARD (spec `virtual_keyboard_and_chords_panel_spec.md` Part I) + +### 5.1 Nền tảng có sẵn +- `app/static/js/services/unifiedMidiRouter.js` đã có: `dispatchMidiEvent({ command: 'NOTE_ON'|'NOTE_OFF', channel, pitch, velocity, sourceType })`, `allocateChannel(trackId, isPercussion)` (ch 0-8,10-14 melodic / 9 percussion), `bridgeConnected` → NativeBridgeService hoặc `onFallback` (SF/WASM). → Tận dụng, không viết router mới. +- Audio preview: nếu bridge active → `NativeBridgeService.dispatchMidiEvent` (âm thật VSTi/SF2 đã load trên channel); nếu không → fallback hiện có (SonicSF/Carla) — giữ nguyên cơ chế. +- Recording: transport Record + track armed đã có; cần hook NOTE_ON/NOTE_OFF vào recorder tạo note trong `midiItems`. + +### 5.2 Giải pháp (thêm vào `app.jsx` — nhất quán monolith; code thuần logic tách service nếu >100 dòng) +- **Bước A — Component `VirtualMidiKeyboard`:** + - Floating window draggable, luôn trên timeline (`z-index` cao, không nuốt phím tắt transport Space/R). + - 2 octave keybed hiển thị mapping bảng spec §3: `Z S X D C V G B N M J` (root octave) + `Q 2 W 3 E R 5 T 6 Y 7 U I` (+1 oct), `O` (+2 C). Vẽ key trắng/đen theo Key Type. + - Control bar: Oct -/+, Octave Shift hiển thị (default C4), Velocity slider (1..127, default 100), Channel select (1..16, mặc định channel của track đang chọn/arm), Transpose, Scale Highlight (dropdown scale — phục vụ highlight, không bắt buộc chặn phím). + - Toggle: phím **F2** + menu **Tools → Virtual MIDI Keyboard**. Không chặn khi focus trong `/