278 lines
26 KiB
Markdown
278 lines
26 KiB
Markdown
# 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 <token>` 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 `<input>/<textarea>`.
|
||
- Pitch formula đúng spec: `Pitch = (Octave + 1) * 12 + PitchOffset` với root = C.
|
||
- KeyDown: bỏ `e.repeat`; highlight key; `dispatchMidiEvent NOTE_ON` (velocity 0..1 = slider/127); KeyUp: NOTE_OFF.
|
||
- **Live record:** nếu transport đang Record và track đang arm → tạo note `{ pitch, start_beat, duration_beats, velocity }` vào `item.source_data.notes`/`midiItems[].notes` của item MIDI đang mở (hoặc item active) → dispatch `DAW_STATE_UPDATED` để canvas redraw (pattern đã có trong app).
|
||
- Persist `localStorage`: `{ visible, x, y, octave, velocity, channel, transpose, scale }`; khôi phục khi mở.
|
||
- **Bước B — State + menu:**
|
||
- `const [vkbOpen, setVkbOpen] = useState(false)` + ref lưu rect.
|
||
- Global keydown listener cho F2 (tránh trùng transport), menu Tools thêm item.
|
||
- **Bước C — Âm thanh:** không code engine mới — chỉ gọi router. Nếu track chưa có instrument: giữ hành vi fallback hiện có (SF mặc định/Carla), kèm toast khi track chưa arm/chưa có instrument.
|
||
|
||
### 5.3 Verify (spec §IV checklist 1-3)
|
||
- F2 mở/đóng; kéo thả được; vị trí nhớ sau reload.
|
||
- Bấm Z/S/X/D/C/V/G/B/N/M/J/Q/2/W/3/E/R/5/T/6/Y/7/U/I → key sáng, âm phát (bridge VSTi nếu có, không thì SF fallback); octave +/- đổi cao độ đúng 12 semitone.
|
||
- Record: arm track → R → bấm phím → note hiện realtime trên timeline/piano roll → Stop → MIDI item hoàn chỉnh.
|
||
- Bấm phím trong ô text input → không trigger note (pass-through).
|
||
|
||
---
|
||
|
||
## PHASE 6 — CHORDS PANEL (spec Part II)
|
||
|
||
### 6.1 Giải pháp
|
||
- **Bước A — Component `ChordsPanel`** (modal centered / dock phải):
|
||
- Trigger: menu **Insert → Chords panel**, phím **Shift+K**, nút 🎼 trên piano roll toolbar.
|
||
- 3 tab: **Preset Library | Custom Builder | Internet/AI Search** (AI tab defer — xem 6.3).
|
||
- Filter: Style, Key, Scale, Search box.
|
||
- **Bước B — Preset Library** (bảng spec §III.2, hardcode 6 styles):
|
||
- Pop/Ballad `I-V-vi-IV`, Jazz/Neo-Soul `ii7-V7-Imaj7-VI7`, Cinematic/Epic `i-VI-III-VII`, EDM `vi-IV-I-V`, Lofi `i7-iv7-v7-i7`, R&B `Imaj7-iii7-IVmaj7-V13`.
|
||
- Mỗi preset: nút [Preview] (gửi note tạm qua router NOTE_ON/NOTE_OFF nhanh), [Insert to Timeline], [Favorite ★] (localStorage).
|
||
- **Bước C — Custom Builder:**
|
||
- Input: tên, genre tag, chord tokens (roman numeral `I-vi-IV-V` hoặc chord name `Cmaj7-Am9-Fadd9-G13`), rhythm pattern (whole/quarter/arpeggio/strum).
|
||
- Lưu `localStorage['daw_user_custom_chords']`; dispatch `CUSTOM_CHORD_SAVED` để panel tự refresh (spec §III.3).
|
||
- **Bước D — Chord insertion engine (spec §III.5):**
|
||
- Parse chord symbol → pitch array (ví dụ `Cmaj7` = [60,64,67,71]; áp voicing: root/first inversion/drop-2).
|
||
- Insert tại playhead (hoặc beat click): `StartBeat(Ck) = Beat_insert + Σ duration trước`; tạo `MIDINote { pitch, start_beat, duration_beats, velocity: 0.8 }` vào MIDI item active; dispatch `DAW_STATE_UPDATED`.
|
||
- Code parse thuần hàm → để service riêng `app/static/js/services/chordTheory.js` (test được, xem §6.4).
|
||
- **Bước E — Internet/AI Search:**
|
||
- Tận dụng `app/static/js/services/aiGateway.js` (đã có tool-calling infra). Thêm tool `search_or_generate_chords` + endpoint backend `/api/v1/ai/search-chords` (FastAPI, gọi model qua aiGateway/AI_PROXY hiện có).
|
||
- Kết quả hiển thị progression + [Listen Preview] [Save] [Insert] — spec §III.4.
|
||
- **Defer nếu AI backend chưa cấu hình key:** tab hiện thông báo "AI chưa cấu hình" thay vì crash — xem rủi ro.
|
||
|
||
### 6.2 Verify (spec §IV checklist 4-7)
|
||
- Shift+K / Insert menu mở panel; chọn Jazz Neo-Soul → Insert → `Dm7-G7-Cmaj7-A7` chèn đúng vị trí playhead, đúng duration.
|
||
- Custom builder lưu → xuất hiện lại sau reload; event CUSTOM_CHORD_SAVED refresh không cần reload trang.
|
||
- AI search (nếu cấu hình): query "Hotel California" → progression Bm-F#7-A-E7-G-D-Em-F#7.
|
||
|
||
### 6.3 Defer (`ponytail:`)
|
||
- Scale Highlight chặn phím ngoài scale (chỉ highlight, không chặn) — thêm khi có yêu cầu.
|
||
- Arpeggiation/strum preview phức tạp trong preset (chỉ sustain preview) — thêm khi cần.
|
||
- AI chord search phụ thuộc key model — tách phase riêng nếu chưa có key.
|
||
|
||
### 6.4 Test đơn vị (non-trivial logic)
|
||
- `chordTheory.js`: 1 file test nhỏ (Node assert, không framework) — parse `Cmaj7`, `Am9`, `Dm7`, `G13`, `I-vi-IV-V` với key C, voicing root/inversion/drop-2 ra đúng pitch array. Chạy: `node app/static/js/services/chordTheory.test.js`.
|
||
|
||
---
|
||
|
||
## PHASE 7 — BUILD TỔNG & VERIFICATION CHECKLIST
|
||
|
||
```bash
|
||
node build.mjs
|
||
python -m PyInstaller engine.spec --clean --noconfirm
|
||
cp -r dist/daw_engine/* src-tauri/target/release/daw_engine/
|
||
```
|
||
|
||
| # | Test | Kỳ vọng |
|
||
|---|------|---------|
|
||
| 1 | Export MIDI (project 2 track MIDI) | File .mid tải về, mở đúng, velocity 1..127 |
|
||
| 2 | Export Mix WAV loop 4 bars | Đúng độ dài, không fade ở biên, loop nối mượt |
|
||
| 3 | Export Mix WAV bình thường | Không regression, có FX CHAIN + master |
|
||
| 4 | Export mp3/ogg không server | Fallback WAV + toast |
|
||
| 5 | Master FX CHAIN | Header mới, builtin + VST 1 chain, realtime + export đúng |
|
||
| 6 | Track FX CHAIN qua nút FX TCP | Mở đúng track, chain đủ builtin + VST |
|
||
| 7 | Project cũ sau refactor | Chain cũ hiện đúng, không mất cấu hình |
|
||
| 8 | F2 Virtual MIDI Keyboard | Mở/đóng, bấm phím ra âm, record vào timeline |
|
||
| 9 | Chords panel | Insert progression đúng vị trí, custom lưu được |
|
||
| 10 | grep UI | Không còn "MASTERING PANEL"/"FX RACK PANEL" |
|
||
|
||
Commit theo từng phase (message tiếng Việt, kiểu `fix(export): ...`), `git status` sạch cuối mỗi phase.
|
||
|
||
---
|
||
|
||
## RỦI RO / LƯU Ý
|
||
- **WebView2 blob download** là nghi vấn chính cho cả Phase 1 & 2 — xác nhận bằng chẩn đoán trước khi chọn hướng fix (server-download là hướng an toàn nhất vì đã có pattern `/api/v1/audio/download/{file_id}`).
|
||
- **Rebuild engine bắt buộc** sau mọi đổi frontend — quên copy `dist/daw_engine/*` là test nhầm bản cũ (đã vấp ở phase trước).
|
||
- **Auth API**: endpoint export mới nên dùng `get_optional_user` (không bắt buộc token) cho đồng bộ với `/mix` hiện tại, nhưng nếu UI đã có token thì gửi kèm `Authorization: Bearer` (an toàn).
|
||
- **Không đụng logic DSP** khi refactor FX CHAIN (chỉ đổi tên/hợp nhất render) — giảm rủi ro regression âm thanh.
|
||
- **state cũ project**: `fxChain`/`vstFxChain`/`masteringSettings.vstFxChain` giữ nguyên schema — không migration dữ liệu.
|