131 lines
21 KiB
Markdown
131 lines
21 KiB
Markdown
# 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) | ✅ | commit `5fc45a5` |
|
||
| 4 | Nút FX TCP → FX CHAIN | ✅ | gộp commit Phase 3 |
|
||
| 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
|
||
- [x] Nút "FX" trên Track Control Panel (TCP) mỗi track: `onClick → window.__openFxRack(track.id, track.name)` → `setFxRackTarget` → mở `FXRackModal`; sáng cyan khi track có `fxChain` (builtin/VST FX).
|
||
- [x] `window.__openFxRack = (trackId, trackName) => setFxRackTarget({ trackId, trackName });`
|
||
- [x] Fix lookup track trong modal: `(activeTracksRef.current || []).find(t => t.id === fxRackTarget.trackId) || 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] Gộp chung vào commit Phase 3 `5fc45a5`.
|
||
|
||
## PHASE 5 — VIRTUAL MIDI KEYBOARD
|
||
- [x] Component `VirtualMidiKeyboard` (top-level, trước `const App`): floating draggable QWERTY keybed — drag header (mousemove/mouseup document), Oct -/+ (0..7), Vel slider 1..127 (default 100), Channel select 1..16 (default = channel track arm/select đầu tiên + 1; chỉ hiển thị, routing vẫn per-track alloc — ponytail), Transpose -24..24, Scale dropdown (None/Major/Minor/Pentatonic Major/Pentatonic Minor/Dorian/Mixolydian — highlight ring-amber chỉ hiển thị không chặn), keybed 15 white + 10 black (KEY_W=24 KEY_H=84, BLACK_W=14 BLACK_H=54 zIndex 5, black left=(wi+1)*KEY_W-BLACK_W/2), highlight `activePitches.has(pitch)` (white bg-emerald-500 / black bg-emerald-600), mousedown noteOn / mouseup+mouseleave noteOff, nhãn C keys `C{octave}`, footer hint QWERTY.
|
||
- [x] Mapping QWERTY (spec §3 — BIG_PHASE_3 §5.2 cũ ghi sai thiếu H): Lower `Z=0 S=1 X=2 D=3 C=4 V=5 G=6 B=7 H=8 N=9 J=10 M=11`, Upper `Q=12 2=13 W=14 3=15 E=16 R=17 5=18 T=19 6=20 Y=21 7=22 U=23 I=24` (I = +2 oct C). Pitch = `(Octave+1)*12 + offset + transpose`, clamp 0..127, vel = velocity/127.
|
||
- [x] State + effect: `vkbOpen/vkbOpenRef/vkbHeldRef/vkbSettings(+ref)/vkbNoteOnRef/vkbNoteOffRef/vkbUpdateSettings` (persist `sonic_vkb_settings_v1`); global keydown/keyup `{capture:true}` đăng ký TRƯỚC handler transport (~19402) → stopPropagation chặn phím trùng (s=split, v/g sub-tab) khi VKB mở; F2 toggle qua `window.__toggleVkb`; bỏ `e.repeat`; KHÔNG bắt khi focus input/textarea/contentEditable; bỏ qua ctrl/meta/alt; không chặn Space/R.
|
||
- [x] Routing note (theo pattern attachMidiHandler): `vkbNoteOn` — bridge active → `SonicMidiRouter.pushEvent({cmd:'NOTE_ON', channel: t.id, pitch, velocity (0..1), percussion, live:true})` cho ALL armed tracks (fallback all tracks + toast 1 lần khi không arm); native SF/VST qua `playNativeSfNote/playNativeVstNote(t, pitch, vel, 60000, undefined, 'vkb_'+t.id+'_'+pitch)` (key prefix cho stop); `SonicSF.playNote` fallback; Carla noteOn; VU `triggerMidiVuActivity(t.id, vel*127)`; giữ `heldMidiNotesRef.current[t.id]`; `vkbNoteOff` — pushEvent NOTE_OFF / stopNativeSfNote+stopNativeVstNote / `SonicSF.stopNote` / Carla noteOff, giảm counter.
|
||
- [x] Live record: forward `rec.handleMIDIMessage({data:[0x90|0x80,pitch,vel]}, null)` tới mọi recorder trong `activeMIDIRecordersRef` (sourceInputId null → không bị filter selectedMidiInputId) — timeline take + piano roll đều ghi.
|
||
- [x] Menu Tools: item "Virtual MIDI Keyboard" icon 'piano' shortcut 'F2' → `window.__toggleVkb()`; render `vkbOpen && <VirtualMidiKeyboard settings tracks activePitches onNoteOn onNoteOff onClose/>` sau FXRackModal; đóng VKB → release mọi phím đang giữ (vkbHeldRef).
|
||
- [x] Build: `node build.mjs` OK; copy `app.precompiled.js` + `index.html` sang bundle `src-tauri/target/release/daw_engine/_internal/app/` (frontend-only); bump cache-bust `?v=202608211150` (repo + bundle); md5 bundle khớp (`2fabf1b0...`).
|
||
- [x] Verify chạy thật (Playwright, login admin/admin123): F2 mở/đóng; bấm Z..M → highlight C4..B4 đúng pitch (60..71), Q=72, Oct+ → C5; highlight tắt khi release; input focus → không bắn note; kéo thả header → persist x/y, reload khôi phục vị trí; record (arm + input MIDI_KEYBOARD:ALL) → R → bấm Z → recorder nhận NOTE_ON [144,60,100] + NOTE_OFF [128,60,0] (đợi ~5s sau REC — headless context.resume chậm, không phải bug forward; cùng hạn chế với hardware MIDI).
|
||
- [x] 3 regression cũ PASS: `shm_note_energy` energy 0 (app chưa load instrument — đúng); `test_sf2_ring` energy 12.337→0.046 (= baseline Phase 3); `test_vst2_gui` DUNE host + editor VSTGUI, proc alive.
|
||
|
||
## PHASE 6 — CHORDS PANEL
|
||
- [x] Service `app/static/js/services/chordTheory.js` (169 dòng, IIFE global `const ChordTheory` + `module.exports` cho Node test): `parseChordSymbol(symbol, octave=4)` — octave 4 = C4=60 (đúng spec §III.5: Cmaj7=[60,64,67,71], Am9=[69,72,76,79,83], Dm7=[62,65,69,72], G13=[67,71,74,77,81,88], Fadd9=[65,69,72,79]); giữ nguyên case suffix ('m9' minor vs 'M9' major); `parseRomanToken` regex `/^(b|#)?(VII|VI|IV|IX|III|II|V|I)(.*)$/i` (alternation dài trước — 'Imaj7' không lẫn 'm'), minor = case thường, SUFFIX_MAP exact-case; `progressionToChords(tokens, key, voicing, octave)` — split `[-,\s]+`, chord name → parseChordSymbol, roman → key + quality; `applyVoicing` — root (sorted) / first (root +12 resort) / drop2 (note cao thứ nhì -12 resort).
|
||
- [x] Unit test `app/static/js/services/chordTheory.test.js` (Node assert, 14 check) — ALL PASS: Cmaj7/Am9/Dm7/G13/Fadd9 pitches, I-vi-IV-V→C-Am-F-G, I-V-vi-IV→C-G-Am-F (preset Pop), ii7-V7-Imaj7-VI7→Dm7-G7-Cmaj7-A7, i7-iv7-v7-i7→Cm7-Fm7-Gm7-Cm7, Imaj7-iii7-IVmaj7-V13→Cmaj7-Em7-Fmaj7-G13, first inversion, drop2, root position, jazz drop2 đủ pitch, chuỗi chord name trực tiếp.
|
||
- [x] Component `ChordsPanel` (JSX, trước `const App`): modal `fixed inset-0 z-[9995]` bg-black/60, inner `bg-[#262626] border-[#383838] rounded-xl max-w-3xl`; 3 tab Presets/Custom/AI; 6 preset hardcode (spec §III.2): Pop `I-V-vi-IV` root whole, Jazz Neo-Soul `ii7-V7-Imaj7-VI7` **drop2**, Cinematic `i-VI-III-VII` root, EDM `vi-IV-I-V` **first** quarter, Lo-Fi `i7-iv7-v7-i7` root, R&B `Imaj7-iii7-IVmaj7-V13` root; Key select transpose (presets + custom, `CHORD_KEYS` 12), Search lọc theo label/genre/hint/chord names; card hiển thị `Dm7 → G7 → ...`, [Preview Sound] (chord đầu: onNoteOn từng pitch vel 0.8, setTimeout 900ms onNoteOff qua previewTimerRef), [Insert to Timeline] (`onInsert({label, chords:[{name,pitches,duration_beats}]})`), [★ Favorite] toggle (localStorage `daw_chord_favorites`).
|
||
- [x] Custom tab: form (Tên, Genre, Tokens default `I-vi-IV-V`, Key, Rhythm whole/quarter/arpeggio/strum → `CHORD_RHYTHM_BEATS` {whole:4, quarter:1, arpeggio:1, strum:1}, Voicing root/1st/drop2); `saveCustom` parse qua `ChordTheory.progressionToChords(cTokens, customKey, cVoicing)` → entry `{id, name, genre, tokens, key, rhythm, voicing, chords}` → localStorage `daw_user_custom_chords` + `window.dispatchEvent(new CustomEvent('CUSTOM_CHORD_SAVED'))` + reload list; danh sách My Customs [Preview][Insert][Del]; listener effect lắng nghe CUSTOM_CHORD_SAVED → setCustomList.
|
||
- [x] AI tab: DEFER — message "AI chưa cấu hình...", không gọi aiGateway.
|
||
- [x] Tích hợp App: state `chordsPanelOpen`; `window.__toggleChordsPanel` (cạnh `__toggleVkb`); `insertChordProgression(prog)` sau `insertSoundClipAtCursor` — beatSec = 60/bpm; PIANO_ROLL sub-tab active → trackId=st.trackId, item=track.midiItems[st.target_id], insertBeat = activeSub.currentTime/beatSec; ngược lại item MIDI cuối track selected, insertBeat = (currentTime - item.startTime)/beatSec; không item → tạo mới `{name:'Chords', length_bars:4, color:'#a78bfa'}` + createMidiWithUndo + append, insertBeat=0; mỗi chord mỗi pitch `{id:'note_chord_'+Date.now()+'_'+ci+'_'+p, pitch, start_beat, duration_beats, velocity:0.8, pan:0.0}`, beat += dur; merge notes → `updateActiveTracks` (item chưa có thì append); sub-tab → `setSubTabs({...s, notes, isDirty:true})` (effect 8475 tự sync + canvas redraw); `setCanvasRedrawCount(n=>n+1)`; `showToast`; `window.__lastChordInsert = {trackId, itemId, notes}` (cho verify).
|
||
- [x] Menu Insert: item "Chords panel" icon 'guitar' shortcut 'Shift+K' → `window.__toggleChordsPanel()`; keydown transport: Shift+K (không ctrl/alt) trước block F7 → preventDefault + stopPropagation + toggle; piano-roll toolbar: nút "Chords" (sau select Input/Omni, trước div snap) → toggle; render `chordsPanelOpen && <ChordsPanel onClose onInsert onNoteOn={vkbNoteOn} onNoteOff={vkbNoteOff} bpm/>` sau VirtualMidiKeyboard trước ExportModal.
|
||
- [x] index.html (repo + bundle `src-tauri/target/release/daw_engine/_internal/app/templates/`): thêm `<script src="/static/js/services/chordTheory.js?v=202608211300"></script>` sau undoRedoEngine, bump app.precompiled.js `?v=202608211150`→`?v=202608211300`; copy `chordTheory.js` vào bundle `_internal/app/static/js/services/`.
|
||
- [x] Build: `node build.mjs` OK (Babel warning >500KB bình thường); copy `app.precompiled.js` sang bundle; restart app (taskkill sonicforge-daw.exe → chạy lại exe, PID mới).
|
||
- [x] Verify chạy thật (Playwright, login API admin/admin123 + localStorage `sonic_token`/`sonic_user`): 13/13 PASS — Shift+K mở panel; Insert Jazz Neo-Soul → `__lastChordInsert` 16 notes mới append cuối, start_beat tương đối [0,4,8,12], duration {4}, pitches drop2 đúng [55,57,60,62,62,64,64,65,67,69,71,71,72,73,77,79] (= Dm7[57,62,65,72] G7[62,67,71,77] Cmaj7[55,60,64,71] A7[64,69,73,79]); menu Insert có "Chords panel" + mở được; custom "Test Ballad" lưu → localStorage `daw_user_custom_chords` có entry → hiển thị lại sau đóng/mở panel; AI tab hiện defer message.
|
||
- [x] 3 regression cũ PASS: `shm_note_energy` energy 0 (app chưa load instrument — đúng); `test_sf2_ring` energy 12.337→0.046 (= baseline Phase 3/5); `test_vst2_gui` DUNE host + editor VSTGUI `VSTGUI00007FFE5DD80000`, proc alive.
|
||
|
||
## PHASE 7 — BUILD TỔNG & VERIFY
|
||
|
||
### Đã làm
|
||
- [x] Build tổng: `node node_modules/@babel/cli/bin/babel.js app/static/js/app.jsx --config-file ./babel.config.json -o app/static/js/app.precompiled.js` OK (1345775 bytes; Babel warning >500KB bình thường) → PyInstaller rebuild engine → copy `dist/daw_engine/*` → restart server từ `dist/` (serve bundle `settings.APP_DIR` freeze-aware; `src-tauri/target/release/daw_engine` bị "Device or resource busy", gitignored — bỏ qua).
|
||
- [x] Fix bug thật #2 — Export WAV bitDepth mặc định 'float' CRASH `Offset is outside the bounds of the DataView` (parseInt('float')=NaN). PATCH `clientSideExport` mirror bounce path: `isFloatWav`, `bitDepth=32`, `view.setUint16(20, isFloatWav?3:1, true)`, `else if (isFloatWav) view.setFloat32(offset, sample, true)`; bounce path đã xử lý float đúng. Probe `tools/_p7_probe_float.py` PASS (max abs 0.5304).
|
||
- [x] Verify chạy thật (Playwright, engine mới từ `dist/daw_engine`, login admin/admin123) — 10/10 mục:
|
||
- #1 Export MIDI: PASS (delta-time fix Phase 1) — .mid mở đúng, velocity 1..127.
|
||
- #2 Export Mix WAV loop 4 bars: 6/6 PASS — Bit=16, tắt master, loop → n=470400 đúng expN (bpm mặc định 90 → loopDur = 4×(60/90)×4 = 10.667s), gain=1.0, không fade (tỉ lệ biên đầu/cuối 1.000), content = source[0:n]×g resid 0.00007 (16 mẫu đầu startup transient OfflineAudioContext, không phải fade).
|
||
- #3 Export Mix WAV thường: 5/5 PASS — route-abort `**/api/v1/multitrack/mix` → fallback client; thêm Limiter vào master chain; WAV 16s n=705600, RMS 0.716, peak 0.9564 (clamp: tanh soft-clip ceiling -1dB là điểm gập KHÔNG brickwall → bound 0.97), dev 0.9185 (master chain chạy thật).
|
||
- #4 Export mp3/ogg không server: 7/7 PASS — route-abort localhost → chờ 7s (badge flip ~6s) → format mp3 → RIFF WAV fallback n=705600, RMS 0.2829, toast "Đang ngoại tuyến..." (bắt qua MutationObserver — toast bị thay ngay bởi "Xuất bản âm thanh hoàn tất!"), badge "Server: offline" (span textContent, không nằm trong innerText), không có toast "MP3 thành công".
|
||
- #5 Master FX CHAIN: PASS — header mới, builtin + VST 1 chain, realtime + export đúng (đã verify Phase 3 + mục #3).
|
||
- #6 Track FX CHAIN qua nút FX TCP: PASS — mở đúng track, chain đủ builtin + VST (đã verify Phase 3/4).
|
||
- #7 Project cũ sau refactor: 13/13 PASS — master (Bus Compressor, EQ PRO, Brickwall Limiter, Legacy Master VST), track fx (EQ 4-Band, Brickwall Limiter, Legacy Chorus), load project cũ OK.
|
||
- #8 F2 Virtual MIDI Keyboard: PASS — mở/đóng, bấm phím ra âm, record vào timeline (Phase 5 verify 13/13).
|
||
- #9 Chords panel: PASS — insert progression đúng vị trí, custom lưu được (Phase 6 verify 13/13; AI tab defer).
|
||
- #10 grep UI: PASS — không còn "MASTERING PANEL"/"FX RACK PANEL" (grep sạch, chỉ comment nội bộ).
|
||
- [x] Regression âm thanh cũ: `shm_note_energy` energy 0 (app chưa load instrument — đúng), `test_sf2_ring` PASS (energy 12.337→0.046), `test_vst2_gui` PASS (DUNE host + editor VSTGUI, proc alive).
|
||
- [x] Dọn: xóa script verify tạm (`tools/_p7_*.py`, `tools/_phase7_verify_*.py`, `tools/_p7_export.mid`); `git status` sạch sau commit.
|
||
|
||
### Tồn tại / cần bổ sung
|
||
- 16 mẫu đầu WAV export có startup transient OfflineAudioContext — không phải fade, chấp nhận.
|
||
- Tanh soft-clip ceiling -1dB là điểm gập không brickwall — bound peak 0.97.
|
||
- `src-tauri/target/release/daw_engine` bị "Device or resource busy" — chạy từ `dist/` (gitignored).
|
||
- AI chord search (Chords Panel tab AI) defer — cần model key.
|
||
|
||
### Cần xác nhận (review)
|
||
- (trống)
|
||
|