Files

26 KiB
Raw Permalink Blame History

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)

# 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

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.