1435 lines
205 KiB
Markdown
1435 lines
205 KiB
Markdown
### [2026-08-03] Task: Wave Observer + Module Vector Display Canvas trong FX Rack Panel (unified_fx_rack_panel_update.md)
|
||
- **Tóm tắt thay đổi:** Tích hợp theo spec cập nhật:
|
||
1. **Scope analysers per-track (post-FX)**: `getOrCreateTrackNode` thêm `scopeSplitter` + `scopeAnalyserL/R` (fftSize 2048) nối từ `pannerNode` (output context, SAU FX chain — đúng sơ đồ spec §III.3); `analyserNode` chính tăng 256→2048. Expose `window.__getTrackScopeAnalysers(trackId)` → {L, R, sr}.
|
||
2. **Wave Observer UI trong FXRackModal**: canvas scope + toolbar đúng spec — Input L/R meters, **Channel** (Stereo/Left/Right/Mid/Side), **Mode** (Waveform/Lissajous/Spectrum), **Duration** slider 0.1-5s (ring buffer history 5s × sampleRate), **V.Zoom** slider -12..+20 dB, **Pause/Resume**. Render loop rAF: waveform vẽ L cyan + R amber (stereo), lissajous scatter L vs R, spectrum bar log 20Hz-20kHz.
|
||
3. **Interactive Module Vector Display Canvas**: canvas EQ response curve trong EQ view — vẽ |H(f)| xấp xỉ (4 band cascaded: lowshelf/peaking/highshelf log-domain) 20Hz-20kHz, cập nhật realtime khi kéo slider band.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608033500)
|
||
- **Ghi chú/Test (nếu có):** BUILD OK 927915 bytes, node --check OK, `pytest` 86 passed. Scope chỉ có tín hiệu khi track đang play (analyser tồn tại lúc playback); dừng → flatline + ghi chú.
|
||
---
|
||
|
||
### [2026-08-03] Task: FX Rack Panel tác dụng lên soundfont instrument trên track (route FluidSynth output qua track gainNode)
|
||
- **Tóm tắt thay đổi:** Trước đây MIDI/soundfont qua FluidSynth (ScriptProcessor → `_gainNode` gain 0.3 → masterBus.input) **bỏ qua hoàn toàn track gainNode + fxEntry** → FX Rack (EQ/Comp/Limiter/Exciter/Rebalance) không ảnh hưởng instrument. Fix:
|
||
1. **soundfontPlayer.js**: thêm `setOutputDestination(node)` (disconnect + nối `_gainNode` tới node chỉ định; `null` → về masterBus.input) + `getOutputNode()`; biến `_pendingOutputDestination` áp dụng khi `_gainNode` được tạo sau (4 nơi tạo gainNode đều tôn trọng).
|
||
2. **app.jsx `updateSfRouting()`**: nếu **đúng 1 track MIDI audible** (có midiItems + `computeTrackAudibleGain>0`) → `setOutputDestination(track.gainNode)` → toàn bộ chuỗi track tác dụng: **FX Rack chain → fader volume → pan → mute/solo gain → master**. Nhiều hơn 1 track MIDI audible → fallback masterBus.input (shared worklet không tách được; CC7 mute vẫn đúng). Expose `window.__updateSfRouting`.
|
||
3. **Gọi updateSfRouting ở 5 điểm**: getOrCreateTrackNode (node mới), startTrackPlayback (cuối schedule), applyAllTrackMuteSolo (đổi mute/solo), stopAllPlayback (về masterBus), effect sync `[tracks, sessionTabs]` (load/undo).
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js` (+`?v=202608031400` trong index.html), `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608033400)
|
||
- **Ghi chú/Test (nếu có):** BUILD OK 915831 bytes, balance 0, `pytest` 86 passed. Giới hạn: >1 track MIDI audible → FX không áp (cần multi-engine để tách, chưa làm).
|
||
---
|
||
|
||
### [2026-08-03] Task: Unified FX Rack Panel theo unified_fx_rack_panel.md (panel FX dùng chung cho track + rebuild graph độc lập)
|
||
- **Tóm tắt thay đổi:** Cài đặt cơ chế Unified FX Rack Panel (Hệ thống quản lý Module FX dùng chung):
|
||
1. **Module factory dùng chung**: `createTrackFxModule(type, ctx, params)` — factory DSP giống mastering, giờ nhận `params` (EQ g1-g4, Comp threshold/ratio/makeup, Lim ceiling, Exc drive, Rebal mid/side) lưu theo từng chain entry; thêm `TRACK_EQ_PRESETS` (flat/vocal_clarity/bass_punch/warm_tape/guitar_edge).
|
||
2. **Cấu trúc track FX có điểm rebuild**: `getOrCreateTrackNode` wire `gainNode → fxEntry → [modules active] → fxLegacyIn → chorus/reverb/direct → pannerNode` (lưu fxEntry/fxLegacyIn trên node).
|
||
3. **`rebuildTrackFxGraph(trackId)`** (đúng `rebuildContextAudioGraph` spec §III.2): disconnect `fxEntry`, nối lại qua modules ACTIVE theo thứ tự hiện tại — chỉ ảnh hưởng track đó, không đụng track khác/master bus; chạy LIVE khi đang play; expose `window.__rebuildTrackFxGraph`.
|
||
4. **FXRackModal** — panel unified cho track: chain rack (drag-drop reorder, power toggle, delete, nút [+] thêm 5 loại module), view tham số theo module (EQ 4 band + preset dropdown, Comp, Limiter, Exciter, Rebalance), header hiển thị tên track; mọi thay đổi → update track.fxChain + `__rebuildTrackFxGraph` ngay.
|
||
5. **Nút [FX]** trên TrackStripConsole → `window.__openFxRack(track.id, track.name)` — mở panel bind đúng track (bỏ modal inline cũ). App: state `fxRackTarget` + render FXRackModal (track lookup + updateTrackProp).
|
||
- **SỰ CỐ KHÔI PHỤC FILE:** trong lúc edit, app.jsx bị ghi đè nhầm (chỉ còn 2000 dòng). Đã khôi phục từ git HEAD `ec5fede` (commit chứa toàn bộ fix tới loop-fix — user đã commit) + replay lại các edit unified rack; xác minh bằng build + balance ngoặc.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608033300)
|
||
- **Ghi chú/Test (nếu có):** `node build.mjs` BUILD OK 914635 bytes, syntax OK, bracket balance 0, `pytest` 86 passed.
|
||
---
|
||
|
||
### [2026-08-03] Task: Loop vẫn giãn selection tới bar 18 — mở rộng hasSel + verify bundle serve
|
||
- **Tóm tắt thay đổi:** User báo vẫn lỗi sau fix trước. Kiểm tra toàn diện: (1) Không có path nào khác giãn selection khi loop bật (SubTabToolbar/onLoop là code chết; toggleLoop là Media Explorer preview; useEffect BPM chỉ chạy khi đổi tempo). (2) Mở rộng điều kiện `hasSel` trong handler nút loop: ngoài `selectionMode==='local'`, còn nhận selection qua `selectionStart/selectionEnd` hợp lệ (phòng trường hợp sweep không set 'local'). (3) **Verify bằng server thật** (uvicorn 9131): file `app.precompiled.js?v=202608033200` được serve có chứa `hasSel=selectionMode===` + comment fix — chứng minh bundle workspace đã đúng. Nếu user vẫn thấy lỗi → trình duyệt cache hoặc đang chạy deployment khác (không phải workspace).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608033200; sửa double `</script>` phát sinh khi bump)
|
||
- **Ghi chú/Test (nếu có):** `pytest` 86 passed (chưa chạy lại lần này — chỉ đổi frontend).
|
||
---
|
||
|
||
### [2026-08-03] Task: Fix loop button ghi đè vùng chọn thành 0→18 bar (MAIN SESSION/SECTION-TAB)
|
||
- **Tóm tắt thay đổi:** Khi quét chọn vùng duration rồi bấm nút loop, vùng chọn bị tự đổi thành 0→18 bar. Nguyên nhân: handler nút loop (nhánh main timeline, không có sub-tab active) LUÔN tự derive loop end từ content tracks (`maxEnd + 2 bar` — content 16 bar → 18) rồi `setSelectionStart(0); setSelectionEnd(loopEnd)` — ghi đè vùng chọn của user. Fix: **tôn trọng selection có sẵn** (`hasSel = selectionMode==='local' && selLeft/selRight hợp lệ`) — chỉ auto-derive khi CHƯA có vùng chọn nào. Áp dụng cho cả MAIN SESSION lẫn SECTION-TAB (cả 2 đều đi qua nhánh main timeline khi không có sub-tab; section-tab dùng chung selection handler nên sweep cũng set `selectionMode='local'`). Kèm theo: `handleTrackLaneMouseDown` giờ `setSelectionCleared(false)` khi bắt đầu chọn vùng mới — nếu user từng Ctrl+click xoá chọn thì vùng mới vẫn loop được.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608033100)
|
||
- **Ghi chú/Test (nếu có):** `node build.mjs` OK, syntax OK, `pytest` 86 passed.
|
||
---
|
||
|
||
### [2026-08-03] Task: Fix React error #310 khi mở Mixer modal (hooks sau early return)
|
||
- **Tóm tắt thay đổi:** Sau fix `isFxActive`, mở Mixer modal vẫn lỗi `Minified React error #310` ("Rendered more hooks than during the previous render"). Nguyên nhân: khi thêm dynamic module chain, 2 hooks `dragChainIndexRef` (useRef) + `addModuleOpen` (useState) được khai báo SAU dòng `if (!isOpen) return null;` trong MasteringModal → số hooks giữa các render thay đổi khi modal đóng/mở (return null sớm bỏ qua 2 hooks) → React #310. Fix: di chuyển 2 hooks lên TRƯỚC early return (cạnh useEffect [isOpen]).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608032900)
|
||
- **Ghi chú/Test (nếu có):** verify: hooks tại 9252-9253 trước `if (!isOpen) return null;` (9255), không còn hook nào sau early return trong MasteringModal; syntax OK; bracket balance 0.
|
||
---
|
||
|
||
### [2026-08-03] Task: Fix "ReferenceError: isFxActive is not defined" khi mở Mixer modal
|
||
- **Tóm tắt thay đổi:** Khi mở Mixer modal bị lỗi `Uncaught ReferenceError: isFxActive is not defined`. Nguyên nhân: lúc thêm FX chain editor cho TrackStripConsole, tôi vô tình xóa luôn khai báo `const [isFxActive, setIsFxActive]` riêng của TrackStripConsole (git HEAD có 2 khai báo: master strip + track strip; sau khi dọn duplicate thì chỉ còn 1 của master strip) → nút FX Power trong TrackStripConsole tham chiếu biến không tồn tại. Fix: khôi phục khai báo `isFxActive` trong TrackStripConsole (cạnh `fxChainOpen`).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608032800)
|
||
- **Ghi chú/Test (nếu có):** verify: 2 khai báo `isFxActive` (master strip 1154 + track strip 1460), bundle có `isFxActive` ×5, syntax OK, bracket balance 0.
|
||
---
|
||
|
||
### [2026-08-03] Task: Nâng cấp Mastering panel theo mastering_expand.md (EQ presets + module factory + reorder + track FX reuse)
|
||
- **Tóm tắt thay đổi:** Làm 4 yêu cầu spec mastering_expand.md:
|
||
1. **EQ Presets** (§II.1): `EQ_PRESET_LIBRARY` (flat/vocal_clarity/bass_punch/warm_tape) + dropdown trong EQ view — `applyEQPreset` ramp freq/gain/Q trên 4 biquad filter + sync knobs/canvas.
|
||
2. **Module factory + 5 module mới** (§II.2): thêm DSP cho **Bus Compressor** (DynamicsCompressor + makeup), **Brickwall Limiter** (ratio 20:1, knee 0), **Harmonic Exciter** (WaveShaper + HP 2kHz + dry/wet), **Master Rebalance** (M/S gains qua L/R crossfeed) trong initMasterBus; mỗi module có input/output gain riêng.
|
||
3. **Module reordering + dynamic re-route** (§II.3): `rebuildMasteringGraph(active, chain)` — disconnect mọi boundary node rồi nối chuỗi active modules giữa inputAnalyser→outputAnalyser. Chain strip giờ render ĐỘNG từ `ozState.chain` (drag-drop reorder, power toggle, delete, nút [+] mở modal thêm module 7 loại). `toggleMasteringOnMaster` dùng chain signature cho idempotency. Migration: project cũ không có `chain` → default [eq, imager, maximizer] + params mới.
|
||
4. **Reuse cho track FX** (§II.4): `createTrackFxModule(type, ctx)` — factory tạo instance DSP giống mastering cho từng track; `getOrCreateTrackNode` wire `track.fxChain` (chuỗi module) trước chorus/reverb; serialize/deserialize `fx_chain`; nút **FX** trên TrackStripConsole mở modal editor (thêm/xóa/toggle từng module Comp/Limiter/Exciter/M-S/EQ).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608032700)
|
||
- **Ghi chú/Test (nếu có):** `node build.mjs` OK, syntax OK, bracket balance 0, `pytest` 86 passed. Lưu ý: module mới có param riêng (compThreshold/compRatio/compMakeup, limThreshold, excDrive, rebalMid/rebalSide) được lưu trong mastering_settings.
|
||
---
|
||
|
||
### [2026-08-03] Task: Cài đặt Imager module theo imager_spec.md (M/S width + vectorscope + correlation meter)
|
||
- **Tóm tắt thay đổi:** Làm đúng spec imager_spec.md trong MASTERING PANEL (Mixer F7):
|
||
1. **DSP width semantics chuẩn spec**: 0% = MONO (S×0), 100% = Original (S×1), 200% = 2× Width (S×2). Giữ cấu trúc 4-band crossover (20-100Hz / 100Hz-1kHz / 1k-6kHz / 6k-20kHz) với matrix L/R crossfeed tương đương M/S (M = (L+R)/2 giữ nguyên, S = (L−R)/2 × w/100 — chứng minh: g1=(w+100)/200, g2=(100−w)/200 → mid=(g1+g2)=1, side=(g1−g2)=w/100).
|
||
2. **Migration**: project cũ lưu scale −100..+100 (0 = original) → tự +100 mỗi band khi load (0→100, 15→115, 35→135, 50→150), dùng marker `imagerScale:'v2'` cho project mới. Default mới theo khuyến nghị spec: Band1=0% (MONO maker), Band2=115%, Band3=135%, Band4=150%.
|
||
3. **Vectorscope thật (Polar M/S)**: thay chấm ngẫu nhiên giả bằng trace thật X=(L+R), Y=(L−R) từ leftAnalyser/rightAnalyser (post-mastering) — mono → nằm ngang, width tăng → trải dọc.
|
||
4. **Phase Correlation meter thật**: ρ = Σ(L·R)/√(ΣL²·ΣR²) (−1..+1), gauge vẽ zone màu theo spec (+0.5..+1 xanh an toàn, 0..+0.5 vàng cẩn trọng, <0 đỏ nguy hiểm) + hiển thị số "Corr:".
|
||
5. UI: slider range 0-200%, nhãn band theo spec, hint "0% = Mono · 100% = Original · 200% = 2× Width".
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608032600)
|
||
- **Ghi chú/Test (nếu có):** verify DSP (w=0→side 0, w=100→side 1, w=200→side 2, mid luôn 1), migration (0,15,35,50 → 100,115,135,150; v2 giữ nguyên), slider trong bundle. `pytest` 86 passed.
|
||
---
|
||
|
||
### [2026-08-03] Task: Mute/Solo realtime cho MIDI items — dùng CC7 (channel volume) của FluidSynth
|
||
- **Tóm tắt thay đổi:** Mute/unmute/solo chưa realtime với **MIDI items** vì FluidSynth WASM render toàn bộ channel vào **1 worklet → 1 `_gainNode` chung** (`_workletNode.connect(_gainNode)`), nên gain node của track không câm được MIDI (chỉ audio clips + section sub-tracks qua track gain mới bị ảnh hưởng). Fix: mỗi track MIDI sở hữu **channel riêng** (`ensureTrackMidiChannel`, 0-15 trừ 9) → dùng **CC7 (channel volume)** của FluidSynth (`window.SonicSF.controllerChange(ch, 7, 100|0)`) — áp realtime kể cả với notes đang vang. Thêm vào `applyAllTrackMuteSolo` (nút M/S) + effect sync `[tracks, sessionTabs]` (load/undo). Khi mute → CC7=0 (notes đang phát câm ngay); unmute → CC7=100 (notes đang phát vang lại + cơ chế becameAudible→restart vẫn giữ). Audio clips/section vẫn qua track gain như cũ.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608032500)
|
||
- **Ghi chú/Test (nếu có):** `node build.mjs` OK, syntax OK, verify `controllerChange(ch,7,...)` ×2 trong bundle; `pytest` 86 passed.
|
||
---
|
||
|
||
### [2026-08-03] Task: Fix items rớt/dồn sai track khi load-save project (3 bug)
|
||
- **Tóm tắt thay đổi:** (1) **`serializeTracksList` (app.jsx)**: chuỗi `if/else-if` theo `trackType` chỉ serialize MỘT loại items → track có cả clips + midiItems/sections bị **rớt items khi save** (mất dữ liệu âm thầm, có thể gây cảm giác "items biến mất/dồn chỗ"). Sửa: serialize ĐỘC LẬP từng loại items có trên track. (2) **`upgrade_project_json_if_needed` (projects.py)**: hardcode `start_bar = startTime/4.0` + `duration_bars = 4.0` → vị trí items sai (lệch 2× ở 120bpm, càng lệch khi tempo khác). Sửa: dùng `seconds_per_bar = (60/bpm)*4` từ bpm của project. (3) **`loadAudioBuffersForTracks` (app.jsx)**: merge buffer theo **array index** → nếu state đổi giữa lúc fetch (race: mở project khác khi buffer-load cũ chưa xong) thì tracks bị xáo trộn, items rơi vào track sai. Sửa: merge theo **track ID** + clip ID.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/api/v1/projects.py`, `app/templates/index.html` (bump v=202608032400), `tests/test_project_upgrade.py` (mới)
|
||
- **Ghi chú/Test (nếu có):** verify: round-trip serialize→deserialize giữ đúng track + vị trí (đã test bằng code bundle thật); track hỗn hợp giờ ra `AUDIO_ITEM,MIDI_ITEM`; `pytest` **86 passed** (3 test mới cho upgrade). Lưu ý: deserialize/upgrade vốn đã map đúng từng track — nếu user vẫn thấy items dồn track 1 sau khi hard-refresh, cần kiểm tra dữ liệu project cụ thể (và bản bundle trình duyệt đang chạy).
|
||
---
|
||
|
||
### [2026-08-03] Task: Fix unmute không phát lại khi đang play (sources bị loop-restart bỏ qua)
|
||
- **Tóm tắt thay đổi:** Khi đang play, mute → câm (OK, gain 0) nhưng unmute → không nghe lại được. Nguyên nhân: nếu có loop restart (`stopAllPlayback` + `startTrackPlayback` trong updatePlayhead), vòng lặp mới **bỏ qua track đang mute/solo** (isPlayable check) → sources của track không được tạo lại → chỉ set gain khi unmute không "hồi sinh" được nguồn đã không tồn tại. Fix trong `applyAllTrackMuteSolo`: theo dõi `trackAudibleRef` (audibility từng track), khi phát hiện chuyển tiếp **inaudible → audible** (unmute / tắt solo) và `isPlaying` + không đang RECORDING → `stopAllPlayback()` + `startTrackPlayback(currentTime)` re-schedule lại từ playhead hiện tại (đúng cơ chế toggleTrackSoloEvaluate đã dùng cho solo). Mute thuần (audible→inaudible) vẫn chỉ set gain (tức thì, không gián đoạn track khác). Effect sync `[tracks, sessionTabs]` giờ cũng cập nhật `trackAudibleRef` để nhất quán khi load/undo.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608032300)
|
||
- **Ghi chú/Test (nếu có):** `node build.mjs` OK, syntax OK, `pytest` 83 passed.
|
||
---
|
||
|
||
### [2026-08-03] Task: Mute/Solo realtime — tác dụng ngay lên items đang phát của track
|
||
- **Tóm tắt thay đổi:** Nút M/S ở cả MixerStrip (mixer F7) và TrackStripConsole (track strip) giờ tác dụng **realtime**: toggle mute/solo lập tức set gain của track node đang phát (crossfade 20ms, không click). Cơ chế: `computeTrackAudibleGain(list, track)` (mute luôn tắt; nếu có bất kỳ solo → chỉ track solo nghe được; ngược lại theo volumeDb), `setTrackNodeGain(node, gain)` áp vào `gainNode` của track node. `window.__applyTrackMuteSolo(trackId, patch)` được gọi từ: (1) nút M/S của cả 2 strip (patch state + áp ngay), (2) `getOrCreateTrackNode` khi tạo node (items của track muted/soloed bắt đầu đúng trạng thái), (3) `startSubTabPlayback` cho chain mới, (4) effect sync `[tracks, sessionTabs]` với signature `muted|solo|volumeDb` (chỉ re-apply khi thay đổi — phủ load project/undo/section tab). Vì audio clip `source.connect(gainNode)` và MIDI note (đường oscillator fallback) đều qua track gainNode → mute/solo có tác dụng lên toàn bộ items của track.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608032200)
|
||
- **Ghi chú/Test (nếu có):** `node build.mjs` OK, syntax OK, `pytest` 83 passed. Giới hạn: MIDI qua FluidSynth WASM render chung 1 worklet → không tách theo track (cùng giới hạn với bypass); đường oscillator fallback thì có tác dụng.
|
||
---
|
||
|
||
### [2026-08-03] Task: Đồng bộ hành vi click+drag item (clip = move như section/MIDI; Alt+click+drag = quét chọn)
|
||
- **Tóm tắt thay đổi:** (1) **Thống nhất**: click + drag trên MỌI item (section/MIDI/audio clip) = **di chuyển**; trước đây audio clip click+drag = quét chọn vùng (gây cảm giác "không đáp ứng"). (2) Hành động cũ của audio clip chuyển sang **Alt+click+drag = quét chọn duration** (gọi `onTrackLaneMouseDown` → local sweep select); Alt+right-edge vẫn = time-stretch. (3) **Fix "thỉnh thoảng không đáp ứng"**: mọi item giờ dùng cơ chế **pending drag có threshold 5px** — click thuần chỉ chọn item (không tạo undo/toast, không vô tình di chuyển do rung chuột), di chuột >5px mới bắt đầu drag. Cơ chế: `handleSetPendingDragMove` (mới) lưu `pendingDragRef` với `duplicate:false`; effect pending-drag route: clip đơn → `handleClipDragStartRef` (clip machinery), còn lại → `handleSectionItemDragStartRef` (multiIds đã hỗ trợ clip). `handleSetPendingDrag` (Ctrl+click copy) giờ lưu `duplicate:true`. Sửa lookup clip trong nhánh non-duplicate của `handleSectionItemDragStart`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608032100)
|
||
- **Ghi chú/Test (nếu có):** `node build.mjs` OK, syntax OK, verify `duplicate:true`/`!pdSnap.duplicate` trong bundle. `pytest` 83 passed. Grab tool (kéo nhanh) giữ nguyên.
|
||
---
|
||
|
||
### [2026-08-03] Task: Bypass trên nút Routing Matrix (track strip TCP) + bypass bỏ cả track FX
|
||
- **Tóm tắt thay đổi:** (1) Nút **Routing Matrix** trong `TrackStripConsole` (TCP strip, cạnh M/S) giờ là nút **Bypass** — tooltip "Bypass: track KHÔNG qua FX + mastering ở Main out", active style xanh khi bật. (2) Thay đổi điểm lấy tín hiệu dry: trong `getOrCreateTrackNode`, `dryGain` giờ tap từ **`gainNode` (PRE-FX)** thay vì `analyserNode` (post-FX) → khi bypass, channel bỏ qua **cả track FX (chorus/reverb) lẫn mastering chain** ở Main out; đường routeGain giữ nguyên (post-FX → mastering) khi không bypass. (3) Xác nhận Mixer Panel đã ở phím **F7** (code sẵn: F7 → `__toggleMixerRef`).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608032000)
|
||
- **Ghi chú/Test (nếu có):** `node build.mjs` OK, syntax OK, không còn "Routing Matrix", `pytest` 83 passed.
|
||
---
|
||
|
||
### [2026-08-03] Task: Fix TDZ "Cannot access 'sessionTabs' before initialization" sau khi thêm nút Bypass
|
||
- **Tóm tắt thay đổi:** `useEffect` sync `trackMasteringBypassMap` đặt ở ~11786 nhưng dependency array `[tracks, sessionTabs]` được đánh giá ngay tại chỗ gọi — trước khi `sessionTabs` khai báo (12239) → `ReferenceError: Cannot access 'sessionTabs' before initialization` khi chạy app. Fix: di chuyển useEffect xuống sau khối khai báo `subTabs`/`sessionTabs`/`sessionTabsRef` (TDZ-safe). `window.__setTrackMasteringBypass` an toàn vì chỉ truy cập `activeTrackNodesRef` bên trong body hàm (lúc click).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608031900)
|
||
- **Ghi chú/Test (nếu có):** `node build.mjs` OK, verify trong bundle: effect @453168 sau sessionTabs decl @451814. `pytest` 83 passed. Người dùng chạy `npm run build` trên workspace là được.
|
||
---
|
||
|
||
### [2026-08-03] Task: Nút Bypass Mastering cho từng track strip trong Mixer Panel
|
||
- **Tóm tắt thay đổi:** Thêm nút **B** (Bypass) trong mỗi track strip của Mixer Panel (cạnh M/S). Khi bật ON: âm thanh track đi qua **dry bus mới** (`masterBus.dryInput → dryOutput → output`) — **bỏ qua toàn bộ chuỗi mastering** (EQ / Imager / Maximizer) nhưng vẫn qua master volume + metering ở Main out. Cơ chế: `createMasteringRoute()` tạo 2 đường gain bù nhau (routeGain → `masterBus.input` qua mastering, dryGain → `dryInput`); `setMasteringRoute()` crossfade 20ms khi toggle (không click). Áp dụng tại: `getOrCreateTrackNode` (node track chính, toggle live qua `node.route`), `startSubTabPlayback` (clip playback). Map trạng thái `trackMasteringBypassMap` sync từ tracks state qua useEffect; `window.__setTrackMasteringBypass` toggle ngay cho track đang phát. Lưu/đọc project: `mastering_bypass` trong serialize/deserialize (schema không chặn additionalProperties nên không cần sửa).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608031800)
|
||
- **Ghi chú/Test (nếu có):** `node build.mjs` rebuild OK, syntax OK, `pytest` 83 passed. **Giới hạn:** track MIDI/SoundFont dùng chung 1 bộ render FluidSynth (1 worklet → 1 gain) nên bypass hiện áp dụng cho track AUDIO (clip) + oscillator fallback; track MIDI dùng FluidSynth WASM chưa tách được theo track (cần refactor renderer) — sẽ làm tiếp nếu cần.
|
||
---
|
||
|
||
### [2026-08-03] Task: Rebuild app.precompiled.js từ app.jsx (fix "handleOpenProject is not defined")
|
||
- **Tóm tắt thay đổi:** Lỗi `Uncaught ReferenceError: handleOpenProject is not defined` khi click Open → bundle `app.precompiled.js` mà trình duyệt tải bị lệch với `app.jsx` (bundle cũ không chứa định nghĩa hàm ở scope đúng). `npm run build` không chạy được vì Babel 8 ESM-only (`ERR_REQUIRE_ESM`). Giải pháp: build lại bundle bằng **@babel/standalone@7** qua `build.mjs` (script mới, mirror đúng lệnh build trong package.json), tạo `app.precompiled.js` mới (858KB, syntax OK, đủ `const handleOpenProject` + 3 references, kèm fix restoreLastSessionProject). Đã smoke-test: trích deserializer từ bundle mới chạy với 6 project khôi phục → 6/6 OK, "Rose (autosave 03/08)" đủ 39 MIDI notes. Bump cache-buster lên `v=202608031700`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.precompiled.js` (rebuild), `build.mjs` (NEW — rebuild thủ công khi Babel 8 lỗi), `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `pytest tests/` → 83 passed. Node portable dùng: `/tmp/node-v20.18.0-linux-x64/bin/node` (máy không có node hệ thống). Trên máy deployment: copy 3 file (app.jsx, app.precompiled.js, index.html) hoặc chạy `node build.mjs` rồi hard refresh.
|
||
---
|
||
|
||
### [2026-08-03] Task: Fix auto-restore dự án lỗi "Không tìm thấy dự án" sau khi khôi phục DB
|
||
- **Tóm tắt thay đổi:** Sau khi DB bị xóa/tạo lại, `localStorage.sonic_project_id` của trình duyệt vẫn trỏ tới project cũ đã mất → mỗi lần tải trang `restoreLastSessionProject` gọi API, nhận 404 "Không tìm thấy dự án" và chỉ `console.warn` vĩnh viễn. Fix: khi restore thất bại, tự xóa `sonic_project_id` + `sonic_project_name` khỏi localStorage để lỗi không lặp lại. Bump cache-buster precompiled lên `v=202608031600`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Verified qua API: cả 6 project khôi phục mở được (GET /cloud/{id} → 200 + main_session đầy đủ). Người dùng cần hard refresh (Ctrl+Shift+R) để nạp bundle mới, mở "Rose (autosave 03/08)" 1 lần để ghim project hiện tại.
|
||
---
|
||
|
||
### [2026-08-03] Task: Khôi phục các dự án cloud cũ bị mất (git recovery)
|
||
- **Tóm tắt thay đổi:** Các dự án cloud cũ ('Test' x4, 'Rose' từ 22/07) và project đang làm dở "Rose" (autosave 03/08, 2 track MIDI+AUDIO, 39 notes, bpm 128) đã được khôi phục vào `app/storage/sonicforge.db` dưới user_id admin hiện tại từ **git history** (`app/storage/sonicforge.db` từng được commit trước khi vào `.gitignore`). Nguyên nhân mất: DB bị xóa/tạo lại nhiều lần trong quá trình dev (test suite `test_auth_and_quota` xóa DB → admin được re-seed với UUID mới → project cũ gắn với user_id cũ không hiển thị). Đã dọn các project/user test rác, backup DB trước khi khôi phục tại `/tmp/sonicforge_db_before_restore.db`.
|
||
- **Các file ảnh hưởng:** `app/storage/sonicforge.db` (dữ liệu), `tools/restore_cloud_projects.py` (NEW — script khôi phục cho deployment khác), `tools/cloud_projects_backup.json` (NEW — 6 project dạng portable)
|
||
- **Ghi chú/Test (nếu có):** Verified qua API: login admin → `GET /api/v1/projects/cloud` trả đủ 6 project; mở "Rose (autosave 03/08)" đủ 39 MIDI notes. Trên máy deployment thật (nếu DB khác): `python3 tools/restore_cloud_projects.py <duong-dan>/sonicforge.db`. Từ giờ test suite không đụng DB dev (xem entry conftest).
|
||
---
|
||
|
||
### [2026-08-03] Task: Cô lập test suite khỏi DB dev + khôi phục mật khẩu admin
|
||
- **Tóm tắt thay đổi:** (1) `DB_PATH` trong `app/models/user.py` giờ có thể override bằng env `SONICFORGE_DB_PATH`. (2) Thêm `tests/conftest.py` set env này sang `/tmp/sonicforge_test.db` trước khi mọi app module được import → pytest không bao giờ sửa DB dev nữa (trước đây `test_auth_and_quota` seed + đổi mật khẩu admin ngay trong `app/storage/sonicforge.db`, khiến login `admin123` fail). (3) Reset mật khẩu admin DB dev về `admin123` (`must_change_password=0`).
|
||
- **Các file ảnh hưởng:** `app/models/user.py`, `tests/conftest.py` (NEW)
|
||
- **Ghi chú/Test (nếu có):** `pytest tests/` → 83 passed, 5 skipped. Đã verify live: login admin/admin123 → 200, sai mật khẩu → 400. Nếu deployment khác cũng bị dính (chạy test trên cùng DB), reset thủ công: `UPDATE users SET hashed_password='<hash_of_admin123>', must_change_password=0 WHERE username='admin'` hoặc xóa DB để `seed_admin` tạo lại.
|
||
---
|
||
|
||
### [2026-08-03] Task: Security audit + bug fixes (bảo mật, bug chức năng, cải thiện)
|
||
- **Tóm tắt thay đổi:** (1) **SSRF** `/api/v1/ai/proxy`: thêm auth bắt buộc + chặn cloud metadata/link-local (169.254.0.0/16), chặn IP private trừ khi host nằm trong danh sách AI provider user đã cấu hình (cho phép Ollama localhost:11434), chặn scheme không phải http/https, không forward header X-Auth-Token lên upstream. (2) **Path traversal** `/plugins/render`: `output_filename` chỉ lấy basename + ép đuôi .wav. (3) **Path traversal** toàn bộ `/audio/*`: helper `_safe_file_id`/`_resolve_storage_path` (basename + chỉ đọc trong uploads/processed). (4) **SECRET_KEY**: bỏ hardcode, tự sinh random bền vững lưu `app/storage/.secret_key` (ưu tiên env SECRET_KEY). (5) **media.py** (`computer`/`browse`/`file`): yêu cầu auth — login giờ set HttpOnly cookie `sf_token`, `get_current_user` nhận token từ Bearer / X-Auth-Token / cookie nên frontend raw fetch vẫn hoạt động. (6) Upload audio + soundfont: streaming theo chunk + giới hạn size (1GB/2GB) thay vì đọc cả file vào RAM. (7) **Quota bypass**: `update_cloud_project` giờ kiểm tra quota như `save_cloud_project`. (8) `enforce_password_changed` (bắt buộc đổi mật khẩu lần đầu) được wire vào upload/edit/render/ai-scan/ai-cut/python-tool/upload-soundfont. (9) **render_engine**: fix resample bị bỏ qua (`sr != sample_rate` trước là no-op → giờ resample_poly), render tôn trọng **solo** track, cache buffer section theo `section_id`, thay print → logger. (10) **vst_engine**: thống nhất FluidSynth API low-level CFFI (high-level `Synth()`/`FluidSynth()` không tồn tại trong binding này). (11) Rate-limit login theo IP (10 lần/15 phút), validate độ mạnh password khi register. (12) main.py: `on_event` → `lifespan`, CORS `allow_credentials=False`, xóa stub rỗng. (13) SQLite: WAL + foreign_keys=ON + seed user `anonymous` placeholder (FK hợp lệ cho project ẩn danh). (14) Pydantic v2 `model_dump()` thay `dict()`.
|
||
- **Các file ảnh hưởng:** `app/core/auth.py`, `app/api/v1/auth.py`, `app/api/v1/ai_proxy.py`, `app/api/v1/plugins.py`, `app/api/v1/audio.py`, `app/api/v1/media.py`, `app/api/v1/projects.py`, `app/api/v1/multitrack.py`, `app/api/v1/user_config.py`, `app/core/render_engine.py`, `app/core/vst_engine.py`, `app/models/user.py`, `app/main.py`, `app/static/js/services/aiGateway.js` (gửi X-Auth-Token khi gọi proxy), `tests/test_security_hardening.py` (NEW, 14 test), `tests/test_plugin_api.py`
|
||
- **Ghi chú/Test (nếu có):** `pytest tests/` → 83 passed, 5 skipped. Đã verify live: proxy không token → 401, metadata → 403; media không token → 401 / có cookie → 200; render `output_filename=/tmp/x.wav` → path nằm trong PROCESSED_DIR. Lưu ý: nếu triển khai cũ đang chạy, restart server để tạo `.secret_key` (token cũ sẽ hết hạn vì secret đổi). Login mật khẩu admin mặc định vẫn `admin123` (đã khôi phục trong DB dev).
|
||
---
|
||
|
||
### [2026-07-31 07:03] Task: Fix Lucide icons not rendering on new tracks (TCP + SECTION-TAB)
|
||
- **Tóm tắt thay đổi:** `useEffect` gọi `lucide.createIcons()` thiếu `activeTracks` trong dependency array → khi track được tạo (user/AI/MIDI import/clone section), DOM nodes mới có `data-lucide` nhưng không được chuyển thành SVG → icon ẩn hoặc hiển thị sai. Fix: thêm `activeTracks` vào deps để auto-refresh icons sau mỗi lần track list thay đổi.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** Không cần build. Fix áp dụng cho tất cả đường dẫn tạo track: addNewTrack, insertTrackBelow, AI cut/composition/rearrange, MIDI import, section clone.
|
||
---
|
||
|
||
### [2026-07-31 07:09] Task: Allow multiple simultaneous solo buttons (Track TCP)
|
||
- **Tóm tắt thay đổi:** `toggleTrackSoloEvaluate` trước đây set `solo: false` cho tất cả track trừ track được click → chỉ cho phép 1 solo. Fix: toggle solo chỉ trên track được click, giữ nguyên trạng thái solo của track khác. `soloedTrackId` (single track ID) được thay bằng `hasAnySolo` (boolean). `startTrackPlayback` dùng `track.solo` trực tiếp thay vì so sánh với `soloedTrackId`. Loop restart với solo: iterate tất cả soloed tracks thay vì chỉ play 1 track. UI highlight các nút solo dùng `track.solo`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** Bấm solo track A → active, bấm solo track B → cả A và B đều solo. Audio playback chỉ play track có solo = true.
|
||
---
|
||
|
||
### [2026-07-28 07:56] Task: Fix PIANO_ROLL activeTracks + instrument auto-assign trong SECTION-TAB
|
||
- **Tóm tắt thay đổi:** Fix `activeTracks` useMemo: khi `activeTab` là PIANO_ROLL sub-tab từ SECTION-TAB (`parent_tab_id` bắt đầu `session_`), trả về tracks của section tab thay vì main tracks. Trước đây `activeTracks` luôn trả về main tracks khi activeTab !== session_xxx → dropdown MIDI item rỗng, track name không hiển thị, instrument lookup sai. Fix: check `subTabs` cho PIANO_ROLL type để resolve đúng parent session tracks.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Cần restart server + clear browser cache.
|
||
---
|
||
|
||
### [2026-07-28 07:47] Task: Sync MIDI item editing + ghost notes từ MAIN vào SECTION-TAB
|
||
- **Tóm tắt thay đổi:** Fix `handleEditSectionInTab` xoá `midiItems: []` khi clone section tracks. SECTION-TAB giờ giữ nguyên MIDI items từ section data → dropdown MIDI item switcher hoạt động, ghost notes extract từ các track khác trong section, instrument settings được preserve. Piano Roll Tab dùng chung component `PianoRollTabEditor` nên tất cả tính năng (ghost, session sync, CC lane, export, AI) đều hoạt động trên cả MAIN và SECTION-TAB.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes. Cần lưu section trước (handleSaveSectionTab) để tích luỹ midiItems vào section.tracks, sau đó mở lại section tab để kiểm tra ghost notes + MIDI dropdown.
|
||
---
|
||
|
||
### [2026-07-28 07:13] Task: Instrument selector modal — 2 cột + font 14px
|
||
- **Tóm tắt thay đổi:** Mở rộng modal `max-w-md` → `max-w-4xl`. Font size từ `text-[10px]`/`text-xs` → `text-sm` (14px). Bố cục 2 cột: trái danh sách soundfont, phải instruments của SF đang chọn. Click SF trái → lọc instrument phải. Nút "All Instruments" để xem tất cả. Search filter cả 2 cột.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes. Synth button → modal 2 cột → chọn SF trái → instrument phải.
|
||
---
|
||
|
||
### [2026-07-26 18:05] Task: Fix SpessaSynth CDN 404 + AudioWorklet init
|
||
- **Tóm tắt thay đổi:** Sửa CDN URL từ `@latest/dist/spessasynth_lib.js` (404) → `@4.3.1/dist/index.js` (200). Dùng `WorkletSynthesizer` class + `audioWorklet.addModule(processor.min.js)`, `soundBankManager.addSoundBank()`, `await isReady`. Fallback WorkerSynthesizer nếu AudioWorklet không khả dụng.
|
||
- **Các file ảnh hưởng:** `app/templates/index.html`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** SpessaSynth CDN load OK (200). Cần kiểm tra browser console.
|
||
---
|
||
|
||
### [2026-07-27 16:16] Task: Tăng chiều cao + fix phím mũi tên cho Copilot prompt + fix Babel build
|
||
- **Tóm tắt thay đổi:** Tăng rows từ 2 → 4, thêm resize-y. Sửa ArrowUp/ArrowDown: chỉ kích hoạt lịch sử khi con trỏ ở đầu/cuối text. Fix lỗi dư `}` tại line 6795 khiến Babel build failed (pre-existing). Build thành công qua Babel, precompiled.js được sinh ra tự động.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Kiểm tra prompt input rows=4, resize-y, phím mũi tên di chuyển con trỏ trong multi-line text. Chạy `npm run build` để kiểm tra.
|
||
---
|
||
|
||
### [2026-07-26 20:15] Task: Stabilize client preview (oscillator + FluidSynth server)
|
||
- **Tóm tắt thay đổi:** Kết luận sau debug: SpessaSynth parser (fromArrayBuffer) không extract được presets từ SF2. Client luôn dùng oscillator (ADSR theo program). Server-side FluidSynth render cho âm thanh chính xác khi Export. Xoá debug logs, clean code.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/api/v1/plugins.py`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Preview qua oscillator luôn có âm thanh. Export WAV dùng FluidSynth với SoundFont thật.
|
||
---
|
||
|
||
### [2026-07-26 17:50] Task: Implement SF3 conversion + SpessaSynth client player
|
||
- **Tóm tắt thay đổi:** Python SF2→SF3 converter (ffmpeg Ogg Vorbis), startup auto-convert, download endpoint ưu tiên SF3. Client: IndexedDB cache, soundfontPlayer.js dual-mode (SpessaSynth + oscillator fallback), init + loadSF trong app.jsx.
|
||
- **Các file ảnh hưởng:** `Dockerfile`, `app/core/soundfont_converter.py` (NEW), `app/api/v1/plugins.py`, `app/main.py`, `app/templates/index.html`, `app/static/js/services/soundfontStorage.js` (NEW), `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** DSK 11M→1.1MB (90%), SGM 529M→18MB (97%). SpessaSynth load từ CDN. Fallback oscillator nếu offline.
|
||
---
|
||
|
||
### [2026-07-26 17:25] Task: Fix duplicate Synth + MIDI keyboard silent handler
|
||
- **Tóm tắt thay đổi:** Xoá nút Synth nhỏ giữa ARM và Monitor (dư thừa). Sửa MIDI keyboard handler: bỏ try/catch rỗng, thêm fallback tìm track armed trong main session, pass synth_engine context.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** ARM track → MIDI keyboard → playNote với instrument đã chọn. Server render dùng đúng SGM/SoundFont; client preview dùng oscillator emulation (giới hạn browser).
|
||
---
|
||
|
||
### [2026-07-26 17:20] Task: MIDI keyboard plays selected instrument on armed main track
|
||
- **Tóm tắt thay đổi:** Khi ARM track trong MAIN SESSION/SECTION-TAB, nhấn MIDI Keyboard sẽ play note với instrument đã chọn (synth_engine/instrumentProgram). Fallback: nếu không có sub-tab PIANO_ROLL nào đang armed, tìm track đã ARM trong main session và dùng instrument của nó. Audio routing qua track's gainNode nếu có.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** ARM track → chọn SF instrument → bấm MIDI keyboard → nghe âm thanh instrument đã chọn.
|
||
---
|
||
|
||
### [2026-07-26 17:10] Task: Add Drum channel toggle on track panel
|
||
- **Tóm tắt thay đổi:** Thêm button 🥁 trên track panel (giữa Solo và Record) để chuyển track sang drum channel 9. Khi active: set `is_percussion=true`, `soundfont_bank=128`. Thêm 🥁 indicator cho percussion presets (bank=128) trong instrument selector.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** Click 🥁 → track chuyển drum mode → render chạy channel 9.
|
||
---
|
||
|
||
### [2026-07-26 17:05] Task: Fix FluidSynth API mismatch (silent fallback)
|
||
- **Tóm tắt thay đổi:** Pyfluidsynth thực tế là low-level CFFI wrapper, không có class `FluidSynth()`. Dùng sai API → tất cả SoundFont render fallback về oscillator (silence). Fix: dùng `new_fluid_synth`, `fluid_synth_sfload`, `fluid_synth_program_select`, `fluid_synth_write_s16_stereo`. Đồng bộ internal clock bằng cách render silence qua `write_s16_stereo` thay vì chỉ advance cursor.
|
||
- **Các file ảnh hưởng:** `app/core/render_engine.py`
|
||
- **Ghi chú/Test (nếu có):** DSK BAN-DI (prog=5) và SGM Brass (prog=56) đều có audio riêng biệt. Render peak ~0.04-0.056, spectral profile khác nhau rõ rệt.
|
||
---
|
||
|
||
### [2026-07-26 16:55] Task: Fix SoundFont program selection flow
|
||
- **Tóm tắt thay đổi:** Sửa `_find_sf2_path` case-insensitive (catalog IDs lowercase vs filename mixed-case), thêm `synth_engine`, `soundfont_id`, `instrument_source` vào serialize/deserialize để project save/load không mất instrument selection.
|
||
- **Các file ảnh hưởng:** `app/core/render_engine.py`, `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** Verified: `_find_sf2_path('sgm_v2.01')` → `SGM_v2.01.sf2`, render flat fields + synth_engine struct + roundtrip serialization all pass.
|
||
---
|
||
|
||
### [2026-07-26 16:45] Task: Implement VST3/SoundFont engine plan
|
||
- **Tóm tắt thay đổi:** Fix SF2 path resolution (hardcoded → multi-dir scan), add `synth_engine` struct parsing in render & UI, 3-level fallback chain, pass context to client SoundFont player.
|
||
- **Các file ảnh hưởng:** `app/core/render_engine.py`, `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/services/aiGateway.js`
|
||
- **Ghi chú/Test (nếu có):** Verified: catalog API returns 31 condensed instruments, render with flat fields + synth_engine struct + drums (bank=128) + missing SF fallback all succeed.
|
||
---
|
||
|
||
### [2026-07-25 06:57] Task: Fix Piano Roll runtime errors (rollBeats TDZ + handleCCMouseMove)
|
||
- **Tóm tắt thay đổi:** Sửa lỗi `Cannot access 'rollBeats' before initialization` bằng cách di chuyển khai báo `rollBeats`/`rollBeatsRef` lên trước `totalBeats`. Thêm hàm `handleCCMouseMove` bị thiếu và cập nhật `handleCCMouseDown` hỗ trợ Ctrl+Click paint velocity.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `cd /home/locpham/SonicForgeStudio && npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-25 07:25] Task: 4 tính năng mới (Piano Roll zoom fill + Shift+scroll playhead + auto-scroll drag + SoundFont cache)
|
||
- **Tóm tắt thay đổi:** (1) Piano Roll zoom-out không còn màn hình đen — bars fill toàn bộ viewport. (2) Shift+scroll trong Piano Roll di chuyển playhead và play notes MIDI như fast-forward. (3) Khi drag section/MIDI/clip đến cạnh phải timeline, auto-scroll container. (4) Server-side cache FluidSynth instances + PluginManager singleton + list_soundfont_instruments cache.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/core/vst_engine.py`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. `vst_engine.py` thêm `load_soundfont_cached`, `release_soundfont`, `get_plugin_manager`, `_SF_INSTRUMENTS_CACHE` — refcount-based cache.
|
||
---
|
||
|
||
### [2026-07-26 09:36] Task: Piano Roll 4 features (SNAP, velocity selected, auto-scroll, Synth button)
|
||
- **Tóm tắt thay đổi:** (1) SNAP trong MIDI tab — grid snap cho note drawing, selection marquee, loop range. (2) Ctrl+drag velocity — selected notes màu xanh dương, affect only selected; no selection = paint all. (3) Auto-scroll Piano Roll khi brush-drag gần cạnh top/bottom. (4) Synth button trong MIDI tab toolbar — mở instrument selector khi nhấn. Layout fix: thêm `h-full` cho outer div.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Fix thiếu 1 `)` tại line 5959 return statement (gây build error pre-existing).
|
||
---
|
||
|
||
### [2026-07-26 10:08] Task: Fix auto-scroll continuous + CC velocity batch + flicker
|
||
- **Tóm tắt thay đổi:** (1) Brush-draw gần cạnh top/bottom giờ dùng `setInterval` 30ms để cuộn liên tục (trước đây cuộn 1 lần per mousemove). Stop interval khi mouseup hoặc chuột rời khỏi threshold. (2) Ctrl+drag velocity cho selected notes — gộp tất cả `setNotes` vào 1 batch call (thay vì 1 call per note), tránh flicker do multi re-render. (3) `handleCCMouseDown` selectedMode cũng batch `setNotes`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Thêm ref `brushAutoScrollRef` cho setInterval auto-scroll. Batch `setNotes` trong cả `handleCCMouseDown` và `handleCCMouseMove` để tránh flicker.
|
||
---
|
||
|
||
### [2026-07-26 10:12] Task: Fix brush pitch-based scroll + CC velocity per-note paint
|
||
- **Tóm tắt thay đổi:** (1) Brush auto-scroll: chuyển từ scroll theo viewport edge (clientY) sang pitch-based — scroll khi note mới được vẽ ở pitch gần rìa visible area (6 note threshold). Dùng `NoteHeight * 2` scroll step, setInterval 30ms. (2) CC velocity selected mode: thay vì paint all selected notes cùng lúc, paint từng note một theo vị trí chuột — tìm note gần cursor beat nhất, chỉ paint note đó nếu đang được selected.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Thay `lastPainted` trong selectedMode từ array all indices thành array single-node index.
|
||
---
|
||
|
||
### [2026-07-26 10:20] Task: Fix brush scroll speed + CC snap + non-selected flicker
|
||
- **Tóm tắt thay đổi:** (1) Giảm auto-scroll step từ `NoteHeight * 2` xuống `max(1, floor(NoteHeight * 0.5))` — cuộn chậm hơn để draw note đúng pitch. (2) CC lane `handleCCMouseDown` và `handleCCMouseMove` dùng `getSnapBeat(beat, snapVal)` khi tìm note index — snap đúng grid. (3) Non-selected CC flicker: xóa `paintNote` helper, inline `setNotes` trực tiếp trong mousedown, không gọi setNotes riêng lẻ gây re-render dư thừa.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-25 07:25] Task: Fix auto-scroll + maxDuration tab isolation + 1-bar margin
|
||
- **Tóm tắt thay đổi:** (1) `maxDuration` dùng `activeTracks` + 4-bar buffer. (2) Cách ly MAIN vs SECTION-TAB. (3) Clip/section/MIDI drag/stretch/resize clamp 1-bar from right. (4) Clip drag + stretched clip dùng `updateActiveTracks`. (5) Stretched clip handler thêm auto-scroll.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass.
|
||
|
||
### [2026-07-25 07:31] Task: Fix zoom cursor + right edge black + AI Copilot for Piano Roll
|
||
- **Tóm tắt thay đổi:** (1) Tách `drawWidth` (content) và `cssWidth` (CSS) — canvas coordinate system không còn bị sai khi zoom out. (2) Thêm `min-width` fill viewport, không còn mảng đen phải. (3) Thêm AI Copilot input + handler trong Piano Roll — gõ prompt tạo MIDI notes trực tiếp.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-25 08:08] Task: Fix velocity draw position, shift+scroll play, canvas redraw deps
|
||
- **Tóm tắt thay đổi:** (1) CC lane velocity stem vẽ tại start note thay vì center. (2) Shift+scroll chỉ play note khi playhead vượt qua start_beat. (3) Thêm `viewWidth`/`viewBeats` vào dependency array grid canvas + CC canvas để auto-redraw khi resize/zoom.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-25 08:31] Task: Fix AI Copilot tab scope + hide bottom panels + context menu Insert + autoscroll
|
||
- **Tóm tắt thay đổi:** (1) AI Copilot context dùng `activeTracks` thay `tracks` — chỉ tác động tab đang active. (2) Bottom panels mặc định ẩn. (3) Right-click track timeline có 4 mục Insert. (4) Thêm auto-scroll vào stretched clip resize. Sửa `insertSectionAtPlayhead` dùng `updateActiveTracks`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass.
|
||
|
||
### [2026-07-25 08:36] Task: Fix auto-scroll + maxDuration tab isolation + 1-bar margin
|
||
- **Tóm tắt thay đổi:** (1) `maxDuration` dùng `activeTracks` + 4-bar buffer. (2) Cách ly MAIN vs SECTION-TAB. (3) Clip/section/MIDI drag/stretch/resize clamp 1-bar from right. (4) Clip drag + stretched clip dùng `updateActiveTracks`. (5) Stretched clip handler thêm auto-scroll.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass.
|
||
|
||
### [2026-07-25 08:53] Task: Add timeline left/right margin + dynamic buffer expansion
|
||
- **Tóm tắt thay đổi:** (1) PADDING_LEFT thay đổi từ 2s thành 2-bar margin ở cả WaveformLane và TempoTrackLane. (2) Thêm state `scrollBufferExtra` + auto-expand 4 bars khi auto-scroll gần đến rìa timeline. (3) `maxDuration` thêm `scrollBufferExtra` + 6 bars static buffer. (4) Clip drag/stretch dùng `activeTracks` + `updateActiveTracks` thay `tracks`/`setTracks`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass.
|
||
|
||
### [2026-07-25 09:04] Task: Fix left margin 1-beat, items always visible, bar/timeline sync
|
||
- **Tóm tắt thay đổi:** (1) PADDING_LEFT từ 2-bar xuống 1-beat ở cả WaveformLane và TempoTrackLane. (2) Drag clamp left ≥ 1 beat, right ≤ maxDuration - 1 bar. (3) Bar numbers + grid vẽ lại đồng bộ với scroll+zoom.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass.
|
||
|
||
### [2026-07-25 09:08] Task: Fix Piano Roll CC lane always visible + resizable height
|
||
- **Tóm tắt thay đổi:** (1) `showCC` default `true` — CC lane luôn hiển thị. (2) Thêm `ccHeight` state + resize handle (drag top edge) — cho phép thay đổi độ cao CC lane. (3) Canvas dùng `ccHeight` thay hardcoded 80px.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass.
|
||
|
||
### [2026-07-25 09:18] Task: Fix brush velocity isolation from CC lane
|
||
- **Tóm tắt thay đổi:** Thêm `brushVelocityRef` riêng biệt (0.8) — brush không còn bị ảnh hưởng bởi velocity paint từ CC lane.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass.
|
||
|
||
### [2026-07-25 09:20] Task: Auto-follow dragged item in viewport
|
||
- **Tóm tắt thay đổi:** (1) Clip drag: sau mỗi update, nếu item ra khỏi vùng 120px viewport edge, tự scroll để item luôn thấy. (2) Section/MIDI drag: tương tự. (3) Section/MIDI resize: theo dõi edge đang kéo.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass.
|
||
|
||
### [2026-07-25 09:23] Task: Sync bar ruler redraw during drag
|
||
- **Tóm tắt thay đổi:** (1) TempoTrackLane nhận `canvasRedrawCount` prop + deps — force redraw bar numbers khi drag. (2) Cả 3 drag handler increment `setCanvasRedrawCount` sau auto-follow scroll. (3) Sub-tab TempoTrackLane thêm `viewportWidth` + `scrollLeft` + `canvasRedrawCount` — đồng bộ ruler với timeline.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass.
|
||
|
||
### [2026-07-25 09:30] Task: Fix bar numbers disappear past bar 12 during drag
|
||
- **Tóm tắt thay đổi:** TempoTrackLane đọc `scrollLeft` từ DOM (walk parent chain tìm scroll container) thay vì React prop — canvas luôn dùng scroll position thực tế, không lag do async state.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):**
|
||
|
||
### [2026-07-25 09:58] Task: Fix drag speed + bar numbers visibility + snap section items
|
||
- **Tóm tắt thay đổi:** (1) Xoá auto-follow phải (gây jump), chỉ giữ auto-follow trái 60px — scroll phải nhờ `autoScrollTimeline` mượt. (2) Thêm `PADDING_RIGHT` vào `tEnd` ở TempoTrackLane và WaveformLane — bar numbers + grid hiển thị đến sát rìa phải. (3) `if (localX < 0 || localX > drawWidth) continue;` — skip phần tử ngoài canvas. (4) Buffer 12 bars cho `maxDuration`. (5) Thêm `snapTime` vào section/MIDI item drag. (6) Thêm snap option `'4'` (1 bar). (7) Đồng bộ `PADDING_LEFT = barDuration` ở cả 2 lane.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass.
|
||
|
||
### [2026-07-25 10:08] Task: Merge tempo bar + timebar, beat-based ruler, snap grid
|
||
- **Tóm tắt thay đổi:** Thay thế seconds-based ruler (0.00s, 1.00s...) + TempoTrackLane bằng TimelineRuler (48px canvas) hiển thị bar:beat grid lines + snap sub-ticks + BPM label. Grid WaveformLane khớp ruler, snapTime() đã dùng grid spacing beat — snap items đúng vị trí ruler.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-25 10:35] Task: Restore time duration labels on TimelineRuler + TempoTrackLane
|
||
- **Tóm tắt thay đổi:** Thêm time duration labels (formatTimeSimple: "0.00s", "1.00s",...) vào TimelineRuler và TempoTrackLane. Hiển thị dạng màu cam (#ffb464) ở chân ruler, động điều chỉnh interval (1s/2s/5s/10s...) theo zoom level. Time labels đã bị mất từ commit ff44fd4 khi merge time bar + tempo bar thành TimelineRuler.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-25 10:39] Task: Sync TempoTrackLane leadIn + time labels on ruler top
|
||
- **Tóm tắt thay đổi:** Thêm `leadInMargin` prop vào TempoTrackLane để đồng bộ column-0 (time=0) với vị trí leadIn của TimelineRuler. Thêm CLIP_BUFFER vào TempoTrackLane. Di chuyển time duration labels trong TimelineRuler từ dưới đáy (y=40) lên phía trên (y=11).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-25 11:09] Task: Fix TempoTrackLane scroll sync during drag auto-scroll
|
||
- **Tóm tắt thay đổi:** TempoTrackLane/WaveformLane/TimelineRuler hiện đọc scrollLeft trực tiếp từ DOM wrapper (canvases tìm parent scrollable) thay vì React prop. Tránh lệch vị trí khi auto-scroll nhanh trong clip drag do React state batching.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-25 11:04] Task: Fix PADDING_LEFT alignment + right-side bar rendering
|
||
- **Tóm tắt thay đổi:** Set PADDING_LEFT=0 trong TimelineRuler/WaveformLane/TempoTrackLane để grid column 0 khớp với clip position 0 (trước đây lệch 8px). TempoTrackLane dùng CLIP_BUFFER/zoom cho tEnd thay vì PADDING_LEFT để bar/beat vẽ đủ xa về phía phải khi scroll.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-25 10:58] Task: Fix clip/section snap to column 0
|
||
- **Tóm tắt thay đổi:** Xóa `Math.max(beatSec, ...)` trong clip drag (10325), section drag (10495), section resize (10576) — thay bằng `Math.max(0, ...)` để item có thể snap vào column 0.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-25 10:54] Task: Fix TempoTrackLane height + clamp selection at column 0
|
||
- **Tóm tắt thay đổi:** Fix TempoTrackLane canvas height = 40px cố định (thay vì parentElement.clientHeight gây tràn). Wrapper `h-10 shrink-0 overflow-hidden` để không đẩy track lanes lệch TCP. Clamp selLeft/selRight/ruler handlers >= 0 để selection không vào vùng âm.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-25 10:44] Task: Zero lead-in, time ruler + tempo track split layout
|
||
- **Tóm tắt thay đổi:** Xóa leadInMargin (về 0) — time 0 tại mép trái, chỉ còn 8px PADDING_LEFT. TimelineRuler đơn giản thành time-only ruler (40px, chỉ hiển thị time labels). Thêm TempoTrackLane ngay dưới (40px, bars/beats/BPM). Selection overlay top từ 48→80px. Đồng bộ WaveformLane PADDING_LEFT=8/zoom.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-25 10:18] Task: Lead-in margin, bar 0-indexed, grid clip fix
|
||
- **Tóm tắt thay đổi:** Thêm leadInMargin (1 bar) đẩy timeline content sang phải — bar 0, time 0s tại mép margin. Playhead bắt đầu từ đó. Bar numbering 0-indexed (bar 0 thay vì 1). Sửa CLIP_BUFFER từ 200px → max(400, barWidth+200) để bar number không bị che. Đồng bộ leadInMargin vào tStart/tEnd canvas + mouse handlers (ruler, clip drag, section resize).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-25 11:32] Task: Redraw canvas on section item duration resize
|
||
- **Tóm tắt thay đổi:** Thêm `setCanvasRedrawCount(n => n + 1)` trong resize mousemove handler để WaveformLane redraw khi section/MIDI item duration thay đổi.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-25 17:22] Task: Piano roll toolbar - transport, ARM/INPUT, AI bar range
|
||
- **Tóm tắt thay đổi:** Thêm transport (play/stop/back/next/record), ARM+INPUT select MIDI, AI bar range (bar x-y) vào piano roll toolbar. handleAISend phát hiện piano roll active → MIDI prompt. Thêm selectedMidiInputId + handleMidiInputSelect.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-25 22:05] Task: Fix piano roll selection, playback, marquee drag/resize/move
|
||
- **Tóm tắt thay đổi:** (1) Fix playback duration — min 16 beats when no notes exist. (2) Selection marquee: green transparent (#22c55e), full-height from top to CC lane. (3) Document-level mousemove/mouseup for marquee drag (không dừng khi ra khỏi grid). (4) Edge resize handles (left/right) cho marquee. (5) Body drag (move) cho marquee. Hiển thị marquee trên cả grid canvas và CC canvas.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `node --check app/static/js/app.precompiled.js` — syntax OK.
|
||
---
|
||
|
||
### [2026-07-25 22:18] Task: Fix marquee mouseleave, loop reschedule, tempo drag
|
||
- **Tóm tắt thay đổi:** (1) Separate onMouseLeave from onMouseUp for grid canvas — preserve marquee when mouse leaves canvas. (2) Add MIDI note rescheduling on piano roll loop restart — 2nd+ loop iteration now plays sound. (3) Tempo bar drag already uses document-level listeners — works across columns.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.precompiled.js`
|
||
---
|
||
|
||
### [2026-07-26 10:24] Task: Fix scroll speed match mouse + CC flicker useLayoutEffect
|
||
- **Tóm tắt thay đổi:** (1) Brush auto-scroll: thêm direct scroll target — mỗi mousemove tính pitchPixel, nếu gần rìa visible area (2 note margin) thì scroll trực tiếp tới target để tốc độ cuộn = tốc độ chuột. Giữ interval 16ms fallback cho trường hợp chuột ở rìa viewport. (2) CC flicker: đổi `React.useEffect` → `React.useLayoutEffect` cho CC canvas rendering effect — canvas paint trước browser paint, loại bỏ flash khi state thay đổi.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Direct scroll target: `container.scrollTop = target` trên mỗi mousemove khi pitch ở gần rìa.
|
||
---
|
||
|
||
### [2026-07-26 10:28] Task: Fix velocity flicker - grid canvas useLayoutEffect
|
||
- **Tóm tắt thay đổi:** Đổi grid canvas effect từ `React.useEffect` → `React.useLayoutEffect`. Nguyên nhân flicker: khi velocity thay đổi, grid canvas (useEffect) vẽ async sau paint → hiển thị velocity fill cũ 1 frame, trong khi CC canvas (useLayoutEffect) đã vẽ sync trước paint → hiển thị stem mới. Sự mismatch giữa 2 canvas tạo flicker. Fix: cả grid + CC canvas đều dùng useLayoutEffect, đồng bộ trước paint.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 10:39] Task: Fix brush shape draw + remove transport + loop repeat sound
|
||
- **Tóm tắt thay đổi:** (1) Brush drawing: thay vì redistribute notes evenly, append từng note mới tại beat position thực tế (`getSnapBeat(beat, snapVal)`) — notes theo đúng hình dạng brush stroke. (2) Xóa transport controls (back/play/stop/record/forward) khỏi MIDI tab toolbar — chỉ dùng global transport. (3) Loop repeat: thêm `schedulePianoRollMidi` helper — reschedule MIDI notes qua SonicSF mỗi khi loop repeat, fix lỗi notes trong vùng chọn không có âm thanh khi play lặp lại.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 10:44] Task: Fix brush drawing smooth - track pitch + beat delta
|
||
- **Tóm tắt thay đổi:** Brush drawing: theo dõi cả `lastDrawBeat` và `visitedPitches`. Tạo note mới khi pitch thay đổi HOẶC beat di chuyển >= minBeatStep (25% của grid duration). Dùng raw `beat` thay vì `getSnapBeat` để note theo sát vị trí chuột. Khởi tạo `lastDrawBeat: start` trong draggedNote. Fix lỗi draw bị ngắt quãng do chỉ tạo note khi pitch thay đổi.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 10:47] Task: Fix brush draw only pitch change + CC velocity flicker skip
|
||
- **Tóm tắt thay đổi:** (1) Brush draw: chỉ tạo note mới khi `pitchChanged` (pitch chưa visited). Nếu cùng pitch, extend `duration_beats` của note cuối cùng trong `brushIds` — không tạo note mới trùng pitch. (2) CC velocity: thêm check `Math.abs(currentVal - val) > 0.001` trước mỗi `setNotes` — skip render khi velocity/pan không thay đổi, loại bỏ flicker cho cả selected + non-selected mode.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. currentVal tính: velocity = note.velocity || 0.8; pan = (note.pan || 0) / 2 + 0.5.
|
||
|
||
### [2026-07-26 10:49] Task: Fix brush draw per-note duration + noteStartBeats tracking
|
||
- **Tóm tắt thay đổi:** (1) Thêm `noteStartBeats` array trong `draggedNote` — track start beat của từng note. Khi tạo note mới (pitchChanged), fix duration của note trước đó thành `beat - prevNoteBeat`. Khi extend (cùng pitch), chỉ extend note cuối với `beat - lastNoteBeat` (không extend toàn bộ stroke). (2) Khởi tạo `noteStartBeats: [start]` trong setDraggedNote. Fix lỗi note cuối có duration dài bằng toàn bộ brush stroke.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 10:55] Task: Fix brush draw pitch revisit by comparing lastDrawnPitch
|
||
- **Tóm tắt thay đổi:** Thay `!visited.includes(snappedPitch)` bằng `snappedPitch !== lastDrawnPitch`. Bug cũ: khi vẽ C→D→C, visited.includes(C) = true → không tạo note mới → note D cũ bị extend. Fix: so sánh với pitch của note cuối cùng, cho phép vẽ tiếp khi pitch thay đổi dù đã visit trước đó. Bỏ `visitedPitches` (không còn dùng).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 10:58] Task: Fix CC note detection with Y-aware stem matching
|
||
- **Tóm tắt thay đổi:** Thêm `findCCNoteIndex(beat, mouseY, ccH)` helper — tìm note trong CC lane dựa trên cả X (beat) và Y (vị trí stem top). Khi nhiều note cùng beat (chord), so sánh `mouseY` với stem top của từng note, chọn note có stem gần nhất. Dùng helper này thay thế tất cả `notes.findIndex` và nearest-fallback trong `handleCCMouseDown` và `handleCCMouseMove`. Fix lỗi không detect được note khi velocity gần nhau hoặc cùng beat.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 11:03] Task: Remove MIDI tab loop button + Ctrl+click clear range
|
||
- **Tóm tắt thay đổi:** (1) Xóa nút loop (repeat) khỏi MIDI tab toolbar — dùng global transport loop button thay thế. (2) Ctrl/Meta+Click trên ruler (tempo track lane) xóa loop range: set loopStartBeat/loopEndBeat = null, selectionStart/selectionEnd = null.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 11:06] Task: Fix MIDI tab real-time instrument change - restart playback
|
||
- **Tóm tắt thay đổi:** `setTrackInstrumentWithProgram`: khi đổi instrument, kiểm tra nếu có PIANO_ROLL sub-tab với trackId đang playing → gọi `stopAllPlayback()` + `setTimeout` 50ms để `schedulePianoRollMidi` + `startSubTabPlayback` với instrument mới. Fix lỗi MIDI tab không cập nhật realtime khi thay đổi Synth.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 11:07] Task: Fix instrument change breaks loop playback
|
||
- **Tóm tắt thay đổi:** Khi đổi instrument trong lúc MIDI tab đang play loop, restart code giờ cũng reset `startBufferOffsetRef` và gọi `requestAnimationFrame(updatePlayhead)` để tiếp tục vòng lặp animation. Fix lỗi loop bị ngắt sau khi change instrument.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 11:17] Task: Real-time velocity, ruler play, export MIDI button
|
||
- **Tóm tắt thay đổi:** (1) Thêm `onRescheduleMidi` prop — khi edit velocity trong CC lane lúc đang play, gọi `window.SonicSF.stopAll()` + `schedulePianoRollMidi` với velocity mới. (2) Click ruler: set `currentTime` + nếu đang play thì `onStop` + `setTimeout` `onPlayPause` để play từ vị trí click. (3) Thêm nút Export cạnh Save — inline MIDI writer, xuất `.mid` file từ notes array.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 11:27] Task: Fix realtime velocity stale notes + ruler click restart
|
||
- **Tóm tắt thay đổi:** (1) Compute `updatedNotes` synchronously trước khi gọi `setNotes`, pass into `onRescheduleMidi(updatedNotes)`. `schedulePianoRollMidi` nhận tham số `notesOverride` — dùng notes mới thay vì `st.notes` (stale). (2) Ruler click: thay `onStop()` (reset currentTime) bằng `stopAllPlayback()` trực tiếp + `schedulePianoRollMidi` + `startSubTabPlayback` + RAF từ `clickTime`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 11:32] Task: Fix range auto-loop + gain fade-out crackling
|
||
- **Tóm tắt thay đổi:** (1) Xóa `setIsLooping(true)` và `isLooping: true` khỏi ruler drag handler — range selection không còn tự động loop, chỉ loop khi nút LOOP global được kích hoạt. (2) Thay `window.SonicSF.stopAll()` abrupt bằng gain fade-out: `linearRampToValueAtTime(0.001, +40ms)` → delay 50-60ms → `stopAll` + reschedule → `linearRampToValueAtTime(volLinear, +15ms)`. Áp dụng cho cả `onRescheduleMidi` và `setTrackInstrumentWithProgram`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 11:35] Task: Fix ruler click playhead - use onStop+onPlayPause props
|
||
- **Tóm tắt thay đổi:** Sửa ruler click handler — xóa calls tới `stopAllPlayback`/`schedulePianoRollMidi`/`updatePlayhead` (không có trong scope PianoRollTabEditor). Dùng props: `setSubTabs({currentTime, isPlaying: false})` → `onStop()` → timeout 40ms → `setSubTabs({currentTime})` → `onPlayPause()`. Playhead di chuyển và play tiếp từ vị trí click.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 11:40] Task: Fix ruler cursor+range drag + SonicSF smooth stopAll
|
||
- **Tóm tắt thay đổi:** (1) Ruler: cursor default, click trong range → drag move (hand cursor → move), click ngoài range → seek/drag tạo selection mới. (2) `soundfontPlayer.js`: `activeOscillators` lưu `{osc, gain}` thay vì `osc`; `stopAll` ramp gain về 0 trong 8ms trước khi `osc.stop(10ms)` — loại bỏ crackling khi realtime update. Cũng xóa `setIsLooping(true)` trong ruler drag cũ.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 11:56] Task: Fix Piano Roll playhead seek + sound cracking
|
||
- **Tóm tắt thay đổi:** (1) Click ruler khi Piano Roll đang play → seek đến vị trí mới và tiếp tục play (không dừng). (2) ADSR envelope trong `soundfontPlayer.js` dùng `linearRampToValueAtTime` thay `setValueAtTime`/`exponentialRampToValueAtTime` để loại bỏ gain jump gây crackling. `stopAll` ramp gain về 0 trong 20ms trước khi stop oscillator.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 12:20] Task: Fix Piano Roll seek-to-start during playback
|
||
- **Tóm tắt thay đổi:** Thêm hàm `seekPlaybackTo(time)` xử lý seek khi đang play cho cả sub-tab và main timeline. Nút "Quay lại đầu" (`skip-back`) và "Đầu vùng chọn" (`step-back`) giờ restart playback từ vị trí mới: stop → update refs → reschedule MIDI → restart.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 12:13] Task: Fix Piano Roll loop/stop behavior
|
||
- **Tóm tắt thay đổi:** (1) Sub-tab loop selection chỉ active khi nút Loop (st.isLooping) bật — không còn bị ảnh hưởng bởi global isLoopingSelection. (2) Stop dừng ngay lập tức: `SonicSF.stopAll()` set gain 0 và stop oscillator tại ctx.currentTime, không ramp.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||
|
||
### [2026-07-26 12:31] Task: VST/SoundFont inspection engine + AI schema
|
||
- **Summary:** New `SoundFontInspector` (sf2utils) scans .sf2 files, generates full/condensed catalog. Added `/soundfonts/catalog` API. Updated AI tool schema with `soundfont_id/bank/program`. Condensed catalog auto-injected into system prompt. Server render now passes bank/program + MIDI channel routing (drums → ch9). Added `DecentSamplerManager` + Pianobook render path. Client `SonicSF`: `applyAITrackInstrument`, `controllerChange`, `programChange`. Catalog cache invalidated on upload.
|
||
- **Files:** `requirements.txt`, `app/core/soundfont_inspector.py` (new), `app/api/v1/plugins.py`, `app/core/vst_engine.py`, `app/core/render_engine.py`, `app/static/js/services/aiGateway.js`, `app/static/js/services/api.js`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.jsx`, `Dockerfile`, `.gitignore`, `vst_plugins/` (new), `samples/pianobook/` (new)
|
||
- **Tests:** `python3 -m pytest tests/ -v` → 61 passed, 1 pre-existing failure. Python syntax check OK on all modified files.
|
||
---
|
||
|
||
### [2026-07-26 16:11] Task: Mount daw_assets host dirs into Docker
|
||
- **Summary:** Created `/home/locpham/daw_assets/{vst3,soundfonts,pianobook}` on host. Mounted them into `web`, `worker`, and `beat` containers at `/opt/daw_engine/vst3`, `/opt/daw_engine/soundfonts`, `/opt/daw_engine/samples/pianobook`.
|
||
- **Files:** `docker-compose.yml`
|
||
- **Tests:** N/A
|
||
---
|
||
|
||
### [2026-07-26 16:21] Task: Implement Synth button with VST3 selection
|
||
- **Summary:** Fixed Synth button to properly select and use system-installed VST3 plugins. Fixed field name mismatch (`instrument_id` vs `instrument`) between UI and render engine. Track auto-switches to MIDI type when VST assigned. SoundFont preset selection now passes `soundfont_bank` + `soundfont_program` to server. Serialize/deserialize includes all instrument fields.
|
||
- **Files:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/core/render_engine.py`
|
||
- **Tests:** `pytest` - 61 passed, 1 pre-existing failure.
|
||
---
|
||
|
||
### [2026-07-26 20:45] Task: Fix timeline play — schedule noteOn via setTimeout
|
||
- **Tóm tắt thay đổi:** SpessaSynth noteOn(ch, pitch, vel, enableReverb) không nhận exactAudioTime. Timeline truyền startTime vào vị trí thứ 4 → SpessaSynth ép kiểu thành enableReverb=true → tất cả notes play ở T=0. Fix: tính delay = startTime - currentTime, dùng setTimeout để schedule noteOn đúng thời điểm.
|
||
- **Các file ảnh hưởng:** app/static/js/services/soundfontPlayer.js, app/static/js/app.precompiled.js
|
||
- **Ghi chú/Test (nếu có):** MIDI keyboard play ngay. Timeline play các note đúng thời điểm.
|
||
---
|
||
|
||
### [2026-07-26 21:00] Task: Sync loop button + auto-derive loop boundaries
|
||
- **Tóm tắt thay đổi:** Nút Loop trên transport controls giờ đồng bộ với sub-tab đang active. Khi bật loop: tự động tính loopEnd từ MIDI notes, section clips, hoặc audio duration. Gán selectionStart=0, selectionEnd=loopEndTime. Main timeline cũng tự derive loop end từ tracks/items/clips.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** Bật Loop → playhead loop từ đầu đến cuối item/clip và ngược lại.
|
||
|
||
### [2026-07-26 21:15] Task: Fix stop + loop for all tab types
|
||
- **Tóm tắt thay đổi:** `stopAll` giờ huỷ tất cả `setTimeout` đã schedule cho SpessaSynth notes (cả noteOn lẫn noteOff), gọi `allNotesOff(0-15)`. Loop hoạt động trên MAIN SESSION (qua `isLoopingSelection` + `selLeft/selRight`), SECTION-TAB và PIANO_ROLL (qua `st.isLooping` + `st.selectionStart/End`).
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** Stop = dừng ngay. Loop = phát từ đầu đến cuối item/clip rồi lặp lại.
|
||
|
||
### [2026-07-26 21:30] Task: Sticky ruler + tempo lane on track scroll
|
||
- **Tóm tắt thay đổi:** Wrap TimelineRuler và TempoTrackLane trong `sticky top-0 z-30 bg-[#1a1a1a]`. Khi cuộn tracks lên, ruler và tempo lane vẫn hiển thị dưới toolbar. Áp dụng cho cả MAIN SESSION và sub-tab views.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** Cuộn tracks lên → ruler + tempo lane dính ở top.
|
||
|
||
### [2026-07-26 22:05] Task: Fix multi-track bank loading (no delete, graceful 404)
|
||
- **Tóm tắt thay đổi:** Xoá `deleteSoundBank` — SpessaSynth giữ nhiều banks đồng thời. Kiểm tra `soundBankList` trước khi tải lại. 404 từ server không làm mất bank đã load trước đó.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`
|
||
- **Ghi chú/Test (nếu có):** 3 tracks với 3 SF khác nhau → SpessaSynth giữ cả 3 → noteOn dùng đúng preset từ bank tương ứng.
|
||
|
||
### [2026-07-27 07:55] Task: Fix item duration when BPM changes
|
||
- **Tóm tắt thay đổi:** Thêm `length_bars` vào midiItems/sections. `useEffect` trên BPM recalc `duration = length_bars * currentSecondsPerBar`. Fix section hardcoded 4s → `4 * secondsPerBar`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** Tạo 8 bars item ở BPM 80 → đổi BPM 120 → item vẫn 8 bars.
|
||
|
||
### [2026-07-27 08:20] Task: Fix TempoTrackLane bar spacing on BPM change
|
||
- **Tóm tắt thay đổi:** DOM traversal tìm scrollLeft bị gãy do sticky wrapper (tìm nhầm div có `position: sticky` với scrollLeft=0). Xoá DOM lookup, dùng `scrollLeft` prop trực tiếp. Fix cho cả TimelineRuler và TempoTrackLane.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** Thay đổi BPM → bars trên tempo track lane vẽ đúng khoảng cách.
|
||
|
||
### [2026-07-27 08:25] Task: Fix bar width drift for non-integer BPM
|
||
- **Tóm tắt thay đổi:** Vòng lặp `t += beatDuration` tích luỹ sai số dấu phẩy động (đặc biệt BPM lẻ như 77, 81). Chuyển sang `t = bn * beatDuration` với bn là số beat nguyên — mỗi bar line tính độc lập, không sai số tích luỹ.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** BPM 77 → tất cả bars đều rộng bằng nhau.
|
||
|
||
### [2026-07-27 08:40] Task: TimelineRuler draws bar markers aligned with TempoTrackLane
|
||
- **Tóm tắt thay đổi:** Thêm bar markers vào TimelineRuler dùng cùng `bn * barDuration` với TempoTrackLane. Số bar hiển thị ở giữa ruler. Time markers vẫn giữ phía dưới. Cùng BPM prop → luôn đồng bộ với tempo track.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** Thay đổi BPM → TimeRuler bars + TempoTrackLane bars luôn đồng nhất.
|
||
|
||
### [2026-07-27 08:48] Fix: equal bar widths + selection tempo/time mode
|
||
- **WaveformLane:** Integer beat loop thay vì `t += beatDuration` (fix bar width drift)
|
||
- **Selection mode:** Nút toggle ♪T (theo tempo - đổi BPM → selection thay đổi) / ⏱T (cố định thời gian)
|
||
|
||
### [2026-07-27 09:25] Feat: eager soundfont scan at startup
|
||
- **App load**: fetch full catalog with all instrument presets
|
||
- **Merge**: gán presets vào từng soundfont trong instrumentSelectorData
|
||
- **Synth button**: hiển thị presets từ cache, không cần API call per-SF
|
||
|
||
### [2026-07-27 09:55] Feat: Synth button shows flat instrument list from all SFs
|
||
- **openInstrumentSelector**: flatten presets from all SFs + soundfont name tag
|
||
- **Modal**: search bar + flat instrument list + soundfont name prefix
|
||
- **Fix**: `2ad2922` had extra closing paren in PluginManagerModal (build fix)
|
||
- **Arch**: loại bỏ hai-bước, click Synth → chọn instrument trực tiếp
|
||
|
||
### [2026-07-27 11:05] Feat: MIDI keyboard NoteOff sustain + CC/pitch bend realtime
|
||
- **NoteOff**: durationMs=60000, NoteOff gọi stopNote(ch,pitch) → note tắt ngay
|
||
- **MIDI channel**: truyền msg.data[0]&0x0F vào playNote/stopNote
|
||
- **CC64 sustain/CC1 modulation**: forward qua SonicSF.controllerChange
|
||
- **Pitch Bend**: forward qua SonicSF.pitchBend (14-bit)
|
||
|
||
### [2026-07-27 11:20] Fix: All Notes Off (CC123) before MIDI NoteOn
|
||
- **Nguyên nhân:** SoundFonts như saxophone có release envelope dài, noteOff không tắt âm ngay
|
||
- **Fix:** Gửi `controllerChange(ch, 123, 0)` = All Notes Off trước mỗi NoteOn từ MIDI keyboard
|
||
- **Kết quả:** Note trước tắt ngay khi play note mới, không còn đè âm
|
||
|
||
### [2026-07-27 11:45] Fix: lazyInit in playNote + 3 root cause fixes
|
||
- **Root cause 1:** NoteOn with velocity=0 treated as NoteOff ✅ (cmd === 0x8 || (cmd === 0x9 && vel === 0))
|
||
- **Root cause 2:** Same channel extraction for NoteOn+NoteOff ✅ (msg.data[0] & 0x0F)
|
||
- **Root cause 3:** pitch/channel always integers from msg.data[] ✅
|
||
- **Added:** _lazyInit() called from playNote when _initialized false
|
||
- **Oscillator fallback:** 2000ms instead of 60000ms (quick preview before init)
|
||
|
||
### [2026-07-27 12:05] Task: Fix Tremolo String continuous sustain after MIDI note-off
|
||
- **Tóm tắt thay đổi:** Root cause: `stopNote()` used CC 123 (All Notes Off) which merely enters SoundFont's release envelope. Sustained instruments like Tremolo Strings (GM#44) have 2-5s release tails. Fix: replaced CC 123 with CC 120 (All Sound Off) in `stopNote()` and `stopAll()`. CC 120 sets `voice.isActive = false` immediately, bypassing the release envelope entirely, so the note stops instantly on key release.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`
|
||
- **Ghi chú/Test (nếu có):** Test với MIDI keyboard + Tremolo Strings instrument: note tắt ngay khi release key. CC 120 tác dụng lên toàn bộ channel (kill all voices) — phù hợp với keyboard preview use-case.
|
||
|
||
### [2026-07-27 12:45] Task: Migrate SpessaSynth → FluidSynth WASM engine
|
||
- **Tóm tắt thay đổi:** Thay thế SpessaSynth (JS/AudioWorklet) bằng FluidSynth WASM (C++ compiled via Emscripten). CDN `@enikey87/fluidsynth-emscripten@0.1.1` cho dev (localhost), self-host `/static/js/vendor/` cho production. Giải quyết triệt để stuck notes (Tremolo/Saxophone) nhờ FluidSynth xử lý Gen 44 loop mode chuẩn SF2.04. Thêm `fluidsynth-bridge.js` AudioWorklet nhận PCM từ main-thread FluidSynth render loop. Giữ nguyên `window.SonicSF` API surface (0 thay đổi ở app.jsx).
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html`, `app/static/js/services/fluidsynthLoader.js` (NEW), `app/static/js/worklets/fluidsynth-bridge.js` (NEW), `app/static/js/vendor/libfluidsynth-2.3.0-sf3.js` (NEW), `app/static/js/vendor/libfluidsynth-2.3.0-sf3.wasm` (NEW)
|
||
- **Ghi chú/Test (nếu có):** Server render (pyfluidsynth) + client preview (FluidSynth WASM) = cùng C++ core → 100% audio parity. Cần clear browser cache. Babel 8 syntax error pre-existing (JSX `{ if(...) { } }` trong app.jsx), không rebuild được app.precompiled.js.
|
||
---
|
||
|
||
### [2026-07-27 15:59] Fix: _activeNotes array — multi-track ARM cùng pitch
|
||
- **Tóm tắt thay đổi:** Khi 2 track ARM play cùng MIDI pitch, `_activeNotes['0:60']` bị overwrite (track2 ghi đè track1). `stopNote` chỉ stop 1 track. Fix: `_activeNotes[key] = [ch1, ch2, ...]` array. `stopNote` iterate all channels.
|
||
- **Các file ảnh hưởng:** `soundfontPlayer.js`
|
||
- **Ghi chú/Test (nếu có):** ARM 2 track, play note → thả phím → cả 2 track stop.
|
||
---
|
||
|
||
### [2026-07-27 15:47] Fix: Multi-track ARM + activeNotes cho stopNote
|
||
- **Tóm tắt thay đổi:** MIDI handler `find(t=>isArmed)` → `filter(...)` loop all armed tracks. Thêm `_activeNotes[ch:pitch]` map để `stopNote` lookup đúng channel sau khi đổi SoundFont (channel allocation thay đổi).
|
||
- **Các file ảnh hưởng:** `soundfontPlayer.js`, `app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** ARM 2 track cùng SF khác instrument → cả 2 play. Đổi SF → noteOff vẫn tìm đúng voice.
|
||
---
|
||
|
||
### [2026-07-27 16:05] Fix: program_select thay bank_select+program_change (SF preset search order)
|
||
- **Tóm tắt thay đổi:** `bank_select` + `program_change` dùng search order qua tất cả SF đã load. Khi track 1 load SF2, preset lookup trên channel 0 tìm thấy preset của SF2 thay vì SF1 (SF2 là last-loaded → priority). Fix: `fluid_synth_program_select(synth, ch, sfHandle, bank, prog)` chỉ định rõ SoundFont ID, không phụ thuộc search order. Track 2 giữ nguyên instrument của SF1.
|
||
- **Các file ảnh hưởng:** `soundfontPlayer.js`
|
||
- **Ghi chú/Test (nếu có):** Track 1 SF1→SF2, track 2 SF1 → track 2 không bị ảnh hưởng.
|
||
---
|
||
|
||
### [2026-07-27 12:15] Task: Fix SpessaSynth loop voice not releasing (layer 2)
|
||
- **Tóm tắt thay đổi:** CC 120 vẫn không đủ vì SpessaSynth 4.3.1 AudioWorklet có bug: looped voices trong MIDI message pipeline xử lý CC 120 sai (`processMessage`). Fix: thêm `noteOn(ch, pitch, 0)` (MIDI noteOff alternate path) + `_synthInstance.post({channelNumber:ch, type:"stopAll", data:1})` gửi lệnh trực tiếp đến worklet qua `handleMessage` — bypass hoàn toàn MIDI pipeline.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Cần clear cache browser (index.html cache-bust param updated).
|
||
---
|
||
|
||
### [2026-07-27 17:07] Task: MIDI Ghost Notes + Dropdown Item Switcher + Session Sync Mode
|
||
- **Tóm tắt thay đổi:** Thêm ghost notes cho Piano Roll tab: tất cả MIDI items không được chọn thành ghost notes (25% opacity, dashed border). Dropdown thay thế tab title để chuyển nhanh MIDI item đang edit. Hai chế độ xem: Session Sync (ghost visible, bar labels aligned với session) và Isolated (bar 0, no ghost).
|
||
- **Các file ảnh hưởng:** `app/static/js/services/ghostNoteExtractor.js`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Kiểm tra dropdown list đúng tất cả MIDI items. Switch item → ghost notes của item cũ hiện ra. Nút 🌐 Session/📋 Isolated chuyển chế độ. 👻 Ghost toggle chỉ hoạt động ở Session mode.
|
||
|
||
### [2026-07-28 07:03] Task: Auto-scan soundfont cho Synth button
|
||
- **Tóm tắt thay đổi:** Thêm `SoundFontAutoScanner` — background daemon thread quét thư mục `/opt/daw_engine/soundfonts` và `app/storage/soundfonts` mỗi 30s. Dùng `sf_scan_state.json` để track file đã quét (theo size+mtime), chỉ inspect file mới/thay đổi. Catalog dùng scanner cache thay vì regenerate từ đầu mỗi request. `/soundfonts/catalog` API dùng scanner catalog. Upload/delete trigger scan tức thì. Thêm `.gitignore` cho `*.db` và `sf_scan_state.json`.
|
||
- **Các file ảnh hưởng:** `app/core/soundfont_scanner.py` (NEW), `app/api/v1/plugins.py`, `app/core/soundfont_inspector.py`, `app/main.py`, `.gitignore`
|
||
- **Ghi chú/Test (nếu có):** `python3 -c "from app.core.soundfont_scanner import SoundFontAutoScanner; s=SoundFontAutoScanner(); print(s.scan_once())"` — scan hoạt động. Condensed catalog dùng scanner's full_catalog tránh duplicate scan.
|
||
---
|
||
|
||
- **Tóm tắt thay đổi:** Verify 4 rules: (1) MIDI item nhận instrument từ track cha qua `handleSwitchMidiItem` copy `instrumentProgram` từ track → sub-tab; (2) Section item KHÔNG nhận instrument (reset về null khi clone tracks trong `handleEditSectionInTab`); (3) Track khác không bị ảnh hưởng do `setTrackInstrumentWithProgram` filter by `trackId`; (4) Đổi instrument trong PianoRoll tab → áp dụng ngược lại cho track qua `openInstrumentSelector` → `setTrackInstrumentWithProgram`. Tất cả đều OK, không bug.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx` (verify lines 4642-4667, 5905, 6763-6807, 8422-8424), `app/core/render_engine.py` (verify lines 95-115)
|
||
- **Ghi chú/Test (nếu có):** No code changes needed. All 4 rules confirmed working.
|
||
---
|
||
|
||
### [2026-07-27 20:37] Task: Fix cross-track instrument interference (missed channel param)
|
||
- **Tóm tắt thay đổi:** Sửa 6 vị trí gọi `SonicSF.playNote()` thiếu tham số `channel` + `synthEngine`, khiến FluidSynth mặc định `ch=0` cho mọi track → `program_change` trên channel 0 đè lên nhau giữa các track. Fix: thêm channel computation (từ `track.midiChannel` hoặc track index) + truyền `synth_engine` vào `playNote` tại `startTrackPlayback`, section sub-track MIDI, `playMidiPreviewNote`, piano roll Alt+scroll preview, note click preview, keybed onMouseDown/onMouseEnter. `setTrackInstrumentWithProgram` đã filter đúng `trackId` — lỗi không nằm ở logic set instrument mà ở playback.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx` (lines 4846, 5121, 5661-5671, 10117-10124, 10179-10201, 10285-10307)
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Cần test: main session play 2 tracks MIDI khác instrument → mỗi track giữ instrument riêng. Piano Roll preview note → không ảnh hưởng track khác.
|
||
---
|
||
|
||
### [2026-07-27 20:44] Task: Apply md/41_INSTRUMENT.md spec to codebase
|
||
- **Tóm tắt thay đổi:** Implement spec: (1) Tạo `pianoRollTabService.js` với `getParentTrackByItemId`, `buildActiveScope`, `getTrackMidiChannel`; (2) Thêm `parent_track_id` vào tất cả MIDI items creation (deserialize, recording, AI, API); (3) `handleSwitchMidiItem` dùng `buildActiveScope` pattern — thêm `active_scope` vào sub-tab state; (4) Toolbar Piano Roll hiển thị `(Belongs to: Track X)` và `Synth (Track X): Instrument`; (5) Đồng bộ channel assignment về `trackIndex % 16` (spec §4); (6) Toàn bộ `playNote` calls dùng `SonicPianoRoll.getTrackMidiChannel` thay for-loop channel compute.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/pianoRollTabService.js` (NEW), `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Cần test: multi-item piano roll switch item → scope cập nhật đúng. Parent track label xuất hiện trên toolbar.
|
||
---
|
||
|
||
### [2026-07-27 21:25] Task: Fix _playNoteFluid else branch leaking cross-track SF
|
||
- **Tóm tắt thay đổi:** Khi track chưa set instrument, `_playNoteFluid` gọi `program_change(ch, 0)`. FluidSynth search program 0 trong ALL loaded SoundFonts → nếu chỉ có SoundFont của Track 2 được load, program 0 của SF đó gán cho channel của Track 1. Fix: bỏ `program_change`, dùng oscillator fallback trực tiếp. Track không instrument → sine wave, không leak instrument từ track khác.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Cần test: Track 2 set instrument, Track 1 không set → Track 1 play sine wave (không phải SF program 0 của Track 2).
|
||
---
|
||
|
||
### [2026-07-27 21:29] Task: Fix multi-track ARM only first track produces sound
|
||
- **Tóm tắt thay đổi:** ONMIDIMESSAGE handler (`app.jsx:6954-6998`) dùng raw MIDI hardware channel (`msg.data[0] & 0x0F`) thay vì dedicated channel của track, và `find(t => t.isArmed)` chỉ lấy track ARM đầu tiên. Fix: dùng `filter(t => t.isArmed)` + `getTrackMidiChannel` cho từng track — MIDI Note On/Off, CC, Pitch Bend đều route tới ALL armed tracks trên dedicated channel của mỗi track.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx` (lines 6960-7040), `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Cần test: ARM cả 2 track, bấm phím MIDI → cả 2 track đều phát ra instrument riêng. NoteOff gửi đúng channel.
|
||
---
|
||
|
||
### [2026-07-27 21:37] Task: Ctrl+drag on ruler for global selection
|
||
- **Tóm tắt thay đổi:** `handleRulerMouseDown` cũ khi `e.ctrlKey` chỉ clear selection rồi return. Fix: Ctrl+click trên ruler khởi tạo `selectionMode='global'`, set `rulerDragStartRef` và `isDraggingRulerRef=true` → document mousemove handler (đã có sẵn) cập nhật selection range, selection overlay (đã render sẵn) hiển thị vùng chọn.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx` (line 11133-11142), `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Cần test: Ctrl+drag trên ruler → selection overlay xuất hiện. Shift+click vẫn extend selection.
|
||
---
|
||
|
||
### [2026-07-27 21:47] Task: Ctrl+drag sweep select + multi-item drag/copy
|
||
- **Tóm tắt thay đổi:** (1) Ctrl+drag trên empty space (track lane) → sweep select (vùng chọn màu amber), mouseup → chọn tất cả items chạm vùng sweep. (2) Selected items highlight (viền vàng dày). (3) Ctrl+drag trên section/MIDI item → duplicate item. (4) Drag trên selected item → move cả nhóm items đã chọn. (5) Ctrl+drag trên selected item → copy cả nhóm.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Cần test: Ctrl+drag empty space → sweep overlay + selected items highlight. Click+drag selected item → cả nhóm move. Ctrl+drag selected item → copy nhóm.
|
||
---
|
||
|
||
### [2026-07-28 08:09] Task: Fix Ctrl+Click+Drag không chọn item trong SECTION-TAB
|
||
- **Tóm tắt thay đổi:** Xoá toggle selection (select/deselect) khỏi Ctrl+Click handler cho section/MIDI items. Trước đây Ctrl+Click vừa chọn item vừa set pending drag → item bị khoá cứng (selected), không thể kéo copy. Fix: Ctrl+Click chỉ set pending drag để copy, không chạm selection state.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Ctrl+Click+Drag section/MIDI item → copy đến vị trí mới, item không bị selected.
|
||
---
|
||
|
||
### [2026-07-28 11:17] Task: AI Copilot Suggestion Chips + Typeahead (spec 44_AI_SUGGEST_PROMT.md)
|
||
- **Tóm tắt thay đổi:** Thêm suggestion chips bar trên Copilot Prompt: context-aware (MIDI selected → amber rearrange chips, no selection → indigo creation chips). Typeahead dropdown với keyword matching khi gõ ≥2 ký tự, Tab để chọn. `getSelectedMidiItemInfo` helper. Backend ai_presets.py thêm 4 rearrange presets. `promptTemplateManager.js` thêm `getContextualSuggestions(hasSelectedItem)`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/promptTemplateManager.js`, `app/api/v1/ai_presets.py`, `tests/test_ai_presets.py`
|
||
- **Ghi chú/Test (nếu có):** `npx babel` compile pass. `pytest tests/test_ai_presets.py -v` — 7/7 pass.
|
||
---
|
||
|
||
### [2026-07-28 11:02] Task: AI Rearrange Scenario Detection (spec 45_SCENARIAO_AI.md)
|
||
- **Tóm tắt thay đổi:** Thêm 9 rearrange scenarios vào `aiGateway.js` (arpeggio, harmonies, syncopation, walking bass, jazz swing, synthwave, cinematic, simplify, passing tones) với keyword mapping → `detectRearrangeScenario()`. `buildRearrangeMessage` tự động inject specific technique rules dựa trên scenario detect được. Thêm 9 rearrange presets vào `promptTemplateManager.js` với category "Rearrange / Variation".
|
||
- **Các file ảnh hưởng:** `app/static/js/services/aiGateway.js`, `app/static/js/services/promptTemplateManager.js`
|
||
- **Ghi chú/Test (nếu có):** `node --check` pass. `npx babel` compile pass. Preset Manager hiển thị 12 presets (3 cũ + 9 rearrange). Gõ "arpeggio" → AI nhận dạng scenario → inject technique "Subdivide sustained chord notes into 0.25-beat arpeggiated steps".
|
||
---
|
||
|
||
### [2026-07-28 10:52] Task: MIDI Rearrange Feature (spec 41_AIMIDI_FIX.md)
|
||
- **Tóm tắt thay đổi:** Tạo `midiExtractor.js` — `extractSelectedMIDIContext` trích xuất notes + note names từ selected MIDI item. Thêm `REARRANGE_TOOL_SPEC` + `buildRearrangeMessage` trong `aiGateway.js`. `handleAISend` phát hiện selected MIDI item → dùng rearrange flow riêng (source notes → AI → `rearrange_midi_melody` → A/B track bên dưới source). Handler `rearrangeMidiMelody` tạo track `[AI Rearrange] <title>` + MIDI item ngay dưới source track.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/midiExtractor.js` (NEW), `app/static/js/services/aiGateway.js`, `app/static/js/services/dawCommandDispatcher.js`, `app/static/js/app.jsx`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `node --check` pass. `npx babel` compile pass. Cần chọn MIDI item → gõ prompt rearrange → track A/B mới xuất hiện.
|
||
---
|
||
|
||
### [2026-07-28 10:39] Task: AI MIDI Prompt Template & Preset Engine (spec 33_MIDI_AI.md)
|
||
- **Tóm tắt thay đổi:** Tạo `PromptTemplateManager` service (keyword matching với scoring, CRUD, star/fav toggle). Backend `ai_presets.py` CRUD router (JSON file storage, auth isolation). Refactor `AIPresetModal` dùng service + star toggle + backend API. Piano Roll AI wiring. 7 tests cho matching + backend CRUD + auth isolation.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/promptTemplateManager.js` (NEW), `app/api/v1/ai_presets.py` (NEW), `tests/test_ai_presets.py` (NEW), `app/main.py`, `app/static/js/services/api.js`, `app/static/js/app.jsx`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `python3 -m pytest tests/test_ai_presets.py -v` — 7 tests pass. Pre-existing 1 test fail (`test_api_user_ai_config` permission error, unrelated).
|
||
---
|
||
|
||
### [2026-07-28 10:15] Task: Dropdown CC lane — Velocity, Sustain, Modulation, Pitch Bend, Pan
|
||
- **Tóm tắt thay đổi:** Thay button group (velocity/pan) bằng `<select>` dropdown với 5 options. Hiển thị mặc định là Velocity. Button toggle CC lane hiển thị short label (Vel/Sus/Mod/Bend/Pan/CC).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. CC lane dropdown có 5 options, default Velocity.
|
||
---
|
||
|
||
### [2026-07-28 10:10] Task: Ctrl+Click vùng trống không sweep select, chỉ deselect
|
||
- **Tóm tắt thay đổi:** Thay `onSweepSelectStart` bằng `onClearSelection` trong Ctrl+Click vùng không có items. Trước đây gọi `handleSweepSelectStart` → yellow cursor + mousemove chọn items. Fix: `onClearSelection = () => setSelectedItemIds(new Set())` — chỉ clear selection, không sweep.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Ctrl+click vùng trống → deselect hết, không yellow cursor.
|
||
---
|
||
|
||
### [2026-07-28 10:06] Task: Ctrl+S trong SECTION-TAB chỉ lưu section, không export SF
|
||
- **Tóm tắt thay đổi:** Di chuyển SECTION-TAB save check từ handler thứ 2 (dead code, shadowed bởi export handler) lên handler đầu tiên. Line 8066: nếu `activeTab.startsWith('session_')` → `handleSaveSectionTab`, else → `handleExportSFS`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. SECTION-TAB Ctrl+S → lưu section. MAIN Ctrl+S → export SF.
|
||
---
|
||
|
||
### [2026-07-28 09:58] Task: Ctrl+S lưu section, Ctrl+A chọn tất cả items
|
||
- **Tóm tắt thay đổi:** Thêm `activeTab.startsWith('session_')` branch trong Ctrl+S handler → gọi `handleSaveSectionTab`. Thêm Ctrl+A handler: collect tất cả sections/midiItems/clips từ `activeTracksRef.current` vào `selectedItemIds`. Hoạt động trên MAIN SESSION và SECTION-TAB.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. SECTION-TAB Ctrl+S → lưu section. Ctrl+A → chọn tất cả items.
|
||
---
|
||
|
||
### [2026-07-28 09:55] Task: Remove Insert Section từ context menu trong SECTION-TAB
|
||
- **Tóm tắt thay đổi:** Thêm `!sessionTabs.some(s => s.id === activeTab)` cho "Insert Section" button trong JSX context menu. Section không thể chứa section item. Data-driven menu (line 14905) đã có check này.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Right-click trong SECTION-TAB → không thấy "Insert Section".
|
||
---
|
||
|
||
### [2026-07-28 09:50] Task: Program change ở note time, không phải call time
|
||
- **Tóm tắt thay đổi:** Di chuyển `program_change`/`program_select`/`bank_select` từ synchronous (call time) vào trong `doNote` (note time via setTimeout) trong `_playNoteFluid`. Trước đây tất cả program_changes xảy ra ngay khi `startTrackPlayback` chạy → MIDI items processed trước sections → section's program_change override cuối cùng → ALL notes (section + MIDI) play với section instrument. Fix: mỗi note có program_change tại đúng thời điểm của nó.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`
|
||
- **Ghi chú/Test (nếu có):** load. TRACK 1: | SECTION ITEM (inst B) | MIDI ITEM (inst A) | → section plays inst B, MIDI item plays inst A.
|
||
---
|
||
|
||
### [2026-07-28 09:30] Task: Thêm _isSectionClone flag — tắt âm thanh mặc định trong SECTION-TAB
|
||
- **Tóm tắt thay đổi:** Thêm `_isSectionClone: true` vào section cloned tracks. Line 10283 kiểm tra flag: nếu section clone và không instrument → `undefined` (silent); nếu MAIN track và không instrument → `0` (GM Piano). Trước đây section cloned tracks đi qua line 10283 với `instrumentProgram: undefined` → default 0 → play GM Piano.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. SECTION-TAB không instrument → im lặng. MAIN track không instrument → GM Piano.
|
||
---
|
||
|
||
### [2026-07-28 09:27] Task: Silent fallback — không nghe âm thanh mặc định
|
||
- **Tóm tắt thay đổi:** Thay oscillator fallback bằng silent return trong `_playNoteFluid` (soundfontPlayer.js:395). Khi không có instrument data, không play bất kỳ âm thanh nào (cả oscillator lẫn FluidSynth).
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`
|
||
- **Ghi chú/Test (nếu có):** Section sub-track MIDI items không instrument → im lặng. MAIN track MIDI items không instrument → GM Piano (program 0).
|
||
---
|
||
|
||
### [2026-07-28 09:25] Task: Restore program default 0 cho MAIN track MIDI playback
|
||
- **Tóm tắt thay đổi:** Restore line 10283 `instrumentProgram !== undefined ? instrumentProgram : 0` — MAIN track MIDI items cần program 0 để play (GM Piano) khi không load instrument. Chỉ section sub-track (line 10393) giữ `subTrack.instrumentProgram` (undefined → oscillator fallback) để tránh load MAIN SoundFont.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. MAIN track MIDI items play GM Piano khi không instrument. Section sub-track MIDI items dùng oscillator fallback.
|
||
---
|
||
|
||
### [2026-07-28 09:21] Task: Revert fix playhead — gây lỗi không play được
|
||
- **Tóm tắt thay đổi:** Revert 2 changes từ 5d31563: (1) xoá `requestAnimationFrame(updatePlayhead)` thừa trong `handlePlayPause` (effect đã xử lý). (2) xoá `setCurrentTime(0)` trong `handleEditSectionInTab` (gây reset currentTime global). Giữ nguyên các fix instrument isolation + program default.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Cần kiểm tra Play trên MAIN và SECTION-TAB.
|
||
---
|
||
|
||
### [2026-07-28 09:40] Task: Cả main + section MIDI playback không default program 0
|
||
- **Tóm tắt thay đổi:** Thay đổi line 10283 giống line 10393: `instrumentProgram !== undefined ? ... : 0` → `instrumentProgram`. Trước đây main playback cũng default program 0 khi `instrumentProgram` undefined → `_playNoteFluid` gọi `program_change(ch,0)` trên FluidSynth → load program 0 của MAIN SoundFont. Fix: undefined → oscillator fallback.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. MIDI items không instrument → oscillator fallback (không load SoundFont). MIDI items có instrument → FluidSynth play đúng program.
|
||
---
|
||
|
||
### [2026-07-28 09:37] Task: Section sub-track MIDI playback không default program 0
|
||
- **Tóm tắt thay đổi:** Line 10393 `subTrack.instrumentProgram !== undefined ? subTrack.instrumentProgram : 0` → `subTrack.instrumentProgram`. Trước đây khi `instrumentProgram` undefined (section clone), program default 0 → `_playNoteFluid` gọi `program_change(ch, 0)` trên FluidSynth → load program 0 của MAIN session's SoundFont. Fix: undefined program → `_playNoteFluid` dùng oscillator fallback (không load SoundFont).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Play MAIN session → section MIDI items không load SoundFont. User load instrument trong section tab → program set → FluidSynth play đúng.
|
||
---
|
||
|
||
### [2026-07-28 08:49] Task: Revert Synth button ẩn — track có thể chứa cả section + MIDI
|
||
- **Tóm tắt thay đổi:** Revert conditional Synth button (815a6fa). Track có section item con VÀ midi item con → Synth vẫn hiển thị. Instrument chỉ áp dụng cho MIDI item, không ảnh hưởng section item (được xử lý bởi data isolation fix trước đó: section clone null hết soundfont fields).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Track sections + midiItems → Synth hiển thị, instrument chỉ tác động midiItems.
|
||
---
|
||
|
||
### [2026-07-28 08:36] Task: Synth chỉ hiển thị cho MIDI track, không cho section/audio track
|
||
- **Tóm tắt thay đổi:** Synth button trong TCP giờ conditional: chỉ hiển thị khi track có midiItems (>0) và KHÔNG có sections. FX button giữ nguyên cho mọi track type. Logic đúng: Synth áp dụng cho MIDI item, FX áp dụng cho section/MIDI/audio clip item.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Track có sections → ẩn Synth. Track MIDI → hiện Synth. Track audio → ẩn Synth. FX hiện ở mọi track.
|
||
---
|
||
|
||
### [2026-07-28 08:31] Task: Section track auto-load soundfont 1/prog 0 từ MAIN qua `...t` spread
|
||
- **Tóm tắt thay đổi:** `...t` trong fallback branch spread `soundfont_id`, `soundfont_bank`, `soundfont_program`, `instrument_source` từ parent MAIN track. Dù đã null `instrumentProgram` và `synth_engine`, các field `soundfont_*` vẫn khiến FluidSynth load soundfont khi play. Fix: override tất cả soundfont fields thành null/undefined.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. MAIN loaded SF1/prog5 → mở section tab → section tracks không có SF.
|
||
---
|
||
|
||
### [2026-07-28 08:27] Task: KHÔNG auto-load instrument MAIN vào SECTION-TAB track
|
||
- **Tóm tắt thay đổi:** Revert kế thừa instrument từ parent MAIN track. Section tracks giữ `instrumentId: null, instrumentProgram: undefined, instrumentName: null, synth_engine: undefined`. Trước đây kế thừa instrument (f2ac70e) → SECTION-TAB auto-load program 0 của SoundFont từ MAIN. User tự chọn instrument trong SECTION-TAB qua Synth button (đã hỗ trợ qua `updateActiveTracks` routing).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Load instrument MAIN → mở section tab → section tracks KHÔNG có instrument.
|
||
---
|
||
|
||
### [2026-07-28 08:12] Task: Sync instrument load methods từ MAIN vào SECTION-TAB PIANO_ROLL
|
||
- **Tóm tắt thay đổi:** Fix `updateActiveTracksRef` — khi `activeTab` là PIANO_ROLL sub-tab có `parent_tab_id` trỏ đến SECTION-TAB, routing instrument update vào `setSessionTabs` thay vì `setTracks` (main). Trước đây đổi instrument trong PIANO_ROLL của SECTION-TAB ghi vào main tracks (vô hiệu). Cùng logic với `activeTracks` useMemo fix trước đó.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Mở SECTION-TAB → PIANO_ROLL → Synth → chọn instrument → verify instrument được áp dụng cho section track (không phải main track).
|
||
---
|
||
|
||
### [2026-07-28 18:17] Task: Keyboard shortcuts Ctrl+A select all + Del delete selected items/groups
|
||
- **Tóm tắt thay đổi:** (1) Ctrl+A đã tồn tại để chọn tất cả items trên timeline. (2) Thêm xử lý phím Delete/Backspace cho `selectedItemIds`: xóa tất cả section, MIDI item, và audio clip đang được chọn khỏi track, xóa selection, hiện toast thông báo.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** Build có lỗi parse pre-existing (Babel 8) không liên quan đến thay đổi này.
|
||
---
|
||
|
||
### [2026-07-29 09:40] Task: Reset to 33cf804 — clean slate for mixer reimplementation
|
||
- **Tóm tắt thay đổi:** Hard reset về commit 33cf804, xóa toàn bộ mixer code (MasterChannelStrip, TrackChannelStrip, MixerConsole, AudioMixerGraphManager, setOutputNode) và MIDI keyboard note sustain. Giữ lại TimelineRuler (tempo bar/timebar/lead-in/0-index). Backup mixer panel code lưu trong `mixer_panel_backup_*.patch`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/services/audioMixerGraphManager.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes. Sẵn sàng cài đặt lại mixer panel display-only.
|
||
---
|
||
|
||
### [2026-07-29 09:46] Task: Install Mixer Panel display-only with F7 toggle
|
||
- **Tóm tắt thay đổi:** Thêm MixerStrip component (display-only, không audio routing). Mixer panel bottom dock với drag handle resize (80-400px). Track strip hiển thị index, M/S buttons, fader slider, dB readout, track name. Master strip với "MAIN OUT" label. F7 toggle + status bar button + View menu item. onUpdateTrack thay đổi mute/solo/volumeDb trực tiếp.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 09:55] Task: Mixer panel — VU meter, fader scaling, track name color
|
||
- **Tóm tắt thay đổi:** Master strip thêm fader placeholder + VU meter. MixerStrip fader container `items-end` → `items-stretch` để fill chiều cao. Thêm VU meter (volume-dependent color: green/amber/red, height theo volumeDb). Track name dùng `track.color` thay vì fixed text-zinc-400.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 10:02] Task: Fix stuck MIDI notes when ARM toggled off mid-note
|
||
- **Tóm tắt thay đổi:** Xóa `if (!st.isArmed) return;` trong note-off handler. Note-off giờ stopNote trên ALL tracks thay vì chỉ armed tracks — FluidSynth stopNote trên channel không có note đang play là no-op.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 10:08] Task: Fix AI Suggestion list changing on item select/deselect
|
||
- **Tóm tắt thay đổi:** Xóa filter `hasSelItem ? ...` khỏi `suggestions` — luôn hiển thị toàn bộ preset list thay vì chỉ rearrange/favorites khi có item được chọn. Tránh tự động thêm/xóa suggestion khi click chọn/bỏ chọn item.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 10:12] Task: Restore AI suggestion filter by selection + fix stale instance
|
||
- **Tóm tắt thay đổi:** Restore filter: khi chọn MIDI item chỉ hiện Rearrange/favorites, không chọn thì hiện tất cả. Fix gốc: cache PromptTemplateManager qua `aiPromptMgrRef` thay vì `new` mỗi render — tránh thay đổi array reference gây re-render suggestion list.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 10:17] Task: Fix filter including wrong presets in AI suggestion list
|
||
- **Tóm tắt thay đổi:** Xóa `|| p.is_favorite` khỏi filter khi có MIDI item selected — chỉ lọc theo `category === 'Rearrange / Variation'`, tránh preset favorite bị lọt vào list không đúng.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 10:19] Task: Show all presets always, no filter by selection
|
||
- **Tóm tắt thay đổi:** Luôn hiển thị `promptMgr.presets` đầy đủ, không filter theo selection state — tránh preset đầu tiên nhảy vào list khi bỏ chọn item.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 10:42] Task: Fix multi-item drag merging all items into target track
|
||
- **Tóm tắt thay đổi:** Khi cross-track drag, chỉ item đang được kéo (`drag.itemId`) được chuyển track — các item được chọn khác giữ nguyên track cũ, chỉ cập nhật vị trí ngang (delta). Fix `isDraggedItem` check trong multiIds loop.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 10:48] Task: Fix group drag — all items move, clamp to track 0, auto-add track
|
||
- **Tóm tắt thay đổi:** Restore group drag: tất cả selected items di chuyển cùng nhau. Thêm clamp targetTrackId về track đầu tiên (0). Auto-add track mới khi kéo xuống dưới track cuối cùng.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 10:50] Task: Fix group drag merging items — keep items on original tracks
|
||
- **Tóm tắt thay đổi:** Xóa toàn bộ cross-track logic (currentTrackMap, crossTracks). MultiIds drag chỉ cập nhật time position (delta) trên từng track — items luôn giữ nguyên track cũ, không gộp.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 10:55] Task: Fix group drag cross-track — each item keeps relative track offset
|
||
- **Tóm tắt thay đổi:** Khi drag group cross-track, mỗi item tính target track riêng dựa trên `srcIdx + crossOffset`. Item trên track 1 kéo xuống 2 track → đến track 3. Item trên track 2 kéo xuống 2 track → đến track 4. Không gộp vào 1 track.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 10:58] Task: Fix group drag cross-track stale track index bug
|
||
- **Tóm tắt thay đổi:** Sửa `trkIds` dùng `prev.map(tr => tr.id)` (từ live state trong callback) thay vì `activeTracksRef.current`. Sửa insert section dùng `for` loop tìm srcItem đúng thay vì `flatMap` sai.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 11:00] Task: Fix group drag — use ORIGINAL trackId from drag start
|
||
- **Tóm tắt thay đổi:** Lưu `trackId: t.id` vào `multiIds` lúc bắt đầu drag. Dùng `inf.trackId` (original) để tính `origIdx + crossOffset` — không dùng `currentTrackMap` (live state có thể stale). Item track1 drag n track → track1+n, item track2+n track → track2+n.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 11:04] Task: Fix single-item cross-track drag
|
||
- **Tóm tắt thay đổi:** Sửa single-item drag path: mỗi track filter `drag.itemId` riêng, chỉ insert vào `targetTrackId`. Xóa duplicate `setDraggedSectionItem`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 11:06] Task: Fix cross-track drag — compute target track from mouse Y
|
||
- **Tóm tắt thay đổi:** Thay `hoveredTrackIdRef` bằng tính toán target track từ mouse Y position + track heights (`prev[ti].height`). Không phụ thuộc `onMouseEnter` event có thể không fire khi drag.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 11:07] Task: Auto-add tracks when drag exceeds existing track count
|
||
- **Tóm tắt thay đổi:** Tính `maxTrg` từ tất cả items trong multiIds. Nếu `prev.length <= maxTrg`, tự động thêm track mới vào `prev` trước khi xử lí. Item track2 cần xuống track3 → auto-add track3.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 11:10] Task: Fix targetTrackId undefined outside closure
|
||
- **Tóm tắt thay đổi:** Chuyển target track computation ra ngoài `updateActiveTracks` callback để `setDraggedSectionItem` thấy `targetTrackId`. Compute dùng `activeTracksRef` cho track heights.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 11:12] Task: Fix drag cross-track — use ORIGINAL baseTrackId, never update
|
||
- **Tóm tắt thay đổi:** Dùng `dragBaseTrackId` từ `multiIds[drag.itemId].trackId` (không đổi) thay vì `drag.trackId` (bị update mỗi mousemove). Xóa `setDraggedSectionItem` update trackId — không cần thiết. crossOffset ổn định xuyên suốt drag.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 11:16] Task: Rewrite drag handler — separate multi/single paths, pre-compute crossOffset
|
||
- **Tóm tắt thay đổi:** Tách multiIds path và single path. Pre-compute crossOffset + outTracks ngoài `updateActiveTracks`. outTracks merge vào prev bằng filter id unique. Mỗi item compute targetTid từ origIdx+crossOffset, filter + upsert trên target track.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 11:27] Task: Fix seed_admin() resetting admin password on every restart
|
||
- **Tóm tắt thay đổi:** Xóa `else` branch trong `seed_admin()` — từng kiểm tra `if not verify_password(default_pwd, ...)` và reset password admin về `admin123` + `must_change_password=1` mỗi lần server restart nếu password đã bị đổi. Giờ chỉ create admin khi chưa tồn tại.
|
||
- **Các file ảnh hưởng:** `app/core/auth.py`
|
||
---
|
||
|
||
### [2026-07-29 11:34] Task: Fix instrument list not loading on new machine after login
|
||
- **Tóm tắt thay đổi:** Thêm `listPlugins()` + `getSoundfontCatalog()` fetch trong `handleAuthSuccess` — re-fetch instrument data sau khi login vì useEffect mount chạy trước khi token được set, fail silent.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 11:45] Task: AI SUGGEST auto-update after saving preset in AI Preset Manager
|
||
- **Tóm tắt thay đổi:** Thêm `aiPresetVersion` counter, increment khi AIPresetModal đóng. Trong AI panel, so sánh version với `window.__aiPresetVersion`, nếu khác thì gọi `aiPromptMgrRef.current.loadPresets()` để reload từ localStorage.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 11:53] Task: Synth button shows track dropdown for quick instrument assign
|
||
- **Tóm tắt thay đổi:** Synth button TCP giờ mở dropdown list tất cả tracks thay vì mở modal trực tiếp. Chọn track trong dropdown → modal instrument selector mở cho track đó. Không cần đóng modal để click Synth từng track.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 11:57] Task: Fix Synth button — track selector inside instrument modal
|
||
- **Tóm tắt thay đổi:** Xóa dropdown track riêng (synthTrackDropdownId). Thêm `<select>` track list trong modal Select Instrument, giữa title và textbox tìm kiếm. Chọn track → mở instrument cho track đó trong cùng modal.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 12:00] Task: Fix missing aiTypeaheadRef definition
|
||
- **Tóm tắt thay đổi:** Thêm `aiTypeaheadRef = useRef(null)` — ref được dùng trong typeahead dropdown nhưng chưa được khai báo, gây ReferenceError.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 12:03] Task: Select Instrument no longer auto-closes on instrument pick
|
||
- **Tóm tắt thay đổi:** Xóa `setInstrumentSelectorTrackId(null)` khỏi `setTrackInstrumentWithProgram` — modal không tự đóng khi chọn instrument. Người dùng đổi track trong dropdown, chọn instrument, modal giữ nguyên. Đóng bằng nút ✕ hoặc click outside.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-29 15:39] Task: Implement full Ctrl-Z/Ctrl-Y undo/redo for MAIN SESSION and SECTION-TAB
|
||
- **Tóm tắt thay đổi:** (1) Tạo `UndoRedoEngine` service (`app/static/js/services/undoRedoEngine.js`) — global stack hỗ trợ cross-cutting state changes (BPM, playhead, selection, AI prompt, instrument). (2) Extend `handleUndo`/`handleRedo` trong `app.jsx` để ưu tiên `UndoRedoEngine`, fallback về track-level undo stack cũ. (3) Áp dụng undo/redo cho: Delete/Create items (section, midi, clip, track), text AI Prompt (native textarea undo stack + engine integration), instrument assignment, playhead position, item drag position, selection changes, BPM/tempo changes. (4) Update undo/redo buttons disabled state + keyboard shortcut indicators. (5) Global Ctrl+Z/Y handler skip INPUT/TEXTAREA focus để text input native undo hoạt động đúng.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/undoRedoEngine.js` (NEW), `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
---
|
||
|
||
### [2026-07-30 12:37] Task: Install Master Strip Console in mixer panel
|
||
- **Tóm tắt thay đổi:** (1) Thêm CSS styles cho Master Strip Console (`strip-bg`, `fader-track`, `btn-daw`, `btn-teal-active`, `fader-slider`) vào `app/templates/index.html`. (2) Tạo component `MasterStripConsole` trong `app/static/js/app.jsx` — thay thế MASTER strip cũ (fader đơn giản) bằng console theo spec `md/47_MASTER_STRIP_CONSOLE.md`: dual stereo VU meter canvas với dB scales L/R, vertical fader -60~+12dB, button stack (Mono/M/S/Route/FX/Power/Trim/Info), pan knob, dB readout, RMS footer. (3) Tích hợp test signal engine (Play/Stop test audio vào masterBus.input), FX button mở MasteringModal, Power button toggle mastering active/bypass. (4) VU meter animation loop sử dụng `masterBus.leftAnalyser`/`rightAnalyser` thay vì synthetic data.
|
||
- **Các file ảnh hưởng:** `app/templates/index.html`, `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes (Babel note: deoptimised styling for file >500KB, không phải lỗi).
|
||
---
|
||
|
||
### [2026-07-30 12:53] Task: Install Master Strip + Track Strip Console in mixer panel
|
||
- **Tóm tắt thay đổi:** (1) Refine `MasterStripConsole` theo spec `md/47`: top button "MASTERING PANEL", pan knob pointer-drag (L/R/center label), mute/mono buttons toggle `.btn-mute-active`/`.btn-mono-active`, VU meter dùng synthetic post-fader simulation khi muted. (2) Tạo `TrackStripConsole` từ spec `md/48_TRACK_STRIP_COMPONENT.md`: `w-[145px]` với top color accent bar, pan rotary dial, peak dB fader rail + LED VU canvas, button stack (M/S/Route/FX/Power/Auto/Phase Ø), record arm row, track name, color footer index. (3) CSS mới: `.strip-bg-track`, `.fader-track-bg`, `.btn-solo-active`, `.btn-arm-active`, `@keyframes pulse-red`, cập nhật `.btn-daw:active`/`fader-slider` thumb từ spec. (4) Replace `MixerStrip` → `TrackStripConsole` trong mixer panel.
|
||
- **Các file ảnh hưởng:** `app/templates/index.html`, `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||
|
||
### [2026-07-30 21:15] Task: PIANO ROLL draw notes snapped to grid
|
||
- **Tóm tắt thay đổi:** Note `start_beat` + `duration_beats` được snap bằng `getSnapBeat` trước khi vẽ lên canvas. Áp dụng cho active notes, ghost notes, recording notes.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Note hiển thị thẳng hàng với grid lines theo snap value.
|
||
---
|
||
|
||
### [2026-07-30 21:07] Task: Fix PIANO ROLL snap grid — snap values inverted in 3 places
|
||
- **Tóm tắt thay đổi:** snapVal mapping bị đảo ngược: snap='1' (1 beat) cho q=4.0 (4 beats) → grid vẽ mỗi 4 beat, note snap mỗi 4 beat. Fix: `getSnapBeat`, `getSnapDuration`, grid drawing: snap='1'→1.0, '1/2'→0.5, '1/4'→0.25, '1/8'→0.125, '1/16'→0.0625, '1/32'→0.03125, '4'→4.0.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Piano roll snap=1 → grid mỗi 1 beat, note snap đúng vị trí.
|
||
---
|
||
|
||
### [2026-07-30 21:02] Task: Fix audio clips lost — draw clip rect even without buffer + store duration in deserialize
|
||
- **Tóm tắt thay đổi:** WaveformLane từng `if (!clip.buffer) return;` skip toàn bộ clip (kể cả rect/label). Fix: draw rect/label luôn, chỉ skip waveform khi null buffer. Thêm label "(audio chưa được tải)". Deserialize giờ restore `duration` trên clip từ `duration_bars`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Clip không có buffer vẫn hiện rect + label + duration đúng.
|
||
---
|
||
|
||
### [2026-07-30 20:58] Task: Fix snap default + Power button mastering bypass
|
||
- **Tóm tắt thay đổi:** (1) snapValue mặc định `'1'` (thay `'free'`), snapVal PIANO_ROLL mặc định `'1'` (thay `'1/16'`); (2) Power button trong MasterStripConsole: bỏ `isFxActive` local state dùng chung sai với FX button, đọc `masteringSettings.masterConnected` trực tiếp → toggle `masterConnected` + set `isBypassed` tương ứng.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Load project → snap = 1. Mixer → Power button → mastering bật/tắt đúng.
|
||
---
|
||
|
||
### [2026-07-30 20:52] Task: Fix audio clip items lost after reload
|
||
- **Tóm tắt thay đổi:** 3 root causes: (1) `serverFileId` không được lưu vào clip/track khi upload audio — `insertSoundClipAtCursor` gọi `uploadToServer` nhưng chỉ lưu vào global `serverFileIdMap`, không set `t.serverFileId`; (2) Serialize dùng `t.serverFileId` (null) → `audio_file_url` rỗng → Deserialize không lấy được fileId → `loadAudioBuffersForTracks` không load được buffer; (3) WaveformLane canvas `clip.buffer.numberOfChannels` crash khi buffer null → clip items không vẽ được. Fix: set `serverFileId` trên clip + track khi upload; serialize/deserialize `server_file_id` trong `source_data`; guard `if (!clip.buffer) return` trong WaveformLane.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Chèn audio clip → lưu project → reload → clip hiển thị + waveform load lại.
|
||
---
|
||
|
||
### [2026-07-30 20:46] Task: Auto-save 5min + Backup 30min + Backup management UI
|
||
- **Tóm tắt thay đổi:** (1) DB: thêm bảng `project_backups` + migration cột `is_backup`/`original_id`; (2) Backend: `POST/POST /cloud/{id}/backup`, `GET /cloud/{id}/backups`, `DELETE /cloud/backups/{id}`, `POST /cloud/backups/cleanup` — tự động giới hạn retention (mặc định 10, cấu hình 5-20); (3) Frontend API: 4 methods backup; (4) App: `setInterval` 5 phút auto-save + 30 phút backup; (5) ProfileModal: nút Backup (hiện số lượng) mở rộng danh sách, xóa từng bản, thanh cấu hình retention slider 5-20, nút dọn dẹp.
|
||
- **Các file ảnh hưởng:** `app/models/user.py`, `app/api/v1/projects.py`, `app/static/js/services/api.js`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Cần test: mở dự án Cloud → đợi 30p → backup tự động tạo; slider 5-20 lưu vào localStorage; nút 🧹 DỌN BACKUP giữ lại đúng số lượng.
|
||
---
|
||
|
||
### [2026-07-30 20:36] Task: Fix project load crash — loadAudioBuffersForTracks scoped inside ProfileModal
|
||
- **Tóm tắt thay đổi:** `loadAudioBuffersForTracks` defined inside `ProfileModal`, but `restoreLastSessionProject` in App component called it → ReferenceError on startup + silent catch → project not restored. Fix: move function to App component, pass as prop to ProfileModal.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Mở lại trang → dự án cũ được khôi phục.
|
||
---
|
||
|
||
### [2026-07-30 20:32] Task: Add "Tạo preset với cấu trúc" button in AI Preset Manager
|
||
- **Tóm tắt thay đổi:** Thêm `handleNewStructured` — tạo preset mới với template có cấu trúc (mục: thể loại, bố cục, nhạc cụ, hòa âm, hiệu ứng, MIDI). Button xanh lục "Tạo preset với cấu trúc" bên trái "Quay lại" trong form edit.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||
- **Ghi chú/Test (nếu có):** Nhấn "Tạo mới" → form edit hiện button xanh "Tạo preset với cấu trúc" → click → pre-fill template.
|
||
---
|
||
|
||
### [2026-07-30 22:48] Task: Fix audio clip mất âm thanh sau khi mở lại dự án
|
||
- **Tóm tắt thay đổi:** Sửa 3 lỗi khiến clip audio không tải được buffer sau khi save/reload project: (1) `deserializeTracksList` thiếu fallback từ `audio_file_url` khi `server_file_id` không có trong source_data (2) `serializeTracksList` không lưu `server_file_id` ở track level (3) legacy upgrade function trong Python không copy `server_file_id` vào source_data.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/api/v1/projects.py`
|
||
- **Ghi chú/Test (nếu có):** Mở lại dự án có clip audio → waveform hiển thị + playback có âm thanh.
|
||
|
||
---
|
||
### [2026-07-31 05:42] Task: Cải thiện AI Preset Manager - Category dropdown + Generator modal
|
||
- **Tóm tắt thay đổi:** (1) Phần DANH MỤC trong form tạo preset đổi từ input text sang dropdown `<select>` có đầy đủ danh mục từ presets + danh mục đã lưu trong localStorage, cho phép người dùng chọn danh mục có sẵn hoặc nhấn "+ Nhập danh mục mới..." để thêm; danh mục mới được lưu vào `midi_prompt_categories` trong localStorage để load lần sau. (2) Nút "Tạo preset với cấu trúc" mở modal mới chứa iframe `/ai-prompt-generator` phục vụ nội dung HTML từ `md/49_AI_PROMPT_GENERATOR.md`, cho phép người dùng dùng giao diện đầy đủ để tạo preset; đóng modal sẽ tự động refresh danh sách preset từ localStorage.
|
||
- **Các file ảnh hưởng:** `app/main.py`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Backend route `/ai-prompt-generator` trả về HTML từ `md/49_AI_PROMPT_GENERATOR.md`. Test: mở AI Preset Manager → Tạo mới → dropdown DANH MỤC hiện danh sách → thêm danh mục mới → lưu → reload modal vẫn giữ danh mục mới. Click "Tạo preset với cấu trúc" → mở iframe generator.
|
||
|
||
---
|
||
### [2026-07-31 06:15] Task: Fix preview layout in AI Prompt Generator modal
|
||
- **Tóm tắt thay đổi:** Sửa layout right column của generator page: header thêm `shrink-0` để không bị co lại; container preview đổi thành `flex-1 flex flex-col min-h-0` thay vì `overflow-y-auto`; `#promptOutputText` đổi từ `h-full` sang `flex-1` + `overflow-y-auto` để bottom luôn neo với cạnh modal và nội dung scroll trong element.
|
||
- **Các file ảnh hưởng:** `md/49_AI_PROMPT_GENERATOR.md`
|
||
- **Ghi chú/Test (nếu có):** Refresh trang generator → right column fill full height → content scroll bên trong #promptOutputText.
|
||
|
||
---
|
||
### [2026-07-31 06:35] Task: Apply generator-to-preset-manager transfer spec
|
||
- **Tóm tắt thay đổi:** Áp dụng spec từ md/50_APPLY_GENERATOR.md vào md/49_AI_PROMPT_GENERATOR.md: (1) Category input chuyển thành `<select id="presetCategorySelect">`; (2) Thêm nút "Tạo preset có cấu trúc" (xanh lá) bên trái action bar; (3) autoFillPresetModal cập nhật logic infer category từ role/keywords; (4) Thêm handler `btnCreateStructuredPreset` thay static number bằng template variable; (5) Save handler dùng `presetCategorySelect.value`, dispatch `AI_PRESET_SAVED` CustomEvent.
|
||
- **Các file ảnh hưởng:** `md/49_AI_PROMPT_GENERATOR.md`
|
||
- **Ghi chú/Test (nếu có):** Refresh generator → Lưu thành Preset → Category tự chọn dropdown → Tạo preset có cấu trúc → số bars/beats biến thành {{bars}}/{{total_beats}}.
|
||
|
||
---
|
||
### [2026-07-31 06:42] Task: Transfer generator data to parent AI PRESET MANAGER form
|
||
- **Tóm tắt thay đổi:** Thay vì mở modal standalone trong iframe generator, "Lưu thành Preset" giờ postMessage `GENERATOR_PRESET_DATA` lên parent window. React AIPresetModal thêm `message` listener nhận data, auto-populate form (name, category, keywords, bars, bpm, scale, template), set editingPreset='new' và đóng generator modal. Nút "Tạo preset có cấu trúc" trong React form vẫn hoạt động độc lập.
|
||
- **Các file ảnh hưởng:** `md/49_AI_PROMPT_GENERATOR.md`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Flow: generator iframe → Lưu thành Preset → form React tự điền → generator modal đóng.
|
||
|
||
### [2026-07-31 07:37] Task: Fix 3 bugs - panText, cloud project load, auto-restore
|
||
- **Tóm tắt thay đổi:**
|
||
1. Thêm `const [panText, setPanText] = React.useState('center')` vào MasterStripConsole để fix `Uncaught ReferenceError: panText is not defined`.
|
||
2. Fix `restoreLastSessionProject`: xóa bỏ `localStorage.removeItem('sonic_project_id')` trong catch block để không xóa project ID khi API lỗi tạm thời → auto-load trên page reload hoạt động lại.
|
||
3. Thêm `.catch()` vào `loadAudioBuffersForTracks` trong `handleOpenProject` và `restoreLastSessionProject` để tránh unhandled promise rejection gây gián đoạn load project.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Cần reload page và test Open Project + Mixer Panel sau fix.
|
||
|
||
### [2026-07-31 08:01] Task: Fix MIDI note creation duration & add Ctrl+Shift+Click split
|
||
- **Tóm tắt thay đổi:**
|
||
1. Thêm `lastNoteDurationRef` để ghi nhớ duration của note vừa tạo → click tạo note mới dùng duration đó thay vì snap duration mặc định.
|
||
2. Ctrl+Shift+Click vào giữa MIDI note → tách note đó thành 2 notes tại vị trí click. Ctrl+Shift+Click vào vùng trống → nhân bản các note đang chọn (giữ hành vi cũ).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Kiểm tra: 1) tạo note → thay đổi duration → tạo note mới → duration được kế thừa. 2) Ctrl+Shift+Click giữa note → note bị cắt thành 2.
|
||
|
||
### [2026-07-31 08:09] Task: Bump precompiled cache version for piano roll split fix
|
||
- **Tóm tắt thay đổi:** Cập nhật cache-busting parameter `app.precompiled.js?v=202607310809` để trình duyệt load bản JS mới nhất có fix Ctrl+Shift+Click split MIDI note.
|
||
- **Các file ảnh hưởng:** `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Hard reload (Ctrl+F5) để lấy bản JS mới.
|
||
|
||
### [2026-07-31 08:40] Task: Click track name triggers realtime MIDI playback
|
||
- **Tóm tắt thay đổi:** Click vào track name ở cột trái → tự động play ngay MIDI notes của track đó (nếu có piano roll tab). Dùng `schedulePianoRollMidi(prTab, 0)` + `startSubTabPlayback(prTab, 0)` + `stopAllPlayback()`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload để load JS mới. Click track có MIDI notes → nghe thấy phát ngay.
|
||
|
||
### [2026-07-31 08:50] Task: Piano roll instrument load + multitrack play select
|
||
- **Tóm tắt thay đổi:**
|
||
1. `handleEditMidiInTab`: khi mở piano roll tab bằng double-click MIDI item → lưu `instrumentId`, `synth_engine` vào tab, fallback `instrumentProgram` từ `synth_engine.soundfont_program`, gọi `SonicSF.selectInstrument` để load nhạc cụ của track.
|
||
2. Đổi `activePlayTrackIds` từ single value sang mảng → click nhiều nút tên track ở cột trái piano roll để active nhiều track cùng lúc play multitrack.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Test: double-click MIDI item có nhạc cụ → play đúng nhạc cụ. Click nhiều track name → play multitrack.
|
||
|
||
### [2026-07-31 09:20] Task: Multitrack play select plays immediately
|
||
- **Tóm tắt thay đổi:** Thêm `handlePianoRollRealtimePlay(trackIds)` ở App: khi click nút track name trong piano roll → tính ngay ghost layers cho các track được chọn → `stopAllPlayback()` + `SonicSF.stopAll()` → `schedulePianoRollMidi(playTab, offset)` → play ngay realtime không cần bấm play.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Click track button → nghe ngay. Click thêm/bớt track → re-schedule lại tức thì.
|
||
|
||
### [2026-07-31 09:32] Task: Drag-drop MIDI file creates new tracks
|
||
- **Tóm tắt thay đổi:** Thêm `handleDropMidiToNewTracks(file)` — drop file .mid/.midi vào vùng track container → parse MIDI → tạo track mới cho từng MIDI track và append MIDI item. Container có `onDrop` xử lý MIDI (hoặc audio → load vào selected track); lane drop cũng route MIDI sang hàm mới.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Kéo file .mid vào vùng track → track mới xuất hiện với MIDI.
|
||
|
||
### [2026-07-31 09:48] Task: Piano roll shows all same-track MIDI items notes as active
|
||
- **Tóm tắt thay đổi:** `ghostNoteExtractor.js`: bỏ filter time-overlap cho các MIDI items cùng track (`isSameTrack`) → tất cả notes của mọi MIDI item trong cùng track đều được gộp vào ghost layer. Render: same-track layers vẽ nổi bật (amber, alpha 0.65 + border) = trạng thái active. Effect sync: same-track layers luôn được đưa vào `ghostPlayLayers` → play đủ cả track; cross-track vẫn theo `activePlayTrackIds`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/static/js/services/ghostNoteExtractor.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Mở MIDI item của track → thấy tất cả notes của các MIDI item cùng track hiển thị amber nổi bật; play → nghe đủ track.
|
||
|
||
### [2026-07-31 09:52] Task: Opened MIDI item notes darker than same-track siblings
|
||
- **Tóm tắt thay đổi:** Render piano roll: notes của MIDI item đang mở (main layer) đậm hơn — fill `rgba(234,179,8,0.5)`, viền `#f59e0b` (chọn: xanh `rgba(59,130,246,0.5)`). Ghost notes của các MIDI items cùng track nhạt hơn (alpha 0.35, fill amber 0.35) → phân biệt item đang active vs tham chiếu.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Mở MIDI item → notes item đó đậm, notes item khác cùng track mờ hơn.
|
||
|
||
### [2026-07-31 09:55] Task: Focused MIDI item notes brighter
|
||
- **Tóm tắt thay đổi:** Notes của MIDI item được focus (đang mở / được chọn / đang play) sáng hơn: fill `rgba(253,224,71,0.6)`, viền `#fde047`; khi đang play sáng hơn nữa `rgba(254,240,138,0.75)` viền `#fef08a`; chọn: xanh `rgba(96,165,250,0.6)`. Ghost siblings cùng track vẫn mờ (alpha 0.35).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Mở MIDI item → notes sáng; bấm play → sáng hơn; item khác cùng track mờ hơn.
|
||
|
||
### [2026-07-31 10:02] Task: Per-MIDI-item focus brightness in piano roll
|
||
- **Tóm tắt thay đổi:** Mỗi ghost note có `item_id` (ghostNoteExtractor.js). Render piano roll tính `focusedItemId`: selection → item đang mở; đang play → item chứa playhead (`st.currentTime` trong khoảng `startTime..startTime+duration`). Notes của item được focus vẽ sáng (fill vàng `0.6`/`0.7` + viền `#fde047`, đang play `0.75`), notes của các item khác cùng track mờ (alpha 0.3, fill tối `#7a6a10`). Cross-track giữ ghost 0.25.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/static/js/services/ghostNoteExtractor.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Mở track 3 MIDI items → play: playhead chạy đến item nào item đó sáng, các item khác mờ.
|
||
|
||
### [2026-07-31 10:05] Task: Click note focuses its MIDI item
|
||
- **Tóm tắt thay đổi:** Thêm state `focusItemId` trong PianoRollTabEditor. Click vào note chính → focus item đang mở (`st.target_id`). Click vào ghost note (note của MIDI item khác cùng track) → focus chuyển sang item đó (không vẽ note mới). Reset focus khi đổi `st.target_id`. Ưu tiên focus: đang play → item theo playhead; có note chọn → item đang mở; còn lại → `focusItemId` (theo click).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Click note của item 2 (ghost) → item 2 sáng, item 1&3 mờ.
|
||
|
||
### [2026-07-31 10:10] Task: All same-track MIDI items selectable to focus
|
||
- **Tóm tắt thay đổi:** Cột trái PIANO ROLL: dưới nút tên track (đang mở) hiển thị các nút con cho từng MIDI item trong track đó. Click nút MIDI item → `setFocusItemId(im.id)` → item đó sáng, các item khác cùng track mờ. Nút item đang focus: vàng đậm; item đang mở: viền vàng; còn lại: xám.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Click nút MIDI item bất kỳ trong track → focus chuyển sang item đó.
|
||
|
||
### [2026-07-31 10:10] Task: Revert left-column MIDI item buttons
|
||
- **Tóm tắt thay đổi:** Bỏ các nút MIDI items con ở cột trái (không cần). Giữ cơ chế hiện có: các MIDI items cùng track hiển thị sáng/ít sáng theo focus; click vào note mờ (ít sáng) của MIDI item khác → focus chuyển sang item đó.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload.
|
||
|
||
### [2026-07-31 10:15] Task: Ctrl+click dim note selects + focuses item
|
||
- **Tóm tắt thay đổi:** Trong handleGridMouseDown (ctrl+click, không bấm vào note chính): hit-test trên same-track ghost notes → nếu trúng note mờ → toggle chọn note đó (thêm vào `selectedNoteIds`, vẽ xanh sáng + viền) và `setFocusItemId(item_id)` → active MIDI item chứa note. Empty space vẫn mở marquee như cũ.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Ctrl+click note mờ của MIDI item khác → note chuyển xanh + item đó sáng lên.
|
||
|
||
### [2026-07-31 10:13] Task: Selected dim note's MIDI item wins focus
|
||
- **Tóm tắt thay đổi:** Fix focus resolution khi có selection: duyệt `selectedNoteIds` từ cuối (note chọn gần nhất) — main note → item đang mở; dim note (same-track) → `item_id` của nó. Ưu tiên: selection → playhead → `focusItemId`. GHOST NOTE chỉ là note của MIDI items track khác (alpha 0.25).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Ctrl+click note mờ item 2 → item 2 sáng dù item 1 đang mở.
|
||
|
||
### [2026-07-31 10:19] Task: Click wait-to-active note switches to editing that item
|
||
- **Tóm tắt thay đổi:** Click thường vào note mờ (wait-to-active) của MIDI item khác cùng track → `setFocusItemId` + `handleSwitchMidiItem(item_id)` → item đó trở thành target đang mở (editable), notes load vào editor, các item khác thành mờ. Ctrl+click vẫn giữ select+focus (không switch).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Click note mờ item 2 → item 2 mở ra để chỉnh sửa notes.
|
||
|
||
### [2026-07-31 10:22] Task: Piano roll dropdown lists tracks only
|
||
- **Tóm tắt thay đổi:** Dropdown trên toolbar PIANO ROLL: thay vì liệt kê tất cả MIDI items (`allMidiItems`), chỉ liệt kê các track có MIDI items (dedupe theo track id). Chọn track → `handleSwitchMidiItem` sang MIDI item đầu tiên của track đó. `value` = `st.trackId`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Dropdown chỉ hiện tên các track.
|
||
|
||
### [2026-07-31 10:26] Task: Fix master strip PWR mastering bypass
|
||
- **Tóm tắt thay đổi:** Nút PWR (Bật/Tắt MASTERING PANEL Bypass) ở Master strip: khi bật ON trước đây set `isBypassed: true` → `toggleMasteringOnMaster(true, true)` giữ routing bypass → âm thanh vẫn bypass. Fix: toggle luôn set `isBypassed: false` + flip `masterConnected` → bật lên là mastering active (EQ→Imager→Maximizer), tắt là disconnect.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Bật PWR → âm thanh qua mastering (không còn bypass); tắt → bypass/disconnect.
|
||
|
||
### [2026-07-31 10:32] Task: Track VU meter idle when not audible
|
||
- **Tóm tắt thay đổi:** Track VU meter loop (line ~18446): tính `isAudible` theo solo/mute (`anySolo ? track.solo : !track.muted`) — nếu track không phát ra âm thanh thực (muted hoặc bị solo out) thì gán `audioPeak=0` và bỏ qua `midiVuActivityRef` flash → VU meter đứng yên thay vì animation giả khi solo track khác.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Solo track 1 → track 2 (MIDI) VU không còn animation.
|
||
|
||
### [2026-07-31 10:35] Task: Realtime solo toggle during playback
|
||
- **Tóm tắt thay đổi:** `toggleTrackSoloEvaluate`: khi toggle solo trong lúc play → stop rồi restart đúng: main timeline gọi `startTrackPlayback(currentTime)` (đã đổi sang đọc `activeTracksRef.current` để nhận solo state mới); piano roll subtab đang play → `schedulePianoRollMidi` + `startSubTabPlayback`. Trước đây restart thiếu lệnh start nên solo không áp dụng realtime.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Bấm SOLO track trong lúc play → chỉ track đó còn nghe, track khác dừng ngay.
|
||
|
||
### [2026-07-31 10:44] Task: Drop MIDI on existing track loads into that track
|
||
- **Tóm tắt thay đổi:** Lane onDrop: drop MIDI/audio lên track có sẵn → `loadFileOnTrack(track.id, f)` (MIDI mở ở track đó; multi-track MIDI: track đầu vào track drop, còn lại tạo track mới). Drop ở vùng trống container vẫn tạo track mới (`handleDropMidiToNewTracks`).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Kéo file .mid lên track hiện có → MIDI nạp vào track đó.
|
||
|
||
### [2026-07-31 10:47] Task: Fix MIDI playback crackling
|
||
- **Tóm tắt thay đổi:** `soundfontPlayer.js`: (1) skip program change nếu channel đã có đúng instrument (`_channels[ch]` cache) — trước đây mỗi note đều reload soundfont → glitch/crackle; (2) bỏ `console.log` per-note; (3) render loop: worklet gửi `CONSUMED` feedback để `queueDepth` chính xác, `QUEUE_TARGET` 4→8, interval nhanh hơn → hết underrun (silence gap); (4) `stopAll` chỉ dùng `all_notes_off` (release) thay vì `all_sounds_off` (kill tức thì) → hết pop khi stop. `fluidsynth-bridge.js`: post `CONSUMED` sau splice.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/static/js/worklets/fluidsynth-bridge.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Play MIDI note → nghe mượt, hết lụp bụp.
|
||
|
||
### [2026-07-31 10:50] Task: Fix MIDI crackling v2
|
||
- **Tóm tắt thay đổi:** `soundfontPlayer.js`: (1) render loop dùng wall-clock time-based queue accounting thay vì async CONSUMED message (hết race gây underrun); (2) `QUEUE_TARGET` 8→16 (~170ms buffer chống jank main thread); (3) bỏ 400ms release hack → `noteoff` đúng duration (FluidSynth tự release mượt, hết chồng voice); (4) `synth.verbose=0` + `synth.gain=1.0`. `fluidsynth-bridge.js`: bỏ post CONSUMED không còn dùng.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/static/js/worklets/fluidsynth-bridge.js`
|
||
- **Ghi chú/Test (nếu có):** Hard reload. Play MIDI với instrument → hết lụp bụp.
|
||
|
||
### [2026-07-31 11:25] Task: Media Explorer panel (F6) + mutual exclusion with Mixer (F7)
|
||
- **Tóm tắt thay đổi:** Tạo `MediaExplorerPanel` component theo spec `md/51_MEDIA_EXPLORER.md` (REAPER-style: toolbar, directory tree + file list, transport/preview với canvas visualizer + metadata + status bar), render ở hàng dưới trên status bar (giống Mixer panel, resize được, lưu `studio_media_explorer_height`). Gán F6 toggle panel với `preventDefault`+`stopPropagation` ở cả keydown capture của App (MAIN SESSION/SECTION-TAB) và `PianoRollTabEditor`. F6/F7 mutual exclusion: bật panel này tự tắt panel kia (`window.__toggleMediaExplorerRef` / `__toggleMixerRef`). Bỏ dock placeholder `media_explorer` cũ. Thêm button status bar "Media Explorer Panel (F6)". Thêm CSS `.me-fader-slider` + `.file-row-selected` vào `index.html`, bump version precompiled.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` + `node --check` OK; smoke test jsdom: F6 hiện Media Explorer & tắt Mixer, F7 ngược lại. Hard reload để test thủ công.
|
||
|
||
### [2026-07-31 13:05] Task: My Computer tree truy cập filesystem thật
|
||
- **Tóm tắt thay đổi:** Backend mới `app/api/v1/media.py` (+ mount trong `app/main.py`): `GET /api/v1/media/computer` liệt kê ổ đĩa/mount point (Windows drive letters / Linux `/proc/mounts`), `GET /api/v1/media/browse?path=` liệt kê thư mục con + file audio/MIDI, `GET /api/v1/media/file?path=` serve file cục bộ để preview (chặn ext không phải media). Frontend `MediaExplorerPanel`: node "My Computer" click → fetch roots, expand/collapse tree (2 cấp con), click thư mục → liệt kê file vào list, preview audio local qua `/media/file`, nút Back/Up/Refresh + address bar hiểu `folder === 'computer'`; metadata hiển thị Local Audio/Local MIDI.
|
||
- **Các file ảnh hưởng:** `app/api/v1/media.py`, `app/main.py`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Test trực tiếp: roots 8 mounts, browse `/home/locpham` 22 dirs; serve wav → content-type audio/wav; chặn `/etc/shadow`. Smoke jsdom: click My Computer → drive root hiển thị → click drive → list `beat.wav`/`loop.mid`, address hiện path. Hard reload để test thủ công.
|
||
|
||
### [2026-08-02 14:15] Task: Fix Window Explorer API không hoạt động
|
||
- **Tóm tắt thay đổi:** `app/api/v1/media.py` `list_computer_roots`: lọc `/proc/mounts` chỉ giữ filesystem thật (`REAL_FS_TYPES`: ext4/xfs/btrfs/ntfs/vfat...), bỏ pseudo/docker/systemd mounts (`/run/credentials/...`, overlay, tmpfs, proc...) trước đó list ra 8 mount rác không duyệt được. Bỏ import `windll` thừa. Frontend `MediaExplorerPanel`: bỏ `setComputerRoots([])` nuốt lỗi — giờ API fail → fallback về root `/` + `showToast` báo lỗi; `browseComputerDir` fail → toast thay vì im lặng; `goComputerParent` sửa edge case Windows root (`C:\`); `fileDuration` match theo `path` cho file local; size MIDI local hiện "MIDI TPQN".
|
||
- **Các file ảnh hưởng:** `app/api/v1/media.py`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Test router riêng: `/computer` giờ trả đúng `/` + `/boot/efi` (bỏ rác), browse root 21 dirs; smoke jsdom: click My Computer → drive render → browse file OK; API fail → fallback root OK. Lưu ý: cần restart server FastAPI để load router mới + hard reload browser.
|
||
|
||
### [2026-08-02 14:30] Task: Media Explorer duyệt toàn bộ folder + files
|
||
- **Tóm tắt thay đổi:** `app/api/v1/media.py` `/browse`: trả về TẤT CẢ file (bỏ lọc media) kèm `kind` (`midi`/`audio`/`other`). Frontend `MediaExplorerPanel`: (1) tree My Computer đệ quy sâu vô hạn (`renderComputerNode`) — click folder chọn + load files, click +/- expand/collapse ở mọi cấp; (2) file list merge dirs + files, click file chọn/preview, click folder vào thư mục; (3) icon theo loại (`fa-folder`/`fa-music`/`fa-file-audio`/`fa-file`); (4) guard: file `kind==='other'` không preview; metadata Type hiện "Local File".
|
||
- **Các file ảnh hưởng:** `app/api/v1/media.py`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Test backend: browse trả subdir + a.wav(audio)/b.mid(midi)/notes.txt+readme.md(other). Smoke jsdom: root → list folder+file, expand `/data` → `/data/deep` → list loop.mid OK. Hard reload.
|
||
|
||
### [2026-08-02 14:45] Task: My Computer mở explorer client local qua File System Access API
|
||
- **Tóm tắt thay đổi:** `MediaExplorerPanel` "My Computer" giờ dùng Web API `window.showDirectoryPicker()` (Chrome/Edge) để mở Windows Explorer của MÁY CLIENT chọn thư mục, duyệt qua `FileSystemDirectoryHandle` (`handle.entries()`) — không còn phụ thuộc server/cloud. Tree đệ quy giữ handle mỗi node, `computerMode='client'` + `clientRoot`. Preview audio/MIDI đọc trực tiếp `handle.getFile() → arrayBuffer → decodeAudioData` (helper `readLocalFileBuffer`), không cần URL server. Back/Up/Refresh hoạt động cả 2 mode. Trình duyệt không hỗ trợ FS API → fallback về server API cũ.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom mock `showDirectoryPicker` + handle tree: click My Computer → picker → drive `C:` hiển thị → browse `Users`/`readme.txt` → expand `Users` → `me` OK. Chỉ hoạt động Chrome/Edge (FS Access API). Hard reload.
|
||
|
||
### [2026-08-02 14:55] Task: My Computer client liệt kê folder + files bên phải
|
||
- **Tóm tắt thay đổi:** Fix file list folder rows gọi `browseComputerDir(f.path)` (chuỗi) → đổi thành `browseComputerDir(f)` (entry object) để client mode duyệt đúng qua handle. Khi nhấn My Computer (client-side, `showDirectoryPicker`): picker chọn thư mục → tree trái hiện root, file list phải merge `dirs + files` (folder `fa-folder` + file theo kind), click folder trong list → vào thư mục.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom mock FS handle: picker → root `C:` → list Users(folder)+readme.txt → click Users row → vào `root/Users` → me hiển thị OK. Hard reload.
|
||
|
||
### [2026-08-02 16:40] Task: Media Explorer panel — resize cây thư mục
|
||
- **Tóm tắt thay đổi:** `MediaExplorerPanel` directory tree bỏ class `w-44` cố định → width động qua state `treeWidth` (default 176px, min 110 max 420, ref `treeWidthRef`). Thêm drag handle `w-1.5 cursor-ew-resize` bên phải tree (mousedown/mousemove/mouseup trên document, `startTreeResize`). File list bên phải tự co giãn theo `flex-1`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom: kéo handle +80px → tree width 176→256px OK, clamp 110–420. Hard reload.
|
||
|
||
### [2026-08-02 16:50] Task: Media Explorer preview không cập nhật realtime khi chọn file
|
||
- **Tóm tắt thay đổi:** `MediaExplorerPanel`: (1) `handleSelect` giờ clear ngay `setPeaks(null)` + `setAudioBuffer(null)` + `stopMediaPlayback()` khi chọn file mới → canvas xóa dữ liệu cũ ngay; (2) thêm `selectTokenRef` guard async race: `loadWaveform` và `playSelected` check token sau mỗi await, kết quả cũ (file trước) không ghi đè file mới chọn; (3) gọi `drawCanvas` qua effect phụ thuộc `[peaks, audioBuffer, selected]` → vẽ lại realtime.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom: click file → row selected + metadata MIDI events hiển thị; switch nhanh liên tục → file cuối đúng được chọn, không bị race. Hard reload.
|
||
|
||
### [2026-08-02 17:15] Task: Media Explorer — persist tree width, canvas realtime, Synth instrument cho MIDI
|
||
- **Tóm tắt thay đổi:** (1) Tree width lưu vào `localStorage['studio_media_explorer_tree_width']` khi kéo resize (onUp), khởi tạo đọc lại — reload vẫn giữ độ rộng. (2) Canvas cập nhật realtime khi click file: clear `peaks`/`audioBuffer`/`midiNotes` ngay khi chọn + `selectTokenRef` chống race async; MIDI real vẽ piano-roll nốt thật (màu xám theo pitch 48-84). (3) Nút **Synth** cạnh Auto-Play: dropdown chọn SoundFont + preset (load qua `listPlugins`+`listSoundfontInstruments`), lưu vào `localStorage['studio_media_explorer_synth']`; preview MIDI file thật play qua `window.SonicSF.playNote` với instrument đã chọn (`parseMidiFile` expose ra `window.parseMidiFile` vì nằm trong App scope).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom: tree resize → localStorage `276`; Synth dropdown hiện Piano/Grand → chọn persisted; click `tune.mid` → SonicSF.playNote gọi với program 0. Hard reload.
|
||
|
||
### [2026-08-02 17:18] Task: Fix MIDI auto-play không stop khi click file midi khác
|
||
- **Tóm tắt thay đổi:** `stopMediaPlayback` (MediaExplorerPanel) giờ gọi `window.SonicSF.stopAll()` trước khi play file mới → hủy timers note-on/note-off đã schedule của file MIDI trước (trước đây chỉ stop audio source + cancel rAF, nốt MIDI cũ vẫn kêu). Đảm bảo khi click file midi khác, bản cũ dừng ngay.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom: play a.mid (4 nốt) → click b.mid → stopAll gọi, b.mid play 4 nốt pitch 72, không overlap. Hard reload.
|
||
|
||
### [2026-08-02 17:24] Task: Fix MIDI auto-play đọc nhầm file cũ khi click file mới
|
||
- **Tóm tắt thay đổi:** `playMidiPreview` (MediaExplorerPanel) thiếu re-check token sau `await window.SonicSF.selectInstrument(...)` — khi click file A rồi nhanh tới file B, invocation cũ (A) chờ selectInstrument xong rồi mới schedule notes, ghi đè lên file B mới chọn → auto-play nghe file cũ nhưng canvas hiển thị file mới. Thêm re-check `selectTokenRef.current !== token` ngay sau await (giống pattern đã có ở audio path sau `decodeAudioData`).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom 2 kịch bản race: (1) A read chậm 200ms + B nhanh 10ms → chỉ B play; (2) A nhanh + B chậm → chỉ B play (A bị stopAll). Canvas luôn = file cuối click. Hard reload.
|
||
|
||
### [2026-08-02 17:35] Task: Fix canvas "No file selected" lần đầu + Tempo control
|
||
- **Tóm tắt thay đổi:** (1) Fix canvas hiển thị sai lần đầu khi click file MIDI: `startCanvasClock` rAF tick giữ closure cũ của `drawCanvas` (đọc `selected` cũ = null) và vẽ đè "No file selected" mỗi frame. Chuyển `drawCanvas`/`fileDuration`/clock sang đọc state mới nhất qua refs (`selectedRef`, `peaksRef`, `audioBufferRef`, `midiNotesRef`, `midiTotalRef`, `isPlayingRef`, `isPausedRef`, `currentTimeRef`, `tempoRef`). (2) Thêm **Tempo** control (BPM, +/- input, min 40 max 300) cạnh nút Synth — ảnh hưởng tốc độ preview MIDI (`playMidiPreview` dùng `tempoRef` thay `synthInst.bpm`), ruler canvas, footer status; lưu `localStorage['studio_media_explorer_tempo']`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom: click a.mid lần đầu → canvas vẽ ruler (0,1,2..) không còn "No file selected", audio play; Tempo 120 → +1 → localStorage `121`. Hard reload.
|
||
|
||
### [2026-08-02 18:05] Task: Canvas scroll + giữ state folder/files khi toggle + restore session
|
||
- **Tóm tắt thay đổi:** (1) `drawCanvas` scroll khi content rộng hơn khung: pxPerBeat 42 / pxPerSec 50, playhead di chuyển tới giữa khung (`min(w/2, t*pxPerSec)`) rồi dừng, content scroll trái với offset `max(0, min(contentW-w, t*pxPerSec - w/2))`; ruler scroll đồng bộ. (2) Panel giờ **luôn mounted** (display:none khi ẩn) thay vì `showMediaExplorer &&` → toggle F6 giữ nguyên folder/files/tree/selection. (3) Session persistence `localStorage['studio_media_explorer_session_v1']` (folder, mode, path, roots, tree, files, selected — bỏ handle) + lưu `FileSystemDirectoryHandle` vào IndexedDB `sonicforge_media_explorer/root_handle` để client mode restore được handle thật sau reload; `restoreSession` + re-browse lại folder đã load → cây tree + file list hiển thị đúng thư mục cũ, tên thư mục loaded luôn hiện trong tree.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom: toggle F6 off/on → tree+files+selection giữ nguyên; reload (fresh DOM + IndexedDB stub) → folder MyMusic, files a.mid/b.mid, selection restore. Scroll math: t=10s → offset 350, playhead 150 (center), t=29s clamp 1200. Hard reload.
|
||
|
||
### [2026-08-02 18:50] Task: Media Explorer — bars MIDI, synth realtime, loop, icons, lưu folder path
|
||
- **Tóm tắt thay đổi:** (1) `parseMidiFile` trả thêm `totalBeats`/`bars`/`bpm`/`ticksPerBeat` → metadata + footer + ruler canvas hiển thị bars đúng (thay vì suy từ duration). (2) Thêm `synthInstRef` — `selectSynthInst` re-schedule preview MIDI ngay khi đổi instrument đang phát (realtime). (3) MIDI preview hỗ trợ **loop**: `loopTimerRef` setInterval re-schedule notes mỗi vòng khi `isLooping`; `toggleLoop` cũng re-schedule khi đang preview; `stopMediaPlayback` clear interval. (4) Thêm **FontAwesome CDN** vào index.html → icon Stop/Play/Pause/Loop + Back/Forward/Up/Refresh hiển thị. (5) `openMyComputer` restore thư mục đã mở từ session (click My Computer → hiện content folder trong ô File, không mở picker lại nếu đã có session + handle IndexedDB).
|
||
- **Các file ảnh hưởng:** `app/templates/index.html`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom: metadata Bars/Beats/BPM hiển thị, footer "Bar X/Y"; đổi synth Strings→prog 48 re-schedule OK; loop bật→notes 64→96 (re-schedule vòng 2); icons đủ; folder restore khi click My Computer OK. Hard reload.
|
||
|
||
### [2026-08-02 19:30] Task: Fix canvas audio + drag & drop Media Explorer → timeline
|
||
- **Tóm tắt thay đổi:** (1) Fix canvas audio hiển thị sai: waveform server trả `duration` nhưng không lưu → `dur=0` nên ruler/playhead/time sai. Thêm state `audioDuration` (set ở loadWaveform local/server + playSelected, clear ở handleSelect), `fileDuration` ưu tiên dùng; bar width waveform sửa theo `contentW` thay vì `w` để scroll đúng. (2) Drag & drop file từ Media Explorer vào timeline MAIN SESSION + SECTION-TAB: file row `draggable` + `onDragStart` set `window.__mediaExplorerDragFile`; App thêm `resolveMediaExplorerDropFile()` chuyển entry → real File (client handle `getFile()`, server `file_id` fetch download, local `path` fetch `/media/file`); drop handlers trên track lane + wrapper timeline xử lý cả drag ME lẫn drag OS file (MIDI → new track / audio → load vào track).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom: wav client → metadata Duration 3.00s (trước là 0); drag file row → payload text/plain=name + `__mediaExplorerDragFile` set → drop vào track lane → resolve thành File + load (uploadToServer gọi). Hard reload.
|
||
|
||
### [2026-08-02 19:36] Task: Fix canvas audioclip giãn đầy khung preview
|
||
- **Tóm tắt thay đổi:** `drawCanvas` branch audio: bỏ `contentW = Math.max(w, dur*pxPerSec)` (giãn đầy frame) → `contentW = max(1, dur*pxPerSec)` với `pxPerBeat=42`, `pxPerSec = 42*bpm/60` (cùng tỉ lệ tempo với MIDI). Clip ngắn hiển thị đúng chiều rộng theo tempo (2s@120 → 168px), clip dài scroll; playhead theo tỉ lệ tempo (`t*pxPerSec`, khóa giữa khi scroll). Ruler cùng scale (bỏ stretch `Math.max(w, ...)`). Thêm `audioDuration` vào deps effect redraw.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Probe drawCanvas: 2s@120 → contentW=168, waveform maxX 167.8 < 300 (không giãn); 10s → 840 scroll; 2s@240 → 336 (rộng hơn theo tempo). Hard reload.
|
||
|
||
### [2026-08-02 19:56] Task: My Computer auto-load cây + Favorites thư mục
|
||
- **Tóm tắt thay đổi:** (1) Khi mở My Computer client-side (pick xong root), tự động browse + expand tối đa 10 thư mục con cấp 1 (`browseComputerDir`/`browseClientDir` giờ trả về `{dirs, files}` để auto-expand) → cây hiển thị cấu trúc ổ đĩa/thư mục ngay. (2) **Favorites**: node "Favorites" ngay dưới My Computer (hiện khi có favorite, icon `fa-star`); chuột phải folder trong cây (hoặc trong Favorites) → context menu "Thêm vào Favorites"/"Gỡ khỏi Favorites" + "Mở thư mục"; lưu vào `localStorage['studio_media_explorer_favorites_v1']` `[{path, name}]`; click favorite mở nhanh folder (tìm handle trong `computerTree`, hoặc walk lại từ `clientRoot` qua `getDirectoryHandle` theo segments path, fallback `browseComputerDir`); node folder có favorite hiện icon star.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom: pick root → C: + Users + Program Files auto-load; right-click Users → menu → favorite saved localStorage + node Favorites hiện; click favorite → mở `root/Users`. Hard reload.
|
||
|
||
### [2026-08-02 19:65] Task: My Computer auto-scan + playhead chạy tới cuối khi hết scroll
|
||
- **Tóm tắt thay đổi:** (1) `openMyComputer` bỏ `showDirectoryPicker` khi click node My Computer — giờ tự động quét server API (`/api/v1/media/computer` liệt kê ổ đĩa máy local + `/browse` expand ổ đĩa → expand thư mục con cấp 1), cây hiển thị ngay không cần hộp thoại; vẫn restore session/handle IndexedDB nếu có. (2) Playhead trong `drawCanvas` (MIDI + audio): công thức `max(0, min(w, t*pxPerSec - offset))` — khi content rộng hơn frame, playhead chạy tới giữa rồi khóa ở giữa trong khi content scroll trái; khi offset đạt max (contentW-w) thì playhead tiếp tục chạy với tốc độ play tới cuối canvas.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke: click My Computer → picker NOT called, DATA/HOME + Music/user auto-load; playhead math: big clip t=1→84, t=3→150 (center), t=5→150 locked, t=10→300 (end). Hard reload.
|
||
|
||
### [2026-08-02 19:75] Task: Fix My Computer client-side tự động quét cây
|
||
- **Tóm tắt thay đổi:** `openMyComputer` ưu tiên **client-side**: load root handle từ IndexedDB → tự động quét cây thư mục client (C:, Users, Program Files...) mà KHÔNG gọi `showDirectoryPicker` (đã gỡ hẳn). Fix bug cũ: `if (savedHandle && ... && !computerRoots)` luôn false vì `computerRoots` vừa được set bởi server API → client handle không bao giờ restore được → gây ra hộp thoại picker. Giờ: (1) nếu có client handle → auto-scan client tree + expand; (2) không có → fallback server API `/api/v1/media/computer` tự động quét ổ đĩa máy local. Mọi path đều không mở hộp thoại.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom: IndexedDB có client handle `C:` → click My Computer → picker NOT called, cây C:/Users/Program Files auto-load. Không handle → server API Root (/) auto-load, picker NOT called. Hard reload (browser cache cũ có thể vẫn hiện picker). Commit.
|
||
|
||
### [2026-08-02 19:85] Task: Fix My Computer hiển thị nhầm Favorites thay vì cây hệ thống
|
||
- **Tóm tắt thay đổi:** `openMyComputer` bỏ block restore session cũ (đọc `studio_media_explorer_session_v1` từ localStorage ghi đè `computerRoots`/`computerTree` bằng dữ liệu favorites/stale → khi click My Computer hiện thư mục favorite cũ thay vì cây client). Giờ click My Computer luôn: (1) load client root handle từ IndexedDB → auto-scan cây client thật (C:, Users, Program Files...) hoặc (2) fallback server API scan ổ đĩa local. Không còn ghi đè bằng session cũ.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom: seed session cũ chứa `OLD_FAV` + favorites → click My Computer → picker NOT called, hiện C:/Users/Program Files (client tree), OLD_FAV không hiển thị. Hard reload.
|
||
|
||
### [2026-08-02 19:95] Task: My Computer luôn hiển thị cây hệ thống client, Favorites node riêng
|
||
- **Tóm tắt thay đổi:** `restoreSession` giờ CHỈ khôi phục favorites + folder/mode/rootName — BỎ khôi phục `computerRoots`/`computerPath`/`computerFiles`/`computerTree`/`selected` (nguyên nhân click My Computer vẫn mở thư mục cũ từ session). Mount effect chỉ nối lại FileSystemDirectoryHandle (clientRoot) không browse folder cũ. Click My Computer luôn: load client handle → auto-scan cây hệ thống client (C:, Users, Windows...) hoặc fallback server API scan ổ đĩa. Favorites node hiển thị sát dưới node My Computer (đã có từ trước).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom: seed session OLD_STUFF + favorite Users → click My Computer → picker NOT called, cây client C:/Users/Windows hiển thị, OLD_STUFF không còn, Favorites node + favorite Users có icon star. Hard reload.
|
||
|
||
### [2026-08-02 20:01] Task: Fix My Computer chỉ hiện 1 thư mục xanh không có ổ đĩa/thư mục
|
||
- **Tóm tắt thay đổi:** `openMyComputer`: nếu có client root handle đã lưu (IndexedDB) nhưng quyền đọc bị thu hồi sau reload (handle.entries() throw/empty) thì trước đây vẫn `return` → chỉ hiện node gốc (icon xanh) không có con. Giờ: (1) gọi `queryPermission`/`requestPermission` trước khi quét; (2) chỉ giữ client path nếu `browsed.dirs.length > 0`; (3) nếu quét client lỗi/rỗng → fallback server API scan ổ đĩa + expand thư mục con.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Smoke jsdom 2 kịch bản: (1) handle bị revoke (entries throw, permission denied) → fallback server: 2 drives (fa-hard-drive) + 5 folder, DATA/HOME/Music/Docs hiển thị; (2) handle khỏe (granted) → cây client C: + Users/me/Windows. Hard reload.
|
||
|
||
### [2026-08-02 20:10] Task: Click My Computer PHẢI hiện cây thư mục ổ đĩa + thư mục client-side
|
||
- **Tóm tắt thay đổi:** `openMyComputer` refactor thành helper `useClientRoot`: (1) client handle đã lưu (IndexedDB) + permission granted → auto-scan cây client; (2) KHÔNG có handle/không có quyền → gọi `window.showDirectoryPicker()` để cấp quyền đọc thư mục client rồi hiện cây client (lần đầu sẽ hiện picker 1 lần); (3) không hỗ trợ API/người dùng hủy → fallback server scan `/api/v1/media/computer` (ổ đĩa máy local). Bỏ điều kiện `browsed.dirs.length` để luôn giữ client mode sau khi chọn. Fix cha-con: `browseClientDir` gắn `parent: handle` cho từng thư mục con, `browseComputerDir` giữ `entry.parent` khi node chưa có trong tree → nút Up (↑) điều hướng đúng trong cây client.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` (babel) thành công, grep bundle chứa `showDirectoryPicker`/`useClientRoot`/`parent:handle`. Smoke: không handle → click My Computer → picker hiện, chọn thư mục → cây client expand. Có handle → không picker, cây client auto-load.
|
||
|
||
### [2026-08-02 20:56] Task: Fix "Unable to preventDefault inside passive event listener invocation"
|
||
- **Tóm tắt thay đổi:** Media Explorer preview canvas dùng React `onWheel={handleCanvasWheel}` có `e.preventDefault()` — React gắn `wheel` passive tại root nên Chrome log lỗi "Unable to preventDefault inside passive event listener invocation" và page vẫn scroll. Thay bằng native listener: `useEffect` + `canvasRef` + `addEventListener('wheel', h, { passive: false })` qua `handleCanvasWheelRef` (luôn gọi handler mới nhất), bỏ prop `onWheel` khỏi `<canvas>`. Giờ block scroll thật + hết lỗi console.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` OK; bundle: `handleCanvasWheelRef` có mặt, `onWheel:handleCanvasWheel` đã bỏ. Scroll chuột trên canvas preview (có file selected) → zoom hoạt động, không lỗi passive. Lưu ý: các onWheel tương tự ở MixerStrip/MasterStrip/TrackStripConsole (pan dial, fader) vẫn còn cảnh báo này — chưa sửa.
|
||
|
||
### [2026-08-02 21:40] Task: Instrument Sonatina soundfont không phát âm thanh
|
||
- **Tóm tắt thay đổi:** Nguyên nhân gốc: FluidSynth WASM (soundfontPlayer) KHÔNG decode được mẫu Ogg Vorbis/SF3 — mọi sample có bit OGG_VORBIS bị từ chối ("unknown flags... unsupported compression") → chọn instrument từ soundfont .sf3 (Sonatina đang ở dạng SF3 do startup convert) → câm hoàn toàn. Ngoài ra converter SF2→SF3 cũ còn 2 bug: (1) thiếu chunk id 'smpl' khi dựng lại file → sfload -1; (2) offset đọc shdr sai (đọc [end,loopstart,loopend,rate] thay vì [start,end,...]) → smpl rỗng. Fix: (1) `sf3_to_sf2()` + `_decode_sample_ogg()` chuyển SF3→SF2 (giải nén OGG về PCM, đúng semantics end-exclusive, loop absolute, xóa bit OGG) — đã verify phát âm thanh cả native lẫn WASM; (2) endpoint download ưu tiên .sf2, nếu chỉ có .sf3 thì chuyển SF3→SF2 on-demand (cache vào upload dir); (3) vô hiệu startup convert SF2→SF3 (`main.py`) — client không chơi được SF3; (4) client `loadSoundFont` nếu cache IndexedDB cũ chứa buffer hỏng thì xóa cache + tải lại; (5) sửa `_sf2_to_sf3_python` (smpl id, shdr offset đúng, end-exclusive, version 3.0, bit OGG, loop relative) + gate `_sf3_plays_audio` xóa SF3 không phát được.
|
||
- **Các file ảnh hưởng:** `app/core/soundfont_converter.py`, `app/api/v1/plugins.py`, `app/main.py`, `app/static/js/services/soundfontPlayer.js`
|
||
- **Ghi chú/Test (nếu có):** Test Node/WASM + native: SF2 gốc rms 0.007 (OK); SF3 convert mới sfload=1 nhưng câm (WASM từ chối OGG); SF3→SF2 convert ra file 52.37MB phát rms 0.007-0.02 (native + WASM). Test endpoint mô phỏng: upload SF3-only → download trả SF2 phát được. `pytest tests/test_vst_engine.py` 12 passed. Lưu ý: scipy thiếu nên test_plugin_api không import được (môi trường dev).
|
||
|
||
### [2026-08-03 07:55] Task: Media Explorer preview - loop liên tục vùng chọn + margin 2px quanh canvas
|
||
- **Tóm tắt thay đổi:** (1) Loop preview giờ chạy liên tục vô hạn cho đến khi nhấn Stop: `startCanvasClock` đọc refs (`isLoopingRef`/`selStartRef`/`selEndRef`) thay vì closure cũ nên việc bật loop giữa lúc đang play được phản ánh ngay, playhead wrap đúng theo `loopStartSec` (trừ offset gốc), không còn tự `stopMediaPlayback()` khi hết selection; `playMidiPreview` dùng `isLoopingRef.current` khi lập lịch interval (trước đây closure `isLooping` cũ → bật loop không tạo interval) và hủy interval khi tắt loop; `toggleLoop` sync `isLoopingRef` ngay + cập nhật `loopStart`/`loopEnd` cho audio đang phát theo selection hiện tại; `playSelected` dùng refs cho loop points/startOffset. (2) Container render canvas thêm `p-0.5` (2px) để quét chọn vùng không vượt ra ngoài khung preview.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` (babel) thành công. Smoke: chọn file audio → quét chọn 1 đoạn → bật Loop → phát liên tục vùng chọn đến khi nhấn Stop (playhead wrap đúng, selection overlay vẫn hiển thị khi rAF redraw nhờ `drawSelStart`/`drawSelEnd`). MIDI: bật loop khi đang preview → interval reschedule vùng chọn.
|
||
|
||
### [2026-08-03 10:15] Task: Fix MIDI ARM không phát âm thanh sau khi load instrument
|
||
- **Tóm tắt thay đổi:** Sửa `soundfontPlayer.js` để phím MIDI trên track ARM phát đúng instrument: (1) `_playNoteFluid` không còn để state mặc định của channel (bank 0, program 0, không sfId) che mất `synth_engine` của track → `finalSfId` luôn đúng; (2) tự load soundfont lười (lazy) ngay trong đường phát note nếu font chưa vào `_sfHandleMap` (track pick nhanh từ dropdown không gọi `selectInstrument` → trước đây rơi vào `bank_select`/`program_change` trên synth không có soundfont → câm lặng); (3) `selectInstrument` không còn đổi hướng channel khi đã truyền channel tường minh (sửa lỗi 2 track dùng chung instrument bị giật channel). Bump version cache `soundfontPlayer.js` trong `index.html`.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Test logic bằng harness `node /tmp/kilo/test_sonicsf_fix.js` (mô phỏng FluidSynth): S1 lazy-load + program_select + noteon, S2 channel cấu hình sẵn không sfload lại, S3 program-only fallback, S4 no-instrument vẫn silent, S5 2 track cùng instrument giữ channel riêng — ALL PASSED. Cần hard-refresh trình duyệt để nạp `soundfontPlayer.js` bản mới.
|
||
|
||
### [2026-08-03 10:30] Task: Fix ARM nhiều track - instrument sai theo track (channel state che mất synth_engine)
|
||
- **Tóm tắt thay đổi:** `_playNoteFluid` trước đây ưu tiên state channel (`_channels[ch].sfId`) làm nguồn instrument cho note → khi ARM nhiều track hoặc track đổi instrument qua dropdown nhanh, note bị phát theo instrument cũ/khác đang "dính" trên channel (leftover state hoặc 2 track trùng channel) → sai instrument từng track. Sửa: `synth_engine` của track là nguồn quyết định; channel state chỉ là cache (chỉ dùng khi note không có engine). Khi note mang engine khác với channel đang giữ, tự động `program_select` lại đúng instrument trước `noteon`; vẫn lazy-load soundfont nếu chưa nạp. Bump version cache `soundfontPlayer.js` trong `index.html`.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Harness `node /tmp/kilo/test_multi_track.js` (mô phỏng routing ARM như app.jsx): (A) 3 track 3 instrument distinct channel → noteon đúng, không re-select thừa; (B) 3 track quick-pick (không midiChannel, channel đang giữ instrument cũ) → mỗi track re-select đúng Q1/Q2/Q3 trước noteon; (C) 2 track trùng channel 0 → track 2 re-select program 40 trước noteon. Harness `test_sonicsf_fix.js` (single-track S1-S5) vẫn ALL PASSED. Hard refresh trình duyệt để nạp bản mới.
|
||
|
||
### [2026-08-03 10:40] Task: Fix lag khi ARM sau reload + hết âm thanh sau vài lần nhấn MIDI
|
||
- **Tóm tắt thay đổi:** (1) **Dedup load soundfont**: `loadSoundFont` trước đây không chặn các lệnh gọi đồng thời — nhấn phím MIDI liên tục (hoặc nhiều track ARM) kích hoạt lazy-load cùng lúc → SGM-V2.01 bị `sfload` 4 lần (handle 1,2,3,4 trong log), mỗi bản ~52MB trong heap WASM 256MB → cạn bộ nhớ, các lần load sau fail → câm sau vài lần nhấn + mỗi note chờ load (lag). Thêm `_loadPromises[sfId]` dedup (1 font = 1 lần load, cache cả kết quả fail) + `_sfloadSeq` cho filename temp duy nhất tránh trùng tên khi load 2 font khác nhau đồng thời. (2) **Preload instrument khi mở/khôi phục dự án**: thêm `preloadTrackInstruments(tracks)` gọi `selectInstrument` cho từng track có soundfont ngay sau khi `setTracks` trong `handleOpenProject` và `restoreLastSessionProject` → nhấn MIDI key đầu tiên không còn lag (font đã nạp sẵn).
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` (babel) OK, bundle chứa `preloadTrackInstruments`. Harness `node /tmp/kilo/test_dedup.js`: 8 lần `loadSoundFont` cùng 1 font đồng thời → chỉ 1 `sfload`, 1 lần đọc cache; load lại font đã nạp → 0 sfload; 2 font khác nhau đồng thời → đúng 2 sfload — ALL PASSED. `test_sonicsf_fix.js` + `test_multi_track.js` vẫn ALL PASSED. Hard refresh trình duyệt.
|
||
|
||
### [2026-08-03 11:00] Task: ARM multitrack - mỗi track dùng channel MIDI riêng, không làm đổi instrument track khác
|
||
- **Tóm tắt thay đổi:** FluidSynth có 16 channel; nếu 2 track trùng channel thì ARM track này sẽ `program_select` đè instrument của track kia. Thêm cơ chế channel riêng cho từng track trong app.jsx: `trackMidiChannelsRef` + `ensureTrackMidiChannel`/`assignTrackMidiChannel` cấp channel ổn định, duy nhất (0-15, bỏ qua 9 - slot percussion cổ điển), tự sửa khi 2 track trùng channel được lưu. Áp dụng ở mọi nơi tính channel: `setTrackInstrumentWithProgram`, `setTrackInstrument` (dropdown nhanh), `preloadTrackInstruments`, routing ARM note-on (armedTracks + piano-roll sub-tab), note-off/CC/PitchBend (bỏ fallback `index % 16` — tránh dừng nhầm note của track khác). Lưu `midi_channel` khi save project + restore; reset map channel khi mở/khôi phục dự án. Kết hợp doNote cũ (synth_engine quyết định + re-select trước noteon) → mỗi track luôn phát đúng instrument của nó dù ARM nhiều track, chord/hợp âm đồng thời không còn đổi âm sắc nhau.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` OK, bundle chứa `assignTrackMidiChannel` (8 chỗ). Harness `node /tmp/kilo/test_channels.js`: 3 track → channel 0,1,2 duy nhất; gọi lại ổn định; track mới lấy channel trống; track lưu trùng channel được cấp lại; track lưu channel duy nhất được giữ; percussion giữ 9 + track trống khác lấy channel riêng — ALL PASSED. 3 harness cũ (single/multi/dedup) vẫn PASS. Hard refresh.
|
||
|
||
### [2026-08-03 11:30] Task: Fix instrument sau làm đổi thông số instrument trước + ReferenceError preloadTrackInstruments + warning channel 9
|
||
- **Tóm tắt thay đổi:** (1) **sfload reset_presets 1→0** trong `soundfontPlayer._tryLoadSFL`: trước đây mỗi lần load soundfont mới, FluidSynth reset preset của MỌI channel về preset 0 của font mới → load instrument sau làm đổi âm sắc (decay/loop...) của instrument trước dù channel đã `program_select` riêng; giờ channel giữ nguyên instrument, doNote/selectInstrument tự `program_select` rõ ràng. (2) **ReferenceError `preloadTrackInstruments`**: hàm bị đặt nhầm bên trong `ProfileModal` (không cùng scope với `restoreLastSessionProject` trong `App`) → lỗi khi reload. Chuyển `preloadTrackInstruments` lên module-level (self-contained, chỉ warm font cache). Đồng thời sửa lỗi tiềm ẩn của `handleOpenProject` trong ProfileModal: truyền thêm props App-scoped (`setAppWarningModal`, `bpm`, `setBpm`, `setMasteringSettings`, `setSessionTabs`, `setSubTabs`, `trackMidiChannelsRef`) — trước đây click mở dự án trong modal sẽ ReferenceError. (3) **"No preset found on channel 9 [bank=128 prog=0]"**: lọc warning benign trong `printErr` (font không có preset bank 128 thì note chỉ câm, không phải lỗi) + reset=0 giảm nguồn sinh ra nó.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` OK, bundle syntax OK. Harness `node /tmp/kilo/test_reset.js`: sfload reset=0, load B không re-select channel A, noteon ch0/ch1 không kèm program_select thừa — PASSED. 5 harness còn lại (single/multi/dedup/channels) vẫn PASS. Deprecation ScriptProcessorNode giữ nguyên (đổi sang AudioWorklet có rủi ro crackle, cần test audio thật). Hard refresh.
|
||
|
||
### [2026-08-03 11:50] Task: Play MIDI item luôn dùng instrument của track chứa item (kể cả item duplicate)
|
||
- **Tóm tắt thay đổi:** Rà soát mọi đường phát MIDI trong app.jsx để chắc chắn playNote luôn nhận channel riêng của track (`assignTrackMidiChannel`) + `synth_engine` của track đó: (1) **fix bug `startLocalTrackPlayback`** (local selection loop) — trước đây gọi `playNote` KHÔNG truyền channel/synth_engine → rơi về channel 0, phát nhầm instrument của track đang giữ channel 0; giờ truyền `lcCh` + `track.synth_engine`. (2) Đồng nhất các đường còn lại (transport `startTrackPlayback`, section sub-track, `schedulePianoRollMidi`, `playMidiPreviewNote`, piano-roll canvas/click/keybed/brush preview, ghost layers) từ `getTrackMidiChannel` → `assignTrackMidiChannel` để mọi path dùng đúng dedicated channel của track. Vì playback lấy `synth_engine`/`instrumentProgram` từ track CHỨA item (item không lưu instrument, duplicate chỉ copy notes) nên khi duplicate/move MIDI item sang track khác, item sẽ tự động phát instrument đã load ở track đó.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` OK, bundle syntax OK, `getTrackMidiChannel` không còn trong app.jsx (0), `assignTrackMidiChannel` có 19 chỗ. Bundle chứa `lcCh=assignTrackMidiChannel(track,tracks)` + playNote truyền `lcCh,track.synth_engine`. 5 harness (single/multi/dedup/channels/reset) vẫn PASS. Hard refresh.
|
||
|
||
### [2026-08-03 12:10] Task: Fix ReferenceError assignTrackMidiChannel + bật AudioWorklet thay ScriptProcessor
|
||
- **Tóm tắt thay đổi:** (1) **ReferenceError `assignTrackMidiChannel is not defined`**: allocator channel (trackMidiChannelsRef/ensureTrackMidiChannel/assignTrackMidiChannel) đặt bên trong `App`, nhưng các sub-component piano roll (`PianoRollTabEditor` — canvas preview, click note, draw/paint brush, keybed) là module-level → không thấy hàm → lỗi khi render/play. Chuyển allocator lên **module-level** (đầu file, trước PianoRollTabEditor), App dùng chung bản đó (bỏ khai báo `useRef` cục bộ trong App; reset `trackMidiChannelsRef.current = {}` khi load dự án vẫn dùng chung object). (2) **Deprecation ScriptProcessorNode**: bật **AudioWorklet** `fluidsynth-bridge` làm mặc định (`_useScriptNode = false`), chỉ fallback ScriptProcessor khi `new AudioWorkletNode`/`addModule` thất bại → hết warning deprecation; `_startRenderLoop` (queue 16 block × 512 sample ~186ms headroom, interval 2× frame rate) vốn đã được thiết kế cho worklet.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` OK, bundle syntax OK, bundle load smoke test (stub React/ReactDOM/window) OK. Harness `test_sonicsf_fix.js` với mock AudioWorkletNode + performance: `workletCreated=1 scriptProcCreated=0` (worklet được dùng), S1-S5 vẫn PASS; trước đó (thiếu mock performance) xác nhận fallback ScriptProcessor hoạt động. 4 harness còn lại vẫn PASS. Hard refresh.
|
||
|
||
### [2026-08-03 12:30] Task: Fix crash AudioWorklet - "Cannot set properties of undefined (setting '0')" khi output mono
|
||
- **Tóm tắt thay đổi:** `FluidSynthBridge.process` luôn ghi stereo `out[0]`/`out[1]`; khi thiết bị/context xuất mono (chỉ 1 channel), `out[1]` là `undefined` → `out[1][i] = ...` ném TypeError, dừng audio. Fix: (1) `fluidsynth-bridge.js` xử lý mọi số channel output (vòng lặp theo `out.length`, chẵn=lấy L, lẻ=lấy R, trường hợp cạn queue ghi 0 cho từng channel); (2) `soundfontPlayer.js` tạo `AudioWorkletNode` với `outputChannelCount: [2]`, `channelCount: 2`, `channelCountMode: 'explicit'` để ép stereo (FluidSynth render stereo) thay vì để device quyết định.
|
||
- **Các file ảnh hưởng:** `app/static/js/worklets/fluidsynth-bridge.js`, `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Harness `node /tmp/kilo/test_worklet.js` mô phỏng process(): (1) output mono 1 channel → mirror left, không crash; (2) stereo → L/R đúng; (3) queue cạn → silence. Tất cả PASS. 5 harness khác vẫn PASS. Worklet + soundfontPlayer `node --check` OK. Không cần build bundle (2 file served trực tiếp). Hard refresh.
|
||
|
||
### [2026-08-03 12:45] Task: Fix không có âm thanh khi play MIDI item - quay lại ScriptProcessor làm renderer mặc định
|
||
- **Tóm tắt thay đổi:** Sau khi bật AudioWorklet làm mặc định, audio không phát khi play MIDI item trên track (môi trường user bị 2 lỗi liên tiếp: crash mono rồi câm). ScriptProcessor là đường đã được xác nhận hoạt động ổn định (mọi test trước đó). Quay lại `_useScriptNode = true` (ScriptProcessor mặc định) để đảm bảo âm thanh; giữ code AudioWorklet (đã mono-safe) + thêm cache-busting `?v=202608031240` cho URL `addModule` để khi bật lại không bị cache bản cũ. Deprecation warning quay lại nhưng chỉ là cảnh báo cosmetic.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Harness `node /tmp/kilo/test_render.js` (path ScriptProcessor): workletCreated=0, ScriptProcessor được tạo, playNote → noteon, onaudioprocess render ra audio peak 0.25 — ALL PASSED. `test_sonicsf_fix.js` cập nhật assertion ScriptProcessor mặc định → rc=0. 6 harness còn lại PASS (test_worklet rc báo 1 do glitch shell nhưng result file ghi ALL TESTS PASSED). Không cần build bundle (file served trực tiếp). Hard refresh.
|
||
|
||
### [2026-08-03 13:00] Task: Fix piano roll tab không phát âm thanh instrument của track
|
||
- **Tóm tắt thay đổi:** `schedulePianoRollMidi` cộng `sessionBeatOffset` (vị trí tuyệt đối của item trong project, đơn vị beat) vào `start_beat` của mọi note → note được lên lịch ở **thời điểm tuyệt đối** của project, trong khi playhead/tab piano roll là **item-relative** (0 = đầu item). Item đặt sau vị trí 0 (vd 10s) → nhấn play nghe không có âm thanh trong 10s (note được schedule ở now+10s), sau đó mới kêu → cảm giác "không có âm thanh instrument". Fix: bỏ `sessionBeatOffset` ở cả loop note chính và loop ghost (ghost `relative_start_beat` vốn đã item-relative) — note phát theo vị trí tương đối item, khớp playhead; item ở vị trí 0 không đổi hành vi. `sessionSyncMode` chỉ là chế độ hiển thị ghost/context, không liên quan timing phát.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` OK, bundle syntax OK, hết `sessionBeatOffset` (0 chỗ). Harness `node /tmp/kilo/test_pr_timing.js` mô phỏng timing mới: item ở project 10s, nhấn play mới → note đầu phát ngay +0s (trước đây +10s), note 2 ở +2s; resume 1.2s → bỏ note đầu, schedule note sau; item vị trí 0 không đổi — ALL PASSED. 6 harness còn lại PASS. Hard refresh.
|
||
|
||
### [2026-08-03 13:15] Task: Fix mất soundfont khi ARM + BiquadFilterNode state is bad
|
||
- **Tóm tắt thay đổi:** (1) **Master chain bị NaN**: `applyMasteringSettings` chạy mỗi lần `getAudioContext()`; nếu `window.currentMasteringSettings` bị thiếu field/NaN (vd project lưu cũ, slider kéo cực hạn) thì `eqLowFilter.gain.setTargetAtTime(undefined/NaN)` → biquad "state is bad" → master chain im lặng → mọi audio (kể cả MIDI/ARM) biến mất ("mất soundfont"). Thêm `clamp()` chặn mọi tham số EQ (±24dB), imager (±100), maximizer, ceiling — NaN/thiếu → 0 (trung tính), cực hạn → giới hạn an toàn. (2) `toggleMasteringOnMaster` trở nên idempotent (chỉ disconnect/reconnect khi trạng thái đổi) — trước đây gọi lại liên tục từ `getAudioContext()` gây reconnect nhanh → biquad mất ổn định. (3) **loadSoundFont không cache lỗi vĩnh viễn**: lỗi tải thoáng qua (mạng 503, áp lực bộ nhớ) trước đây bị giữ trong `_loadPromises` → instrument câm vĩnh viễn tới khi reload; giờ xóa cache lỗi để note kế tiếp thử lại và tự hồi phục.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` OK, bundle syntax OK. Harness `node /tmp/kilo/test_retry.js`: clamp undefined/NaN → 0, cực hạn → giới hạn; load font lỗi 503 lần 1 → lần 2 retry thành công (2 fetches) → note phát được — ALL PASSED. 8 harness còn lại PASS. Deprecation ScriptProcessorNode vẫn còn (giữ ScriptProcessor vì ổn định, worklet từng gây lỗi thiết bị). Hard refresh.
|
||
|
||
### [2026-08-03 13:30] Task: Hết ScriptProcessor deprecation (bật AudioWorklet) + hết BiquadFilterNode state is bad (signature guard)
|
||
- **Tóm tắt thay đổi:** (1) **Deprecation**: bật lại AudioWorklet làm renderer mặc định (`_useScriptNode = false`) — worklet giờ đã mono-safe + ép stereo `outputChannelCount:[2]` + URL `addModule` có cache-busting `?v=` nên luôn nạp bản đã sửa; fallback ScriptProcessor khi tạo worklet thất bại. Trước đây "mất tiếng khi transport" là do worklet cũ (pre-mono-fix) bị cache. (2) **BiquadFilterNode: state is bad**: `applyMasteringSettings` được gọi mỗi lần `getAudioContext()` (stopAll/play/VU…), liên tục `setTargetAtTime` vào EQ biquad = "fast parameter automation" → Chromium báo state bad dù giá trị hợp lệ. Thêm **signature guard**: chỉ áp dụng khi giá trị thực sự đổi → bỏ chùm automation lặp, giữ clamp chống NaN. `toggleMasteringOnMaster` vẫn idempotent.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` OK. Harness `test_render.js` (worklet mặc định): workletCreated=1 scriptProcCreated=0, noteon=1, 26 PCM frames posted, peak 0.25 — ALL PASSED. `test_sonicsf_fix.js` cập nhật assertion worklet → PASS. 8 harness còn lại PASS. Hard refresh (Ctrl+F5) — lần đầu sẽ tải worklet mới. Nếu nghe crackle/mất tiếng khi phát, báo lại để quay lại ScriptProcessor.
|
||
|
||
### [2026-08-03 13:40] Task: Fix Lỗi không có âm thanh - quay lại ScriptProcessor (quyết định cuối)
|
||
- **Tóm tắt thay đổi:** Bật AudioWorklet lần 2 vẫn làm mất tiếng trên máy user. Nguyên nhân thực sự: worklet là push model (`setInterval` trên main thread) — khi main thread bận (load font/WASM decode/UI), vòng lặp nghẽn → worklet hết frame → câm. ScriptProcessor là pull-based (`onaudioprocess` do audio thread gọi) nên không bao giờ đói dữ liệu. **Quyết định cuối: giữ ScriptProcessor làm renderer mặc định vĩnh viễn** (`_useScriptNode = true`); warning deprecation chỉ cosmetic. Worklet (đã mono-safe + stereo-forced + cache-busting) giữ lại như opt-in để test sau.
|
||
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** Harness `test_render.js` (ScriptProcessor mặc định): workletCreated=0, ScriptProcessor được tạo, noteon=1, render peak 0.25 — ALL PASSED. `test_sonicsf_fix.js` assertion ScriptProcessor → PASS. 8 harness còn lại PASS. Hard refresh (Ctrl+F5) — âm thanh hoạt động lại.
|
||
|
||
### [2026-08-03 13:45] Task: Fix mất âm thanh khi nhấp đôi MIDI item vào Piano Roll
|
||
- **Tóm tắt thay đổi:** `handleEditMidiInTab` (nhấp đôi MIDI item → mở PIANO ROLL tab) preload instrument bằng channel cũ `trkIdx % 16`, trong khi MỌI đường phát (schedulePianoRollMidi, transport, ARM) dùng dedicated channel (`assignTrackMidiChannel`). Channel lệch nhau → tab-open `selectInstrument` cấu hình nhầm channel (có thể đè instrument của track khác nếu trùng channel 0-15), khiến âm thanh trong/pianô sau khi mở tab không còn đúng. Sửa: dùng `assignTrackMidiChannel(track, allTrks)` cho preload khi mở tab → khớp đúng channel phát.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` OK, bundle syntax OK. Harness `node /tmp/kilo/test_dbl.js` mô phỏng toàn bộ flow double-click (init → tab-open selectInstrument → schedulePianoRollMidi playNote → ScriptProcessor render): noteon đúng channel, render peak 0.25 (không câm) — ALL PASSED. 9 harness còn lại PASS. Nếu vẫn mất âm thanh sau hard refresh, cần xác định rõ: câm toàn cục hay chỉ play trong piano roll, và có console error gì.
|
||
|
||
### [2026-08-03 14:00] Task: Fix câm toàn cục sau nhấp đôi MIDI item + BiquadFilterNode state is bad (triệt để)
|
||
- **Tóm tắt thay đổi:** User xác nhận: sau nhấp đôi MIDI item vào Piano Roll, câm TOÀN CỤC (main cũng hết tiếng) + lỗi `BiquadFilterNode: state is bad` vẫn còn. Nguyên nhân: master chain (EQ/imager biquad) bị `toggleMasteringOnMaster` + `applyMasteringSettings` gọi lại **mỗi lần** `getAudioContext()` (play, stopAll, double-click, VU...) — burst "fast parameter automation" vào biquad làm Chromium báo state bad, và nếu routing master bị ngắt giữa chừng → câm toàn cục. Fix triệt để: (1) **gỡ mastering khỏi getAudioContext** — master chain chỉ được áp dụng 1 lần khi `initMasterBus` tạo (nếu có `currentMasteringSettings`) + qua React effect khi settings đổi; (2) **time constant 0.01 → 0.05** + `cancelScheduledValues` trước mỗi `setTargetAtTime` EQ — hết tích tụ automation event khi kéo slider; (3) `toggleMasteringOnMaster` bọc try/catch, **luôn reconnect lại routing hợp lệ** dù lỗi — không bao giờ để inputAnalyser bị ngắt không nối (gây câm toàn cục).
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` OK, bundle syntax OK, `toggleMasteringOnMaster` còn 3 chỗ (def + initMasterBus + effect). 9 harness (single/multi/dedup/reset/channels/render/pr_timing/retry/dbl) vẫn PASS. Hard refresh (Ctrl+F5). Nếu lỗi biquad vẫn hiện khi bật MasteringModal và kéo EQ, đó có thể là false-positive Chromium (parallel biquad) — báo tôi để tôi đổi cấu trúc imager/EQ.
|
||
|
||
### [2026-08-03 14:15] Task: Fix câm toàn cục - IN peak có, OUT peak không (master chain chết)
|
||
- **Tóm tắt thay đổi:** User chẩn đoán: mở Mastering modal, IN peak có tín hiệu nhưng OUT peak trống → tín hiệu chết TRONG master chain. Khắc phục triệt để 3 nguyên nhân có thể làm chain câm: (1) **WaveShaper `curve = null`**: một số engine xuất CÂM khi curve null (identity) — đổi luôn sang identity table `Float32Array([-1,1])` (passthrough chủ động, không bao giờ null) ở cả init và khi maximizer tắt. (2) **Tần số filter vượt Nyquist**: `eqHighFilter` 10000Hz / imager crossover 6000Hz trên thiết bị sample rate thấp (8/11/16kHz) → hệ số biquad NaN → `BiquadFilterNode: state is bad` → chain câm. Thêm `clampF(v) = min(v, sampleRate*0.45)` cho mọi biquad. (3) **Watchdog an toàn**: trong Mastering modal, nếu `masteringActive` mà IN peak > 0.01 còn OUT peak < 0.001 (chain hỏng) → tự `toggleMasteringOnMaster(false)` về routing trực tiếp để âm thanh KHÔNG BAO GIỜ bị câm toàn cục.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` OK, bundle chứa `maxFilterFreq`, `Chain broken`, `Float32Array([-1,1])`. 9 harness vẫn PASS. Hard refresh (Ctrl+F5) → thử play (main + piano roll) + bật mastering. Nếu OUT peak vẫn trống, watchdog sẽ tự bypass và log `[Mastering] Chain broken...` — báo tôi message đó.
|
||
|
||
### [2026-08-03 14:30] Task: Media Explorer - auto tempo theo MIDI file, gõ tempo tay, focus folder cha, điều hướng tree bằng phím mũi tên
|
||
- **Tóm tắt thay đổi:** (1) **Auto set tempo**: click MIDI file → `handleSelect` set tempo từ metadata `f.bpm`; `playMidiPreview` sau khi parse set tempo theo `midiResult[0].bpm` (clamp 40-300) trước khi schedule → preview phát đúng tempo file. (2) **Gõ tempo tay**: input tempo dùng `tempoText` (string) cho phép gõ tự do (trước đây clamp 40-300 ngay khi gõ chặn việc nhập số < 40), commit khi hợp lệ hoặc blur/Enter; `commitTempo` còn re-schedule MIDI preview đang phát theo tempo mới. (3) **Focus folder cha**: click file → expand các node cha + `centerTreeNodeInPane` cuộn tree pane (ref `treePaneRef`) để folder cha hiện GIỮA ô tree. (4) **Phím mũi tên**: khi panel active (`window.mediaExplorerActive`) và ở computer mode, ArrowUp/Down di chuyển cursor qua node hiển thị (dùng `computerPathRef`/`computerTreeRef`/`computerRootsRef` để tránh stale closure trong keydown `[]`), ArrowRight expand/load, ArrowLeft collapse hoặc về thư mục cha; `browseComputerDirRef` tránh stale `browseComputerDir`.
|
||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/templates/index.html`
|
||
- **Ghi chú/Test (nếu có):** `npm run build` OK, bundle chứa commitTempo/navigateTreeTo/centerTreeNodeInPane/getVisibleTreePaths/treePaneRef/tempoText. Harness `node /tmp/kilo/test_tree.js` mô phỏng flatten tree + Up/Down/Left logic — ALL PASSED. 9 harness còn lại PASS. Hard refresh.
|