26 KiB
26 KiB
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)
- 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.
- 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).
- 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 --noconfirmtừ repo root → outputdist/daw_engine/→ copy toàn folder sangsrc-tauri/target/release/daw_engine/. - C++ bridge: build trong
native_bridge/bằngcmake --build build --config Release --target plugin_host daw_vst_bridge→ copyplugin_host.exe+daw_vst_bridge.exesangsrc-tauri/binaries/(gồm bản-x86_64-pc-windows-msvc.exe) +src-tauri/target/release/.
- Frontend: sửa trong
- 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úcsrc-tauri/). Chỉ đổi khi có lý do bắt buộc và ghi rõ trong commit. - 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/.recoveredkhông dùng, file log rác — rồigit statusphải sạch (chỉ còn thay đổi chủ đích).
Repo:
C:/Users/locpham/SonicForgeStudio· branch:standalone-shm-bridgeFrontend là 1 file monolith:app/static/js/app.jsx(JSX) → build bằngnode build.mjs→app/static/js/app.precompiled.js(file được template nạp:app/templates/index.htmlloadapp.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.jsxlà LF;native_bridge/C++ là CRLF — kiểm tra bằngfiletrướ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()trongapp/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 (headerMThd, từng trackMTrk, delta time bằngwriteVLQ) →Blob→ download bằnga.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)
- WebView2 chặn blob download:
URL.createObjectURL+a.click()với thuộc tínhdownloadkhô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. - 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):
- 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-portnếu cần). - Nếu không có download → xác nhận giả thuyết WebView2 chặn blob.
- 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
- 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/midivàoapp/api/v1/audio.py(hoặc file mớiapp/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,
writeVLQgiống frontend, velocity scale chuẩn*127, bỏ track không có note). - Trả
file_id(lưu vàoapp/storage/processed/) +download_url→ frontend tải quaGET /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).
- Request:
- 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()(capabilitydialog:defaultđã có) + ghi file qua... lưu ý thiếu permission fs — cần thêmfs:defaultvàosrc-tauri/capabilities/default.jsonnếu đi hướng này. → B1 đơn giản hơn, không đụng Rust/capabilities.
- B1 (khuyên dùng): Server-side export endpoint. Thêm
- Bước C — Cập nhật UI:
triggerMidiExport()gọi API mới, hiện spinnersetIsExporting, 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:
- 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'. - Client-side
clientSideExport()(anchor ~26734):OfflineAudioContextbuild 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). - 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-side (
- Server path gửi
fade_in_ms: 0, fade_out_ms: 0rồi nhưngClipConfigbackend mặc định 150/150 — kiểm tra worker có tôn trọng 0 không (xemapp/tasks/worker.pymix_multitrack_task).
2.2 Nghi vấn root cause (chẩn đoán trước)
- Sau standalone,
serverStatusthườ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 traserverStatuskhi chạy app thật. - 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. - File tải về qua blob download — cùng vấn đề WebView2 như Phase 1.
- 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:
clientSideExport: bọcOfflineAudioContextbằng try/catch; trước khi render gọiawait ensureAudioUnlocked()(unlock context bằngresume()/silent buffer nếu chưa unlock).- 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 raapp/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). - 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):
- Thêm state
exportSettings.loop(boolean) +exportSettings.loopBars(số ô nhịp, default 4) vàouseState(anchor ~17249). 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).- Khi
loop = true:- Nguồn buộc = vùng loop:
duration = loopBars * (60/bpm) * 4giâ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 pathclientSideExportkhông áp fade (clip buffer thô — xác nhậnbuildOfflineTrackNodekhô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; noteponytail:).
- Nguồn buộc = vùng loop:
- Đả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").
- Thêm state
- 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/bpmgiâ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..., metaTRACK_FX_METAanchor ~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. HeaderMASTERING SUITE ... WEB MASTERING V10.5(anchor ~13503). StatemasteringSettings(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
FXtrên TCP (anchor ~2740) →window.__openFxRack(track.id, track.name)(định nghĩa ~18065) → setfxRackTarget→ renderFXRackModal(anchor ~32645).
- Master: nút "MASTERING PANEL" trong
3.2 Mục tiêu (yêu cầu)
- Bỏ tên "MASTERING PANEL"/"FX RACK PANEL" → thống nhất "FX CHAIN".
- 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.
- 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ớitarget = { 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/MasteringModallàm wrapper mỏng, body dùng chung componentFxChainBody). - 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 →masteringSettingsbuiltin +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ả.
- Hiển thị 1 hàng chain duy nhất: mỗi slot =
- Bước C — Đổi tên toàn bộ hiển thị:
FXRackModalheader →{track.name} — FX CHAIN.MasteringModalheader →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/fxActivePWR (đừ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.
- Track: giữ graph hiện có —
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
FXtrên TCP track đã gọiwindow.__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.vstFxChainkhô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ữatracksvàactiveTracks/sessionTabs), hoặcFXRackModaltrảnullsớm khitrackundefined. Sửa lookup: dùngallTracksgồm main + session tabs ((activeTracksRef.current || tracks)như triggerWavExport làm). - Bước C — Sau Phase 3: nút FX mở
FxChainModalvớitarget = { 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ặconFallback(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-indexcao, 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 + PitchOffsetvớ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àoitem.source_data.notes/midiItems[].notescủa item MIDI đang mở (hoặc item active) → dispatchDAW_STATE_UPDATEDđể canvas redraw (pattern đã có trong app). - Persist
localStorage:{ visible, x, y, octave, velocity, channel, transpose, scale }; khôi phục khi mở.
- Floating window draggable, luôn trên timeline (
- 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-Soulii7-V7-Imaj7-VI7, Cinematic/Epici-VI-III-VII, EDMvi-IV-I-V, Lofii7-iv7-v7-i7, R&BImaj7-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).
- Pop/Ballad
- Bước C — Custom Builder:
- Input: tên, genre tag, chord tokens (roman numeral
I-vi-IV-Vhoặc chord nameCmaj7-Am9-Fadd9-G13), rhythm pattern (whole/quarter/arpeggio/strum). - Lưu
localStorage['daw_user_custom_chords']; dispatchCUSTOM_CHORD_SAVEDđể panel tự refresh (spec §III.3).
- Input: tên, genre tag, chord tokens (roman numeral
- 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ạoMIDINote { pitch, start_beat, duration_beats, velocity: 0.8 }vào MIDI item active; dispatchDAW_STATE_UPDATED. - Code parse thuần hàm → để service riêng
app/static/js/services/chordTheory.js(test được, xem §6.4).
- Parse chord symbol → pitch array (ví dụ
- Bước E — Internet/AI Search:
- Tận dụng
app/static/js/services/aiGateway.js(đã có tool-calling infra). Thêm toolsearch_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.
- Tận dụng
6.2 Verify (spec §IV checklist 4-7)
- Shift+K / Insert menu mở panel; chọn Jazz Neo-Soul → Insert →
Dm7-G7-Cmaj7-A7chè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) — parseCmaj7,Am9,Dm7,G13,I-vi-IV-Vvớ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/mixhiện tại, nhưng nếu UI đã có token thì gửi kèmAuthorization: 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.vstFxChaingiữ nguyên schema — không migration dữ liệu.