22 Commits

Author SHA1 Message Date
3dtours 8e3c652551 IMPROVE: PIANO ROLL TAB khi play thì playhead luôn nằm chính giữa piano roll 2026-08-07 22:10:37 +07:00
3dtours 393df8fd7b FIX: cho phép shift+click để chọn notes, drag để di chuyển các notes, ctrl+drag để copy nhanh các notes 2026-08-07 21:48:14 +07:00
3dtours 688a7a57e2 FIX: cho phép copy/cut các notes được chọn trong PIANO ROLL TAB 2026-08-07 21:39:18 +07:00
3dtours 652e745d0a IMPROVE: thay đổi giao diện của PIANO ROLL TAB 2026-08-07 21:22:13 +07:00
3dtours 45ab31d42e IMPROVE: thêm các tính năng cho MIDI trong PIANO ROLL TAB 2026-08-07 20:46:47 +07:00
3dtours c3182f6baa FIX: sửa lỗi không hiển thị đúng nội dung của SECTION-TAB trong section item 2026-08-07 18:58:29 +07:00
3dtours 1d9b426d15 FIX: sửa lỗi copy/cut paste không đúng item 2026-08-07 17:06:05 +07:00
3dtours 4f34491f3e FIX: sửa lỗi hiển thị animation của VU meter khi play MIDI 2026-08-07 15:57:49 +07:00
3dtours 9e269d3b79 FIX: sửa lỗi hiển thị animation của VU meter 2026-08-07 15:40:15 +07:00
3dtours 58c6ec794d FIX: sửa lỗi hiển thị số bar bị sai 2026-08-07 15:07:06 +07:00
3dtours 173a1f227b FIX: sửa lỗi lưu thay đổi trong SECTION-TAB thì cũng tạo duration trên MAIN SESSION 2026-08-07 11:08:39 +07:00
3dtours c1aa1eb4d4 FIX: sửa lỗi phát hiện đã không còn play midi note để PIANO ROLL TAB quay về vị trí 0 2026-08-07 10:41:45 +07:00
3dtours 6c16bf8410 FIX: sửa lỗi đồng bộ playhead vị trí play giữa MAIN SESSION/SECTION-TAB/PIANO ROLL TAB 2026-08-07 10:37:34 +07:00
3dtours 4981785913 FIX: sửa lỗi playhead đồng bộ giữa các tab, nhấn space có thể bind với phím play 2026-08-07 10:27:21 +07:00
3dtours bca661bffd FIX: sửa lỗi chuyển từ tab PIANO ROLL TAB sang MAIN SESSION/SECTION-TAB không đồng bộ playhead 2026-08-07 10:12:18 +07:00
3dtours 4c820bfa77 FIX: sửa lỗi chuyển từ tab PIANO ROLL TAB sang MAIN SESSION và SECTION-TAB thì bị câm 2026-08-07 09:35:40 +07:00
3dtours 8cdf2942b9 FIX: sửa lỗi chuyển từ tab MAIN SESSION sang SECTION-TAB và PIANO ROLL TAB thì bị bypass 2026-08-07 09:13:03 +07:00
3dtours 23675dab83 FIX: Đã fix preview Media Explorer (v202608062345) 2026-08-06 20:45:56 +07:00
3dtours 71b9231b57 FIX: Đã fix block lồng + mất MIDI (v202608062330) 2026-08-06 20:42:27 +07:00
3dtours 93dc13b17a FIX: lỗi bypass của SECTION-TAB 2026-08-06 18:19:04 +07:00
3dtours f1e914fa62 FIX: lỗi: CC7 (channel volume) chỉ được set 100/0 (mute/unmute) — volume track KHÔNG áp qua channel → track MIDI dùng chung channel: CC7 của track set cuối thắng → volume của track kia vô tác dụng 2026-08-06 14:10:57 +07:00
3dtours 4dfab954d8 FIX: reset tất cả về mặc định khi tạo new project 2026-08-06 11:47:10 +07:00
6 changed files with 3375 additions and 445 deletions
+29
View File
@@ -259,3 +259,32 @@ async def get_profile(current_user: dict = Depends(get_current_user)):
"max_tracks": row["max_tracks"] or 16
}
}
@router.get("/first-time")
async def auth_first_time():
# "Lần đăng nhập đầu" = tài khoản admin vẫn dùng mật khẩu MẶC ĐỊNH
# (chưa từng đổi). Sau khi đổi lần đầu → first_time = false → UI xóa
# gợi ý username/mật khẩu (Tùy chọn - Admin có thể bỏ trống, lần đầu:
# admin123, nút Điền nhanh).
try:
conn = get_db_connection()
try:
cur = conn.cursor()
cur.execute(
"SELECT id, hashed_password, must_change_password FROM users "
"WHERE LOWER(role) = 'admin' ORDER BY created_at ASC LIMIT 1"
)
row = cur.fetchone()
finally:
conn.close()
if not row:
return {"first_time": True}
still_default = False
try:
still_default = verify_password("admin123", row["hashed_password"])
except Exception:
still_default = False
return {"first_time": bool(row["must_change_password"]) and still_default}
except Exception:
return {"first_time": True}
+1926 -314
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -84,6 +84,11 @@
if (dest === _outputDestination) return;
_gainNode.disconnect();
_gainNode.connect(dest);
// Reconnect VU analyser tap (app.jsx VU tick gắn __vuAnalyser
// trên _gainNode): disconnect() không đối số ngắt MỌI kết
// nối kể cả analyser → sfAudioPeak = 0 mãi → track VU MIDI
// không nhảy khi có âm (user bug 06:35).
if (_gainNode.__vuAnalyser) _gainNode.connect(_gainNode.__vuAnalyser);
_outputDestination = dest;
console.log('[SonicSF] setOutputDestination to:', node ? 'track node (sfEntry)' : 'masterBus.input');
} else {
+2 -2
View File
@@ -16,7 +16,7 @@
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
<script src="/static/js/services/storage.js?v=202608038200"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608060630"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608070635"></script>
<script src="/static/js/services/aiGateway.js?v=202608037200"></script>
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
@@ -24,7 +24,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608061030" defer></script>
<script src="/static/js/app.precompiled.js?v=202608070950" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+884
View File
@@ -2041,3 +2041,887 @@
- **FIX (app.jsx):** nút Đóng chuyển từ cuối toolbar → SAU nút MIDI ghost notes (hàng 1) + `ml-auto` (đẩy phải); nhóm Lưu/Export giữ cuối hàng 2 + `ml-auto`; Export → **Export MIDI**. Emoji 🎵 Chuyển giọng bị patch tool double-escape → sửa bằng emoji thật (verify: 0 double-escape còn lại).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061030), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → hàng 1 phải có [Đóng]; hàng 2 phải: [Lưu] [Export MIDI] ở cuối bên phải.
### [2026-08-06 11:00] Task: Ẩn gợi ý đăng nhập sau lần đăng nhập đầu (username/password hints + nút Điền nhanh)
- **Yêu cầu user:** sau lần đăng nhập hệ thống đầu tiên → xóa gợi ý: "(Tùy chọn - Admin có thể bỏ trống)", "(Lần đầu: admin123)", nút "🔑 Điền Nhanh Tài Khoản Admin".
- **FIX:**
(1) Backend `auth.py`: endpoint **`GET /api/v1/auth/first-time`** → `{first_time: bool}` — true khi admin vẫn dùng mật khẩu mặc định (verify_password admin123 + must_change_password); sau khi đổi → false.
(2) Frontend AuthModal: state `firstTime` + fetch khi modal mở; 3 gợi ý render có điều kiện `firstTime &&` — hết lần đầu → ẩn vĩnh viễn.
- **Các file ảnh hưởng:** `app/api/v1/auth.py`, `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061100), `wiki.md`. Rebuild precompiled + restart backend.
- **Ghi chú/Test:** restart backend → login admin (mật khẩu còn mặc định) → vẫn thấy gợi ý; đổi mật khẩu → logout → login lại → KHÔNG còn gợi ý/nút Điền nhanh.
### [2026-08-06 11:30] Task: Gợi ý đăng nhập vẫn hiện sau khi đổi mật khẩu — fail-closed (ẩn khi endpoint lỗi)
- **Báo cáo user:** đã đổi mật khẩu nhưng gợi ý vẫn xuất hiện.
- **Nguyên nhân nghi:** backend chưa restart → `/api/v1/auth/first-time` 404 → catch cũ `setFirstTime(true)` → gợi ý hiện (fail-open sai hướng).
- **FIX (app.jsx AuthModal):** catch → `setFirstTime(false)` + else branch → fail-closed: endpoint lỗi/404 → **ẨN gợi ý**. Endpoint OK: `verify_password("admin123", hash mới)` = false → first_time=false → ẩn (đã đúng logic backend).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061130), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → login → gợi ý ẩn (kể cả khi backend chưa restart). Khuyến nghị restart backend để endpoint hoạt động đúng.
### [2026-08-06 12:00] Task: New project — reset toàn bộ (đóng tab, clear selection, reset instrument context)
- **Yêu cầu user:** tạo new project → đóng hết tab đang mở, clear selection, reset instrument đã load về mặc định.
- **FIX (app.jsx — 2 nơi: menu File → New Project + Ctrl+N):**
- `stopAllPlayback()` (dừng mọi play)
- `setSubTabs([])` + `setSessionTabs([])` + `setActiveTab('main')` (đóng hết tab)
- `setSelectedItemIds(new Set())` (clear multi-selection)
- Reset context instrument: `trackMidiChannelsRef.current = {}` + clear `trackMidiBypassMap`/`trackAudioBypassMap`/`trackMasteringBypassMap` (delete từng key — const map)
- Reset project: 2 track mặc định, selectedTrackId '1', projectName '', currentProjectId null, xóa localStorage id/name
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061200), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → mở vài tab + chọn track → File → New Project (hoặc Ctrl+N) → tab đóng hết, selection trống, instrument track về mặc định.
### [2026-08-06 12:30] Task: SECTION-TAB 5 bug — Ctrl+S, strip controls, undo, section play, mute isolation
- **Báo cáo user (SECTION-TAB):** (1) Ctrl+S phải lưu section; (2) volume slider + nút strip không tác dụng; (3) không undo/redo cut/paste/delete/move; (4) MAIN play section item câm sau khi lưu; (5) mute ở section mixer mute luôn track MAIN.
- **FIX:**
(1) **Ctrl+S**: handler đầu (15915) save project vô điều kiện + return → block thứ 2 (16007) chết → sửa handler đầu tab-aware: `session_``handleSaveSectionTab` + toast; khác → save project.
(2+5) **Handlers setTracks (MAIN) → updateActiveTracks (context-aware)**: `toggleTrackMute`, `toggleTrackSoloEvaluate`, `updateTrackVolumeDb`, `updateTrackPan`, `updateTrackProp` — section strip giờ tác dụng lên track SECTION (không còn mute track MAIN).
(3) **Undo section**: chuỗi đã context-aware (captureTrackSnapshot/captureAllTracksSnapshot → activeTracks; restore applyTrackState → updateActiveTracks) — sau fix (2) các entry mute/volume đúng context; item ops (delete/move/paste 20417/20603/20764+) đã push đúng.
(4) **Section play**: code path đã verify (18847-18957: schedule clip+midi sub-track → subNode → parent → route → mastering; sec.tracks lưu sau save 16381-16395) — nếu vẫn câm cần log console (`[SonicSF]` noteon + sfId).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061230), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section tab: Ctrl+S lưu section; kéo volume/mute tác dụng trong section; Ctrl+Z sau cut/paste; main play section item có âm; mute section không ảnh hưởng main.
### [2026-08-06 13:00] Task: SECTION-TAB volume/mute — áp gain cho sub-node (key _sub_) realtime
- **Báo cáo user:** volume mixer strip không tác dụng âm thanh; mute section phải CHỈ ảnh hưởng section.
- **Nguyên nhân:** node play section là **sub-node** (key `<parent>_sub_<trackId>` — 18697) — gain chỉ set lúc TẠO node (18703) — `updateTrackVolumeDb`/`applyAllTrackMuteSolo` lookup `activeTrackNodesRef[trackId]` (key thường) → KHÔNG thấy sub-node → volume/mute đổi STATE nhưng âm section không đổi.
- **FIX (app.jsx):**
(1) `updateTrackVolumeDb`: sau khi set gain node thường → **scan `_sub_<trackId>`** → set gain sub-node realtime.
(2) `applyAllTrackMuteSolo`: sau `setTrackNodeGain(node, ...)`**scan `_sub_<t.id>`** → set gain sub-node theo audibleGain (mute/solo section áp node play section — activeTracksRef = section tracks → KHÔNG đụng track MAIN).
(3) (Đã fix vòng trước) handlers setTracks → updateActiveTracks — mute/volume chỉ đổi tracks SECTION.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061300), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section tab: kéo volume nghe đổi ngay; mute section track → câm trong section, main không bị; unmute lại.
### [2026-08-06 13:30] Task: Mixer fader dùng updateTrackProp — volume section vẫn không đổi âm
- **Báo cáo user:** volume slider SECTION-TAB mixer KHÔNG đổi âm lượng.
- **Nguyên nhân:** mixer strip (TrackStripConsole — 27165) truyền `onUpdateTrack: updateTrackProp` — fader (1560) gọi `onUpdateTrack(tid, {volumeDb})` — fix vòng 13:00 đặt scan sub-node trong `updateTrackVolumeDb` (TCP slider) — **mixer dùng updateTrackProp — SAI CHỖ → không áp gain node.**
- **FIX (app.jsx):** `updateTrackProp` — khi props có `volumeDb` → áp gain realtime node thường + scan `_sub_<trackId>` (sub-node section) — mixer section + mixer main đều đổi âm ngay.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061330), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section tab mixer: kéo volume → âm section đổi ngay (cả khi main đang play section item); main mixer cũng đổi ngay.
### [2026-08-06 14:00] Task: SECTION-TAB — volume MIDI track qua CC7 (channel volume) + fix collision channel
- **Báo cáo user:** AI tạo track trong SECTION dùng chung channel track trước → volume track sau vô tác dụng cho đến khi track trước MUTE.
- **Nguyên nhân:** CC7 (channel volume) chỉ set 100/0 (mute/unmute) — VOLUME track KHÔNG áp qua channel → track MIDI dùng chung channel: CC7 cuối thắng → volume 1 track không tác dụng.
- **FIX (app.jsx — 2 chỗ):**
(1) `applyAllTrackMuteSolo` (14135): CC7 = **volume track** (audible ? volLin*100 clamp 1-127 : 0) — không còn 100/0 cứng.
(2) Sync effect (14719): cùng logic CC7-volume — volume slider → trackMuteSoloSig (gồm volumeDb) → effect → CC7 mới.
(3) (Đã có) AI Var clone `midiChannel: undefined` (23288) + `ensureTrackMidiChannel` tự cấp channel riêng khi trùng (30: `!inUse.has(track.midiChannel)`) — collision được xử lý khi assign.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061400), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: tạo AI Var mới → kéo volume từng track → âm đổi theo track; mute track trước → track sau vẫn đúng volume. Nếu track AI Var CŨ (tạo trước fix midiChannel) vẫn dùng chung channel → xóa + tạo lại.
### [2026-08-06 14:30] Task: Nút A (audio bypass) trên section strip không auto-áp — sub-node thiếu route
- **Báo cáo user:** nút A xám (bypass on) nhưng audio không bypass — phải bấm 2 lần mới có tác dụng.
- **Nguyên nhân:** sub-node section (key `<parent>_sub_<subId>`) chỉ có gainNode → parent — **KHÔNG có route (routeGain/dryGain)**`__setTrackBypass` scan node theo `indexOf(trackId + '_sub_')` (sai key — sub-node key bắt đầu bằng PARENT id) → nút A không áp được; node play section không có gì để toggle.
- **FIX (app.jsx):**
(1) `getOrCreateSubTrackNode`: thêm **route riêng** cho sub-track — `routeGain` (mastering path → parent) + `dryGain` (→ masterBus.dryInput) — gain khởi tạo theo `effAudioBypass(subTrack)` — nút A áp ngay từ lúc tạo node.
(2) `__setTrackBypass` (audio): scan `k === trackId || k.endsWith('_sub_' + trackId)` (đúng key sub-node của track đó) → `setMasteringRoute` realtime.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061430), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: nút A track audioclip → xám = bypass ngay (nghe hết mastering); sáng = qua mastering.
### [2026-08-06 15:00] Task: Nút A section — route tạo node dùng effAudioBypass bị override mastering-ON → bypass mất khi stop+play
- **Báo cáo user:** A mặc định tắt → play không bypass; bấm A ON rồi OFF mới bypass; stop + play lại → NOT BYPASS.
- **Nguyên nhân:** sub-node route tạo bằng `effAudioBypass(subTrack)` — hàm này override `false` khi mastering ON (yêu cầu v03:00) → route luôn no-bypass lúc TẠO node → phải bấm A 2 lần (__setTrackBypass set route trực tiếp); stop → node bị xóa/tái tạo → route lại ép false → mất bypass.
- **FIX (app.jsx getOrCreateSubTrackNode):** dùng **map THẬT** `trackAudioBypassMap[subTrack.id]` (trạng thái nút A) — không qua effAudioBypass → route tạo theo nút A; stop+play → node tái tạo → route đúng trạng thái nút A.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061500), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: bật A (xám) → play → bypass ngay; stop + play lại → VẪN bypass; tắt A (sáng) → qua mastering.
### [2026-08-06 15:30] Task: SECTION-TAB audioclip PHẢI bypass mặc định — addNewTrack context-aware (A xám)
- **Yêu cầu user:** nút A mặc định tắt nhưng audioclip item PHẢI bypass khi play; stop + play lại không được mất bypass.
- **FIX (app.jsx addNewTrack 21440):** `audioBypass: isSectionCtx ? true : false` — activeTab `session_` → track mới trong SECTION mặc định **BYPASS** (nút A xám, audio raw); MAIN giữ false (qua mastering).
- **Kết hợp vòng trước:** sub-node route dùng map THẬT (không override mastering-ON) → route đúng nút A; stop+play → node tái tạo → route đúng trạng thái.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061530), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: tạo track mới + drop audioclip → play → BYPASS ngay (A xám); stop + play lại → VẪN bypass. Track CŨ (tạo trước fix, audioBypass false) → bấm A 1 lần (xám) → từ đó bypass đúng.
### [2026-08-06 16:00] Task: SECTION-TAB bypass rules — nút A (audio) + nút ♪ (midi) áp đúng cho sub-node
- **Yêu cầu user:** nút A tắt → bypass Audioclip; A bật → không bypass; nút ♪ tắt → midi bypass; ♪ bật → midi không bypass.
- **FIX (app.jsx):**
(1) `getOrCreateSubTrackNode`: subBypass theo LOẠI item của sub-track — có CLIPS → `trackAudioBypassMap` (nút A); có MIDI → `trackMidiBypassMap` (nút ♪) — route tạo đúng trạng thái nút.
(2) `__setTrackBypass` nhánh 'midi': **scan sub-node** (`endsWith('_sub_'+trackId)`) → `setMasteringRoute` — nút ♪ trên section strip toggle route sub-node MIDI ngay.
- **Kết hợp:** sub-node route (routeGain→parent / dryGain→masterBus.dryInput) — bypass → dry (raw); không bypass → qua mastering.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061600), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: track audioclip — A tắt → bypass, A bật → qua mastering; track MIDI — ♪ tắt → bypass, ♪ bật → qua mastering. Stop+play lại → trạng thái giữ nguyên.
### [2026-08-06 16:30] Task: ReferenceError handleOpenProject — hàm chỉ tồn tại trong ProfileModal, App render reference sai scope
- **Báo cáo user:** bundle 061600 không mở/reload lại dự án — console: `Uncaught ReferenceError: handleOpenProject is not defined`.
- **Nguyên nhân:** `handleOpenProject` (5624) nằm BÊN TRONG component ProfileModal — nhưng OpenProjectModal (File → Open Project) nhận `onOpenCloud` closure ở **App render** (27727: `handleOpenProject(projId, projName)`) — reference ngoài scope → ReferenceError khi bấm Mở.
- **FIX (app.jsx):** thêm **App-level `handleOpenProject`** (trước render) — logic: getCloudProject → deserializeProjectFromSchema → setTracks/mastering/sessionTabs/subTabs + localStorage — giống ProfileModal. ProfileModal giữ bản riêng (tự scope).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061630), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → File → Open Project → bấm MỞ → dự án nạp + reload lại đúng.
### [2026-08-06 17:00] Task: Restore project stack overflow — guard cycle trong deserializeTracksList (SECTION_ITEM tự trỏ)
- **Báo cáo user:** reload page → `restoreLastSessionProject failed: RangeError: Maximum call stack size exceeded`.
- **Nguyên nhân:** `deserializeTracksList` (9023) → SECTION_ITEM (9059) → gọi lại `deserializeTracksList(secContainer.tracks,...)` — section item trỏ về section ĐANG deserialize (hoặc chu trình A→B→A) → recursion VÔ HẠN → stack overflow.
- **FIX (app.jsx deserializeTracksList):**
(1) **Visited-set**: truyền `visitedSections` xuống; SECTION_ITEM trỏ về section có trong set (cycle) → `tracks: null` (không đệ quy tiếp).
(2) **Depth guard**: `curDepth > 12 → return []` — chặn lồng sâu bất thường.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061700), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → reload page → dự án restore bình thường (section tự trỏ → deserialize an toàn, tracks null thay vì crash).
### [2026-08-06 17:30] Task: GỠ override mastering-ON — nút A/♪ quyết định bypass (rules SECTION-TAB)
- **Báo cáo user:** rules bypass vẫn chưa áp được — log: `[Bypass] node created track 1 initial audioBypass= true routeGain= 1 dryGain= 0` — bypass TRUE nhưng routeGain=1 (INVERTED).
- **Nguyên nhân:** `createMasteringRoute` dòng 142 `if (masteringChainOn()) bypass = false;` + `effAudioBypass/effMidiBypass` override mastering-ON (yêu cầu v03:00) → khi mastering ON, route LUÔN ép qua chain → nút A/♪ bị nuốt.
- **FIX (app.jsx):** GỠ override ở 3 chỗ: `createMasteringRoute` + `effMidiBypass` + `effAudioBypass`**NÚT A/♪ quyết định bypass, không phụ thuộc mastering ON** (rules hiện tại của user thắng yêu cầu cũ v03:00).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061730), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → SECTION-TAB: A tắt (xám) → routeGain=0/dryGain=1 (bypass) ngay lần play đầu; A bật → qua mastering. Log `[Bypass] node created ... audioBypass= true routeGain= 0 dryGain= 1` ✓.
### [2026-08-06 18:00] Task: Copy/Paste notes giữa các PIANO ROLL TAB (clipboard)
- **Yêu cầu user:** copy midi note từ PIANO ROLL TAB này sang tab khác.
- **FIX (app.jsx):**
(1) App: state `pianoRollClipboard` + prop `onCopyNotes`/`clipboardNotes` cho PianoRollTabEditor (deep-copy khi copy).
(2) Editor toolbar (hàng 2): nút **📋 Copy** (toàn bộ notes → clipboard + toast) + **📥 Paste** (notes clipboard → append cuối tab hiện tại, sinh id MỚI `note_cp_*`, `pushToUndo` → Ctrl+Z hoàn tác).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061800), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → tab A: Copy → mở tab B → Paste → notes xuất hiện; Ctrl+Z hoàn tác. Verify: 0 double-escape emoji.
### [2026-08-06 18:30] Task: NotSupportedError createBuffer 0 frames — guard Math.max(1) cho mọi createBuffer
- **Báo cáo user:** `Uncaught NotSupportedError: Failed to execute 'createBuffer' ... frames (0) less than or equal to minimum bound (0)`.
- **Nguyên nhân:** các op audio (sub-tab cut/copy/paste, normalize/fade/gain) tính `len`/`data.length` = 0 (vùng chọn rỗng hoặc buffer 0 frame) → `createBuffer(..., 0, ...)` → throw.
- **FIX (app.jsx — guard `Math.max(1, ...)`):** sub-tab Cut (15637/15644), Copy (15672), Paste (15727), clonedBuffer ×3 (15554/15577/15601). Chỗ 11802 đã guard sẵn.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061830), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → thao tác cut/copy audio vùng chọn rỗng → hết crash.
### [2026-08-06 18:45] Task: Section block MAIN không vẽ inner items preview (items chỉ thuộc SECTION-TAB)
- **Báo cáo user:** open project → các item của SECTION-TAB xuất hiện như clip/midi trên MAIN SESSION (ngoài block) — muốn sửa.
- **Chẩn đoán:** file .sfs SẠCH (main track chỉ SECTION_ITEM; section_store chứa nội dung ✓) — KHÔNG leak dữ liệu. Thủ phạm = **RENDER**: main timeline vẽ **preview inner items** bên trong section block (2492-2557 — sub-tracks + waveform + note bars) → trông như "items tải vào MAIN".
- **FIX (app.jsx render):** GỠ preview sub-tracks — section block trên MAIN chỉ hiện **tên + màu + bounds**. Dữ liệu `sec.tracks` VẪN giữ (play section item + SECTION-TAB không ảnh hưởng).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061845), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → open Section_test → MAIN chỉ thấy block section (tên + màu), không còn item bên trong; mở SECTION-TAB → nội dung đầy đủ; play section item → vẫn có âm.
### [2026-08-06 19:00] Task: Audioclip trong section mất sau save+open — loadAudioBuffersForTracks không nạp clip bên trong section
- **Báo cáo user:** project MAIN chỉ có 1 section item (trong section: 1 midi + 1 audioclip). Sau khi lưu (ghi đè) + mở lại → audioclip bị mất, chỉ còn khung "(audio chưa được tải)".
- **Nguyên nhân:** `loadAudioBuffersForTracks` (14841) chỉ nạp `t.buffer` + `t.clips` (cấp MAIN track) — **KHÔNG đệ quy vào section item's `tracks`** → audioclip bên trong section không bao giờ được fetch/decode → buffer null → "(audio chưa được tải)".
- **FIX (app.jsx):** thêm vòng lặp section — `t.sections[].tracks[].clips``tryLoad(serverFileId)` mỗi clip → buffer; trả về `sections: updatedSections` trong cả 2 nhánh return.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061900), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → save project → mở lại → audioclip trong section hiện đầy đủ (hết "(audio chưa được tải)"); play section → có âm cả midi + audio.
### [2026-08-06 19:30] Task: Lưu zoom vào project + cho xóa item lỗi/rỗng (khung viền)
- **Yêu cầu user:** (1) lưu zoom khi save + khôi phục khi mở; (2) cho phép xóa item lỗi (khung viền "audio chưa được tải"/item rỗng/dữ liệu lỗi).
- **FIX (app.jsx):**
(1) **Zoom**: App effect `window.__currentZoom = zoom` → serialize metadata thêm `zoom` → deserialize trả `zoom` → restore + open (2 chỗ) `setZoom(result.zoom)`.
(2) **Guard xóa track** (2 chỗ 17124/20131): item LỖI/rỗng không chặn — `hasClips = clips.some(c => c.buffer)` (clip không buffer = khung viền → không tính); `hasMidi = midiItems.some(m => m.notes?.length)`; `hasSections = sections.some(s => s.tracks?.some(st => st.clips?.some(c => c.buffer) || st.midiItems?.some(m => m.notes?.length)))` — track chỉ chứa item lỗi → xóa được.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061930), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → zoom vào 2x → save → mở lại → zoom giữ 2x. Xóa track chỉ chứa item "(audio chưa được tải)" → xóa được.
### [2026-08-06 19:45] Task: Khôi phục preview items trong section block (sau mở project)
- **Báo cáo user:** sau khi mở lại dự án, section item không vẽ lại các item chứa trong nó.
- **FIX (app.jsx render 2491):** khôi phục preview sub-tracks trong section block (waveform bars + midi note bars) — bị gỡ ở 18:45. Kết hợp fix 19:00 (loadAudioBuffersForTracks đệ quy nạp buffers section) → preview hiện đầy đủ sau open.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061945), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → open project → section block hiện waveform + note bars của items bên trong.
### [2026-08-06 20:00] Task: Audioclip chèn trong SECTION-TAB không lưu — race upload + track buffer-only bị lọc
- **Báo cáo user:** audioclip chèn trong SECTION-TAB không được lưu.
- **Nguyên nhân:** `handleSaveSectionTab` lọc `contentTracks = tracks.filter(tr => tr.clips?.length > 0 || ...)` — track vừa chèn audio (upload async chưa tạo clip — hoặc buffer-only) bị LỌC → audioclip không vào section item → save mất.
- **FIX (app.jsx handleSaveSectionTab):**
(1) Filter thêm `|| !!tr.buffer` — gồm track buffer-only.
(2) Buffer-only → tạo **default clip** (`default_<id>` — buffer, startTime, name) → serialize bắt được AUDIO_ITEM.
(3) Serialize đã fallback `c.serverFileId || t.serverFileId` (8959) ✓ — file id lấy từ track nếu clip chưa có.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062000), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: chèn audioclip → lưu section → save project → mở lại → audioclip còn đầy đủ (có âm).
### [2026-08-06 20:30] Task: Watchdog SECTION-TAB — chỉ rebuild khi NaN (hết playhead reset khi play)
- **Báo cáo user:** audioclip mất → di chuyển playhead đến vị trí đó nhấn play → playhead về đầu track + không play.
- **Nguyên nhân:** watchdog (18168) — section tab không phải piano roll → nhánh `pk < 0.001` — audioclip mất/rest tự nhiên → im lặng 750ms → recovery `stopAllPlayback` (playhead reset 0) + restart → "playhead về đầu + không play".
- **FIX (app.jsx):** thêm `isSectionTab` (`activeTab` bắt đầu `session_`) → nhánh `(isPianoRoll || isSectionTab ? nanOut : ...)` — section tab chỉ rebuild khi output NaN (chain chết), im lặng bình thường KHÔNG recovery → playhead giữ vị trí.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062030), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section tab: play tại vị trí audioclip đã mất → playhead giữ nguyên (không về đầu); play bình thường vẫn hoạt động.
### [2026-08-06 21:00] Task: Section item mở nhấp đôi không chứa audioclip — clone xóa clips
- **Báo cáo user:** mở lại dự án → nhấp đôi section item → chỉ là section item, không chứa các items như trước khi lưu.
- **Nguyên nhân:** `handleEditSectionInTab` (16515) clone `JSON.parse(JSON.stringify(section.tracks))` với **`clips: []`** (reset cố định) → audioclip bị xóa khỏi section editor; JSON clone cũng DROP AudioBuffer.
- **FIX (app.jsx):** clone giữ **clips + buffer THẬT** (`srcClips = (t.clips||[]).map(c => ({...c, buffer: c.buffer||null}))`) — JSON chỉ dùng cho field metadata; buffer gắn lại từ source live.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062100), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → open project → nhấp đôi section item → section editor đầy đủ clips (audioclip có waveform) + midi; play nghe được.
### [2026-08-06 21:30] Task: Reload mất audioclip section — clip thiếu serverFileId → upload buffer-only tại thời điểm lưu
- **Báo cáo user:** reload → mất audioclip đã lưu trong SECTION-TAB.
- **Chẩn đoán (DB):** save mới nhất — `AUDIO_ITEM: server_file_id: None | url: ''` — clip KHÔNG có file reference (upload sớm thất bại/race — uploadToServer trả null "client-side only") → reload → load fail → audioclip mất.
- **FIX (app.jsx):**
(1) Thêm `encodeWavBlob` (WAV encoder tối thiểu — PCM 16-bit).
(2) `handleSaveSectionTab` → async + `ensureClipsUploaded(contentTracks)` — clip chưa serverFileId + có buffer → encode WAV → uploadToServer → gán `c.serverFileId` TRƯỚC khi ghi vào section item → serialized AUDIO_ITEM có file_id → reload nạp được.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062130), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: chèn audioclip → Lưu section (chờ upload — console không có "upload failed") → save project → reload → audioclip còn + có âm.
### [2026-08-06 22:00] Task: Section editor (nhấp đôi) trống sau reload — session tab không nhận buffers (merge nested)
- **Báo cáo user:** section item MAIN vẽ đúng content sau reload, nhưng CHỈNH SỬA (nhấp đôi) → chỉ là block, không phải nội dung đã lưu.
- **Nguyên nhân:** loadAudioBuffersForTracks merge session tabs (14995): `updatedTracks.find(u => u.id === t.id)` — find ở TOP-LEVEL — track của session tab có id TRÙNG track bên TRONG section item (nested) → không tìm thấy → session tab giữ tracks deserialize (clips không buffer) → editor trống/không waveform.
- **FIX (app.jsx):** build `allLoadedTracks` PHẲNG (updatedTracks + section items' inner tracks) → merge session tabs tìm trong map phẳng → session tab nhận tracks có buffers.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062200), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → save + reload → nhấp đôi section item → section editor đầy đủ (audioclip waveform + midi); play nghe được.
### [2026-08-06 22:30] Task: Section item TỰ TRỎ (block lồng) + mất MIDI track — insert thiếu sectionId + fallback s.id
- **Báo cáo user:** SECTION-TAB hiện block lồng (section item) + MIDI item chỉ là hình + play không lên.
- **Chẩn đoán (DB):** section track 01 chứa `SECTION_ITEM: sec_1786020136359 → sec_1786020136359` (TỰ TRỎ chính section); track MIDI (play_VST 292 notes) biến mất khỏi section (console tracks=2).
- **Nguyên nhân:** `insertSectionAtPlayhead` (21822) tạo section item thiếu `sectionId` → serialize (9008) `referenced_section_id: s.sectionId || s.id` fallback → **chính id item = id section → tự trỏ** → deserialize block lồng + sectionStore ghi đè → mất track MIDI.
- **FIX (app.jsx):**
(1) `handleSaveSectionTab` — LỌC section item tự-trỏ khỏi contentTracks (`(s.sectionId||s.id) !== tab.sectionId`).
(2) Serialize (9008) — BỎ fallback `s.id` — chỉ `s.sectionId` (thiếu → block rỗng, không tự-trỏ).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062230), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → project CŨ (đã dính tự-trỏ): xóa block lồng trong section tab + thêm lại MIDI track → lưu → save → reload → section tab đầy đủ (audioclip + midi), play được.
### [2026-08-06 23:00] Task: DIAG — section tab vẫn hiện block sau reload (data sạch + bundle mới)
- **Trạng thái:** DB save sạch (section: 2 tracks midi+audio, không SECTION_ITEM); production đã deploy 2230 (referenced_section_id:s.section + allLoadedTracks ✓) — NHƯNG user vẫn thấy block trong SECTION-TAB + midi mất + play block không âm.
- **Hành động:** thêm diagnostic log sau restore — `[Restore] sessionTab <id> sectionId <sid> tracks N sections N midi N clips N` — để xác định block sinh từ đâu (restored sessionTabs có sections không; hay sinh lúc nhấp đôi).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062300), `wiki.md`. Rebuild precompiled.
- **Chờ user:** build + deploy 2300 → reload → dán log `[Restore] sessionTab` + log lúc nhấp đôi (có `[Play]` + `[SonicSF] noteon` không).
### [2026-08-06 23:30] Task: FIX ID COLLISION merge session tabs — block lồng + mất MIDI (bug từ fix 2200)
- **Báo cáo user:** mất midi item + vẫn hiển thị section block trong SECTION-TAB.
- **Nguyên nhân:** fix 2200 (merge session tabs) dùng `allLoadedTracks` (main tracks + section inner tracks) — `find(u => u.id === t.id)` lấy track ĐẦU TIÊN — **inner track id 1 trùng MAIN track id 1** → session tab Track 01 bị THAY bằng MAIN track 01 (chứa SECTION_ITEM + không midi) → **block lồng + mất MIDI + play block không âm**.
- **FIX (app.jsx):** merge session tabs CHỈ match với `sectionInnerTracks` (inner tracks của section items) — không match track main top-level.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062330), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → deploy → reload → nhấp đôi section item → SECTION-TAB đầy đủ (midi + audioclip đúng vị trí) + play cả 2.
### [2026-08-06 23:45] Task: Media Explorer (F6) — close panel phải tắt preview âm thanh
- **Báo cáo user:** đóng Media Explorer panel → preview âm thanh vẫn phát.
- **Nguyên nhân:** panel KHÔNG unmount khi đóng (ẩn bằng `display:none` 27465) — cleanup unmount không chạy → preview source + SonicSF notes tiếp tục.
- **FIX (app.jsx):** truyền `active={showMediaExplorer}` vào MediaExplorerPanel + effect `if (active === false) stopMediaPlayback()` — đóng F6 → dừng source + loop + SonicSF.stopAll.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062345 — sửa typo `</script></script>` kép), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → F6 → preview 1 file → F6 close → âm dừng ngay.
### [2026-08-07 00:00] Task: VU meter SECTION-TAB không animation khi play
- **Báo cáo user:** play ở MAIN → chuyển SECTION-TAB check item đang play → VU meter không animation realtime.
- **Nguyên nhân (2 lỗi):** (1) VU tick (24603) lookup `trackNodes[trackId]` — key THƯỜNG — node của section tab là SUB-NODE (key `<parent>_sub_<trackId>`) → không khớp → node undefined; (2) sub-node KHÔNG có `analyserNode` (chỉ `{gainNode, pannerNode, route}`) → VU không có gì để đọc.
- **FIX (app.jsx):** (1) VU tick — fallback scan `_sub_<trackId>` keys → vNode; đọc analyser từ `vNode.analyserNode`; (2) getOrCreateSubTrackNode — thêm `analyserNode` (gainNode → analyser → routeGain/dryGain) → VU đọc được tín hiệu section.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070000), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → play MAIN → chuyển SECTION-TAB → VU meters nhảy realtime theo track đang play.
### [2026-08-07 00:30] Task: MIDI section bị bypass mastering (route qua parent) + VU section không đúng nhịp
- **Báo cáo user:** play MAIN → nhấp đôi vào section item → (1) VU meter animation không đúng nhịp âm thanh; (2) âm thanh MIDI item bị bypass qua mastering chain.
- **Nguyên nhân:**
(1) **MIDI bypass**: sub-node routeGain nối vào PARENT NODE → qua route của parent track (parent `audio_bypass: true` → routeGain 0/dryGain 1) → MIDI/audio section bị kéo vào DRY bus dù sub-track ♪/A tắt → "bị bypass mastering".
(2) **VU không đúng nhịp**: scheduling MIDI của section item KHÔNG trigger `triggerMidiVuActivity` (main-track midi có ở 19066) → VU chỉ dựa analyser (trễ/smoothing) → không nhảy theo từng note.
- **FIX (app.jsx):**
(1) getOrCreateSubTrackNode — mastering path `routeGain.connect(window.masterBus.input)` TRỰC TIẾP (không qua parent — parent route có bypass riêng); fallback parentNode khi không có masterBus.
(2) Section midi scheduling (19182) — thêm `triggerMidiVuActivity(subTrack.id, note.velocity)` sau mỗi playNote.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070030), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → play MAIN → nhấp đôi section → MIDI qua mastering (nghe EQ/limiter tác dụng) + VU nhảy đúng theo note.
### [2026-08-07 01:00] Task: MIDI section tab bị bypass — createMasteringRoute dùng audioBypass cho MIDI-only track
- **Báo cáo user (kèm log):** nhấp đôi section → `setOutputDestination to: track node (sfEntry)` + noteon chạy nhưng MIDI bị bypass mastering.
- **Nguyên nhân:** SECTION-TAB play dùng PLAIN node (getOrCreateTrackNode) → `createMasteringRoute` đọc `trackAudioBypassMap`/`track.audioBypass` — section track default `audio_bypass: true` (round 48) → route dry → **MIDI kéo vào dry bus dù ♪ tắt**. (sfBypass/SF route đã đúng ♪ ở 1041/18667 — chỉ AUDIO route sai.)
- **FIX (app.jsx createMasteringRoute):** bypass theo LOẠI nội dung — track **MIDI-only** (midiItems, không clips/buffer) → dùng `trackMidiBypassMap`/`midiBypass` (♪); track có clips → `audioBypass` (A) như cũ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070100), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → nhấp đôi section → MIDI qua mastering (EQ/limiter nghe rõ); log `[Bypass]` routeGain=1/dryGain=0 cho midi track.
### [2026-08-07 01:30] Task: First play sau double-click bị bypass (node STALE) + VU track chứa section item
- **Báo cáo user:** (1) nhấp đôi section → play lần ĐẦU midi bị bypass; stop + play lại → đúng. (2) play section item trên track → VU track đó không animation.
- **Nguyên nhân:**
(1) **Node STALE**: section tab track id TRÙNG main track id — node tạo từ MAIN play (route theo data main — audioBypass=true → dry) được cache lại → first play trong section tab dùng node cũ → bypass; stop (wipe nodes) → play lại → node mới đúng.
(2) **VU track main**: âm section đi thẳng masterBus.input (fix 00:30) → KHÔNG qua node chính → analyser node chính không có tín hiệu → VU đứng yên.
- **FIX (app.jsx):**
(1) Helper `routeBypassFor(track)` (bypass theo content-type — MIDI-only → ♪, có clips → A) — áp: createMasteringRoute (đã có), startTrackPlayback re-apply route mỗi lần schedule (hết stale), mastering-settings effect (14906 thay effAudioBypass).
(2) VU tick — scan sub-nodes (`k.indexOf(trackId + '_sub_') === 0`) khi node chính không có tín hiệu → peak sub-node analyser → VU track main animation theo section.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070130), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → nhấp đôi section → play lần ĐẦU → midi qua mastering ngay; play section trên MAIN → VU track đó nhảy.
### [2026-08-07 02:00] Task: VU MAIN không nhảy cho midi section item — triggerMidiVuActivity thiếu parent
- **Báo cáo user:** (1) first play sau double-click midi vẫn bị bypass (lặp — cần xác nhận bundle 0130); (2) quay về MAIN — section item play midi notes → VU meter không animation.
- **Nguyên nhân (2):** midiVuActivity keyed theo `subTrack.id` (section) — VU MAIN đọc `midiVuActivityRef[mainTrackId]` → miss → VU main đứng yên.
- **FIX (app.jsx 19212):** section midi scheduling trigger CẢ 2 — `triggerMidiVuActivity(subTrack.id, ...)` + `triggerMidiVuActivity(track.id, ...)` (parent/main track) → VU main nhảy theo section item đang play.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070200), `wiki.md`. Rebuild precompiled.
- **Chờ xác nhận bug (1):** user build + test 0130/0200 — nếu first play VẪN bypass → dán console: `[Bypass] node created` (routeGain/dryGain) + `setOutputDestination` lúc nhấp đôi + play lần đầu.
### [2026-08-07 02:30] Task: Re-apply route sub-node khi section item play (main) — hết stale bypass
- **Yêu cầu user:** (1) BẮT BUỘC VU meter hiển thị trên track đang play items (MAIN + SECTION-TAB); (2) chuyển tab MAIN → SECTION-TAB midi item đang play KHÔNG được bypass.
- **FIX (app.jsx 19132):** section item play (main) — sau getOrCreateSubTrackNode — re-apply route sub-node theo `routeBypassFor(subTrack)` (MIDI-only → ♪, clips → A) mỗi lần schedule → hết stale route (tương tự re-apply plain node ở 0130).
- **VU stack hiện tại (các fix trước):** sub-node có analyserNode (00:00) → VU section tab đọc sub-node (00:00); trigger midi VU cho section (00:30); VU main scan sub-nodes khi node chính câm (01:30); trigger parent track cho VU main (02:00). **BẮT BUỘC hiển thị = đã phủ: main track (subPeak + midi activity) + section tab (vNode sub-node + midi activity).**
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070230), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → (1) play section ở MAIN + trong SECTION-TAB → VU nhảy đúng track; (2) MAIN → double-click SECTION-TAB → play lần đầu → MIDI qua mastering (không bypass).
### [2026-08-07 03:30] Task: MIDI section bị bypass khi vào SECTION-TAB — masteringBypass fallback + clone kế thừa flag
- **Báo cáo user:** đang play MAIN SESSION → double-click section item vào SECTION-TAB → âm MIDI item bị bypass (không qua mastering chain).
- **Nguyên nhân (data thật — project "Section test" trong DB):** sub-track section (id TRÙNG main track id 1, 2) lưu `mastering_bypass=True` + `audio_bypass=True` (kế thừa từ track gốc). Mọi hàm quyết định bypass (`effMidiBypass`/`effAudioBypass`/`createMasteringRoute`/`routeBypassFor`/`getOrCreateSubTrackNode`) fallback `?? track.masteringBypass` → khi map chưa sync/thiếu entry (timing khi double-click + play lần đầu) → track MIDI-only bị tính bypass=TRUE → SF/sub-node route vào DRY bus (bypass mastering) dù ♪ tắt. Ngoài ra `handleEditSectionInTab` clone section tracks GIỮ NGUYÊN audioBypass/masteringBypass của track gốc → sync effect (14757) ghi map audio sai cho track section.
- **FIX (app.jsx):**
(1) BỎ fallback `?? masteringBypass` khỏi mọi hàm quyết định route (effMidiBypass/effAudioBypass/createMasteringRoute/routeBypassFor/getOrCreateSubTrackNode) — nút A/♪ + `audioBypass`/`midiBypass` là nguồn duy nhất; `masteringBypass` không còn quyết định route (field legacy kế thừa sai).
(2) `handleEditSectionInTab` (16595) — clone section tracks ép reset bypass theo context: `masteringBypass:false, audioBypass:true (section raw default), midiBypass:false` — hết kế thừa flag track gốc.
(3) Sync effect (14757): `trackMasteringBypassMap[t.id] = !!t.masteringBypass` (trước gán nhầm `!!t.audioBypass`).
(4) `updateSfRouting` (18813): `effMidiBypass(t.id)``effMidiBypass(t)` — truyền string → map lookup sai → sfRouteGain bị ép 1 (♪ vô hiệu qua đường này).
(5) Log chẩn đoán: `[Bypass] sfEntry created track <id> midiBypass=...` (lúc tạo node) + `[Bypass] sf route sync track <id> ...` (updateSfRouting ép route) — test còn lỗi thì dán log.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070330), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → play MAIN → double-click section vào SECTION-TAB → MIDI qua mastering (nghe EQ/limiter); play trong SECTION-TAB → MIDI vẫn qua mastering (♪ tắt); bật ♪ → bypass đúng ý. Nếu còn bypass → dán console `[Bypass]` logs.
### [2026-08-07 03:35] Task: MIDI section vẫn bypass sau 0330 — thêm log chẩn đoán [SFRoute]/[Bypass] reapply
- **Báo cáo user (log 0330):** play MAIN → double-click section → `[SonicSF] setOutputDestination to: track node (sfEntry)` + noteons chạy → bấm Play tab (isPlaying=true) → stopAllPlayback → SF về masterBus.input. Log THIẾU các dòng `[Bypass]`/`[Play] offset` của bản 0330 → chưa xác định được route thực tế (sfRouteGain/routeGain giá trị bao nhiêu lúc nghe bypass).
- **Log mới thêm (app.jsx):**
(1) `[SFRoute] activeTab=... midiAudible=...` — updateSfRouting: track nào audible + đích tính.
(2) `[SFRoute] dest=sfEntry node <id> sfRouteGain=... sfDryGain=...` — đích SF + giá trị route hiện tại TRƯỚC khi ép.
(3) `[SFRoute] dest=gainNode node <id> (no sfEntry) route.routeGain=... dryGain=...` — đích fallback gainNode.
(4) `[Bypass] reapply track <id> contentType=midi/audio bypass=... → routeGain=.../dryGain=...` — startTrackPlayback re-apply route.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070335), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → double-click section vào SECTION-TAB → nghe bypass → **dán TOÀN BỘ console (không filter)** — cần các dòng `[SFRoute]`, `[Bypass]`, `[Play] offset`, `setOutputDestination` để chốt đường bypass chính xác.
### [2026-08-07 03:40] Task: MIDI section bypass — ID TRÙNG main track → node/map collision (CHỐT NGUYÊN NHÂN)
- **Log 0335 (đã chốt):** play MAIN → `[SFRoute] activeTab=main midiAudible=` (rỗng → SF về masterBus.input — MIDI qua mastering ✓). Double-click → `[SFRoute] activeTab=session_... midiAudible=1(midi1)``[SFRoute] dest=gainNode node 1 (no sfEntry) — route.routeGain=0 route.dryGain=1` → SF route vào NODE '1' → **route dry → MIDI section BỊ BYPASS**. (Lưu ý: log soundfontPlayer "track node (sfEntry)" là log SAI — luôn in "sfEntry" cho mọi node khác null; đích thực là gainNode.)
- **Nguyên nhân gốc:** section clone giữ id sub-track TRÙNG main track id (data: main 1/2 = sub 1/2). (1) `updateSfRouting` lookup node theo id → trúng node MAIN (route theo ♪ MAIN = bật = dry) → SF (MIDI section) kéo vào dry dù ♪ section tắt. (2) Bypass map keyed theo id bị GHI ĐÈ CHÉO (clone audioBypass=true ghi đè map main; ♪ main ảnh hưởng route section).
- **FIX (app.jsx):**
(1) MỌI hàm quyết định route (`effMidiBypass`/`effAudioBypass`/`createMasteringRoute`/`routeBypassFor`/sub-node): **ƯU TIÊN field track object (`track.midiBypass`/`track.audioBypass`)** — object phân biệt đúng context; map chỉ fallback (realtime nút bấm, track không field).
(2) `updateSfRouting`: activeTab là `session_*` → lookup node ƯU TIÊN SUB-NODE (key `<parent>_sub_<id>` — route theo ♪ section + thẳng masterBus.input); không có sub-node → null (SF về masterBus.input — qua mastering, KHÔNG bypass). KHÔNG bao giờ route vào node MAIN khi ở section tab.
(3) Sync effect (14754): đảo thứ tự — session/clone TRƯỚC, MAIN SAU (main thắng khi id trùng — map fallback đúng context; clone dùng field).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070340), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → double-click section vào SECTION-TAB → MIDI qua mastering (nghe EQ/limiter); log kỳ vọng: `[SFRoute] dest=gainNode node 1 ...` → KHÔNG còn (sub-node hoặc masterBus.input); `route.routeGain=1`. Play section tab → MIDI qua mastering (♪ tắt); bật ♪ section → bypass đúng ý.
### [2026-08-07 03:45] Task: MIDI bypass khi mở MIDI item SECTION-TAB → PIANO ROLL TAB
- **Báo cáo user:** sau fix 0340 (section tab OK) — mở MIDI item từ SECTION-TAB vào PIANO ROLL TAB → MIDI bị bypass.
- **Nguyên nhân:** cùng cơ chế collision id — PIANO ROLL của track section có `parent_tab_id = session_*`, `trackId` = id sub-track TRÙNG main track id. `updateSfRouting` nhánh PIANO_ROLL lookup `activeTrackNodesRef.current[trackId]` → trúng NODE MAIN (route theo ♪ main — dry) → SF (notes piano roll) kéo vào dry dù ♪ section tắt. Xảy ra ngay khi ĐỔI TAB (effect 14814 → updateSfRouting) — không cần play.
- **FIX (app.jsx updateSfRouting nhánh PIANO_ROLL):** khi `_activeSub.parent_tab_id` startsWith `session_` → lookup node ƯU TIÊN SUB-NODE (key `endsWith('_sub_' + trackId)` — route theo ♪ section + thẳng masterBus.input); không có sub-node → null (SF về masterBus.input — qua mastering, KHÔNG bypass). Không bao giờ route vào node MAIN cho piano roll của section.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070345), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → double-click section (SECTION-TAB) → double-click MIDI item → PIANO ROLL tab → MIDI qua mastering (nghe EQ/limiter); log `[SFRoute]` không còn dest node MAIN (route 0/1) cho piano roll section.
### [2026-08-07 03:50] Task: Playhead vẫn di chuyển trong PIANO ROLL TAB khi mở MIDI item từ SECTION-TAB
- **Báo cáo user:** mở MIDI item từ SECTION-TAB (đang play) → PIANO ROLL TAB → playhead vẫn tiếp tục di chuyển — cần dừng.
- **Phân tích:** piano roll view vẽ playhead theo `st.currentTime` (chỉ cập nhật khi tab play) — khi mở tab (chưa play) code cũ `return` ở nhánh sub-tab KHÔNG setCurrentTime → playhead đứng yên — NHƯNG return đó GIẾT CHẾT rAF loop: main/section play chạy ngầm (handleEditMidiInTab 16427 không dừng âm) → watchdog master-silence chết, âm main không dừng đúng projectEnd, và khi quay lại tab playhead NHẢY VỌT theo `elapsed` thật (trông như vẫn di chuyển/sai lệch).
- **FIX (app.jsx updatePlayhead nhánh sub-tab):** tách 2 trường hợp — (1) sub-tab KHÔNG play: KHÔNG setCurrentTime (playhead đứng yên) NHƯNG nếu `isPlaying` (main/session chạy ngầm) → vẫn check projectEnd (stop đúng lúc) + TIẾP TỤC requestAnimationFrame (loop sống, watchdog sống, quay lại tab không nhảy); (2) sub-tab play: giữ nguyên hành vi cũ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070350), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → double-click section (SECTION-TAB) → double-click MIDI item → PIANO ROLL TAB → playhead KHÔNG di chuyển (đứng yên); âm main tiếp tục + dừng đúng cuối bài; quay lại SECTION-TAB → playhead đúng vị trí âm (không nhảy vọt). Nếu playhead vẫn chạy khi CHƯA bấm play → xác nhận lại: có bấm Space/Play trong piano roll không (bấm play → playhead chạy là đúng hành vi).
### [2026-08-07 03:55] Task: FIX hồi quy 0350 — CÂM âm khi mở PIANO ROLL từ SECTION-TAB (projectEnd=1s ở sub-tab)
- **Báo cáo user (log 0350):** mở MIDI item từ SECTION-TAB → PIANO ROLL → `[SFRoute] activeTab=midi_... midiAudible=1(midi1)``setOutputDestination to: masterBus.input` → KHÔNG noteon tiếp theo → CÂM (bản 0345 play tiếp).
- **Nguyên nhân (hồi quy fix 0350):** nhánh updatePlayhead sub-tab không-play check `updatedTime >= projectEndRef.current` — nhưng `projectEnd` tính theo activeTab: piano roll tab (sub-tab) KHÔNG main/không session → `tab=undefined → max=0`**projectEnd = 1s** → updatedTime (elapsed THẬT từ lúc play main — đã >1s) >= 1 → `stopAllPlayback()` NGAY → panic notes + xóa nodes (activeTrackNodesRef={}) → SF về masterBus.input + KHÔNG noteon = CÂM.
- **FIX (app.jsx):** bỏ check projectEnd khỏi nhánh sub-tab không-play — chỉ `requestAnimationFrame` tiếp tục (loop sống cho playhead không nhảy khi quay lại). Âm main đã schedule hết từ startTrackPlayback — tự hết đúng lúc; quay lại MAIN → nhánh main tự stop tại projectEnd THẬT.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070355), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → double-click section (SECTION-TAB) → double-click MIDI item → PIANO ROLL TAB → âm section TIẾP TỤC play (không câm) + playhead piano roll ĐỨNG YÊN; quay lại SECTION-TAB → playhead đúng vị trí âm.
### [2026-08-07 04:00] Task: Quay lại MAIN/SECTION sau khi Space trong PIANO ROLL → CÂM — resume main play
- **Báo cáo user (log 0355):** mở MIDI item từ SECTION-TAB → PIANO ROLL (playhead đứng yên ✓ — fix 0350/0355); bấm Space → playhead piano roll chạy; **quay lại MAIN/SECTION → CÂM** (log: `[SFRoute] activeTab=main midiAudible=` + `setOutputDestination to: masterBus.input` — SF về masterBus nhưng KHÔNG noteon — main play đã chết).
- **Nguyên nhân:** Space trong PIANO ROLL → `handlePlayPause` nhánh sub-tab → `stopAllPlayback()` DỪNG main/section play (xóa nodes + panic) → chơi piano roll. Quay lại MAIN/SECTION → không còn gì play → câm.
- **FIX (app.jsx):** resume main/section play khi rời piano roll tab đã play:
(1) `mainResumeRef` mới — lưu `{offset: startOffsetTimeRef.current, audioTime: ctx.currentTime}` lúc MỞ piano roll tab khi main đang play (handleEditMidiInTab — nhánh isPlaying=true).
(2) Effect đổi tab (14814): rời PIANO ROLL tab đã play về MAIN hoặc `session_*` + có `mainResumeRef``stopAllPlayback()` + `startTrackPlayback(offset + elapsed thật)` + `setIsPlaying(true)` — âm main/section tiếp tục từ ĐÚNG vị trí (không chơi lại từ đầu, không câm). Các trường hợp khác: clear `mainResumeRef` (không resume trùng).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070400), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → double-click section (SECTION-TAB) → double-click MIDI item → PIANO ROLL (playhead đứng yên) → Space (playhead piano roll chạy) → quay lại MAIN/SECTION → ÂM TIẾP TỤC từ đúng vị trí (log `[Resume] main play từ X.XX s`); KHÔNG bấm Space → quay lại → âm vẫn tiếp tục (không resume trùng).
### [2026-08-07 04:05] Task: FIX 0400 — resume không chạy (mainResumeRef bị clear nhầm bởi chính effect đổi tab)
- **Báo cáo user (log 0400):** piano roll play (Space) → `dest=sfEntry node 1 ... MASTERING` + noteons ✓ → chuyển về SECTION-TAB → `setOutputDestination to: masterBus.input` (node null) → KHÔNG có log `[Resume]` → CÂM.
- **Nguyên nhân:** handleEditMidiInTab set `mainResumeRef` (mở tab lúc main play) → setActiveTab → effect đổi tab (14814) chạy với `prevTab='session_...'``prevSub = subTabsRef.find('session_...')` = **undefined** → rơi vào nhánh `else` cũ → **`mainResumeRef.current = null` — XÓA SẠCH resume point NGAY khi VÀO piano roll** → quay lại section: `_resume=null` → không resume → câm.
- **FIX (app.jsx effect 14814):** nhánh clear đổi thành `else if (prevSub && prevSub.type === 'PIANO_ROLL')` — chỉ clear khi THỰC SỰ rời khỏi piano roll tab (không play — main ngầm tiếp tục tự nhiên). prevTab = main/session (đang VÀO piano roll) → KHÔNG clear — resume point giữ nguyên.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070405), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → double-click section → double-click MIDI item → PIANO ROLL (playhead đứng yên) → Space (piano roll play) → quay lại SECTION-TAB → **log `[Resume] main play từ X.XX s` + ÂM TIẾP TỤC từ đúng vị trí** (SF route: `dest=sfEntry node 1 ... MASTERING`); KHÔNG Space → quay lại → âm ngầm tiếp tục (không resume trùng).
### [2026-08-07 04:10] Task: Playhead PIANO ROLL không di chuyển khi đang play (di chuyển tab qua lại)
- **Báo cáo user (log 0405):** piano roll play hoạt động (schedule 248 notes + noteons + `dest=sfEntry node 1 ... MASTERING` ✓) — nhưng sau khi di chuyển tab qua lại → ở PIANO ROLL TAB playhead không di chuyển khi đang play.
- **Phân tích:** khi rời piano roll tab → effect đổi tab stop sub-tab play (isPlaying:false) + resume main/section (âm chạy). Quay LẠI piano roll tab → tab KHÔNG play → nhánh updatePlayhead sub-tab không-play KHÔNG set st.currentTime → playhead piano roll ĐỨNG YÊN — dù âm MAIN/SECTION (resume) đang phát → user thấy "đang play mà playhead không chạy".
- **FIX (app.jsx updatePlayhead nhánh sub-tab không-play):** nếu `isPlaying` (main/section đang play — refs startOffsetTime/startAudioTime là của main) → setSubTabs `currentTime = startOffsetTimeRef + (ctx.currentTime - startAudioTimeRef)` — playhead piano roll ĐỒNG BỘ theo vị trí âm thật (chạy theo main play). Không có play nào → đứng yên (chế độ edit). Loop luôn tiếp tục rAF.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070410), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → di chuyển tab qua lại (SECTION-TAB ↔ PIANO ROLL) → ở PIANO ROLL TAB playhead chạy theo âm đang phát (đồng bộ vị trí thật); không play gì → playhead đứng yên; bấm Space trong piano roll → playhead chạy theo tab.
### [2026-08-07 04:15] Task: PIANO ROLL — Space không capture play/stop + chuyển tab về MAIN/SECTION bị CÂM
- **Báo cáo user (log 0410):** (1) trong PIANO ROLL TAB bấm Space không play/stop; (2) piano roll play → nhấn play/stop transport → chuyển sang MAIN/SECTION-TAB → CÂM (log: chuyển session → `setOutputDestination to: masterBus.input` + hết noteon — vì effect đổi tab stopAllPlayback dừng piano roll + không có gì resume).
- **Nguyên nhân:**
(1) Keydown handler (15938) bypass MỌI hotkey khi focus INPUT/TEXTAREA/contentEditable — piano roll tab có input focus (snap/rename...) → Space chết.
(2) Effect đổi tab (14814) — rời sub-tab đang play → `stopAllPlayback()` (dừng piano roll — luật cũ "rời sub-tab play → stop") → về MAIN/SECTION không còn âm → CÂM.
- **FIX (app.jsx):**
(1) Keydown handler: activeTab là PIANO ROLL (`midi_*`) → Space LUÔN play/stop (kể cả focus input); phím khác vẫn bypass.
(2) Effect đổi tab: rời PIANO ROLL tab đang play → KHÔNG stopAllPlayback (SF notes đã schedule — tiếp tục kêu tự nhiên — không câm); audio sub-tab (buffer source riêng) giữ stop như cũ. Resume block (mainResumeRef) giữ nguyên — tự stop + resume main nếu có.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070415), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → PIANO ROLL TAB → Space play/stop hoạt động (kể cả khi input focus); piano roll play → chuyển sang MAIN/SECTION-TAB → âm piano roll TIẾP TỤC (hết notes tự nhiên — không câm); quay lại tab → playhead đồng bộ.
### [2026-08-07 04:20] Task: Playhead NGỪNG khi chuyển tab PIANO ROLL (play thuần) → MAIN/SECTION — không đồng bộ âm
- **Báo cáo user:** preview piano roll (play thuần — main chưa play) → chuyển tab sang MAIN/SECTION-TAB → playhead ngừng di chuyển (dù âm piano roll còn kêu — fix 0415 không stop âm).
- **Nguyên nhân:** rời piano roll play → setSubTabs isPlaying:false + KHÔNG stop âm (0415) — nhưng `isPlaying` (main) vẫn false → updatePlayhead nhánh MAIN `if (!isPlaying) return` → playhead đứng yên — KHÔNG đồng bộ với âm piano roll đang phát.
- **FIX (app.jsx effect đổi tab 14814):** rời PIANO ROLL play → về MAIN/`session_*` + mainResumeRef null (play thuần) → `setIsPlaying(true)` — nhánh main updatePlayhead chạy → playhead main/section di chuyển theo elapsed (refs đang là của piano roll play — đồng bộ với âm). Hết bài → nhánh main tự stop. Resume block (mainResumeRef) giữ nguyên ưu tiên trước.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070420), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → PIANO ROLL TAB → Space play (main chưa play) → chuyển MAIN/SECTION-TAB → playhead TIẾP TỤC di chuyển (log `[Playhead] tiếp tục theo piano roll`), âm piano roll vẫn kêu hết notes; quay lại piano roll → playhead đồng bộ.
### [2026-08-07 04:25] Task: Space không play/stop sau khi chuyển tab — focus tab header button nuốt space
- **Báo cáo user:** chuyển tab → focus tự động nằm trên tên tab (tab header button) → nhấn Space thường không có tác dụng — phải click vào tab active thì Space mới capture được.
- **Nguyên nhân:** tab header là `<button>` — sau khi chuyển tab (click header / mở tab bằng double-click item), focus giữ trên button tab header (có thể là header của tab KHÁC — chưa active). Space: keydown → handler play/stop ✓ — NHƯNG browser fire CLICK TỔNG HỢP ở keyup space trên button → click tab header → CHUYỂN TAB — nhìn như Space không play/stop (vừa play vừa đổi tab).
- **FIX (app.jsx keydown handler — block Space):** trước khi play/stop → `document.activeElement` nếu là `BUTTON``blur()` — keyup sau đó target body → KHÔNG click tab header → Space play/stop ổn định ở mọi tab.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070425), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → chuyển tab bằng click header (hoặc mở piano roll bằng double-click) → KHÔNG click thêm → nhấn Space → play/stop đúng tab hiện tại (không chuyển tab, không mất tác dụng).
### [2026-08-07 04:30] Task: PIANO ROLL — Space 2 lần mới stop khi main/section đang play ngầm
- **Báo cáo user:** MAIN/SECTION đang play (MIDI kêu) → chuyển sang PIANO ROLL TAB → Space 1 lần KHÔNG stop (vẫn nghe âm) — Space lần 2 mới stop — "không phát hiện MIDI đang play".
- **Nguyên nhân:** handlePlayPause nhánh sub-tab: `st.isPlaying` (piano roll tab) = false (chưa play tab) → rơi vào else → **stopAllPlayback (dừng main) + PLAY PIANO ROLL ngay** → âm vẫn còn (piano roll) → user tưởng "không stop". Space lần 2: st.isPlaying=true → stop ✓.
- **FIX (app.jsx handlePlayPause nhánh sub-tab):** đầu nhánh — nếu `isPlaying` (main/section đang play ngầm) → `stopAllPlayback()` + setSubTabs isPlaying:false (mọi tab) + `setIsPlaying(false)` → return — Space = STOP trước, không play sub-tab. Space lần sau (đã stop) → play sub-tab bình thường.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070430), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → mở PIANO ROLL tab (main play ngầm) → Space 1 lần → DỪNG hẳn (không còn âm); Space lần nữa → play piano roll (toggle bình thường).
### [2026-08-07 04:35] Task: ĐỒNG BỘ playhead 2 chiều SECTION-TAB ↔ PIANO ROLL (vị trí item)
- **Yêu cầu user:** (1) play MIDI item trên SECTION-TAB ở vị trí X → sang PIANO ROLL → play tiếp tại vị trí tương ứng (X - item.startTime); (2) play PIANO ROLL tại Y → quay lại SECTION-TAB → play tại item.startTime + Y; (3) SECTION play vượt ngoài duration item → PIANO ROLL bắt đầu từ đầu (0).
- **FIX (app.jsx):**
(1) Helper `getPianoRollItemInfo(st)` — tìm item (trackId + target_id) trong context đúng (parent session tab → sessionTabsRef tracks; main → activeTracksRef) → `{startTime, duration}`. Dùng REFS — an toàn closure cũ (rAF loop).
(2) updatePlayhead nhánh sub-tab không-play: `st.currentTime = playPos - item.startTime` (clamp: < startTime hoặc ≥ startTime+duration → 0 — vượt duration → đầu).
(3) Effect đổi tab — rời PIANO ROLL play về MAIN/SECTION: resumeAt = `item.startTime + st.currentTime` (ưu tiên); về MAIN mà item thuộc section → cộng thêm secStart (vị trí section trên main); fallback mainResumeRef; fallback cuối setIsPlaying(true) (playhead chạy theo âm còn kêu).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070435), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → SECTION-TAB play (playhead trong item) → sang PIANO ROLL → playhead đúng vị trí trong item; play piano roll → về SECTION-TAB → play đúng item.startTime + vị trí piano roll (log `[Resume] play tại X.XX s (item.startTime=... + st.currentTime=...)`); SECTION play vượt quá item → piano roll về 0.
### [2026-08-07 04:40] Task: Xác nhận + log chẩn đoán — Đồng bộ 2 chiều MAIN SESSION ↔ PIANO ROLL
- **Yêu cầu user:** đồng bộ 2 chiều MAIN SESSION ↔ PIANO ROLL (tương tự SECTION-TAB ↔ PIANO ROLL 0435): main play ở vị trí X → mở piano roll → playhead trong item đúng (X item.startTime); piano roll play tại Y → về MAIN → play tại item.startTime + Y; main play vượt duration item → piano roll về 0.
- **Trạng thái:** fix 0435 ĐÃ generic — `getPianoRollItemInfo` (parent_tab_id null → `activeTracksRef` = MAIN tracks — item.startTime main global) + 2 nhánh (vào tab: local = ph item.startTime; rời tab: resumeAt = item.startTime + st.currentTime) tự áp dụng cho MAIN. Không cần sửa logic — thêm LOG chẩn đoán để xác nhận.
- **FIX (app.jsx):** nhánh updatePlayhead sub-tab không-play — log `[Sync] PR <id> ph=<vị trí play> item.start=<...> dur=<...> → local=<...>` (rate-limited ~2 lần/giây) — xác nhận giá trị đồng bộ realtime.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070440), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN (playhead trong MIDI item) → mở piano roll → log `[Sync]` cho thấy local tăng theo (ph item.start); play piano roll → về MAIN → log `[Resume] play tại item.startTime + st.currentTime`; main play vượt item → local = 0. Nếu giá trị sai → dán log.
### [2026-08-07 04:45] Task: MAIN/SECTION hết midi note → PIANO ROLL playhead chưa về 0
- **Báo cáo user:** main/section play hết (không còn midi note) → trong PIANO ROLL TAB playhead vẫn giữ vị trí cũ — chưa về 0.
- **Nguyên nhân:** khi play DỪNG (isPlaying → false — hết project/stop) → nhánh updatePlayhead sub-tab không-play `if (isPlaying)` FALSE → KHÔNG setSubTabs → playhead piano roll giữ local cuối (giữa item) — không về 0.
- **FIX (app.jsx):** effect `[isPlaying]` mới — detect transition playing→dừng (prevIsPlayingRef): nếu đang ở PIANO ROLL tab (`midi_*`) + tab KHÔNG play → `setSubTabs currentTime = 0` — playhead về đầu. Tab đang play (piano roll play riêng) → giữ nguyên (không đụng).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070445), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → mở PIANO ROLL (playhead chạy trong item) → đợi play hết (projectEnd) → playhead piano roll TỰ VỀ 0; bấm stop (space) → cũng về 0; piano roll đang play riêng → không bị về 0.
### [2026-08-07 04:50] Task: (1) PIANO ROLL zoom không persist phiên (2) Mở project lưu → timeline zoom out thu nhỏ góc
- **Báo cáo user:** (1) PIANO ROLL TAB không giữ kích thước zoom ở phiên làm việc trước; (2) MAIN SESSION — mở dự án đã lưu → timeline bị zoom out thu nhỏ ở góc — zoom in mới về kích thước đã lưu.
- **Nguyên nhân:**
(1) `rollZoom` (PianoRollTabEditor 6926) là local state khởi tạo 60 — mỗi lần mở tab → reset — không persist.
(2) Serialize project (9267) lưu `window.__currentZoom || 1.0` — nhưng `__currentZoom` do MEDIA EXPLORER set (11583) — không phải zoom timeline (App 13910) → project LUÔN lưu 1.0 → load `setZoom(1.0)` → timeline thu nhỏ góc trái.
- **FIX (app.jsx):**
(1) `rollZoom` khởi tạo từ `localStorage['sf_pr_zoom']` (clamp 15-250) + effect lưu khi đổi.
(2) App zoom effect set `window.__sfTimelineZoom = zoom`; serialize dùng `__sfTimelineZoom || __currentZoom || 1.0`; load (15211 + 24987): `result.zoom > 1 && setZoom` — bỏ qua 1.0 (data cũ lỗi) → giữ zoom hiện tại/localStorage.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070450), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** (1) PIANO ROLL zoom in/out → đóng tab → mở lại (hoặc reload) → zoom giữ nguyên; (2) LƯU project (zoom hiện tại) → mở lại → zoom ĐÚNG kích thước đã lưu (không thu nhỏ); project CŨ (lưu 1.0) → mở → giữ zoom hiện tại (không bị thu nhỏ).
### [2026-08-07 04:55] Task: Zoom reset khi chuyển tab (trong phiên) — thêm log chẩn đoán
- **Báo cáo user:** trong phiên làm việc, chuyển tab → kích thước zoom không lưu lại — bị reset về mặc định.
- **Trạng thái:** zoom MAIN timeline là App state global (không reset khi chuyển tab); piano roll đã persist localStorage (0450). Nghi vấn: effect minZoom (18009) ép `setZoom(minZoom)` khi vào tab có maxDuration nhỏ (section ngắn) — làm mất zoom đã set. Chưa tìm thấy nơi reset về mặc định cố định.
- **FIX (app.jsx):** thêm log chẩn đoán — (1) `[Zoom] đổi tab <prev> → <new> | zoom=<...> | minZoom=<...>` (effect activeTab); (2) `[Zoom] ép lên minZoom=... (zoom=...) tab=... maxDuration=...` (effect minZoom — chỉ khi ép).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070455), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → zoom MAIN (hoặc piano roll) → chuyển tab qua lại → dán log `[Zoom]` — xác định tab nào + giá trị zoom/minZoom khi reset.
### [2026-08-07 05:00] Task: CHỐT — Zoom bị ép khi chuyển tab (log [Zoom] xác nhận)
- **Log user (0455):** vào PIANO ROLL tab → `[Zoom] đổi tab ... → midi_... | zoom=32.78 | minZoom=37.54` + `[Zoom] ép lên minZoom=37.54 (zoom=32.78) tab=midi_... maxDuration=33.88` → về main/section zoom GIỮ 37.54 (không khôi phục 32.78/24.63) = "zoom reset khi chuyển tab".
- **Nguyên nhân:** `maxDuration` tính theo activeTab — PIANO ROLL tab (sub-tab) KHÔNG main/không session → `tab=undefined → max=0` → max = 12 bars buffer (33.88s) → `minZoom = viewportWidth/maxDuration` LỚN (37.54) → effect minZoom (18009) ép `setZoom(minZoom)` → zoom bị đẩy lên → quay lại main không khôi phục (state giữ giá trị bị ép).
- **FIX (app.jsx effect minZoom):** chỉ ép minZoom khi activeTab là `main` hoặc `session_*` — sub-tab (PIANO ROLL/audio) KHÔNG ép (timeline không hiển thị — ép vô nghĩa + hại). Zoom giữ nguyên khi vào/ra sub-tab.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070500), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → zoom MAIN (vd 24.6) → vào SECTION-TAB → vào PIANO ROLL → KHÔNG còn log `[Zoom] ép lên minZoom` → quay lại MAIN → zoom GIỮ NGUYÊN (không reset).
### [2026-08-07 05:05] Task: Section item — giữ kích thước đã resize (không tự đổi khi lưu SECTION-TAB) + MAIN play theo duration item
- **Yêu cầu user:** (1) resize section item trên MAIN → giữ nguyên kích thước — KHÔNG tự động thay đổi khi lưu SECTION-TAB; (2) MAIN SESSION chỉ play duration của section item — mặc kệ SECTION-TAB duration dài bao nhiêu.
- **Nguyên nhân (1):** `handleSaveSectionTab` (16732) ghi đè `duration: durationSec` (tính từ content tab) → mỗi lần lưu tab, section item tự giãn/thu theo content — mất kích thước user đã resize.
- **Trạng thái (2):** ĐÃ ĐÚNG — play MAIN clamp theo `sec.duration` (19154-19156 clip / 19191-19192 midi — activeEnd = min(..., secStart + sec.duration)) + `computeMainSessionEndTime` (254) dùng `s.start + s.duration` (item, không phải content) → playhead dừng đúng theo item. Chỉ cần fix (1) để duration không bị đổi.
- **FIX (app.jsx handleSaveSectionTab):** bỏ ghi đè `duration` (giữ nguyên kích thước item đã resize) — chỉ cập nhật name + tracks; bỏ luôn code tính `durationSec`/`maxEndTime` (dead).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070505), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → resize section item (vd 8s) → mở SECTION-TAB (content dài hơn) → Lưu section (Ctrl+S) → item trên MAIN GIỮ NGUYÊN 8s; play MAIN → chỉ nghe đúng 8s (clamp theo item — content dài hơn không phát tiếp); playhead dừng tại end item.
### [2026-08-07 05:10] Task: MAIN SESSION — playhead vẫn di chuyển khi hết item (chưa nhấn stop) — thêm log chẩn đoán
- **Báo cáo user:** MAIN SESSION — playhead vẫn di chuyển khi chưa nhấn stop; còn item → play âm; hết item → playhead vẫn chạy tiếp.
- **Phân tích:** playhead main dừng tại `projectEnd` (= computeMainSessionEndTime — end item CUỐI, theo s.start + s.duration — KHÔNG buffer). 2 khả năng: (a) hết item nhưng còn item khác dài hơn → playhead chạy qua khoảng trống (âm hết, playhead chạy) — hành vi DAW chuẩn; (b) projectEnd tính SAI (lớn hơn thực tế — item cuối không phải item thật) → playhead chạy quá.
- **FIX (app.jsx):** thêm log chẩn đoán — (1) `[Play] ... projectEnd=X.XX` (startTrackPlayback — projectEnd lúc schedule); (2) `[Stop] projectEnd reached — updatedTime=... projectEnd=...` (khi playhead tới projectEnd — xác nhận có dừng hay không).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070510), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN (section item / midi item) → đợi hết → dán log: `[Play] ... projectEnd=` (giá trị bao nhiêu — có khớp end item cuối không) + có `[Stop] projectEnd reached` không (dừng đúng hay chạy quá).
### [2026-08-07 05:15] Task: MAIN SESSION — playhead chạy qua khoảng im sau khi hết content section item
- **Log user (0510):** `[Stop] projectEnd reached — updatedTime=22.60 projectEnd=22.59` — playhead DỪNG ĐÚNG tại projectEnd — nhưng projectEnd = 22.59 (duration section item — resize thủ công) trong khi CONTENT thực trong tab ngắn hơn (~8s) → im lặng 8→22.59s → user thấy "hết item mà playhead vẫn di chuyển".
- **Nguyên nhân:** projectEnd (main) = computeMainSessionEndTime — section contribution = `s.start + s.duration` (duration ITEM — không phải content thực).
- **FIX (app.jsx projectEnd useMemo):** section contribution = `s.start + min(s.duration, CONTENT end từ tab)` — content ngắn hơn item → projectEnd rút về end content (dừng sớm — hết âm); content dài hơn item → giữ item duration (0505 — MAIN chỉ play duration item). Chỉ áp dụng khi section item là end cuối (max hiện tại).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070515), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN (section item duration 22.59s, content 8s) → playhead DỪNG tại ~8s (hết content — log `[Stop] projectEnd reached — projectEnd≈8`); content dài hơn item → dừng tại item end (cắt như 0505).
### [2026-08-07 05:20] Task: projectEnd chỉ kích hoạt khi nút LOOP bật — play loop liên tục
- **Yêu cầu user:** projectEnd chỉ được kích hoạt khi nút LOOP được bật (cho phép dự án play loop liên tục). LOOP tắt → play 1 lần tới end items (KHÔNG cắt sớm theo content — bỏ hiệu ứng 0515 khi loop tắt).
- **FIX (app.jsx):**
(1) `projectEnd` useMemo — content-min (0515: section end = sec.start + min(item duration, content end)) CHỈ áp dụng khi `isLoopingSelection` (deps thêm isLoopingSelection). LOOP TẮT → projectEnd = computeMainSessionEndTime thuần (end items — play đủ, dừng tại đó).
(2) Nút LOOP transport (25838-25848): BỎ auto-derive selection 0→end items — LOOP bật không selection → loop tại projectEnd (nhánh updatePlayhead `updatedTime >= projectEndRef` loop lại từ 0 — liên tục). User có selection riêng → giữ loop vùng chọn.
- **Hành vi:** LOOP TẮT → playhead chạy tới END ITEMS (22.59) rồi dừng (không cắt 8s content). LOOP BẬT → playhead tới projectEnd (content end ~8s) → LOOP LẠI từ 0 → play liên tục.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070520), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → (1) LOOP TẮT → play MAIN (section 22.59s, content 8s) → play tới 22.59 mới dừng; (2) bấm LOOP → play → loop liên tục tại ~8s (log `[Stop] projectEnd reached — projectEnd≈8` + loop lại từ 0).
### [2026-08-07 05:25] Task: Tăng size Time display (toolbar) + tăng statusbar 150%
- **Yêu cầu user:** tăng size ô hiển thị thời gian (playhead/tổng) trên toolbar; tăng kích thước status bar lên 150%.
- **FIX (app.jsx):**
(1) Toolbar — ô `Time: <playhead> / <total>`: `text-[14px]``text-[20px]` (4 spans: label, playhead, "/", total).
(2) Statusbar — container `h-6``h-9` (36px = 150%), `text-xs``text-[18px]`; các badge trạng thái (Local Sel/Global Sel/Solo Loop/Master Loop) `text-xs``text-[18px]`; Bars selection (Bars/inputs/♪T) + Time selection (Start/End/Len): `text-xs``text-[18px]`, input `w-10``w-[60px]`, `w-16``w-24`, divider `h-4``h-6`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070525), `wiki.md`. Rebuild precompiled (build PASS — text-[20px] ×4, text-[18px] ×18, w-[60px] ×7, h-9 ✓).
- **Ghi chú/Test:** hard refresh → toolbar Time to rõ (20px); statusbar cao hơn + chữ/input 18px (150%).
### [2026-08-07 05:30] Task: Quét chọn (tempo track) không cập nhật bars hiển thị dưới statusbar
- **Báo cáo user:** quét ở tempo track để select duration → vị trí quét + số bars được chọn KHÔNG hiển thị ở bars dưới status bar.
- **Nguyên nhân:** beginBar/endBar/numberBar CHỈ được set từ onChange của input (nhập tay — dock panel cũ + statusbar mới) — KHÔNG có code cập nhật từ sweep/selection → statusbar luôn hiển thị 0/0/1 khi quét.
- **FIX (app.jsx):** thêm effect `[selectionStart, selectionEnd, bpm]` — khi selection hợp lệ (end > start): beginBar = floor(start / secPerBar); endBar = ceil(end / secPerBar) 1; numberBar = endBar beginBar + 1 → quét xong bars statusbar hiển thị đúng vị trí + số bar.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070530), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → quét chọn trên tempo track (vd từ bar 2 tới bar 5) → statusbar hiển thị Bars: 2 - 5 # 4; xóa selection → giữ giá trị cuối (không tự reset).
### [2026-08-07 05:35] Task: Bars hiển thị sai chỉ số — quét 8 bars hiển thị "0 đến 7"
- **Báo cáo user:** số bars đúng (8) nhưng chỉ số sai: quét bar 0→8 hiển thị "0 đến 7" — cần "0 đến 8".
- **Nguyên nhân:** effect (0530) tính `endBar = ceil(end/spb) 1` (bar cuối INCLUSIVE) → 7; numberBar = endBar beginBar + 1 = 8.
- **FIX (app.jsx):**
(1) Effect: `endBar = ceil(end/spb)` (KHÔNG 1 — bar KẾT THÚC vùng quét/chạm vạch); `numberBar = ceil floor` (KHÔNG +1) → quét 8 bars → "0 đến 8 # 8".
(2) Input End Bar (dock 26436 + statusbar 27846): `setSelectionEnd(t + spb)``setSelectionEnd(t)` (selection tới vạch bar b — khớp hiển thị); `setNumberBar(b beginBar)`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070535), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → quét 8 bars từ 0 → BARS hiển thị "0 - 8 # 8"; nhập End Bar = 8 → selection tới vạch 8 (8 bars).
### [2026-08-07 05:40] Task: MAIN SESSION — play section item có âm nhưng track VU không hiển thị
- **Báo cáo user:** play section item ở MAIN — âm phát ra (track) nhưng VU meter không nhảy.
- **Phân tích:** MIDI section (FluidSynth SF) đi thẳng masterBus.input (midiAudible main rỗng → setOutputDestination(null)) — KHÔNG qua node track/sub-node analyser → audioPeak = 0 — VU track chỉ nhảy qua `triggerMidiVuActivity` (midiVuActivityRef). Trigger cũ (19511-19518) VẪN CÒN (fire lúc schedule — không setTimeout cho note delayed). Chưa rõ vì sao VU đứng yên — cần log.
- **FIX (app.jsx):** (1) bổ sung trigger VU cho nhánh note DELAYED (setTimeout khớp nhịp — subTrack.id + track.id) — nhánh instant đã có; (2) thêm log chẩn đoán VU tick: `[VU] <trackId> audio=... midi=... vNode=... analyser=... canvas=...` (rate-limited ~1.3 lần/giây).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070540), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (section item MIDI) → dán log `[VU]` — xác định: midi= có >0 không (trigger fire?); vNode/analyser/canvas có tồn tại không.
### [2026-08-07 05:45] Task: Track VU vẫn diễn animation khi không còn âm (MAIN/SECTION-TAB)
- **Báo cáo user:** track VU meter không diễn đúng — hết âm vẫn nhảy animation.
- **Phân tích:** VU track (MIDI section) chỉ nhảy qua `midiVuActivityRef` (trigger fire lúc schedule). Trigger cũ fire NGAY lúc schedule (kể cả note delay xa — VU nhảy trước âm / note bị cắt vẫn flash) + TRÙNG với trigger mới (05:40) → VU nhảy sai nhịp/kéo dài. Decay 0.9/frame (~0.75s) — sau note cuối VU còn diễn ~0.75s.
- **FIX (app.jsx):**
(1) XÓA trigger cũ fire-ngay (sau block — chạy cho mọi note cả delayed) — chỉ giữ trigger mới: nhánh delayed (setTimeout delay*1000 — khớp nhịp) + nhánh instant.
(2) Decay nhanh: `midiPeak * 0.90``0.72` (tắt sau ~0.35s — hết note → VU dừng nhanh).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070545), `wiki.md`. Rebuild precompiled (build PASS — triggerMidiVuActivity còn 8 chỗ).
- **Ghi chú/Test:** hard refresh → play MAIN (section MIDI) → hết notes → VU dừng trong ~0.35s; nếu VU VẪN nhảy khi hết âm → dán log `[VU]` (midi= giá trị bao nhiêu khi không còn âm).
### [2026-08-07 05:50] Task: VU vẫn animation khi hết âm — log nguồn trigger [VUTrig]
- **Log user (0545):** âm ngừng nhưng `[VU] 1 audio=0.000 midi=0.058``midi=0.661` — midiPeak TĂNG LẠI → triggerMidiVuActivity vẫn fire định kỳ sau khi hết âm.
- **FIX (app.jsx):** thêm log `[VUTrig] <trackId> vel=... peak=... play=<isPlaying> tab=<activeTab>` vào `triggerMidiVuActivity` (rate-limited 1/20 fire) — xác định AI còn fire (setTimeout sót từ main-midi delayed? re-schedule? keyboard preview?).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070550), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (hết âm — VU vẫn nhảy) → dán log `[VUTrig]` — cho biết track nào + lúc nào (play=?) vẫn fire.
### [2026-08-07 05:55] Task: CHỐT — VU vẫn nhảy khi hết âm = setTimeout VU trigger sót sau stop
- **Log user (0550):** sau STOP (`[Play] click isPlaying=true` → stop) — `[VUTrig] 1 vel=0.83/0.78/0.95 ... play= false`**trigger VU vẫn fire sau khi play đã dừng**.
- **Nguyên nhân:** setTimeout VU trigger của note DELAYED (main-midi 19351 + section delayed — khôi phục 05:40) KHÔNG bị hủy khi stop → fire MUỘN sau stop → midiVuActivity set lại → VU nhảy (decay 0.72 vẫn chưa kịp về 0).
- **FIX (app.jsx):** thêm `isPlayingRef` (App — sync mỗi render) + guard `if (!isPlayingRef.current) return;` ở ĐẦU 2 setTimeout VU trigger (main-midi + section delayed) — stop → isPlaying=false → setTimeout sót không fire → VU dừng ngay khi hết âm/stop.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070555), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (MIDI) → STOP giữa chừng → log `[VUTrig]` KHÔNG còn fire sau stop (chỉ fire lúc play= true); hết bài tự nhiên → VU dừng trong ~0.35s.
### [2026-08-07 06:00] Task: VU vẫn nhảy khi playhead ngoài vùng âm — log nguồn peak>0
- **Báo cáo user:** VU vẫn diễn animation khi playhead di chuyển ra khỏi vùng có âm. Log 0555: track VU `midi=0.000` liên tục (đã dừng đúng) — NHƯNG user vẫn thấy nhảy → nguồn KHÁC (master VU? key khác? peak giữa 2 log rate-limited?).
- **FIX (app.jsx):** đổi log chẩn đoán — (1) `[VU] <trackId> audio=... midi=... key=<key>` CHỈ in khi peak > 0.001 (VU nhảy thật — không rate-limit); (2) thêm `[VU-master] peak=...` (master VU khi nhảy) — xác định VU nào còn nhảy ngoài vùng âm.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070600), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN → playhead ra khỏi vùng âm → nếu VU vẫn nhảy → dán log `[VU]`/`[VU-master]` xuất hiện lúc đó (cho biết track/key/nguồn audio hay midi).
### [2026-08-07 06:05] Task: SECTION-TAB — VU nhảy theo velocity trigger (giả) — đọc âm THỰC từ SF output
- **Log user (0600):** SECTION-TAB — click playhead tới vị trí không âm → VU vẫn nhảy: `[VU] 1 midi= 0.98 → 0.85 → ...` (tăng lại theo chu kỳ — trigger fire lúc schedule/re-schedule) nhưng `[VU-master] peak= 0.008` (âm thực gần im) — **VU GIẢ (velocity trigger), không theo âm thực**. Master VU nhảy nhẹ 0.008 liên tục (noise nền).
- **FIX (app.jsx VU tick):**
(1) Tap analyser trên SF output (`SonicSF.getOutputNode()` — âm MIDI THỰC post-FluidSynth) — `sfAudioPeak` gộp vào audioPeak — VU MIDI theo âm THỰC.
(2) Khi SF analyser đã tap → `midiPeak = 0` (bỏ velocity trigger giả) — hết note → SF im → VU 0 ngay (không nhảy giả sau click playhead/re-schedule).
(3) Master VU deadband: `peak < 0.01 → 0` — hết noise nền 0.008.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070605), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → SECTION-TAB play → click playhead tới vị trí không âm → VU KHÔNG nhảy (SF im → 0); vị trí có âm → VU nhảy theo âm thực; master VU không nhảy noise.
### [2026-08-07 06:10] Task: VU vẽ đoạn ngắn (0.008) khi hết âm — deadband track analyser
- **Log user (0605):** `[VU] 1 audio= 0.008 midi= 0.000` — VU vẽ đoạn nhỏ (~-42dB) dù không âm — noise nền trên track analyser (như master 0.008 — đã deadband 06:05).
- **FIX (app.jsx):** track audioPeak deadband — `if (audioPeak < 0.01) audioPeak = 0;` (sau khi gộp sfAudioPeak) — hết âm → VU tắt NGAY (peak 0 → db -60 → vẽ 0).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070610), `wiki.md`. Rebuild precompiled (build PASS — Deadband ×2: master + track).
- **Ghi chú/Test:** hard refresh → play → playhead ra khỏi vùng âm → VU TẮT NGAY (không còn đoạn 0.008); log `[VU]` không còn audio=0.008.
### [2026-08-07 06:15] Task: PIANO ROLL restored (phiên trước) — playhead đứng im khi play MAIN/SECTION
- **Báo cáo user:** đang play MAIN/SECTION → mở lại PIANO ROLL TAB đã mở từ phiên làm việc trước (restored từ project) → playhead đứng im — không đồng bộ vị trí play.
- **Nguyên nhân:** tab restored KHÔNG có `buffer` (AudioBuffer không serialize được) → nhánh updatePlayhead sub-tab `if (!st || !st.buffer) return;` → return sớm → playhead không cập nhật.
- **FIX (app.jsx updatePlayhead nhánh sub-tab):** `if (!st.buffer && st.type !== 'PIANO_ROLL') return;` — PIANO ROLL không cần buffer (chỉ hiển thị playhead — đồng bộ theo vị trí play MAIN/SECTION); `startSubTabPlayback` tự tạo silent buffer khi play. Audio sub-tab vẫn cần buffer (giữ return cũ).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070615), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → load project có piano roll tab mở sẵn → play MAIN/SECTION → mở lại tab piano roll → playhead DI CHUYỂN đúng vị trí play (đồng bộ); bấm play trong tab → vẫn play bình thường (silent buffer tự tạo).
### [2026-08-07 06:20] Task: Mở PIANO ROLL restored lúc MAIN đang play — âm lag ~1s
- **Báo cáo user:** đang play MAIN/SECTION → mở lại PIANO ROLL TAB (restored phiên trước) → âm thanh bị lag ~1s rồi mới phát tiếp.
- **Nguyên nhân:** effect đổi tab → `updateSfRouting` nhánh PIANO_ROLL → `setOutputDestination` (SF _gainNode FluidSynth) — disconnect + reconnect khi âm đang phát → âm MIDI đang kêu bị NGẮT (dedupe chỉ chặn khi đích KHÔNG đổi — đổi masterBus→node piano roll = swap → ngắt).
- **FIX (app.jsx updateSfRouting):** nhánh PIANO_ROLL thêm điều kiện `&& !isPlayingRef.current` — MAIN/SECTION đang play → BỎ QUA nhánh piano (giữ SF route hiện tại — âm tiếp tục); chỉ route vào node piano roll khi KHÔNG play (preview tab — handlePlayPause sub-tab tự stopAllPlayback trước).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070620), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (có âm MIDI) → mở lại piano roll tab restored → ÂM KHÔNG BỊ NGẮT (phát liên tục — không lag); bấm play trong piano roll tab (không play main) → route SF vào node piano roll như cũ (nghe FX track).
### [2026-08-07 06:25] Task: FIX hồi quy 0620 — âm nhỏ/dry khi mở piano roll restored lúc main play
- **Báo cáo user:** bản cũ (0615) lag nhưng âm TO (qua mastering); bản 0620 hết lag nhưng âm NHỎ/không rõ (mất bypass sai).
- **Nguyên nhân:** fix 0620 skip nhánh PIANO_ROLL → rơi xuống nhánh midiAudible (list = section clones khi activeTab=midi_*) → node lookup trúng NODE MAIN '1' (sfEntry) → SF (âm main đang phát) ĐỔI ĐÍCH → sfEntry main → sfRouteGain theo ♪ main (bypass) → âm DRY/nhỏ.
- **FIX (app.jsx updateSfRouting):** guard ĐẦU HÀM — `isPlayingRef.current && activeTab là sub-tab (midi_*/không main/không session)` → RETURN NGAY (KHÔNG đổi SF destination) — âm main đang phát giữ route đã set đúng lúc schedule (masterBus.input — qua mastering — TO). Hết lag (không swap) + hết bypass sai (không route vào node main).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070625), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (âm MIDI TO — qua mastering) → mở lại piano roll tab restored → ÂM VẪN TO (không nhỏ/dry) + KHÔNG lag; bấm play trong piano roll (main không play) → route SF vào node tab như cũ.
### [2026-08-07 06:30] Task: Mở PIANO ROLL vẫn lag (ngắn hơn, 2 lần) — ghost extract block main thread
- **Báo cáo user:** sau 0625 (SF route guard) vẫn lag khi chuyển sang piano roll restored — ngắn hơn + âm lag 2 lần.
- **Nguyên nhân:** PianoRollTabEditor mount → `ghostLayers` useMemo → `extractGhostLayers(activeTracks,...)` duyệt TOÀN BỘ project (tracks + notes) — block main thread 100-500ms → noteon FluidSynth (scheduled qua setTimeout) TRỄ → âm lag (2 noteon trễ = "lag 2 lần").
- **FIX (app.jsx PianoRollTabEditor ghostLayers):** đang play (`isPlaying` prop — main/section) → return [] (BỎ ghost — chỉ là hiển thị tham chiếu, không cần khi âm chạy) — không block → âm không lag. Khi dừng play → tính ghost bình thường (không âm — block OK). Deps thêm isPlaying.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070630), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN → mở piano roll tab restored → ÂM KHÔNG LAG (to, liên tục); dừng play → ghost notes hiển thị lại bình thường.
### [2026-08-07 06:35] Task: CHỐT VU MIDI — analyser tap bị disconnect() ngắt → VU không nhảy khi có âm
- **Báo cáo user:** (1) [lặp] click playhead → vị trí không âm → VU vẫn diễn (log cũ 0600 — velocity trigger giả); (2) MỚI — play MIDI trên track → KHÔNG có VU animation.
- **Nguyên nhân (chốt cả 2):** VU tick (0605) tap analyser trên `_gainNode` SF (âm MIDI thực) — NHƯNG `setOutputDestination` (soundfontPlayer.js 85) gọi `_gainNode.disconnect()` KHÔNG đối số → ngắt MỌI kết nối KỂ CẢ analyser tap → `sfAudioPeak` = 0 MÃI → (2) VU MIDI không nhảy khi có âm; và (1) `_sfTapped` vẫn true → velocity giả bị bỏ nhưng âm thực = 0 → VU chết hoàn toàn.
- **FIX (soundfontPlayer.js):** `setOutputDestination` — sau `_gainNode.connect(dest)``if (_gainNode.__vuAnalyser) _gainNode.connect(_gainNode.__vuAnalyser);` — analyser sống → sfAudioPeak đọc âm MIDI THỰC → VU nhảy khi có âm ✓; velocity giả đã bỏ (midiPeak=0 khi tapped) → hết nhảy giả khi click playhead vị trí không âm ✓.
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js` (bump ?v=202608070635 trong index.html), `wiki.md`. (app.precompiled giữ 0630 — không đổi logic app.jsx).
- **Ghi chú/Test:** hard refresh → (1) play MIDI track → VU nhảy theo âm thực; (2) click playhead tới vị trí không âm → VU tắt ngay; (3) mở piano roll tab → âm to không lag.
### [2026-08-07 06:40] Task: Vào SECTION-TAB lúc MAIN play → CHỈ play section items (tắt âm MAIN items)
- **Yêu cầu user:** play MAIN → di chuyển sang SECTION-TAB → ONLY play items của SECTION-TAB — tắt âm thanh của MAIN items.
- **FIX (app.jsx effect đổi tab 14814):** prevTab='main' → activeTab=`session_*` + đang play (`isPlaying`) → `stopAllPlayback()` (dừng MAIN items) + `startTrackPlayback(_pt)` với activeTracks = section clones (activeTab mới) — play CHỈ section tab items. `_pt` = vị trí play chuyển sang LOCAL của tab (trừ `secStart` — vị trí section trên main — tìm qua `tracks[].sections[]` bằng sectionId) — section items dùng time local 0-based.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070640), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (có main items + section item) → vào SECTION-TAB → CHỈ nghe section items (âm main items tắt — log `[SectionTab] play CHỈ section items tại local X.XX (secStart Y.YY)`); playhead section chạy từ vị trí tương ứng.
### [2026-08-07 06:45] Task: Tắt âm MAIN items (vào SECTION-TAB) → VU phải tắt theo
- **Yêu cầu user:** 0640 đã tắt âm MAIN items khi vào SECTION-TAB ✓ — NHƯNG VU animation của âm items đó VẪN diễn — cần tắt VU.
- **Nguyên nhân:** `midiVuActivityRef` (velocity trigger) còn giá trị cũ — decay 0.72/frame (~0.35s) sau stop → VU main items "diễn" tiếp dù âm đã tắt.
- **FIX (app.jsx stopAllPlayback):** clear `midiVuActivityRef.current = {}` NGAY khi stop — VU tắt tức thì (không decay từ âm cũ). VU sau đó chỉ theo âm THỰC (SF analyser — fix 0635): tab items phát → VU nhảy; im → VU 0.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070645), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (âm main items) → vào SECTION-TAB → âm main tắt + VU main tắt NGAY (không diễn tiếp); tab items phát → VU tab nhảy theo âm thực.
### [2026-08-07 06:50] Task: VU track nhảy nhẹ (0.016/0.023) khi không âm — noise SF analyser
- **Log user (0645):** track 1 + 2 `audio= 0.016/0.023` (CÙNG giá trị — sfAudioPeak — SF analyser noise nền) nhảy nhẹ dù không âm track (master 0.25 = âm thật clip — track VU không liên quan); deadband 0.01 KHÔNG chặn 0.016.
- **FIX (app.jsx):** deadband track `audioPeak < 0.02 → 0` (nâng từ 0.01) — chặn noise SF 0.016/0.023 — VU tắt khi âm < 0.02 (≈ -34dB — âm thật thường lớn hơn).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070650), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play → vị trí không âm → VU track KHÔNG nhảy (0.016 bị chặn); có âm thật (> 0.02) → VU nhảy; master VU giữ deadband 0.01 (âm thật nhỏ vẫn hiển thị).
### [2026-08-07 06:55] Task: VU track = noise (0.023) khi âm MIDI thật 0.25 — guard SF block skip
- **Log user (0650 — SECTION-TAB):** track 1+2 `audio= 0.023/0.031` (noise vNode analyser) TRONG KHI `[VU-master] peak= 0.25` (âm MIDI thật phát — noteon) — VU track không theo âm thật.
- **Nguyên nhân:** guard `if (audioPeak <= 0.001)` quanh block đọc SF analyser — vNode noise 0.023 > 0.001 → block SF SKIP → sfAudioPeak = 0 → VU = noise vNode 0.023 (nhảy nhẹ mãi) — âm MIDI thật (SF → masterBus 0.25) không được đọc.
- **FIX (app.jsx):** (1) LUÔN đọc SF analyser (bỏ guard — sfAudioPeak gộp max vào audioPeak) — VU MIDI theo âm thật; (2) deadband `0.02 → 0.03` — chặn noise 0.023-0.031.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070655), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → chuyển SECTION-TAB (play) → VU track theo âm thật (MIDI 0.25 — nhảy đúng); vị trí không âm → VU tắt (noise 0.023 bị deadband 0.03 chặn).
### [2026-08-07 07:00] Task: Track VU nhảy CÙNG NHAU — bỏ sfAudioPeak gộp chung
- **Log user (0655):** track 1 + 2 `audio= 0.031/0.039` — CÙNG GIÁ TRỊ — nhảy cùng nhau. User muốn: track CÓ âm → nhảy; KHÔNG âm → không (PHÂN BIỆT track).
- **Nguyên nhân:** sfAudioPeak (SF = 1 output CHUNG cho mọi track MIDI) gộp vào audioPeak mọi track → track VU cùng giá trị/nhảy cùng nhau; track audio cũng bị SF chi phối.
- **FIX (app.jsx VU tick):** (1) KHÔNG gộp sfAudioPeak vào audioPeak — audioPeak chỉ theo âm TRACK thật (vNode/sub-node analyser — clip audio phân biệt); (2) MIDI track VU = velocity trigger (midiVuActivity — PHÂN BIỆT track — track nào có note → nhảy) + GATE theo âm SF THỰC (sfAudioPeak < 0.03 → midiPeak = 0 — SF im = hết note/click playhead vị trí không âm → VU tắt ngay — hết nhảy giả).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070700), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play SECTION-TAB → track MIDI (note) nhảy theo velocity khi SF phát; track audio (clip) nhảy theo analyser riêng; track không âm → đứng yên (KHÔNG cùng nhau); click playhead vị trí không âm → VU tắt ngay (SF im → gate).
### [2026-08-07 07:05] Task: Track không item VU vẫn nhảy + VU không đúng nhịp
- **Log user (0700):** [VU] 1 midi= velocity (0.66→1.0 — gate SF mở ✓); track 2 không log (VU 0 ✓). 2 vấn đề: (1) track KHÔNG item vẫn nhảy nhẹ — noise vNode 0.031-0.039 qua deadband 0.03; (2) velocity decay 0.72/frame quá nhanh — VU nhấp nháy không theo âm (sustain) — "không đúng nhịp".
- **FIX (app.jsx VU tick):**
(1) Deadband audioPeak `0.03 → 0.04` — chặn noise 0.031-0.039 — track không item → VU đứng yên.
(2) MIDI VU: bỏ decay nhanh — GATE + GIỮ theo âm SF thực: SF đang phát (sfAudioPeak ≥ 0.03) → midiPeak = velocity (VU theo note, giữ trong lúc âm — đúng nhịp); SF im → midiPeak = 0 + clear midiVuActivityRef (VU tắt ngay).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070705), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play SECTION-TAB → track có note → VU sáng theo note + giữ khi âm kêu (đúng nhịp); track không item → đứng yên; hết âm/click playhead vị trí không âm → VU tắt ngay.
### [2026-08-07 07:10] Task: Note đơn/velocity thấp không có VU — bỏ gate amplitude SF
- **Báo cáo user:** lọc noise quá khắt khe — play note đơn (velocity thấp) → KHÔNG có VU meter (âm SF nhỏ < gate 0.03 → midiPeak = 0).
- **FIX (app.jsx VU tick):**
(1) BỎ gate amplitude SF (sfAudioPeak < 0.03 → midiPeak = 0) — MIDI VU = velocity trigger (fire ĐÚNG NHỊP note: nhánh delayed setTimeout + nhánh instant) — note đơn/velocity thấp → VU nhảy theo velocity ✓.
(2) Decay chậm 0.85 (~1s) — note đơn hiển thị rõ, không nhấp nháy quá nhanh (đúng nhịp âm).
(3) Bỏ luôn block đọc SF analyser (không còn dùng — tiết kiệm CPU/frame).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070710), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play note đơn (velocity thấp) → VU NHẢY (theo velocity); nhóm note → VU nhảy; không âm (vị trí im) → VU tắt sau ~1s; track không item → đứng yên (deadband audioPeak 0.04 — chỉ chặn noise track, không ảnh hưởng MIDI velocity).
### [2026-08-07 07:15] Task: Track 2 section (không item) VU vẫn nhảy + decay quá lâu
- **Báo cáo user:** MAIN — track 1 section item, track 2 midi item. Vào SECTION-TAB — track 1 có midi item, track 2 (cùng duration) KHÔNG item — NHƯNG VU track 2 vẫn diễn; mức diễn không đúng velocity/decay.
- **Nguyên nhân:** (1) noise vNode analyser dao động quanh 0.04 — qua deadband 0.04 → track không item VU nhảy nhẹ (không theo velocity — lung tung); (2) decay 0.85 (~1s) quá lâu — sau hết âm VU "vẫn diễn" ~1s.
- **FIX (app.jsx VU tick):** (1) deadband audioPeak `0.04 → 0.05` — chặn noise quanh 0.04 — track không item đứng yên; (2) decay `0.85 → 0.75` (~0.5s) — note đơn thấy rõ nhưng tắt nhanh sau hết note; (3) log `[VU]` in MỌI key (kể cả peak 0 — rate ~1.3/s — kèm key/peak) — xác nhận nguồn.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070715), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN → vào SECTION-TAB → track 2 (không item) VU ĐỨNG YÊN; track 1 VU theo velocity + tắt ~0.5s sau hết note; nếu track 2 VẪN nhảy → dán log `[VU]` (audio= bao nhiêu — noise hay midi= còn giá trị).
### [2026-08-07 07:20] Task: TÁCH VU meter MAIN SESSION vs SECTION-TAB (item tab nào → VU tab đó)
- **Yêu cầu user:** tách biệt VU meter MAIN vs SECTION-TAB — item của tab nào thì play VU của tab đó (gốc rễ: section clone id TRÙNG main id → VU canvas/trigger key lẫn nhau).
- **FIX (app.jsx):**
(1) Track strip VU canvas — section clone (`track._isSectionClone`) dùng key `'_sess_' + track.id` (main giữ `track.id`).
(2) VU tick — parse key: `_sess_<id>` → trackId = id thuần (node lookup), vuKey = key đầy đủ (midiVuActivityRef theo context).
(3) Trigger MIDI (startTrackPlayback — 4 chỗ main/section clones): fire key theo CONTEXT — activeTab session → `'_sess_' + id`; main → id thuần — mixer/main strip (key id) không nhận trigger section; section strip (key _sess_<id>) chỉ nhận trigger section.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070720), `wiki.md`. Rebuild precompiled (build PASS — _sess_ ×8).
- **Ghi chú/Test:** hard refresh → MAIN play (main items) → VU main nhảy; vào SECTION-TAB (section items play) → VU section nhảy — VU main (mixer) ĐỨNG (không nhận trigger section); track section không item → đứng yên.
### [2026-08-07 07:25] Task: Cut/Copy MIDI item (context menu) → paste ra audio item (SAI)
- **Báo cáo user:** Cut MIDI item bằng context menu → paste ra AUDIO item (track có midi item vị trí 1 + audio item vị trí 2) — copy nhầm.
- **Nguyên nhân:** context menu chỉ biết TRACK (contextMenu = {trackId, time, sectionId}) — Copy/Cut (17542/17593) chỉ xử lý track buffer (audio) → cut midi item → copy nhầm buffer track → paste = audio item.
- **FIX (app.jsx):**
(1) TimelineTrack onContextMenu — detect ITEM dưới chuột: MIDI item (midiItems + duration bounds) + clip (clips + buffer) → truyền `itemId` + `itemType` ('midi'/'clip').
(2) handleContextMenu — nhận itemId/itemType → lưu vào contextMenu state.
(3) contextMenuCopy/Cut — xử lý ITEM TRƯỚC track: MIDI → clipboard {type:'midi', notes, duration(sec), name, color} — paste = MIDI item (doPaste nhánh midi đã có); clip → clipboard {buffer,...}; Cut = copy + XÓA item (updateActiveTracks filter).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070725), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → chuột phải MIDI item → Cut → Paste → ra MIDI ITEM (không phải audio); chuột phải clip → Copy → Paste → clip; chuột phải track (không item) → Copy → track buffer như cũ.
### [2026-08-07 07:30] Task: (1) Lưu project phải lưu tất cả tab — XÁC NHẬN đã đúng (2) Chuyển PIANO ROLL tab → stop âm tab cũ
- **Yêu cầu (1):** ở MAIN bấm lưu/Ctrl+S phải lưu tất cả các tab.
- **Xác nhận:** serializeProjectToSchema ĐÃ lưu đủ — sessionTabs → section_store (tracks đầy đủ) + sub_tabs (id/trackId/target_id/parent_tab_id/notes/currentTime); deserialize restore đủ (9327-9358). Ctrl+S (16295) + nút Save Project (25368) → handleSaveProject → serialize đủ. KHÔNG cần sửa — nếu user thấy thiếu tab sau load → dán schema đã lưu.
- **Yêu cầu (2):** chuyển PIANO ROLL TAB 1 → TAB 2 phải play đúng nội dung từng tab.
- **Nguyên nhân:** rời piano roll tab đang play (luật 04:15 "không stop") → sang tab 2 → âm tab 1 tiếp tục (nghe nhầm nội dung tab 1 khi ở tab 2).
- **FIX (app.jsx effect 14814):** prevSub PIANO_ROLL đang play + activeTab MỚI là SUB-TAB (midi_*/không main/session) → stopAllPlayback (dừng âm tab cũ — play tab mới đúng nội dung). Về MAIN/SECTION → giữ luật cũ (âm tiếp/resume).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070730), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play piano roll tab 1 → chuyển tab 2 → ÂM TAB 1 DỪNG (im — bấm play tab 2 → nghe tab 2); về MAIN → hành vi cũ.
### [2026-08-07 07:35] Task: (1) Lưu ở MAIN phải lưu dirty tabs (2) Xóa midi item → close piano roll tab tương ứng
- **Báo cáo user:** (1) nhấn lưu ở MAIN SESSION không lưu dirty tab (PIANO ROLL đã sửa notes); (2) xóa midi item phải close PIANO ROLL TAB đã mở của item đó.
- **Nguyên nhân (1):** keydown Ctrl+S handler thứ 2 (16388) — nhánh `curTab === 'main'` RỖNG ("handled by main handler" — không làm gì) → MAIN Ctrl+S không save project + không lưu dirty; nút menu Save Project (25370) chỉ gọi handleSaveProject (không lưu dirty).
- **FIX (app.jsx):**
(1) Ctrl+S (16388): nhánh main/session → `handleSaveProject()` + save TẤT CẢ dirty sub-tabs (PIANO_ROLL → handleSaveMidiNotes; SECTION → saveSectionTab; audio → buffer về track); nút Save Project → cùng logic.
(2) Xóa midi item (contextMenuDelete 17480 + contextMenuCut 17666) → `setSubTabs(filter target_id !== itemId)` — close piano roll tab của item; nếu tab đang active bị close → `setActiveTab('main')`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070735), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → sửa notes piano roll (dirty) → về MAIN → Ctrl+S / Save Project → reload → notes ĐÃ LƯU (tab vẫn mở + nội dung mới); xóa midi item → piano roll tab của item đó tự close (+ về main nếu đang active tab đó).
### [2026-08-07 07:40] Task: SECTION-TAB — xóa midi item (Delete key) → close piano roll tab tương ứng
- **Báo cáo user:** trong SECTION-TAB, xóa midi item phải close PIANO ROLL TAB đã mở của item đó.
- **Nguyên nhân:** đường xóa chính là `handleDeleteSelectedItemsRef` (Delete key — chọn item → Delete) — xóa midiItems nhưng KHÔNG close subTab (target_id = item id) — áp cho cả MAIN lẫn SECTION (updateActiveTracks theo context).
- **FIX (app.jsx handleDeleteSelectedItemsRef):** đầu hàm — `setSubTabs(filter: bỏ tab có target_id ∈ idsToDelete)` — close piano roll tab của midi item bị xóa; tab đang active bị close → `setActiveTab('main')`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070740), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → SECTION-TAB chọn midi item → Delete → piano roll tab của item đó TỰ CLOSE (về main nếu đang ở tab đó); MAIN tương tự.
### [2026-08-07 07:45] Task: (1) FIX Cut midi → paste audio (detect bounds sai) (2) Xóa item SECTION-TAB → sync canvas main + giữ tab
- **Báo cáo (1) [lặp]:** cut midi item (context menu) → paste ra audio item — fix 0725 chưa hiệu lực.
- **Nguyên nhân (1):** onContextMenu detect MIDI bounds dùng `(m.duration || 4)` KHÔNG × secondsPerBar (m.duration tính BEATS — contextMenuDelete 17452 có ×) → detect miss → itemType null → Cut/Copy xử lý TRACK buffer (audio). **FIX:** bounds × `_spb` (60/bpm*4).
- **Báo cáo (2) [mới]:** xóa item trong SECTION-TAB → phải cập nhật canvas section item ở MAIN + giữ vị trí SECTION-TAB (không quay về MAIN khi close tab).
- **FIX (2):** (a) helper `syncSectionTabToMain(tabId)` — sync `tab.tracks``sec.tracks` trên MAIN (canvas section item vẽ lại — KHÔNG đổi duration 0505); gọi sau mọi xóa item (Delete key handleDeleteSelectedItemsRef + contextMenuDelete: section/midi/clip + contextMenuCut). (b) điều kiện "về main" khi close tab — CHỈ khi activeTab là `midi_*` (piano roll) bị close — KHÔNG đá SECTION-TAB về main.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070745), `wiki.md`. Rebuild precompiled (build PASS — syncSectionTabToMain ×6).
- **Ghi chú/Test:** hard refresh → (1) chuột phải MIDI item (track có midi + audio) → Cut → Paste → MIDI item (không audio); (2) SECTION-TAB xóa item → section item trên MAIN vẽ lại (nội dung mới) + VẪN Ở SECTION-TAB (không về main).
### [2026-08-07 07:50] Task: (2) Sync canvas MAIN realtime sau cut/delete SECTION-TAB (3) Ctrl+X/C phải cut/copy ITEM
- **Báo cáo user:** (2) cut/delete item SECTION-TAB 1 → quay MAIN → phải cập nhật realtime canvas section item 1 (không cần save); (3) Ctrl+X không cut được item.
- **Nguyên nhân:** (2) syncSectionTabToMain (0745) chạy NGAY (TRƯỚC updateActiveTracks — state chưa áp) → sync data CŨ → canvas cũ; (3) Ctrl+C/X (16344/16349) LUÔN handleCopyTrack/handleCutTrack (track) — không xử lý item được chọn.
- **FIX (app.jsx):**
(2) syncSectionTabToMain — gọi qua `setTimeout(0)` (SAU updateActiveTracks — data mới) ở mọi điểm xóa (handleDeleteSelectedItemsRef + contextMenuDelete section/midi/clip + contextMenuCut).
(3) Ctrl+C/X — có ITEM được chọn (selectedItemIds) → `findItemContext` + `copyItemToClipboard` (MIDI notes / clip buffer — helpers dùng chung) → copy item; Ctrl+X → copy + `handleDeleteSelectedItemsRef` (xóa + close piano roll tab + sync section); không chọn item → fallback track cũ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070750), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → (2) SECTION-TAB cut/delete item → quay MAIN → section item canvas CẬP NHẬT realtime (nội dung mới — không save); (3) chọn MIDI item → Ctrl+C → Ctrl+V → paste MIDI item; chọn item → Ctrl+X → item bị cắt (clipboard) + tab close + canvas sync.
### [2026-08-07 07:55] Task: (2) Paste midi block ngắn — duration sai (3) Ctrl+V paste vào track 1
- **Báo cáo user:** (1) [lặp] cut midi → paste audio — fix 0745 chờ xác nhận bản mới; (2) context menu cut midi → paste → item block NGẮN (không giữ duration); (3) Ctrl+V paste vào TRACK 1 (không phải track được chọn).
- **Nguyên nhân:** (2) midiItem.duration tính GIÂY (insertMidiItem = 4*spb; scheduling 19331 dùng seconds) — nhưng copy midi (copyItemToClipboard + contextMenuCopy/Cut) nhân thêm `(60.0/bpm)` → duration gấp đôi → block sai. (3) handlePasteTrack = `doPaste(selectedTrackId, ...)` — chọn ITEM không set selectedTrackId (giữ '1') → paste track 1.
- **FIX (app.jsx):**
(2) copy midi duration = `item.duration || 4` (GIÂY — bỏ × 60/bpm) — 3 chỗ (helper + contextMenuCopy + contextMenuCut) + bỏ biến bpmVal thừa.
(3) handlePasteTrack — có ITEM được chọn (selectedItemIds) → target = track chứa item (findItemContext); ngược lại selectedTrackId.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070755), `wiki.md`. Rebuild precompiled (build PASS — type:'midi' ×4, duration ×3).
- **Ghi chú/Test:** hard refresh → (2) context menu cut midi → paste → block GIỮ ĐÚNG duration; (3) chọn item trên track 2 → Ctrl+V → paste vào track 2 (không phải track 1); (1) cut midi → paste → MIDI item.
### [2026-08-07 08:00] Task: (1) Ctrl+X bị browser capture (2) Ctrl+V paste sai track 1
- **Báo cáo user:** (1) Ctrl-X bị browser capture — không cut item được; (2) Ctrl+V vẫn paste track 1 — phải paste vào track đang select.
- **Nguyên nhân:** (1) Ctrl+X handler thiếu stopPropagation (browser cut text chiếm); (2) click ITEM — mousedown nhánh hitItem `return` sớm (2919) → `onTrackLaneMouseDown` (setSelectedTrackId) KHÔNG chạy → selectedTrackId giữ track cũ ('1') → Ctrl+V paste sai.
- **FIX (app.jsx):** (1) Ctrl+X — thêm `e.stopPropagation()` (cùng preventDefault). (2) click item (mousedown hitItem) → `onSelectTrack(track.id)` — chọn track chứa item → Ctrl+V paste đúng track (handlePasteTrack đã ưu tiên item → fallback selectedTrackId).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070800), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → (1) chọn item → Ctrl+X → item bị cắt (không bị browser chiếm); (2) chọn item trên track 2 → Ctrl+V → paste vào track 2 (đúng track đang select).
### [2026-08-07 08:05] Task: (1) Ctrl-X không cắt item (2) paste tại vị trí click (3) paste sai track — closure stale
- **Báo cáo user:** (1) click item → Ctrl-X không thấy cắt; (2) paste nên tại vị trí con trỏ CLICK (bên phải playhead → paste tại đó; bên trái → paste tại playhead); (3) Ctrl+V vẫn paste track đầu tiên.
- **Nguyên nhân:** (3) keydown effect `deps []` — closure giữ handlePasteTrack RENDER ĐẦU → `selectedTrackId` stale ('1') → paste track 1 LUÔN (bất kể track đang chọn). (2) doPaste luôn dùng currentTime (playhead). (1) chưa rõ — thêm log.
- **FIX (app.jsx):**
(1) Ctrl+X — thêm `console.log('[CutItem] Ctrl+X — itemId=... found=...')` — xác định đường chạy (item hay fallback track).
(2) `lastClickTimeRef` — set trong handlePlayheadSet (click timeline/ruler) — handlePasteTrack: pasteTime = click (nếu > playhead) else playhead.
(3) `handlePasteTrackRef` (ref mới mỗi render) — Ctrl+V gọi ref (hết stale selectedTrackId).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070805), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → (1) chọn item → Ctrl+X → nếu không cắt → dán log `[CutItem]`; (2) click timeline bên phải playhead → Ctrl+V → paste tại chỗ click; click bên trái → paste tại playhead; (3) chọn item track 2 → Ctrl+V → paste track 2.
### [2026-08-07 08:10] Task: Ctrl+X không cắt item — copyItemToClipboard closure stale (activeTracks)
- **Log user (0805):** `[CutItem] Ctrl+X — itemId= midi_1786099252336 found= 2/midi selSize= 1` — handler chạy, detect đúng item — NHƯNG timeline không cắt.
- **Nguyên nhân:** Ctrl+C/X chạy trong keydown effect `deps []` → closure giữ `copyItemToClipboard` RENDER ĐẦU — `activeTracks` (state) là ARRAY CŨ — item `midi_1786099252336` TẠO SAU render đầu → `activeTracks.find` KHÔNG có item → copy fail → fallback `handleCutTrack` (cắt track — không cắt item).
- **FIX (app.jsx copyItemToClipboard):** `const trk = (activeTracksRef.current || []).find(...)` — dùng REF (luôn mới) thay closure state.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070810), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → chọn item (mới tạo) → Ctrl+X → item bị CẮT (log [CutItem] found= track/type + toast 'Đã cắt item'); Ctrl+C → copy item → Ctrl+V paste đúng.
### [2026-08-07 08:15] Task: Kéo ngắn midi item SECTION-TAB → MAIN canvas vẫn 8 bars
- **Báo cáo user:** midi item 2 (8 bars content) kéo NGẮN còn 4 bars trong SECTION-TAB — MAIN section item canvas vẫn hiển thị 8 bars.
- **Nguyên nhân:** syncSectionTabToMain (0745) chỉ chạy khi XÓA item — kéo NGẮN (resize) / MOVE item KHÔNG sync → sec.tracks giữ duration/vị trí cũ → canvas MAIN vẽ 8 bars.
- **FIX (app.jsx):** thêm sync sau thao tác (activeTab session → setTimeout(0) → syncSectionTabToMain): (a) handleMouseUp của RESIZE item (21651 — kéo edge — duration mới); (b) handleMouseUp của MOVE item (21563 — vị trí mới).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070815), `wiki.md`. Rebuild precompiled (build PASS — syncSectionTabToMain ×8).
- **Ghi chú/Test:** hard refresh → SECTION-TAB kéo ngắn midi item 2 (4 bars) → quay MAIN → section item canvas hiển thị ĐÚNG 4 bars (không còn 8 bars); kéo di chuyển item → MAIN canvas theo vị trí mới.
### [2026-08-07 08:20] Task: MAIN canvas section item vẫn 8 bars — notes không clip theo duration
- **Test user:** kéo ngắn midi item 2 (4 bars) SECTION-TAB → MAIN canvas KHÔNG hiển thị 4 bars (vẫn 8).
- **Nguyên nhân:** trim (kéo ngắn) KHÔNG cắt notes (notes 8 bars vẫn trong item — duration giảm 4) — canvas MAIN vẽ section content: `notes.forEach` vẽ MỌI note (2579) KHÔNG clip theo `item.duration` → 8 bars vẫn hiển thị (sync 08:15 đã cập nhật sec.tracks — nhưng vẽ không clip).
- **FIX (app.jsx canvas MAIN — drawSection subMidi notes):** skip note `noteStartSec >= (item.duration || 0) - 0.01` — note ngoài duration (phần đã trim) KHÔNG vẽ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070820), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → SECTION-TAB kéo ngắn midi item (4 bars) → quay MAIN → section item canvas hiển thị ĐÚNG 4 bars (phần 5-8 trống); scheduling đã skip note ngoài duration (19331).
### [2026-08-07 08:25] Task: PIANO ROLL — ctrl+draw velocity chord (nhiều notes cùng vị trí) chỉ vẽ được 1 note
- **Báo cáo user:** velocity lane — notes CÙNG vị trí (chord) khó draw — draw qua vị trí phải vẽ lại TẤT CẢ velocity (nếu notes không được chọn).
- **Nguyên nhân:** findCCNoteIndex trả 1 note (gần stemTop nhất) → ctrl+draw chỉ đổi 1 note — chord notes còn lại giữ nguyên.
- **FIX (app.jsx PianoRollTabEditor):** helper `findCCNoteIndicesAtBeat(b)` — trả TẤT CẢ indices notes có start_beat == snapped — handleCCMouseDown (nhánh KHÔNG chọn): lastPainted = all-at-beat; handleCCMouseMove (non-selected): vẽ TẤT CẢ unpainted notes tại beat (velocity/pan). Selected mode giữ nguyên (chỉ vẽ notes chọn).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070825), `wiki.md`. Rebuild precompiled (build PASS — findCCNoteIndicesAtBeat ×3).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — chord notes cùng beat — ctrl+draw qua cột velocity → TẤT CẢ notes cùng beat đổi velocity (không chọn); có notes chọn → chỉ notes chọn đổi.
### [2026-08-07 08:30] Task: Tích hợp MIDI Toolkit vào Piano Roll Editor (spec 20:12)
- **Spec:** Scale Highlight/Lock, Arpeggiator, Strummer, Humanize, Chord Generator, Velocity Shaper — tích hợp trực tiếp Piano Roll (không qua FX Rack).
- **ĐÃ CÓ SẴN:** snapPitchToScale + selectedScale (menu chuột phải), snapToScale toggle, humanize basic (7144).
- **THÊM MỚI (app.jsx PianoRollTabEditor):**
1. **Scale:** state `scaleRoot` (C..B); toolbar [Scale ▾][Root ▾][Force] — flatten SCALES; `scaleWithRoot()` (transpose); **highlight grid** (rows in-scale sáng vàng / out-of-scale dim — canvas 7394); `applyForceToScale` (notes chọn/all → snap pitch).
2. **Arpeggiator:** modal (pattern UP/DOWN/UP-DOWN/RANDOM/CHORD, rate 1/4-1/32 + triplet/dotted, octaves 1-4, gate 10-200%) — `applyArpeggiate` (chord notes → sequential).
3. **Strummer:** modal (ms 0-120, direction DOWN/UP/ALTERNATE) — `applyStrum` (group same start_beat, sort pitch, Δt + velocity taper 0.03/note).
4. **Humanize:** modal (timing ±ms, vel ±, dur ±%) — `applyHumanizeModal` (jitter start_beat/velocity/duration) — nút Humanize cũ + Alt+R mở modal.
5. **Chord:** [Chord ▾] Triad/Min7/Sus4/Add9 + [Stamp] mode (click canvas → `applyChordStamp` full chord) + Harmonize +3/+5/+7 (`applyHarmonize`).
6. **Velocity:** [Vel: Comp][target][Norm] — `applyVelocityCompress` (shift về target mean), `applyVelocityNormalize` (max → 127).
7. **Shortcuts:** Alt+A (Arp), Alt+S (Strum), Alt+R (Humanize), Shift+C (Stamp), Shift+S (Lock scale toggle).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070830), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → mở PIANO ROLL → chọn scale (toolbar) → grid highlight; chọn notes → Arp/Strum/Humanize modal → Apply; [Stamp] + click canvas → chord; chọn notes → Comp/Norm velocity; Force đưa notes về scale.
### [2026-08-07 08:35] Task: Toolbar Piano Roll — gom 2 hàng + merge tool trùng
- **Yêu cầu user:** toolbar có công cụ TRÙNG — move + merge vào HÀNG DƯỚI.
- **Thực hiện (app.jsx):**
(1) Move khối [Snap to Scale toggle + Scale: ▾/Root ▾/Force + Tools: Arp/Strum/Humanize + Chord: ▾/Stamp/+3/+5/+7 + Vel: Comp/target/Norm] từ giữa hàng 1 → CUỐI toolbar (hàng 2 — sau nút "🎵 Chuyển giọng") với spacer `flex-basis:100%` ép xuống hàng mới (container vốn flex-wrap 2 hàng).
(2) MERGE trùng: nút "🎚 Humanize + strength" cũ (hàng 1) — XÓA (thay bằng Tools:Humanize modal hàng 2 — modal đã có tham số timing/vel/dur).
(3) Hàng 1 giờ gọn: track select, Snap, ARM, Input, Instrument, AI, CC mode, Vel/Sus/Mod/Bend/Pan, Session/Isolated, Copy/Paste/Undo/Redo/Chuyển giọng, Lưu/Export.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070835), `wiki.md`. Rebuild precompiled (build PASS — humstr đã xóa).
- **Ghi chú/Test:** hard refresh → mở PIANO ROLL → toolbar 2 hàng (hàng 1: cơ bản; hàng 2: Scale/Tools/Chord/Vel) — không còn trùng Humanize.
### [2026-08-07 08:40] Task: Toolbar PIANO ROLL — bỏ Copy/Paste, thêm ARP/STRUM/HUMANIZE, gộp 1 hàng
- **Yêu cầu user:** bỏ 2 nút COPY/PASTE (dùng Ctrl-C/X/V + context menu thay); đặt 3 nút ARP/STRUM/HUMANIZE + [dropdown mức humanize] vào chỗ vừa xóa; di chuyển các thành phần còn lại lên cùng hàng với dãy nút này, phía sau nút Chuyển giọng.
- **Thực hiện (app.jsx PianoRollTabEditor toolbar):**
(1) Xóa nút 📋 Copy + 📥 Paste (8882-8900 cũ) → thay bằng ARP/STRUM/HUMANIZE + select mức (Nhẹ 0.05/Vừa 0.10/Mạnh 0.18 — chọn mức → setHumanizeStrength + mở modal theo mức: timingMs=round(v*100), velRange=round(v*127), durRange=round(v*50)).
(2) Xóa spacer flexBasis:100% (ép hàng 2) + xóa khối "Tools: Arp/Strum/Humanize" trùng (hàng 2) → toolbar GỘP 1 HÀNG: ... Chuyển giọng → [Snap to Scale][Scale ▾][Root ▾][Force][Chord ▾][Stamp][+3/+5/+7][Vel: Comp/Norm] → Lưu/Export.
(3) Ctrl+C/X/V trong PIANO ROLL TAB (keydown 7106): C = copy notes (onCopyNotes + toast), X = copy + xóa hết notes, V = paste clipboardNotes (append — như nút cũ) — deps thêm clipboardNotes (handler re-create khi clipboard đổi).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070840), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → mở PIANO ROLL → toolbar 1 hàng (không còn Copy/Paste); ARP/STRUM/HUMANIZE + mức humanize hoạt động (mở modal); Ctrl+C → Ctrl+V paste notes; Ctrl+X cắt notes; Alt+A/S/R mở tool.
### [2026-08-07 08:45] Task: (1) Lưu + Export MIDI lên trước vị trí nút Đóng, bỏ Đóng (2) Velocity không cập nhật realtime khi đổi độ rộng/bật tắt thanh velocity
- **Yêu cầu user:** (1) mang 2 nút Lưu + Export MIDI lên trước nút Đóng + remove nút Đóng; (2) velocity note không cập nhật realtime khi thay đổi ccHeight (độ rộng) hoặc showCC (bật/tắt thanh velocity).
- **FIX (app.jsx PianoRollTabEditor):**
(1) Xóa nút "Đóng" (onClose) — chèn div ml-auto [Lưu + Export MIDI] (di chuyển từ cuối toolbar — xóa bản cũ); sửa cân bằng ngoặc (bản cũ `"Export MIDI")))` đóng cả toolbar div — thêm `)` tại `"Norm")))`).
(2) Effect vẽ CC canvas — thêm `ccHeight, showCC` vào deps (`[notes, ccMode, rollZoom, viewWidth, selectedNoteIds, renderBeatOffset, ccHeight, showCC]`) — đổi độ rộng/bật tắt → vẽ lại realtime.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070845), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL toolbar: không còn nút Đóng; Lưu/Export ở cuối (trước hàng tool Scale/Chord/Vel); kéo đổi độ rộng thanh velocity / bấm Vel toggle → velocity bars cập nhật ngay.
### [2026-08-07 08:50] Task: FIX TDZ — ccHeight/showCC khai báo SAU effect vẽ CC
- **Lỗi user:** `Uncaught ReferenceError: Cannot access 'ccHeight' before initialization`.
- **Nguyên nhân:** 08:45 thêm ccHeight/showCC vào deps useLayoutEffect vẽ CC (7620) — NHƯNG 2 state khai báo ở 8542 (SAU effect) → deps array tham chiếu biến trước khai báo → TDZ error khi render.
- **FIX (app.jsx):** di chuyển `const [showCC, setShowCC]` + `const [ccHeight, setCcHeight]` LÊN TRƯỚC useLayoutEffect vẽ CC (7619) — xóa bản trùng cũ (8542).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070850), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → mở PIANO ROLL → KHÔNG còn lỗi TDZ; đổi độ rộng thanh velocity / toggle Vel → vẽ lại realtime.
### [2026-08-07 08:55] Task: Tăng size tool từ "Snap to Scale" đến cuối hàng
- **Yêu cầu user:** tăng size các tool từ Snap to Scale đến cuối hàng (Snap to Scale toggle, Scale ▾, Root ▾, Force, Chord ▾, Stamp, +3/+5/+7, Vel: Comp/Norm).
- **FIX (app.jsx):** trong khối (Snap to Scale → Norm) — `text-[10px]``text-xs` (12px) — 9 chỗ (Force/Stamp/+3/+5/+7/Comp/Norm buttons + Chord select...).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070855), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL toolbar → các tool cuối hàng to hơn (text-xs).
### [2026-08-07 09:00] Task: Đồng bộ font hàng tool (Snap to Scale → Norm) lên text-sm
- **Yêu cầu user:** tăng kích thước font chữ trong hàng đó cho ĐỒNG BỘ.
- **FIX (app.jsx):** khối Snap to Scale → Norm — `text-xs``text-sm` (12 chỗ: 5 container div + buttons + Chord select + nhãn) + thêm `text-sm` vào 2 select Scale/Root (mặc định browser) — toàn hàng đồng bộ 14px.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070900), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL toolbar hàng tool — font đồng bộ text-sm (14px).
### [2026-08-07 09:05] Task: Tăng chiều rộng input target velocity (Vel Comp)
- **Yêu cầu user:** tăng chiều rộng scrollbox/input của Vel Comp để chứa đủ số velocity (127).
- **FIX (app.jsx):** input `velocityTarget` (bên cạnh nút Comp) — `w-9``w-14` (đủ 3 chữ số 0-127).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070905), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL → input target (cạnh Comp) rộng hơn, hiện đủ "127".
### [2026-08-07 09:10] Task: Ctrl+C/X trong PIANO ROLL — CHỈ copy/cắt notes được chọn
- **Yêu cầu user:** Ctrl-X/Ctrl-C trong PIANO ROLL TAB chỉ cắt/copy các NOTES ĐƯỢC CHỌN.
- **FIX (app.jsx keydown 7106):** Ctrl+C — copy `notes.filter(selectedNoteIds)` (toast số nốt chọn; không chọn → toast "Chọn notes trước khi copy"); Ctrl+X — copy + xóa notes chọn (filter bỏ selectedNoteIds); Ctrl+V giữ nguyên (paste clipboard).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070910), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — chọn 1 số notes → Ctrl+C → Ctrl+V paste đúng notes chọn; Ctrl+X chỉ cắt notes chọn; không chọn → toast nhắc.
### [2026-08-07 09:15] Task: Ctrl+drag marquee chọn notes → Ctrl+X báo "Chọn notes trước khi cắt"
- **Báo cáo user:** Ctrl+drag marquee chọn notes → Ctrl+X → lỗi "Chọn notes trước khi cắt".
- **Nguyên nhân:** marquee mousemove (8018-8033) ĐÃ chọn notes live (setSelectedNoteIds) — NHƯNG keydown handler (Ctrl+C/X — 7106) deps thiếu `selectedNoteIds` → closure STALE (selectedNoteIds rỗng từ render trước) → filter = [] → toast lỗi.
- **FIX (app.jsx):** (a) thêm `selectedNoteIds` vào deps effect keydown 7106; (b) bổ sung — handleGridMouseUp khi kết thúc marquee → chọn lại notes trong vùng (đảm bảo dù không qua mousemove lần cuối).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070915), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — Ctrl+drag marquee (notes highlight) → Ctrl+X → cắt đúng notes trong vùng; Ctrl+C → copy.
### [2026-08-07 09:20] Task: Ctrl+V trong PIANO ROLL hiện prompt "Nhập Gain điều chỉnh (dB)"
- **Báo cáo user:** nhấn Ctrl-V trong PIANO ROLL TAB → browser prompt "Nhập Gain điều chỉnh (dB):" — cần remove trong piano roll tab.
- **Nguyên nhân:** sub-tab keydown handler (16590) bắt `e.key === 'v'` KHÔNG kiểm tra Ctrl → Ctrl+V cũng trúng → prompt gain (fade gain shortcut 'v' đơn).
- **FIX (app.jsx):** thêm `!ctrl && !e.metaKey && !e.altKey` — chỉ 'v' đơn (không modifier) mới mở prompt; Ctrl+V → chạy paste notes piano roll (handler 7106).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070920), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — Ctrl+V → paste notes (KHÔNG prompt); nhấn 'v' đơn → prompt gain như cũ (sub-tab audio).
### [2026-08-07 09:25] Task: Ctrl+V dán notes tại vị trí con trỏ playhead
- **Yêu cầu user:** Ctrl-V dán các notes đã cut/copy bắt đầu ở vị trí con trỏ playhead đang đứng.
- **FIX (app.jsx keydown 7106 — Ctrl+V):** `pasteBeat = st.currentTime / (60/bpm)` (playhead → beat) — `offset = pasteBeat - minStart(clipboard)` — dán với `start_beat = n.start_beat + offset` (note đầu tiên nằm đúng playhead; clamp ≥ 0).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070925), `wiki.md`. Rebuild precompiled (build PASS — pasteBeat ×2).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — di chuyển playhead → Ctrl+V → notes dán bắt đầu đúng vị trí playhead.
### [2026-08-07 09:30] Task: PIANO ROLL — Shift+click toggle chọn; Ctrl+click+drag COPY nhanh; drag move pitch
- **Yêu cầu user:** (1) Ctrl+Click → Shift+Click để thêm/bớt note vào nhóm chọn; (2) Ctrl+Click giữ + drag → copy nhanh nhóm notes/note đến vị trí mới (cùng pitch — drag đổi vị trí/pitch); (3) drag note/nhóm → vị trí + pitch khác.
- **Trạng thái:** (3) ĐÃ CÓ (7895-7922 — mode 'move' — deltaBeat + deltaPitch).
- **FIX (app.jsx PianoRollTabEditor handleGridMouseDown):**
(1) toggle selection: `e.ctrlKey``e.shiftKey` (Shift+click thêm/bớt note).
(2) Ctrl+click TRÊN NOTE → clone notes (nhóm chọn nếu note trong nhóm, ngược lại note đơn) + setDraggedNote mode 'move' (selectedNotesOffset + clickedOriginalStartBeat) → kéo clones đến vị trí/pitch mới (gốc giữ) — mousemove move có sẵn.
(3) Ctrl+click empty → marquee (giữ); Ctrl+Shift+click → split (giữ).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070930), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — Shift+click thêm/bớt note chọn; Ctrl+giữ+click note → kéo → copy nhóm đến vị trí mới; drag note (không modifier) → move vị trí + pitch.
### [2026-08-07 09:35] Task: FIX Ctrl+click+drag copy — notes không bám vị trí con trỏ
- **Báo cáo user:** ctrl-click-drag — notes không được copy ngay tại vị trí con trỏ chuột.
- **Nguyên nhân:** copy-drag set `startOffsetBeat: beat` (vị trí chuột) — move mousemove tính `deltaBeat = snap(beatMouse - startOffsetBeat) - refOrigStart` → delta bị lệch (Δ=0 → -noteStart → clones nhảy về beat 0 / không bám chuột). Move thường dùng `startOffsetBeat = beat - clickedNote.start_beat` (offset trong note).
- **FIX (app.jsx):** copy-drag → `startOffsetBeat: beat - clickedNote.start_beat` (giống move thường) → kéo → delta = snap(Δ) → clones bám chuột (vị trí + pitch).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070935), `wiki.md`. Rebuild precompiled (build PASS — 2 chỗ đồng bộ).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — Ctrl+click note → kéo → các bản copy theo đúng vị trí con trỏ (beat + pitch).
### [2026-08-07 09:40] Task: PIANO ROLL — (1) nút track column: active ghost → main + MUTE (2) follow playhead (3) status hint động
- **Yêu cầu user:**
(1) Cột trái (nút tên track): click → active ghost notes của track đó thành MAIN notes để chỉnh sửa; cuối nút thêm nút MUTE (mặc định MUTE — unmute → play ghost cùng main notes — chức năng tương tự nút toggle cũ).
(2) Khi play → playhead di chuyển GIỮA piano roll, notes trôi sang trái; stop → playhead về ĐẦU (luôn hiển thị trong view).
(3) Status bar gợi ý: mouse trong piano roll → "Scroll: Up/Down | Drag: Draw notes"; giữ Shift → "Shift+Click: select/unselect..."; giữ Ctrl → "Ctrl+drag: fast copy notes...".
- **FIX (app.jsx):**
(1) Track column (9270): nút tên track → `handleSwitchMidiItem(m.id)` (mở item — chỉnh sửa main notes); thêm nút MUTE (w-6 — mặc định 'M' zinc, unmute '♪' green — toggle activePlayTrackIds + onRealtimePlay).
(2) Effect follow: `[isPlaying, st.currentTime]` — play → `scrollLeft = phBeat*ppb - clientWidth/2` (playhead giữa, notes trôi trái); stop → `scrollLeft = 0` (về đầu).
(3) App state `prHint` + `window.__setPrHint`; piano roll: `prKeyStateRef`/`prMouseInRef` + keydown/keyup + container onMouseEnter/Leave → set hint theo Ctrl/Shift; status bar hiển thị `prHint` (cyan) thay "Scroll: Zoom".
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070940), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — click tên track khác → mở item đó (sửa được); nút M — click → ♪ xanh (play cùng main); play → playhead giữa + trôi trái; stop → về đầu; hover trong piano roll → status bar đổi gợi ý theo Shift/Ctrl.
### [2026-08-07 09:45] Task: (1) Cân bằng tên track cột trái (2) Xóa nhãn "Ctrl+Scroll: Playhead" cố định
- **Yêu cầu user:** (1) sửa chiều cao nút tên track cột trái — tên nút cân bằng giữa nút; (2) xóa nhãn cố định "Ctrl+Scroll: Playhead" cuối status bar — để gợi ý adaptive hiển thị đủ nội dung.
- **FIX (app.jsx):**
(1) Nút tên track: `h-[20px]``h-[26px]` + `flex items-center justify-center` + text `text-[13px] leading-none`; nút MUTE đồng bộ `h-[26px]` + flex center.
(2) Status bar: xóa span "|" + span "Ctrl+Scroll: Playhead" (icon keyboard) — còn `prHint` (cyan) hoặc "Scroll: Zoom" — gợi ý adaptive có đủ chỗ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070945), `wiki.md`. Rebuild precompiled (build PASS — Ctrl+Scroll: Playhead = 0).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — tên track nằm cân bằng giữa nút (26px); status bar cuối không còn "Ctrl+Scroll: Playhead" — gợi ý adaptive hiển thị đầy đủ.
### [2026-08-07 09:50] Task: FIX Follow playhead chưa hoạt động — dùng sai isPlaying (main)
- **Báo cáo user:** follow playhead (0940) vẫn chưa thực hiện được.
- **Nguyên nhân:** effect dùng prop `isPlaying` = MAIN play — piano roll play chỉ set `st.isPlaying` (main vẫn false — handlePlayPause sub-tab stopAllPlayback) → effect luôn rơi nhánh "stop" → scroll về đầu mãi.
- **FIX (app.jsx effect follow):** `const playing = isPlaying || !!st.isPlaying` — follow khi piano roll play LẪN main play; deps thêm `st.isPlaying`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070950), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL → play (tab) → playhead ở giữa + notes trôi trái; stop → về đầu.