# IMPLEMENT_PHASE_3 — Nhật ký thực hiện BIG_PHASE_3 > Cập nhật theo từng phase. Trạng thái: ✅ hoàn thành · 🔄 đang làm · ⏳ chưa làm · ⚠️ cần xác nhận/review > Repo: `C:/Users/locpham/SonicForgeStudio` · branch: `standalone-shm-bridge` ## TỔNG QUAN | Phase | Nội dung | Trạng thái | Ghi chú | |-------|----------|-----------|---------| | 1 | Fix Export MIDI | ✅ | backend + frontend + build + verify xong (commit riêng) | | 2 | Fix Export Mix + loop | ✅ | frontend + build + verify xong (commit riêng) | | 3 | FX CHAIN (hợp nhất modal) | ⏳ | | | 4 | Nút FX TCP → FX CHAIN | ⏳ | wiring đã có, cần verify | | 5 | Virtual MIDI Keyboard | ⏳ | | | 6 | Chords Panel | ⏳ | AI search defer | | 7 | Build tổng + verify | ⏳ | | --- ## PHASE 1 — FIX EXPORT MIDI ### Đã làm - [x] Khảo sát: 2 nơi export MIDI đều dùng blob download (`a.click()` + `URL.createObjectURL`) — nghi vấn WebView2 chặn (đã fix bằng server download, verify chạy thật OK). - [x] Backend: thêm `POST /api/v1/export/midi` vào `app/api/v1/audio.py` — sinh file .mid format 1 (stdlib thuần), lưu vào PROCESSED_DIR, trả `file_id` + `download_url` (download qua GET network — pattern đã chạy ổn định). Bỏ track không nốt; velocity 0..1 → scale 1..127. - [x] Frontend: thêm helper `serverDownloadMidi(payload, filename)` (POST endpoint → anchor download `${API_BASE_URL}${download_url}`); đổi `triggerMidiExport()` (File menu, bỏ velocity `*100`, bỏ rest-track pitch 0) + nút piano roll "Export MIDI" (payload 1 track, guard không nốt). - [x] Build: `node build.mjs` OK + PyInstaller rebuild engine + copy `dist/daw_engine/*` sang `src-tauri/target/release/daw_engine/` + bump cache-bust `app.precompiled.js?v=202608211103`. - [x] Verify: chạy app thật (engine mới), POST `/export/midi` → GET download → parse bytes: format 1, division 480, đủ track, velocity đúng (0.5→64, 1.0→127, 0.8→102), track rỗng bị bỏ (2→1 track). ### Tồn tại / cần bổ sung - (trống) ### Cần xác nhận (review) - (trống) --- ## PHASE 2 — FIX EXPORT MIX + LOOP ### Đã làm - [x] Khảo sát: 3 đường xuất — server `POST /multitrack/mix` (cần `allOnServer && connected`), client `clientSideExport` (OfflineAudioContext, raw buffer không fade), bounce `triggerBounceExport` (realtime capture master bus, trước đây luôn +1.2s tail + blob download). Worker `app/tasks/worker.py` tôn trọng `fade_in_ms/fade_out_ms` từ clip. - [x] UI ExportModal: checkbox "Xuất loop (không fade in / không fade out)" + input "Số ô nhịp" (1..256, mặc định 4). - [x] Helper `downloadBlobViaServer(blob, filename)`: POST `/api/v1/audio/upload` → download `${API_BASE_URL}/api/v1/audio/download/{file_id}`; fail → fallback blob download + throw (pattern Phase 1). - [x] `triggerWavExport`: khi loop → cắt buffer từng track đúng `[loopStart, loopStart+loopDur)` (`loopSecPerBar=(60/bpm)*4`, `loopStart` = selection nếu hợp lệ else playhead), clip `export_loop_clip` startTime 0, luôn đi client path (buffer đã cắt phía client), gọi `clientSideExport(exportTracks, {loopStart, loopDur})`. - [x] `clientSideExport(activeTracks, loopInfo)`: `durationLimit = loopDur` khi loop (không kéo đuôi); midi cache buffer cắt `subarray(0, loopDur*sr)`; **fix bug**: midi cache không được kéo dài `durationLimit` khi loop (file không dư đuôi); WAV thay blob download bằng `downloadBlobViaServer`. - [x] `triggerBounceExport`: khi loop → `durationLimit = loopDur` (bỏ +1.2 tail), sau capture cắt cứng `[loopStart, loopStart+loopDur)` frames khỏi stereo buffer, download qua `downloadBlobViaServer`. - [x] Build: `node build.mjs` OK (2 lần — lần 2 sau fix durationLimit) + PyInstaller rebuild engine + copy `dist/daw_engine/*` sang `src-tauri/target/release/daw_engine/` + bump cache-bust `app.precompiled.js?v=202608211105`. - [x] Verify chạy thật: engine serve `app.precompiled.js` mới (md5 khớp build local, đủ loop UI/loopActive/downloadBlobViaServer/export_loop_clip), index.html `?v=202608211105`; `/upload` + `/download` roundtrip OK; `POST /multitrack/mix` (server path) SUCCESS → file 2.0s đúng, all samples nonzero (không regression). - [x] Self-check toán loop (node `tools/_loopcheck.mjs`): độ dài WAV = `loopBars*4*60/bpm` ±0 sample với mọi tổ hợp (120bpm×4, 120×4 target 48k, 90×8 start 1.5s, 140×1), biên đầu/cuối giữ nguyên mẫu (không fade) — PASS. - [x] Regression cũ: `tools/shm_note_energy.py` (energy 0 — app chưa load instrument, đúng), `tools/test_sf2_ring.py` PASS (energy 12.337, ring decay), `tools/test_vst2_gui.py` PASS (DUNE host + editor VSTGUI, proc alive). ### Tồn tại / cần bổ sung - Loop với track MIDI: midi cache cắt từ đầu cache (`subarray(0, loopDur*sr)`), chưa offset theo `loopStart` — đúng với thiết kế cache preview (cache bắt đầu từ preview), ghi nhận để Phase 7 review nếu cần. ### Cần xác nhận (review) - (trống) ## PHASE 3 — FX CHAIN (BUILTIN DSP + VST FX 1 CHAIN) - [x] `FXRackModal`: header `{track.name} — FX RACK PANEL` → `{track.name} — FX CHAIN` (grep sạch "FX RACK PANEL" còn lại chỉ comment nội bộ dòng 11478). - [x] Gộp CHAIN RACK STRIP + block VST FX CHAIN thành MỘT strip `unifiedChain = [...chain.map(m=>({kind:'builtin',m,i})), ...vstChain.map(s=>({kind:'vst',s,i}))]`: - Slot builtin = card cũ nguyên vẹn (power/toggleMod, name, remove, click → `setActiveType(m.type)`). - Slot vst = card compact (power/toggleVst, name, removeVst, click → `setActiveVstIdx(u.i)`, active = viền teal). - Drag reorder trên unifiedChain → split lại `setChain(nb)` + `setVstChain(nv)`; khi kéo slot vst → `setActiveVstIdx(nv.indexOf(mv.s))` giữ đúng slot đang chọn. - 2 nút thêm cuối strip: `[+]` (addModuleOpen, DSP) + `[VST]` (vstPickerOpen). - [x] Giữ toolbar VST mỏng dưới strip: "Nghe thử (render offline)" (`previewFx`) + checkbox Bypass + hint VST3 áp dụng lúc Export/Offline. - [x] Workspace: khi `activeVstIdx>=0 && < vstChain.length && !fxGuiOpen` → hiện detail VST đầy đủ (vstRow: select đổi plugin, Preset upload, GUI, Carla, lên/xuống, xóa); ngược lại → module editor builtin như cũ. `addMod` giờ reset `setActiveVstIdx(-1)` để quay về editor builtin khi thêm DSP. - [x] `MasteringModal`: header `MASTERING SUITE` → `MASTER FX CHAIN` (giữ badge "WEB MASTERING V10.5" làm subtitle); popup thêm module → `THÊM MODULE VÀO MASTER FX CHAIN`. Chain strip master đã unified sẵn từ trước (builtin + vst3 1 strip). - [x] Nút "MASTERING PANEL" (MasterStripConsole) → "FX CHAIN"; tooltip "Mở/Bật-Tắt MASTERING PANEL ..." → "MASTER FX CHAIN ..."; menu File "Mastering Suite" → "Master FX Chain" (giữ Ctrl+Shift+M). - [x] Phase 4 verify sẵn: `__openFxRack(track.id, track.name)` → `fxRackTarget` → `FXRackModal` lookup track giờ dùng `(activeTracksRef.current || []).find(...) || tracks.find(...) || null` — an toàn khi activeTracks là session-tab clones ≠ root tracks (trước: `tracks.find` có thể null → modal không mở). - [x] Build: `node build.mjs` OK (2 lần — lần 2 sau fix lookup); copy `app/static/js/app.precompiled.js` + `app/templates/index.html` sang bundle `src-tauri/target/release/daw_engine/_internal/app/` (frontend-only, không cần PyInstaller); bump cache-bust `?v=202608211133`. - [x] Verify chạy thật: engine serve precompiled md5 `9d49f209...` khớp build local, index `?v=202608211133`, bundle chứa "FX CHAIN"/"MASTER FX CHAIN"/`unifiedChain`/lookup activeTracksRef; 3 regression cũ PASS (`shm_note_energy` energy 0 = app chưa load instrument đúng; `test_sf2_ring` energy 12.337→0.046; `test_vst2_gui` DUNE host + editor VSTGUI, proc alive). ## PHASE 4 — NÚT FX TCP (chưa bắt đầu) ## PHASE 5 — VIRTUAL MIDI KEYBOARD (chưa bắt đầu) ## PHASE 6 — CHORDS PANEL (chưa bắt đầu) ## PHASE 7 — BUILD TỔNG & VERIFY (chưa bắt đầu)