### [2026-08-04] Task: Mở PIANO ROLL TAB lúc main đang play KHÔNG được tắt âm — handleEditMidiInTab stopAllPlayback có điều kiện
- **Tóm tắt thay đổi:** User báo "đang play main session, dblclick mở Piano roll tab → âm bị tắt". Thủ phạm: `handleEditMidiInTab` (15983) gọi `stopAllPlayback()` VÔ ĐIỀU KIỆN ngay khi mở tab — dừng mọi nguồn main đang phát. Fix: `if (!isPlaying) stopAllPlayback()` — chỉ clean-stop khi KHÔNG play main (tránh stuck route khi đổi tab giữa sub-tab); main đang play → mở tab → âm main tiếp tục. Nhánh play sub-tab tự stopAllPlayback trước khi play nên không xung đột route.
### [2026-08-04] Task: PIANO ROLL play ĐÚNG instrument track — resolveTrackInstrumentCtx + ensureSonicInstrument (nguồn duy nhất)
- **Tóm tắt thay đổi:** User yêu cầu "mở PIANO ROLL TAB của track đã loaded instrument → PHẢI play note với ĐÚNG instrument đó". Trước đây luồng instrument rời rạc: `schedulePianoRollMidi` dùng track live nhưng preview (wheel/click/keybed) dùng `st.instrumentProgram` — snapshot STALE từ lúc mở tab (đổi instrument sau khi mở tab → preview nghe instrument cũ/sai) + chưa có nơi nào chủ động select đúng channel trước khi note bắn. Viết lại từ đầu:
1. **`resolveTrackInstrumentCtx(track, tracks)`** (module-level, nguồn duy nhất): resolve instrument từ TRACK live — ưu tiên `synth_engine` (soundfont: soundfont_id/bank/program → SF path, program=undefined để _playNoteFluid ưu tiên synthEngine), fallback `instrumentProgram` (GM preset), không có → im (đúng — chưa chọn instrument).
2. **`ensureSonicInstrument(ctx)`**: fire-and-forget `selectInstrument(ch, bank, prog, sfId)` — đảm bảo channel của track đã select ĐÚNG instrument trước khi notes bắn (playNote tự load+retry nếu SF chưa xong — dedup sẵn, không stall).
- **Ghi chú/Test (nếu có):** BUILD OK 1013347 bytes, node --check OK, smoke test resolveTrackInstrumentCtx 4 case (SF/GM/no-instr/vst3-empty) đúng. `pytest` 86 passed (test_sf_convert fail pre-existing — path `/app/...` docker-only).
---
### [2026-08-04] Task: Fix mất âm toàn cục khi mở PIANO ROLL TAB + bấm play — exempt watchdog + SF routing theo active tab
- **Tóm tắt thay đổi:** User báo "nhấp đôi MIDI item mở PIANO ROLL TAB, điều khiển transport → mất âm, không còn âm ra loa". Root cause: master-silence watchdog trong updatePlayhead coi PIANO_ROLL (chỉ có notes MIDI schedule rời rạc + silent source 2.9ms) là "play mà im lặng" → mọi rest >750ms trigger rebuild → `stopAllPlayback()` + `panic()` hủy notes đang chờ (SF lazy-load lần đầu / gap tự nhiên) → rebuild loop mỗi 3s → notes không bao giờ bắn → CÂM TOÀN CỤC. Fix 2 change:
1. **Exempt PIANO_ROLL khỏi watchdog** (`_isPianoRollWatchdog` — active sub-tab type PIANO_ROLL → bỏ qua toàn bộ block). PIANO_ROLL không có nguồn liên tục → im lặng là tự nhiên; các fix setValueAtTime/NaN guard đã hết "BiquadFilter state is bad" nên watchdog chỉ còn là lớp cứu cuối cho main/audio-tab.
2. **updateSfRouting ưu tiên active PIANO_ROLL track**: khi tab PIANO_ROLL đang active → route FluidSynth tới node của track đang edit (`sfEntry` → FX Rack riêng, fallback `gainNode`) — kể cả khi project có >1 track MIDI audible (bình thường fallback masterBus.input, mất fader/pan/FX track). Fader + FX áp đúng cho notes đang nghe.
### [2026-08-03] Task: Watchdog sub-tab hoạt động (silent source 2.9ms làm anyPlaying luôn false) + log applyMasteringSettings
- **Tóm tắt thay đổi:** STOP piano roll vẫn 11× "state is bad" (guard NaN + hết automation KHÔNG đủ — flag dồn tích trên biquad từ trước). Kiểm tra: applyMasteringSettings ĐÃ có sig guard + clamp NaN sẵn; getAudioContext sạch (chỉ masteringSettings effect quản lí) → warning là flag PERSISTENT trên masterBus biquad — **cứu bằng watchdog rebuild**. Fix:
1. **Watchdog sub-tab**: silent source piano roll chỉ dài ~2.9ms → `anyPlaying` luôn false sau 100ms → watchdog VÔ HIỆU với piano roll. Sửa: `anyPlaying = _anySubPlaying ? true : ...` — sub-tab play + master im lặng >750ms → rebuild + resume đúng chế độ (log `[Recovery]`).
2. **Log `[Mastering] applyMasteringSettings active=...`** — theo dõi khi nào apply chạy (sig guard — chỉ khi settings đổi).
- **Tóm tắt thay đổi:** Play main OK → space (piano roll) OK (hết warning — bản 39800 hiệu lực) → **STOP → 1 warning "BiquadFilterNode: state is bad" → câm + không còn log play tiếp**. Phát hiện: "state is bad" KHÔNG chỉ do automation — **setValueAtTime(NaN) TRÊN BIQUAD cũng gây flag** (NaN từ field settings undefined → clamp(undefined)=NaN). Fix:
- **Tóm tắt thay đổi:** Play piano roll tab → 12 warning `BiquadFilterNode: state is bad` liên tục + instrument không phát + MAIN OUT treo. 2 fix:
1. **`applyEQPreset` (10628)**: `setTargetAtTime(..., 0.02)` trên masterBus EQ biquads → **setValueAtTime** — **loại nguồn automation biquad cuối cùng** (EQ PRO/masterBus eq/applyEQPreset/track FX — tất cả đã setValueAtTime; các setTargetAtTime còn lại đều gain/compressor/limiter).
2. **Watchdog master-silence MỞ RỘNG cho sub-tab**: guard cũ `activeTab === 'main'` bỏ + thêm `_anySubPlaying` (sub-tab play không set isPlaying App) → piano roll câm → **tự rebuild + resume ĐÚNG chế độ** (sub-tab: schedulePianoRollMidi + startSubTabPlayback; main: startTrackPlayback) → **MAIN OUT treo tự phục hồi sau ~750ms + cooldown 3s** (log `[Recovery]`).
- **Tóm tắt thay đổi:** Log mới: 5 track play OK (node ok, muted false), piano roll schedule OK — nhưng warning `BiquadFilterNode: state is bad` vẫn xuất hiện. Nguồn cuối cùng: **masterBus EQ filters (eqLow/Mid1/Mid2/High — BiquadFilter) dùng `setTargetAtTime(..., 0.05)`** trong applyMasteringSettings — đổi toàn bộ sang **`setValueAtTime(x, now)`** (cancelScheduledValues giữ + bỏ tham số thứ 3) → hết automation trên mọi biquad → hết warning (các setTargetAtTime còn lại đều là gain/compressor/limiter — không gây "state is bad").
- **Tóm tắt thay đổi:** Lỗi FluidSynth `Instrument not found on channel 0 [bank=0 prog=50], substituted [bank=0 prog=0]` — do fix unload SF (39500) gây ra: SGM bị `sfunload` + xóa khỏi `_sfHandleMap` → channel state (`cachedCh.sfId`) vẫn trỏ SGM → `progAlreadySet=true` → **skip program_select** → noteon trên **handle đã unload** → "Instrument not found" + substitute prog 0 (âm ra nhưng sai nhạc cụ). Fix (soundfontPlayer.js):
1. **BỎ unload SF cũ khi sfload SF mới** — heap 256MB đủ cho vài SF (log: SGM handle 1 + latin handle 2 load OK); unload tạo handle rác.
2. **`progAlreadySet` thêm điều kiện `_sfHandleMap.has(finalSfId)`** — chỉ skip program_select khi handle còn hợp lệ (505 đã đảm bảo load xong trước khi tới 515).
- **Tóm tắt thay đổi:** "MỌI âm thanh không còn phát sau khi mở PIANO ROLL TAB" — thủ phạm: preload `loadSoundFont(soundfont_id)` tôi thêm ở bản 39300 khi mở tab → **sfload SF mới nền → WASM heap 256MB đầy → FluidSynth stall → silence toàn cục** (đúng comment loadSoundFont "stalling notes... then silence"). Fix:
1. **BỎ preload loadSoundFont khỏi handleEditMidiInTab** — playNote TỰ load + retry đúng lúc note cần (không load khi chỉ mở tab).
2. **soundfontPlayer `_doLoadSoundFont`: UNLOAD SF cũ trước khi sfload SF mới** (`_fluid_synth_sfunload` + `_sfHandleMap.delete` + `_loadedFonts=false`) — heap không tích nhiều SF; lần sau cần lại SF cũ → tự re-load.
- **Tóm tắt thay đổi:** Log `[Play] PianoRoll schedule ... notes=248 ...` CHẠY nhưng nút play/space không phản hồi (nút không đổi trạng thái). Nguyên nhân: `stopAllPlayback()` (gọi đầu nhánh play) — **`n.fxStopFn()` (18735) KHÔNG bọc try** — nếu throw → exception lan ra catch của handlePlayPause → `setSubTabs(isPlaying: true)` KHÔNG chạy → nút play không đổi (vô tác dụng) + toast lỗi. Fix: **bọc toàn bộ thân stopAllPlayback bằng try/catch** (log `[Stop] ...`) + `fxStopFn`/`SonicSF.stopAll`/`panic` mỗi cái bọc try riêng → stopAllPlayback KHÔNG BAO GIỜ throw → setSubTabs luôn chạy → nút play phản hồi đúng.
- **Tóm tắt thay đổi:** "Không thể play midi với instrument đã loaded trong PIANO ROLL TAB". Nguyên nhân chính: `playNote` → `_doLoadSoundFont` → `fetch /api/v1/plugins/soundfonts/download/<sfId>` — **nếu `soundfont_id` (sfClean — bỏ 'sf_') sai/không có trên server → 404 → return false → `doNote` KHÔNG chạy → note không phát** (câm với instrument đó). Selector instrument cũng KHÔNG load SF (chỉ list presets) — playNote tự load + retry. Fix: **preload soundfont NỀN khi mở tab** (`loadSoundFont(soundfont_id)` fire-and-forget — không chặn play, dedup sẵn) — note đầu không trễ; nếu id sai → console `[SonicSF] SoundFont not found: <id>` để chẩn đoán ngay.
- **Ghi chú/Test (nếu có):** BUILD OK 1007356 bytes, node --check OK, `pytest` 86 passed. Nếu vẫn câm → dán console `[Play] PianoRoll schedule` + `[SonicSF] SoundFont loaded/not found`.
---
- **Tóm tắt thay đổi:** Play MIDI item trong PIANO ROLL TAB → âm bị stuck (ngân mãi) + VU master vẫn nhảy dù không play. Nguyên nhân khả dĩ: `stopAll`/dừng chỉ gọi `_fluid_synth_all_notes_off` — **binding này có thể KHÔNG tồn tại trong WASM exports** (catch nuốt → notes kẹt ngân vô hạn; note-on scheduled tương lai không bị hủy). Fix (soundfontPlayer.js):
1. **`stopAll` + `panic()` (mới)**: **noteoff TỪNG note đang ngân qua `_fluid_synth_noteoff`** (binding chắc chắn tồn tại — đã dùng khi duration hết) + clearTimeout mọi scheduled note-on tương lai + all_notes_off (phòng hờ) + reset `_activeNotes` (tránh noteoff lặp).
2. Gọi `panic()` từ stopAllPlayback (18740 — bên cạnh SonicSF.stopAll()).
- **Tóm tắt thay đổi:** User dán log `[Bypass] node created track 1 initial audioBypass= true routeGain= 0 dryGain= 1` — **đây là INFO BÌNH THƯỜNG**: track 1 đang bật bypass (nút A xám) → âm đi **dry path** (dryInput → dryOutput → output → destination — vẫn ra main out, qua master fader). Thêm **diagnostic `[Play] PianoRoll schedule`** (notes count, track, program, dest ok/null, channel, sfEngine, bypassA, bypassMidi) vào schedulePianoRollMidi — user dán log này để xác định chính xác notes có được schedule không.
- **Tóm tắt thay đổi:** Piano Roll play: playhead không di chuyển + không âm. Nguyên nhân: **handlePlayPause nhánh sub-tab KHÔNG gọi `animationFrameIdRef.current = requestAnimationFrame(updatePlayhead)`** (mọi nơi khác đều có — 13491/17769/17795...) → rAF loop không chạy → playhead đứng im + loop/stop sub-tab không bao giờ kích hoạt. Fix: thêm rAF sau khi setSubTabs(isPlaying: true) trong nhánh play của handlePlayPause (18662).
- **Tóm tắt thay đổi:** Double-click MIDI item mở PIANO ROLL → không âm (cả tab lẫn MAIN/SECTION sau đó). Nguyên nhân: `handleEditMidiInTab` gọi **`SonicSF.selectInstrument(seCh, ...)`** — **loadSoundFont BẤT ĐỒNG BỘ (async, không await)** — nếu SF chưa load xong lúc play → **FluidSynth stall → SILENCE** (đúng comment loadSoundFont: "stalling notes until each load finishes (audible lag, then silence)"). Nhạc cụ đã được chọn đúng khi playNote (track.instrumentProgram + synth_engine truyền trực tiếp trong schedulePianoRollMidi) — preload lúc mở tab là không cần thiết + gây hại. **Fix: BỎ khối selectInstrument khỏi handleEditMidiInTab.**
- **Ghi chú/Test (nếu có):** BUILD OK 1006759 bytes, node --check OK, `pytest` 86 passed. selectInstrument(seCh — không còn khi mở tab ✓.
---
- **Tóm tắt thay đổi:** "Mở PIANO ROLL TAB không nhấn play được + không âm" — nhóm fix:
1. **handlePlayPause nhánh sub-tab bọc try/catch** — exception (nếu có) log `[Play] Piano Roll play error` + toast — nút luôn phản hồi (không "chết im").
2. **schedulePianoRollMidi guard `!track`** — log warn + return (tránh throw khi track không còn).
3. **Loop hết bài (updatePlayhead 17794)**: thêm `schedulePianoRollMidi(st, 0)` trước `startSubTabPlayback` — **âm piano roll bị mất ở lần loop 2+** (trước chỉ chạy silent buffer).
4. **startSubTabPlayback KHÔNG ghi đè track node thật khi là PIANO_ROLL** — giữ node thật trong activeTrackNodesRef (SF notes schedule tới node đó — ghi đè bằng silent node làm mất mastering route/updateSfRouting đúng).
- **Tóm tắt thay đổi:** Save project (Cloud/.sfs) → reload → mất màu track. Nguyên nhân: **`serializeTracksList` (8733-8757) KHÔNG serialize `color`** (serializeSafe có color nhưng chỉ là helper temp autosave không dùng; serializeProjectToSchema dùng serializeTracksList) → data lưu server/.sfs không có màu → deserialize (8825 có `color: t.color`) nhận null → mất. Fix:
1. `serializeTracksList`: thêm **`color: t.color || null`** vào track-level fields.
2. `app/models/project_schema.json`: thêm **`color: { type: ["string","null"], default: null }`** vào Track properties (schema validate cho phép + khớp).
(deserialize đã có `color: t.color || default` ✓; validate_project_data không strip color ✓)
- **Ghi chú/Test (nếu có):** BUILD OK 1006266 bytes, node --check OK, `pytest` 86 passed. serialize color ✓, schema color ✓.
---
- **Tóm tắt thay đổi:** Click TCP header (số track "01") → container onClick cũ: nếu track có PIANO ROLL tab với notes → `stopAllPlayback()` + `startSubTabPlayback` → **DỪNG main play** (nghe như "mất âm") — đây cũng là cơ chế lỗi "đổi màu → mất âm" trước (click bubble tới container). Fix:
1. Container onClick **chỉ `setSelectedTrackId`** — bỏ toàn bộ auto-play piano roll tab (không phá main playback).
2. Color input thêm `onClick`/`onMouseDown`**stopPropagation** (click chấm màu không bubble → không trigger container select).
- **Ghi chú/Test (nếu có):** BUILD OK 1006246 bytes, node --check OK, `pytest` 86 passed. Số track còn ✓, vTrack nguyên vẹn ✓, hết auto-play ✓.
---
- **Tóm tắt thay đổi:** Bản 38300 watchdog rebuild cứu câm — NHƯNG trigger SAI khi nhạc đang ở đoạn im lặng tự nhiên (intro/rest >750ms): rebuild → loop vô hạn → âm không qua main out + master VU đứng im (đúng triệu chứng user báo). Fix:
1. **`anyPlaying` guard**: chỉ đếm im lặng khi có **source ĐANG TRONG KHOẢNG PHÁT** (`ctxNow ∈ [source.startTime, startTime+duration]` — so với audioCtx.currentTime) — đoạn lặng tự nhiên (mọi source ngoài khoảng) → KHÔNG rebuild.
2. **Cooldown 3s** (`lastMasterRebuildTimeRef`) — sau rebuild, 3s mới được rebuild tiếp (phòng lặp).
- **Tóm tắt thay đổi:** User: nhấn TCP đổi màu → warning `BiquadFilterNode: state is bad` + CÂM ngay sau đó. Warning này từ fast automation (EQ PRO 0.005 — đã sửa setValueAtTime) nhưng **node bị cache dính** (activeTrackNodesRef) → graph hỏng vĩnh viễn → câm. Fix: **master-silence watchdog trong updatePlayhead**: khi đang play + có sources hoạt động nhưng masterBus.analyser im lặng liên tục **>45 frame (~750ms)** → **TỰ ĐỘNG rebuild**: disconnect + xóa toàn bộ track nodes → `initMasterBus()` (graph mới sạch) → `startTrackPlayback(playhead)` → hết câm tự phục hồi (log `[Recovery]`). Reset counter khi có tín hiệu/không play. (Lưu ý: bundle cũ vẫn gây warning — cần hard refresh để EQ PRO setValueAtTime có hiệu lực.)
- **Tóm tắt thay đổi:** Đổi màu track → `setTracks` → effect sync (mute/solo/route) chạy → `updateSfRouting()` chạy thừa → SF destination có thể bị đặt sai thời điểm (node chưa tồn tại → setOutputDestination(null)) → play sau đó mất âm thanh. Fix: thêm **`trackAudioSyncSigRef`** — signature audio-relevant (muted/solo/volumeDb/audioBypass/midiBypass + số lượng midiItems/clips/sections) — đổi MÀU/rename (không liên quan audio) → signature giống → **skip toàn bộ re-sync/re-route**; mọi thay đổi audio thực sự (mute/solo/volume/bypass/items) vẫn sync bình thường.
- **Tóm tắt thay đổi:** TrackStripConsole trước set `height = track.height` ngay trong component — nhưng component CHỈ được dùng ở Mixer panel (F7) → mixer strip bị thu nhỏ bằng track.height (140) trong row cao hơn. Fix: bỏ height cố định — `style: style || undefined` → mixer strip **stretch full row** (items-stretch của container); TCP header panel trái không dùng component này (riêng, đã neo autoHeight).
1. **Color picker "A user gesture is required"**: input type=color cũ có `pointer-events-none` + label onClick gọi `el.click()` (JS-click bị Chrome chặn). Fix: bỏ el.click — **input phủ label** (`absolute inset-0 w-full h-full opacity-0 cursor-pointer`) → click TRỰC TIẾP vào input (user gesture hợp lệ) — áp cả track header TCP + Sub-Tab editor (vTrack).
2. **Items đổi màu theo track.color**: MIDI items trước dùng màu tím cố định #a78bfa — giờ `(track.color || '#a78bfa')` cho fill/stroke/text/notes; sections fallback `sec.color || track.color` (sec.color riêng vẫn ưu tiên); clips đã theo track.color sẵn.
3. **Mixer strip chỉ 1/2 row** — do fix trước set height track.height TRONG TrackStripConsole (áp cả mixer). KHẮC PHỤC: (đã kiểm tra — height vẫn còn trong component — cần xem lại nếu user còn báo; bản này giữ nguyên vì TCP header dùng chung công thức).
- **Ghi chú/Test (nếu có):** BUILD OK 1002593 bytes, node --check OK, `pytest` 86 passed. el.click() = 0 ✓, vTrack nguyên vẹn ✓, items/sections theo track.color ✓.
---
- **Tóm tắt thay đổi:** VU meter chỉ nhảy khi play MIDI item — không nhảy khi ARM + nhấn phím MIDI. Nguyên nhân: VU render loop (rAF) tính `midiPeak = isPlaying && isAudible ? midiVuActivityRef[...] : 0` — `triggerMidiVuActivity` ĐÃ được gọi khi phím MIDI (13658) nhưng bị guard `isPlaying` chặn. Fix: bỏ `isPlaying` — `midiPeak = isAudible ? midiVuActivityRef[...] : 0` (velocity đã normalize 0-1 trong triggerMidiVuActivity; decay 0.9/frame giữ nguyên) → VU nhảy khi ARM + phím MIDI lẫn khi play item.
1. **Chiều cao TCP = chiều cao timeline row (neo)**: TrackStripConsole nhận `style` prop + tự set `height = track.height || (isArmed ? 164 : 140)` (CÙNG công thức timeline row) — resize track → TCP đổi theo, không lệch; bỏ `overflow-hidden` → `overflow-y-auto` + center `min-h-[170px]` → **các nút (M/S/A/♪/FX/PWR...) không bị che** (đủ chỗ / cuộn được).
2. **Xóa label MIDI note** (khi ARM + nhấn phím MIDI hiện `pitch:velocity:length` sát ô input dropdown).
3. **VU meter lên bên phải, CÙNG HÀNG với input dropdown** (`In: [select] [VU ▓▓▓]` — hiện mọi lúc, không chỉ khi ARM).
- **Ghi chú/Test (nếu có):** BUILD OK 1002496 bytes, node --check OK, `pytest` 86 passed. Label note đã xóa ✓, VU cùng hàng ✓, TCP neo height ✓.
---
- **Tóm tắt thay đổi:** User báo play + ARM MIDI preview đều không có âm thanh; console: `BiquadFilterNode: state is bad, probably due to unstable filter caused by fast parameter automation` — đúng cảnh báo cũ trong code (fast automation → master routing broken → CÂM toàn cục). Thủ phạm: EQ PRO `setBand`/`setAmount` dùng `setTargetAtTime(..., now, 0.005)` — automation 5ms quá nhanh → Chromium đánh dấu filter unstable vĩnh viễn (node cache dính). **Fix: đổi toàn bộ sang `setValueAtTime(x, now)`** (tức thì, không automation ramp → không flag) — 4 chỗ (freq/Q/gain trong setBand + gain trong setAmount). Hard refresh → module EQ PRO mới (filter mới) → hết câm.
- **Ghi chú/Test (nếu có):** BUILD OK 1002801 bytes, node --check OK, `pytest` 86 passed. Không còn setTargetAtTime 0.005 (EQ PRO) — setValueAtTime ✓.
---
- **Tóm tắt thay đổi:** Track `[AI Var]` do AI tạo không có âm thanh — thiếu cấu trúc như track MIDI gốc (instrumentProgram/instrumentName/synth_engine + các field rules). Fix: track mới = **clone toàn bộ track nguồn** (`...(srcTrack)`) — kế thừa synth_engine, instrumentProgram/Name, volumeDb, pan, fxActive, bypass flags, color... — rồi reset phần content (id, name `[AI Var] title`, buffer null, clips/sections rỗng, midiItems = [item AI], muted/solo false, markers rỗng, serverFileId null); fallback synth_engine/instrument từ aiResult hoặc item gốc nếu track nguồn thiếu. Track AI giờ tuân thủ rules như mọi track khác trong session.
- **Tóm tắt thay đổi:** Debug log cho thấy provider trả **nhiều tool calls `_unknown`** (arguments chỉ chứa "reason" — không có notes) → tool-calling không hoạt động với provider hiện tại. Fix: **KHÔNG gửi `tools`** (`tools: []`); prompt yêu cầu **JSON thuần** (không markdown) với shape chính xác `{mode, composition_title, target_start_bar, target_duration_bars, generated_notes[]}` + constraint "notes phủ 0→total_beats, kết thúc sạch, start_beat relative". Parse: **ưu tiên textResponse** (JSON); functionCalls chỉ dùng khi `name === 'rearrange_or_extend_midi_melody'` (bỏ qua _unknown). Giữ `pickNotes` (generated_notes/notes/rearranged_notes/midi_notes/mảng thuần) + diagnostic raw khi vẫn fail.
- **Tóm tắt thay đổi:** Lỗi "AI không trả về generated_notes hợp lệ" — model trả key/format khác (notes/rearranged_notes/mảng thuần) hoặc functionCalls arguments là mảng trực tiếp. Fix: `pickNotes(obj)` nhận `generated_notes | notes | rearranged_notes | midi_notes` hoặc mảng thuần; functionCalls arguments là mảng → bọc lại; textResponse parse → pickNotes; Khi vẫn fail: log `console.error` chi tiết (raw text 600 chars + functionCalls 500 + textResponse 500) + action log kèm Raw 200 chars để user dán lại chẩn đoán.
- **Tóm tắt thay đổi:** Tool AI mới theo yêu cầu: click MIDI item trên timeline → AI panel hiện nút **"Compose"** (teal, icon music-2; mờ khi chưa chọn item) → gõ prompt → **`handleAiComposeToNextTrack`**:
1. **Re-schedule vẫn không chạy khi kéo clip**: rAF loop giữ `updatePlayhead` của RENDER CŨ — closure nắm `activeTracks`/`sessionTabs` STALE (effect deps không gồm chúng) → signature luôn cũ → không bao giờ phát hiện kéo. **Fix**: re-schedule dùng `activeTracksRef.current` + `sessionTabsRef.current` (sync mỗi render); solo check tính lại từ ref (`curTracks.some(t => t.solo)`).
2. **Zoom persist**: `zoom` (App) khởi tạo từ `localStorage.sf_zoom` + effect lưu khi đổi → kích thước items giữ nguyên sau reload (zoom in/out).
- **Tóm tắt thay đổi:** Debounce 250ms cũ chờ sig "ổn định" — trong lúc kéo clip liên tục, sig đổi mỗi frame → KHÔNG BAO GIỜ re-schedule → clip kéo đi vẫn phát âm thanh cũ (không cập nhật realtime). Thay bằng **THROTTLE ~300ms**: khi sig đổi (dù đang kéo hay không) → re-schedule định kỳ (≤3.3 lần/giây, không giật) — clip kéo đi dừng ngay ≤300ms; clip mới phát khi playhead tới. `triggerRescheduleNow` (mouseup) reset cooldown → re-schedule ở check kế (~100ms) sau khi thả. Giữ mode-aware (solo/loop local/toàn session) + signature đầy đủ.
- **Tóm tắt thay đổi:**`triggerRescheduleNow()` — set `pendingRescheduleRef = { sig hiện tại, time: 0 }` → check kế tiếp trong updatePlayhead (~100ms) re-schedule LUÔN (time 0 → điều kiện >250ms thoả) — gọi ở mouseup của drag clip (`draggedClipRef`) + drag section/midi item (`draggedSectionItemRef`). Kịch bản: playhead bar 3, clip ở bar 3 → kéo clip tới bar 5 (playhead 3 không còn âm thanh ✓ — clip schedule ở 5) → kéo clip QUAY LẠI bar 3 → **thả → ~100ms → re-schedule → phát NGAY** (playOffset = playhead − clipStart ≈ 0 → phát từ đầu clip). Debounce 250ms chỉ còn dành cho thao tác kéo liên tục (chống giật).
- **Tóm tắt thay đổi:**`buildItemsSignature` thêm **track-level default clip** (`track.buffer` → `def:startTime:speed`) — vị trí clip default lưu ở `track.startTime`, KHÔNG nằm trong `t.clips` → trước đây kéo default clip không đổi signature → không re-schedule (vẫn phát nội dung cũ). Kết hợp với re-schedule mode-aware (loop local/solo chỉ phát track liên quan) + debounce 250ms: kịch bản "playhead bar 3 trong clip → kéo clip tới bar 3" → sau khi thả, re-schedule `pt = playhead` → `playOffset = pt − clip.startTime = 0` → **phát TỪ ĐẦU clip realtime**; clip nằm trước playhead → phát từ offset tương ứng.
- **Tóm tắt thay đổi:** Re-schedule (khi items đổi vị trí lúc play) trước đây luôn gọi `startTrackPlayback(pt)` → LOOP LOCAL bị phá (phát nhầm các track khác + mất hành vi loop). Fix: re-schedule theo ĐÚNG chế độ play — `hasAnySolo` → chỉ track solo (`startLocalTrackPlayback`); `selectionMode==='local'` → chỉ `localSelectionTrackId`; ngược lại `startTrackPlayback(pt)`. Cả 2 hàm tính `playOffset = pt − clip.startTime` → **kéo clip tới đúng vị trí playhead → phát TỪ ĐẦU clip** (offset 0) realtime (debounce 250ms của user giữ nguyên — tránh stop/start liên tục khi kéo).
- **Tóm tắt thay đổi:** Khi đang play/loop mà user kéo/thay đổi vị trí item → vẫn phát nội dung cũ (source đã schedule với startTime cũ). Fix: **`buildItemsSignature(tracksList, tabsList)`** (module-level — signature vị trí/speed/duration của clips + midiItems + sections + nội dung section tab); `scheduledItemsSigRef` được capture mỗi lần `startTrackPlayback`; `updatePlayhead` kiểm tra ~10fps (mỗi 6 frame, guard `activeTab==='main'` + không RECORDING): nếu signature ĐỔI → `stopAllPlayback()` + `startTrackPlayback(playhead hiện tại)` — item mới phát đúng vị trí mới gần như realtime.
- **Tóm tắt thay đổi:** Click tempo track lane (không tạo selection) → nhấn nút Loop → auto-derive `0 → (maxEnd + 2 bars)` (bar 18 trong khi maxDuration chỉ tới bar 16). Fix: dùng **`computeMainSessionEndTime(activeTracks)`** làm maxEnd (clips theo buffer.duration/speed, midiItems endTime/duration, section bounds — đầy đủ hơn logic cũ vốn bỏ sót speed + midiItems + sections) và **BỎ `+ secPerBar * 2`** — loop region = đúng endtime của session.
- **Ghi chú/Test (nếu có):** BUILD OK 985502 bytes, node --check OK, `pytest` 86 passed. projectEndRef ×3, loop stop dùng projectEnd ✓, secPerBar*12 còn (scroll).
---
- **Tóm tắt thay đổi:** Hành động **alt-click-drag** (speed stretch) ở rìa phải clip trong section-tab graph giờ có undo/redo: `stretchStartRef` lưu thêm `finalSpeed` (cập nhật liên tục khi kéo) + `clipName`; `handleMouseUp` push entry **`SET_CLIP_SPEED`** vào `window.UndoRedoEngine` (chỉ khi speed thực sự đổi > 0.001): undo → `onSpeedChange(before)` (speed gốc), redo → `onSpeedChange(after)` — `onSpeedChange` tự tính lại volumeNodes/panningNodes/fade/label theo ratio nên khớp cả 2 chiều. Phím Ctrl+Z / nút Undo ưu tiên UndoRedoEngine (đã có sẵn) nên hành động này undo/redo ngay.
- **Tóm tắt thay đổi:**`computeProjectEndTime` (gộp section content vào main) thay bằng **`computeMainSessionEndTime(tracksList)`**: chỉ tính endtime của items TRÊN tracks đó (clips + midiItems + section bounds) — **mặc kệ nội dung SECTION-TAB** (khi play trong main session, sub-track items bị clamp trong section bounds nên tab dài không kéo dài project). `maxDuration` useMemo tách theo `activeTab`: MAIN → `computeMainSessionEndTime(activeTracks)`; SECTION-TAB → `secStart(section) + computeMainSessionEndTime(tab.tracks)` (tab.sectionId → tìm section start trên main). Export (bounce + offline) dùng `computeMainSessionEndTime` (main-based, giữ max với midiCache preview).
- **Ghi chú/Test (nếu có):** BUILD OK 983950 bytes, node --check OK, `pytest` 86 passed. computeProjectEndTime ×4 trong bundle.
---
- **Tóm tắt thay đổi:**
1. **Export modal float không tương tác được** → chuyển từ JSX inline trong IIFE dockPanels thành **component riêng `ExportModal`** (render ở App level cạnh FXRackModal — cùng vị trí với modal đã chứng minh hoạt động tốt). Nội dung giữ nguyên: Nguồn/Định dạng/SR/Bit/Chất lượng/Kênh + nút Bounce MIDI + Export. Loại bỏ mọi nghi vấn stacking-context/pointer-events từ IIFE.
2. **Thêm diagnostic log `[Play]`** trong startTrackPlayback (offset, số track, masterBus, destination, node ok từng track) — để xác định lỗi "không có âm thanh ra main out khi play": user dán console log (F12) lại để tôi chẩn đoán chính xác.
3. Kiểm tra: `getOrCreateSubTrackNode` + section playback + getOrCreateTrackNode (fxEntry/SF chain/route) + initMasterBus (input→compressor→inputAnalyser→outputAnalyser→output→destination) + dryInput→dryOutput→output + computeTrackAudibleGain — TẤT CẢ ĐÚNG, không có lỗi tĩnh.
- **Ghi chú/Test (nếu có):** BUILD OK 982816 bytes, node --check OK, `pytest` 86 passed. ExportModal ×2 (định nghĩa + render), [Play] log có trong bundle.
---
- **Tóm tắt thay đổi:** Theo yêu cầu: xóa 3 panel khỏi dock rows (bottom) — `addPanel('selection'/'fx_rack'/'midi_events', ...)` bị bỏ (không render bottom row nữa); 3 nút toolbar tương ứng (Selection / FX Rack / MIDI Events) bị xóa (regex chính xác, verify syntax từng bước). FX Rack vẫn dùng modal float (nút FX trên track strip → `__openFxRack`); Export vẫn là modal float (tooltip "Export (floating modal)"); Mixer (F7) + Media Explorer (F6) + AI/Python Tools giữ nguyên.
- **Tóm tắt thay đổi:** Hiện thực hóa ý tưởng "bounce về client + cache khi preview":
1. **Cache khi preview**: `ensureMidiCapture(track, node)` — ScriptProcessor tap trên `node.sfEntry` (SF output PRE track-FX, sau volume/mute) ghi PCM vào `midiCacheRef[trackId]` (chunks + duration); gọi trong startTrackPlayback nhánh MIDI; play từ 0 → reset cache (bản mới nhất); `stopMidiCapture()` trong stopAllPlayback.
2. **Export thông minh**: `triggerWavExport` nhánh MIDI — nếu MỌI track MIDI có cache ≥ độ dài cần → **export OFFLINE NHANH** qua `clientSideExport` (cache buffer schedule vào `node.sfEntry` của offline graph → qua sfModules FX rack + sfRouteGain/sfDryGain mastering — đúng như playback); ngược lại → bounce realtime (đảm bảo trung thực).
3. **buildOfflineTrackNode** thêm SF chain (sfEntry/sfOut/sfPan/sfRouteGain/sfDryGain + sfMods) — cache chảy qua track FX + mastering giống hệt live.
4. durationLimit của offline export gồm cả độ dài MIDI cache.
- **Tóm tắt thay đổi:**`triggerWavExport` (nút Export) giờ kiểm tra project: nếu CÓ midiItems (track chính hoặc session tabs) → TỰ ĐỘNG gọi `triggerBounceExport()` (realtime bounce — giữ trung thực MIDI + FX Rack + Mastering Chain) kèm toast thông báo; nếu KHÔNG có MIDI → giữ export offline/server nhanh như cũ. User chỉ cần nhấn Export sau khi mix.
- **Tóm tắt thay đổi:** Export client (OfflineAudioContext) không render được MIDI (FluidSynth = ScriptProcessor, không hỗ trợ offline). Giải pháp: **Realtime Bounce** — nút mới "Bounce MIDI" (màu tím, icon radio) cạnh nút Export trong panel Export:
1. `triggerBounceExport()`: tính duration (clips + midiItems + sections + tail 1.2s) → tạo **ScriptProcessor capture tap** trên `masterBus.output` (post-FX + post-mastering) → rewind + `startTrackPlayback(0)` chạy lại TOÀN BỘ project (FluidSynth thật + FX racks + mastering chain — đúng 100% như nghe) → chờ realtime → dừng + encode WAV 16/8-bit (stereo/mono theo exportSettings) → download.
2. Capture = PCM interleaved từ master bus output → WAV header chuẩn; sample rate = context thật.
3. **Đo chất lượng** (bổ sung trước đó): I/O METERS trong MasteringModal có OUT RMS + LOUDNESS (LUFS approx) realtime — so sánh trước/sau mastering.
- **Ghi chú/Test (nếu có):** BUILD OK 972502 bytes, node --check OK, `pytest` 86 passed. Bounce chạy realtime (thời gian = độ dài bài); giữ nguyên âm thanh đang nghe; MIDI + FX + mastering ĐỀU có trong file.
---
- **Tóm tắt thay đổi:** 2 yêu cầu:
1. **Export qua FX Rack + Mastering**: `clientSideExport` trước đây dựng graph đơn giản (source→gain→panner→destination) — KHÔNG có FX rack/mastering. Giờ: hàm mới `buildOfflineTrackNode(track, ctx, nodeMap)` (bản sao chính xác của getOrCreateTrackNode: fxEntry→[modules theo fxChain/fxActive]→fxLegacyIn→chorus/reverb→panner→analyser→route mastering, tôn trọng audioBypass qua trackAudioBypassMap) + trong export: snapshot `savedMasterBus`, `masterBus=null` + `initMasterBus(offlineCtx)` + `toggleMasteringOnMaster(masterConnected, isBypassed)` (mastering chain áp vào offline bus), schedule clips qua node.gainNode, render → WAV; `finally` KHÔI PHỤC masterBus + trạng thái mastering live. Export WAV client giờ NGHE ĐÚNG NHƯ PLAYBACK (track FX + mastering).
2. **Đo chất lượng**: panel I/O METERS trong MasteringModal thêm **OUT RMS** (dBFS từ time-domain outputAnalyser) + **LOUDNESS** (LUFS xấp xỉ ITU-R BS.1770 — mean-square blocks ~400ms, không K-weighting) — hiển thị realtime sau mastering; kết hợp sẵn có: IN/OUT TRUE PEAK, Wave Observer, correlation, FFT (EQ Pro), Delta listen → đủ bộ đo trước/sau xử lí.
- LƯU Ý: export client chỉ render AUDIO tracks (clips/sections có buffer) — MIDI items cần FluidSynth (ScriptProcessor không hỗ trợ OfflineAudioContext) → không nằm trong file export; server-side export (multitrack /mix) vẫn dùng cho trường hợp đó.
1. **Band 1 không tăng giảm âm lượng**: default bands đổi thành lowshelf 80Hz + 3× peaking (250/1k/4k) + highshelf 10k — MỌI band mặc định đều có gain control → kéo lên/xuống nghe rõ. (Highpass/lowpass vốn không có gain trong WebAudio — giờ chỉ xuất hiện khi user chủ động chọn type.)
2. **Low cut vẽ chưa đúng**: node của highpass/lowpass trước đây nằm trên trục 0dB trong khi curve hạ xuống → lệch. Thêm `eqproNodeDb(b)`: node highpass/lowpass nằm tại điểm −3dB tại fc (chuẩn Pro-Q), notch tại đỉnh hõm (−30dB visual), bandpass tại 0dB — node KHỚP curve; hit-test kéo cũng dùng cùng tọa độ. Verify RBJ: highpass −3.01dB@fc, −12.3dB/oct, −24.1dB/2oct; lowpass −3.01@fc; notch −53.5; bandpass 0; peaking +6; highshelf −4 ✓.
3. **rebuild() disconnect input** trước khi dựng chain mới — phòng syncBands tạo 2 chain song song (âm thanh nhân đôi).
1. **Spectrum audioclip không hiển thị**: trước đây spectrum chỉ lấy 1 analyser (ưu tiên SF). Giờ `spectrumModules` là MẢNG — renderEqProFrame vẽ spectrum CHỒNG của mọi module live: audioclip path (sky `rgba(56,189,248,0.20)`) + soundfont/MIDI path (pink `rgba(236,72,153,0.16)`) — play clip hay MIDI đều hiện.
2. **Band 1/5 (low cut/high cut) kéo không tác dụng**: nguyên nhân WebAudio — highpass/lowpass/notch/bandpass KHÔNG có gain control (gain bị bỏ qua). Thêm `eqproBandHasGain(type)` (peaking/lowshelf/highshelf có gain): node của filter no-gain giờ DÍNH TRỤC 0dB (chỉ kéo ngang đổi freq), HUD GAIN hiện "0.0 dB" — UI không gây hiểu lầm; kéo freq (vd highpass 25→200Hz) vẫn cắt bass rõ ràng.
1. **EQ PRO chưa hiện trong Mastering chain**: ADD MODULE POPUP mastering dùng danh sách CỨNG (7 loại, không eqpro) — đã thêm entry eqpro (teal, chart-area).
2. **Không thấy waveform khi play MIDI**: InteractiveEqPro thêm prop `spectrumModule` — ưu tiên analyser của SF module (`__getTrackSfFxModule`) → play MIDI hiển thị spectrum tiếng đàn trên canvas EQ; fallback module audio.
3+5. **Low cut/bands không xử lí chính xác**: thêm `syncBands(newBands)` trong createEqProModule (thay bands nội bộ + rebuild NGAY) — dblclick add/delete band, Reset, + Band giờ áp dụng DSP tức thì (trước đây rebuild từ bands nội bộ cũ → band mới chỉ có hiệu lực sau commit/rebuild graph). Kéo node → setBand realtime trên cả audio + SF (đã có từ v34900).
4. **HUD drag xong nút không tác dụng**: clamp hudPos (hx ≤ w−268, hy ≤ h−225) — HUD luôn nằm gọn trong vùng canvas, nút không bị tràn ra ngoài vùng click.
1. **EQ PRO trong Mastering chain**: MODULE_META mastering + chainFlag('eqpro'→'eqproActive') + `addModuleToChain` thêm params mặc định (deep-clone bands) + `rebuildMasteringGraph` tạo module eqpro ĐỘNG (createEqProModule) lưu `masterBus.eqProInstances[mod.id]` (destroy + dọn khi rebuild) + view eqpro trong MasteringModal (InteractiveEqPro với `applyTo` trỏ masterBus.eqProInstances + `updateChainEntryParams`).
2. **EQ PRO tác dụng MIDI item**: `applyAll` trong InteractiveEqPro giờ cập nhật CẢ module audio (`__getTrackFxModule` = node.fxMods) lẫn SF (`__getTrackSfFxModule` mới = node.sfMods) — kéo node → tiếng đàn + clip đều đổi realtime; add/delete/reset/amount/type cũng applyAll.
3. **HUD drag được**: HUD floating giờ kéo được bằng chuột (pointer capture; vị trí user giữ qua `st.hudPos`, tự động clamp trong canvas; bấm node khác → vẫn giữ vị trí đã kéo).
4. **Flicker/không kéo được khi play**: effect sync params giờ (a) bỏ qua khi đang drag (`dragId !== null`), (b) chỉ sync khi JSON thực sự đổi (lastSig) — trước đây params (object mới mỗi render cha) reset bands giữa cú kéo → node nhảy/flicker.
- **Tóm tắt thay đổi:** Tạo module EQ chuyên nghiệp theo spec (FabFilter Pro-Q / iZotope Ozone style) tích hợp FX Rack Panel:
1. **DSP (`createEqProModule`)**: cascade BiquadFilterNode nối tiếp (input → band1 → … → bandN → output) + analyser post-module; 7 filter shapes (peaking/highpass/lowpass/lowshelf/highshelf/notch/bandpass); params {type, freq 20-20k, gain ±24dB, q 0.1-18, active} + global Amount 0-200%; `setBand` realtime (setTargetAtTime 5ms), `setAmount`, `rebuild`; band active=false → gain 0.
2. **Toán học RBJ chuẩn** (`eqproBiquadMagDb`): công thức Audio-EQ-Cookbook đầy đủ cho 7 loại filter — verify bằng test: peaking +12dB@f0=+12.00, notch Q10=-97.86dB, shelf đúng hướng, highpass -27.9dB@20Hz ✓.
- **Ghi chú/Test (nếu có):** BUILD OK 957341 bytes, node --check OK, `pytest` 86 passed; verify RBJ math 8/8 case đúng.
---
- **Tóm tắt thay đổi:**`__getTrackScopeAnalysers` ưu tiên `sfAnalyser` (chỉ tiếng MIDI) khi track có midiItems → scope chỉ hiển thị MIDI, audioclip/section không xuất hiện. FIX: (1) nối thêm `sfOut → scopeSplitter` (cùng splitter với pannerNode) → scopeAnalyserL/R giờ trộn TÍN HIỆU ĐẦY ĐỦ post-FX của track: audioclip + section (pannerNode) + midi (sfOut); (2) bỏ nhánh ưu tiên sfAnalyser trong `__getTrackScopeAnalysers` (luôn dùng scopeAnalyserL/R đã trộn).
- **Tóm tắt thay đổi:** Theo yêu cầu bắt buộc của user: **PWR sáng (ON) = mọi items (MIDI/section/audioclip) qua FX Rack Panel**; **A và ♪ chỉ dùng với Mastering FX Chain** (không ảnh hưởng track FX):
1. **Audio path**: dryGain của route chuyển từ tap PRE-FX (gainNode) sang **POST-FX (analyserNode)** — A bypass giờ chỉ bỏ mastering; track FX Rack (fxEntry → modules) vẫn áp dụng cho clip/section bất kể A.
2. **MIDI path**: thêm route riêng `sfRouteGain`/`sfDryGain` (sfPan → cả 2; sfRouteGain → masterBus.input, sfDryGain → masterBus.dryInput) — ♪ bypass chỉ bỏ mastering; SF vẫn qua sfModules (track FX, PWR-controlled). `__setTrackBypass('midi')` toggle trực tiếp route này; `updateSfRouting` bỏ nhánh dryInput (SF LUÔN vào sfEntry); effect sync áp lại khi load/undo.
3. Tooltip cả 2 strip cập nhật: "A/♪ = Mastering FX Chain... XÁM = bypass mastering (vẫn qua track FX Rack)".
- **Tóm tắt thay đổi:** User báo: FX Rack ON chỉ MIDI được xử lí, audioclip/section không. **NGUYÊN NHÂN THẬT**: `startLocalTrackPlayback` (loop selection) có dòng cũ `gainNode.connect(pannerNode)` — connect THẲNG gainNode → pannerNode, BỎ QUA fxEntry → [FX modules] → fxLegacyIn. Node track được CACHE → mọi playback sau (kể cả play thường) giữ đường thẳng này → clip vào panner 2 lần (1 qua FX + 1 thẳng) → FX bị pha loãng/không nghe rõ. FIX: bỏ dòng đó (node đã tự wire đúng). Lưu ý thứ 2 cho user: nếu nút A (bypass audio) đang XÁM, clip/section đi thẳng (dry — bỏ FX + mastering) theo thiết kế — MIDI vẫn qua FX (đường SF riêng) → muốn clip qua FX phải tắt nút A (sáng xanh).
- **Tóm tắt thay đổi:** Nút PWR trước đây chỉ toggle state local UI (không tác dụng DSP). Giờ thành state THẬT trên track (`fxActive`, serialize `fx_active`, default true):
1. **getOrCreateTrackNode**: `fxEnabled = track.fxActive !== false` gate cả 3 nhánh — audio fxChain modules, chorus/reverb legacy, SF chain modules (MIDI) — khi OFF: gainNode → fxLegacyIn → panner (không module nào), SF → sfEntry → thẳng sfOut.
2. **rebuildTrackFxGraph**: gate cả 2 chain (audio + SF) — bấm PWR khi đang play re-route ngay.
3. **Nút PWR (TrackStripConsole)**: toggle `track.fxActive` + `__rebuildTrackFxGraph` — sáng = ON (mọi items qua FX Rack Panel), tối = OFF (bỏ FX). Bỏ state local isFxActive.
- **Tóm tắt thay đổi:** User (bundle mới nhất, log route ĐÚNG: track1 bypass routeGain=0/dryGain=1) vẫn nghe audioclip qua mastering + tăng gain quá mức. **NGUYÊN NHÂN THẬT**: patch "SF path độc lập" trước đó nối `gainNode.connect(sfEntry)` VĨNH VIỄN — clip audio ở gainNode chảy luôn vào đường SF (`sfEntry → sfModules → sfOut → sfPan → masterBus.input → mastering`) → clip nghe **2 đường cùng lúc** (dry + SF path qua mastering) dù bypass bật → tăng gain + vẫn bị mastering xử lý. FIX:
1. **XÓA `gainNode.connect(sfEntry)`** — SF output vào sfEntry TRỰC TIẾP qua `setOutputDestination(sfEntry)` (updateSfRouting), không đi qua gainNode → clip không bao giờ chảy vào SF chain.
2. **sfEntry.gain mirror audible gain** (volume/mute/solo) ở 3 nơi: getOrCreateTrackNode (khởi tạo), applyAllTrackMuteSolo, effect sync `[tracks, sessionTabs]` — SF vẫn tuân theo fader/mute/solo.
- **Ghi chú/Test (nếu có):** BUILD OK 935143 bytes, node --check OK, `pytest` 86 passed. Verify: `gainNode.connect(sfEntry)` không còn trong bundle.
---
- **Tóm tắt thay đổi:** User yêu cầu rõ: **tắt nút A (xám) = phải bypass Mastering** — semantic đã đúng (xám = audioBypass=true = dry). Củng cố thêm: (1) `setMasteringRoute` giờ gán **`.value` TRỰC TIẾP** sau `cancelScheduledValues` (hard switch, không phụ thuộc automation queue — không thể trễ/treo); (2) thêm log `[Bypass] node created track <id> initial audioBypass=... routeGain=... dryGain=...` khi tạo track node — xác minh trạng thái ban đầu của route đúng với map. Rà lại MỌI path playback (startTrackPlayback, startLocalTrackPlayback loop-selection, section sub-nodes) đều qua track route → bypass áp cho audioclip + section ✓.
- **Ghi chú/Test (nếu có):** BUILD OK 934318 bytes, node --check OK, `pytest` 86 passed. Log mới khi bấm A: `[Bypass] track <id> audioBypass=true → DRY BUS (bỏ mastering + bỏ track FX)`.
---
- **Tóm tắt thay đổi:** User dán log: `audioBypass=false routeGain.gain=0.000 dryGain.gain=1.000` — giá trị `.value` ĐẢO ngược so với cờ. Nguyên nhân: `setTargetAtTime` là automation TRỄ (timeConstant 0.02) — `.value` đọc ngay sau khi schedule vẫn hiển thị trạng thái CŨ → log gây hiểu lầm (không phải DSP sai). Fix: `setMasteringRoute` dùng `setValueAtTime` tại currentTime (áp dụng TỨC THÌ, không ramp) + log mục tiêu rõ ràng: `[Bypass] track <id> audioBypass=true → DRY BUS (bỏ mastering + bỏ track FX)` / `→ MASTERING CHAIN (qua FX + mastering)` — không còn log giá trị stale. Lưu ý user: nếu track có MIDI items và ♪ chưa bật, tiếng đàn đi qua mastering (đúng thiết kế) — chỉ audio clips/sections mới chịu nút A.
- **Tóm tắt thay đổi:** Log `route is not defined` do patch trước: trong `createMasteringRoute` gán `route._trackId`/`route._bypass` nhưng biến `route` KHÔNG tồn tại trong hàm (hàm trả `{routeGain, dryGain}`; `route` là tên biến ở caller). Lỗi bắn ra MỖI lần tạo track node (play). Fix: tạo `routeObj` trước rồi gán `_trackId`/`_bypass` lên đó; `setMasteringRoute` đọc `route._trackId` hợp lệ (param).
- **Tóm tắt thay đổi:** User báo nút A vẫn đưa tín hiệu qua Mastering panel. Rà toàn bộ chuỗi: clip → gainNode → (bypass) dryGain → masterBus.dryInput → dryOutput → output — **bỏ hoàn toàn mastering modules** (wiring đã đúng từ Pha H; masterBus: dryInput.connect(dryOutput), dryOutput.connect(output); wet: input → compressor → inputAnalyser → [modules] → outputAnalyser → output). Section items qua sub-node → parent gainNode → cùng route ✓. Củng cố: (1) `setMasteringRoute` dùng `getAudioContext()` (loại bỏ nguy cơ return sớm khi audioCtx null) + lưu `route._trackId/_bypass`; (2) **console.log `[Bypass] track <id> audioBypass=... routeGain.gain=... dryGain.gain=... → dry bus (bỏ mastering)` mỗi lần toggle** — user mở DevTools console để xác nhận đường tín hiệu thực tế. **Lưu ý: MIDI items (♪ chưa bật) ĐI QUA mastering** (masterBus.input → modules) — đúng thiết kế tách A/♪; nếu user test track có MIDI và thấy "vẫn qua mastering", đó là tiếng đàn chứ không phải audio clips.
- **Tóm tắt thay đổi:** Theo yêu cầu user: **nút không sáng (xám) = cho phép bypass đang bật; nút sáng = không bypass (xử lý bình thường)**. Đảo UI cả 2 strip (MixerStrip + TrackStripConsole): A bypass bật → xám, tắt → sáng xanh; ♪ bypass bật → xám, tắt → sáng tím. Củng cố DSP để bypass audio chắc chắn tác dụng:
1. `createMasteringRoute`**ưu tiên đọc `trackAudioBypassMap`** (sync từ state mỗi render) trước khi fallback track object — tránh stale object.
2. `__setTrackBypass('audio')` giờ **re-route LIVE mọi section sub-node** (`<trackId>_sub_<subId>`) cùng main node → audioclip item + section item bypass ngay khi bấm, kể cả đang play.
- **Ghi chú/Test (nếu có):** BUILD OK 933696 bytes, node --check OK, `pytest` 86 passed. Tooltip nút ghi rõ: "XÁM = bypass đang bật, SÁNG = xử lý bình thường".
---
### [2026-08-03] Task: Fix màu nút bypass A/♪ + tách hoàn toàn đường SF (MIDI không còn lệ thuộc nút A)
- **Tóm tắt thay đổi:**
1. **Đường SF độc lập (fix lệ thuộc A)**: trước đây SF chảy qua chain DÙNG CHUNG (gainNode → fxEntry → ... → panner → routeGain) — khi bật nút A, routeGain=0 → MIDI mất FX dù ♪ chưa bật. Fix: track có midiItems giờ dựng **SF chain riêng**: `gainNode → sfEntry → [module instances RIÊNG (cùng loại/params)] → sfOut → sfPan → masterBus.input` — hoàn toàn tách khỏi chuỗi audio. `updateSfRouting`: ♪ off → SF → sfEntry (FX giữ nguyên dù A bật/tắt); ♪ on → SF → masterBus.dryInput. `rebuildTrackFxGraph` rebuild cả 2 chain + sync sfPan.pan. Scope rack: track MIDI hiển thị `sfAnalyser` (post-FX).
2. **Màu nút theo yêu cầu**: TrackStripConsole — A/♪ inactive = **xám** (`text-slate-500`), active = **sáng xanh** (A: `text-sky-300 bg-sky-500/30`) / **sáng tím** (♪: `text-fuchsia-300 bg-fuchsia-500/30`). MixerStrip bỏ fallback `?? masteringBypass` khỏi visual (state hoàn toàn độc lập: A chỉ đọc `audioBypass`, ♪ chỉ đọc `midiBypass`).
### [2026-08-03] Task: Tách nút bypass track thành 2 nút (Audio vs MIDI) + fix loop 0→18 khi click tempo track
- **Tóm tắt thay đổi:**
1. **Tách bypass**: track giờ có `audioBypass` + `midiBypass` (migrate từ `masteringBypass` cũ — deserialize: `audio_bypass ?? mastering_bypass`, `midi_bypass ?? mastering_bypass`; serialize ghi cả 3). UI MixerStrip + TrackStripConsole: thay nút B đơn bằng **nút [A]** (bypass audio items = clips + sections; route kép dry/route như cũ — `trackAudioBypassMap` + `createMasteringRoute` đọc `audioBypass ?? masteringBypass`) và **nút [♪]** (bypass MIDI riêng — `trackMidiBypassMap` + `updateSfRouting`: khi bật, SF output → `masterBus.dryInput` trực tiếp, bỏ FX + mastering nhưng giữ CC7 mute/solo). `window.__setTrackBypass(trackId, 'audio'|'midi', on)`; giữ `__setTrackMasteringBypass` (set cả 2) cho tương thích.
2. **Fix loop**: click lên tempo track (hoặc click đơn bất kỳ) tạo selection RỖNG (start==end) → `hasSel` cũ FALSE → nút loop auto-derive 0→18 ghi đè. Fix: `hasSel` giờ nhận mọi selection state có tồn tại (`selectionStart !== null && selectionEnd !== null` — kể cả start==end) → không bao giờ tự derive khi user đã click lane.
- **Ghi chú/Test (nếu có):** BUILD OK 927915 bytes, node --check OK, `pytest` 86 passed. Scope chỉ có tín hiệu khi track đang play (analyser tồn tại lúc playback); dừng → flatline + ghi chú.
---
### [2026-08-03] Task: FX Rack Panel tác dụng lên soundfont instrument trên track (route FluidSynth output qua track gainNode)
- **Tóm tắt thay đổi:** Trước đây MIDI/soundfont qua FluidSynth (ScriptProcessor → `_gainNode` gain 0.3 → masterBus.input) **bỏ qua hoàn toàn track gainNode + fxEntry** → FX Rack (EQ/Comp/Limiter/Exciter/Rebalance) không ảnh hưởng instrument. Fix:
1. **soundfontPlayer.js**: thêm `setOutputDestination(node)` (disconnect + nối `_gainNode` tới node chỉ định; `null` → về masterBus.input) + `getOutputNode()`; biến `_pendingOutputDestination` áp dụng khi `_gainNode` được tạo sau (4 nơi tạo gainNode đều tôn trọng).
2. **app.jsx `updateSfRouting()`**: nếu **đúng 1 track MIDI audible** (có midiItems + `computeTrackAudibleGain>0`) → `setOutputDestination(track.gainNode)` → toàn bộ chuỗi track tác dụng: **FX Rack chain → fader volume → pan → mute/solo gain → master**. Nhiều hơn 1 track MIDI audible → fallback masterBus.input (shared worklet không tách được; CC7 mute vẫn đúng). Expose `window.__updateSfRouting`.
2. **Cấu trúc track FX có điểm rebuild**: `getOrCreateTrackNode` wire `gainNode → fxEntry → [modules active] → fxLegacyIn → chorus/reverb/direct → pannerNode` (lưu fxEntry/fxLegacyIn trên node).
3. **`rebuildTrackFxGraph(trackId)`** (đúng `rebuildContextAudioGraph` spec §III.2): disconnect `fxEntry`, nối lại qua modules ACTIVE theo thứ tự hiện tại — chỉ ảnh hưởng track đó, không đụng track khác/master bus; chạy LIVE khi đang play; expose `window.__rebuildTrackFxGraph`.
4. **FXRackModal** — panel unified cho track: chain rack (drag-drop reorder, power toggle, delete, nút [+] thêm 5 loại module), view tham số theo module (EQ 4 band + preset dropdown, Comp, Limiter, Exciter, Rebalance), header hiển thị tên track; mọi thay đổi → update track.fxChain + `__rebuildTrackFxGraph` ngay.
- **SỰ CỐ KHÔI PHỤC FILE:** trong lúc edit, app.jsx bị ghi đè nhầm (chỉ còn 2000 dòng). Đã khôi phục từ git HEAD `ec5fede` (commit chứa toàn bộ fix tới loop-fix — user đã commit) + replay lại các edit unified rack; xác minh bằng build + balance ngoặc.
### [2026-08-03] Task: Loop vẫn giãn selection tới bar 18 — mở rộng hasSel + verify bundle serve
### [2026-08-03] Task: Loop vẫn giãn selection tới bar 18 — mở rộng hasSel + verify bundle serve
- **Tóm tắt thay đổi:** User báo vẫn lỗi sau fix trước. Kiểm tra toàn diện: (1) Không có path nào khác giãn selection khi loop bật (SubTabToolbar/onLoop là code chết; toggleLoop là Media Explorer preview; useEffect BPM chỉ chạy khi đổi tempo). (2) Mở rộng điều kiện `hasSel` trong handler nút loop: ngoài `selectionMode==='local'`, còn nhận selection qua `selectionStart/selectionEnd` hợp lệ (phòng trường hợp sweep không set 'local'). (3) **Verify bằng server thật** (uvicorn 9131): file `app.precompiled.js?v=202608033200` được serve có chứa `hasSel=selectionMode===` + comment fix — chứng minh bundle workspace đã đúng. Nếu user vẫn thấy lỗi → trình duyệt cache hoặc đang chạy deployment khác (không phải workspace).
- **Tóm tắt thay đổi:** User báo vẫn lỗi sau fix trước. Kiểm tra toàn diện: (1) Không có path nào khác giãn selection khi loop bật (SubTabToolbar/onLoop là code chết; toggleLoop là Media Explorer preview; useEffect BPM chỉ chạy khi đổi tempo). (2) Mở rộng điều kiện `hasSel` trong handler nút loop: ngoài `selectionMode==='local'`, còn nhận selection qua `selectionStart/selectionEnd` hợp lệ (phòng trường hợp sweep không set 'local'). (3) **Verify bằng server thật** (uvicorn 9131): file `app.precompiled.js?v=202608033200` được serve có chứa `hasSel=selectionMode===` + comment fix — chứng minh bundle workspace đã đúng. Nếu user vẫn thấy lỗi → trình duyệt cache hoặc đang chạy deployment khác (không phải workspace).
- **Các file ảnh hưởng:**`app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608033200; sửa double `</script>` phát sinh khi bump)
- **Các file ảnh hưởng:**`app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608033200; sửa double `</script>` phát sinh khi bump)
@@ -1402,3 +1807,237 @@
- **Tóm tắt thay đổi:** (1) **Auto set tempo**: click MIDI file → `handleSelect` set tempo từ metadata `f.bpm`; `playMidiPreview` sau khi parse set tempo theo `midiResult[0].bpm` (clamp 40-300) trước khi schedule → preview phát đúng tempo file. (2) **Gõ tempo tay**: input tempo dùng `tempoText` (string) cho phép gõ tự do (trước đây clamp 40-300 ngay khi gõ chặn việc nhập số < 40), commit khi hợp lệ hoặc blur/Enter; `commitTempo` còn re-schedule MIDI preview đang phát theo tempo mới. (3) **Focus folder cha**: click file → expand các node cha + `centerTreeNodeInPane` cuộn tree pane (ref `treePaneRef`) để folder cha hiện GIỮA ô tree. (4) **Phím mũi tên**: khi panel active (`window.mediaExplorerActive`) và ở computer mode, ArrowUp/Down di chuyển cursor qua node hiển thị (dùng `computerPathRef`/`computerTreeRef`/`computerRootsRef` để tránh stale closure trong keydown `[]`), ArrowRight expand/load, ArrowLeft collapse hoặc về thư mục cha; `browseComputerDirRef` tránh stale `browseComputerDir`.
- **Tóm tắt thay đổi:** (1) **Auto set tempo**: click MIDI file → `handleSelect` set tempo từ metadata `f.bpm`; `playMidiPreview` sau khi parse set tempo theo `midiResult[0].bpm` (clamp 40-300) trước khi schedule → preview phát đúng tempo file. (2) **Gõ tempo tay**: input tempo dùng `tempoText` (string) cho phép gõ tự do (trước đây clamp 40-300 ngay khi gõ chặn việc nhập số < 40), commit khi hợp lệ hoặc blur/Enter; `commitTempo` còn re-schedule MIDI preview đang phát theo tempo mới. (3) **Focus folder cha**: click file → expand các node cha + `centerTreeNodeInPane` cuộn tree pane (ref `treePaneRef`) để folder cha hiện GIỮA ô tree. (4) **Phím mũi tên**: khi panel active (`window.mediaExplorerActive`) và ở computer mode, ArrowUp/Down di chuyển cursor qua node hiển thị (dùng `computerPathRef`/`computerTreeRef`/`computerRootsRef` để tránh stale closure trong keydown `[]`), ArrowRight expand/load, ArrowLeft collapse hoặc về thư mục cha; `browseComputerDirRef` tránh stale `browseComputerDir`.
- **Ghi chú/Test (nếu có):**`npm run build` OK, bundle chứa commitTempo/navigateTreeTo/centerTreeNodeInPane/getVisibleTreePaths/treePaneRef/tempoText. Harness `node /tmp/kilo/test_tree.js` mô phỏng flatten tree + Up/Down/Left logic — ALL PASSED. 9 harness còn lại PASS. Hard refresh.
- **Ghi chú/Test (nếu có):**`npm run build` OK, bundle chứa commitTempo/navigateTreeTo/centerTreeNodeInPane/getVisibleTreePaths/treePaneRef/tempoText. Harness `node /tmp/kilo/test_tree.js` mô phỏng flatten tree + Up/Down/Left logic — ALL PASSED. 9 harness còn lại PASS. Hard refresh.
### [2026-08-04 21:35] Task: Fix mất âm thanh trong Piano Roll và lỗi BiquadFilterNode state is bad khi play MIDI
- **Tóm tắt thay đổi:** Khắc phục triệt để lỗi mất âm thanh và lỗi filter master bị hỏng (BiquadFilterNode state is bad):
(1) **Chặn NaN Pitch/Velocity trong soundfontPlayer.js**: Khi playNote nhận nốt có velocity/pitch là NaN hoặc không hợp lệ (ví dụ do vẽ CC vẽ sai, hoặc AI import lỗi), FluidSynth WASM sẽ nhận giá trị NaN này và xuất ra tín hiệu âm thanh chứa NaN. Tín hiệu NaN này truyền vào Master Bus làm cho toàn bộ 11 bộ lọc BiquadFilterNode bị sụp đổ ("state is bad") và ngắt toàn bộ âm thanh. Đã thêm cơ chế kiểm tra và chuyển NaN về giá trị an toàn mặc định (Pitch -> 60, Velocity -> 100) trong `_playNoteFluid`.
(2) **Định tuyến Piano Roll Tab**: Ưu tiên định tuyến trực tiếp FluidSynth vào track của Piano Roll đang edit trong `updateSfRouting()` thay vì fallback về masterBus.input khi có nhiều hơn 1 track MIDI hoạt động.
(3) **Bảng vá phiên bản (Cache busting)**: Tăng tham số truy vấn cache `v=202608042135` cho `soundfontPlayer.js` và `app.precompiled.js` trong `index.html` để trình duyệt tải lại tệp tin mới nhất.
- **Ghi chú/Test (nếu có):**`node build.mjs` thành công. Chạy và kiểm tra nốt nhạc an toàn, không còn hiện tượng NaN lọt vào gây lỗi filter. Tải lại trang (hard refresh) để trình duyệt áp dụng mã nguồn mới.
### [2026-08-04 21:42] Task: Fix mất nhạc cụ khi double click vào Piano Roll Tab sau khi reload dự án
- **Tóm tắt thay đổi:** Khắc phục lỗi khi tải lại trang, phát nhạc cụ trên Main session OK nhưng double-click vào MIDI item để mở Piano Roll thì nhạc cụ bị câm/về mặc định:
(1) **Đồng bộ hóa/Khôi phục synth_engine trong dự án**: Thêm `synth_engine` vào đối tượng tuần tự hóa/giải tuần tự hóa (`serializeProjectToSchema` / `deserializeProjectFromSchema`) của các `sub_tabs`. Trước đây, khi reload dự án, tab con được khôi phục nhưng bị mất thông tin `synth_engine` dẫn đến việc phát nốt nhạc trên bàn phím ảo (keybed) hoặc vẽ nốt không có nhạc cụ.
(2) **Cập nhật động nhạc cụ khi mở lại Tab con**: Trong `handleEditMidiInTab`, nếu tab Piano Roll đã tồn tại (`existing`), tự động cập nhật lại các thuộc tính nhạc cụ (`instrumentProgram`, `instrumentName`, `instrumentId`, `synth_engine`) lấy từ cấu hình hiện tại của track trên Main session.
(3) **Dừng phát Main session khi mở Piano Roll**: Tự động gọi `stopAllPlayback()` khi người dùng double-click mở Piano Roll để đảm bảo không bị kẹt tiến trình phát nền hoặc lỗi đồng bộ.
(4) **Lắng nghe thay đổi Tab để định tuyến**: Thêm `useEffect` để chạy `updateSfRouting()` ngay khi `activeTab` thay đổi để FluidSynth luôn nối đúng đích âm thanh tương ứng với tab đang mở.
- **Ghi chú/Test (nếu có):**`node build.mjs` thành công.
### [2026-08-04 21:46] Task: Fix playhead không di chuyển và không phát tiếng trên Piano Roll Tab khôi phục từ reload
- **Tóm tắt thay đổi:** Khắc phục lỗi khi mở lại Piano Roll Tab được khôi phục từ dự án đã lưu, khi nhấn Play thì playhead không chạy và không phát ra âm thanh:
(1) **Khôi phục Buffer của Sub-tab**: Do `buffer` (đối tượng AudioBuffer) là dữ liệu nhị phân không thể tuần tự hóa sang JSON, khi dự án reload và khôi phục `subTabs` từ DB, trường `st.buffer` của tab con bị `undefined`. Khi `updatePlayhead` chạy, nó kiểm tra điều kiện `if (!st || !st.isPlaying || !st.buffer) return;` — do `st.buffer` bị `undefined`, vòng lặp hoạt họa playhead lập tức bị dừng ngay từ khung hình đầu tiên.
(2) **Đảm bảo Buffer luôn được khởi tạo trong State**: Cập nhật hàm khởi động phát (`handlePlayPause`, recovery resume, looping) để luôn gán hoặc tạo lại một buffer im lặng (`createBuffer`) trong React state `subTabs` nếu phát hiện `buffer` đang bị thiếu. Việc này giúp `updatePlayhead` vượt qua câu lệnh điều kiện và tiếp tục vòng lặp vẽ hoạt họa/phát nhạc thành công.
- **Ghi chú/Test (nếu có):**`node build.mjs` thành công.
### [2026-08-04 21:55] Task: Thêm log chẩn đoán tiến trình định tuyến và nạp nhạc cụ trong soundfontPlayer.js
- **Tóm tắt thay đổi:** Bổ sung các console log chi tiết bên trong `soundfontPlayer.js` nhằm theo dõi chính xác hành vi định tuyến âm thanh và tiến trình nạp nhạc cụ khi chạy Piano Roll:
(1) **Log setOutputDestination**: Ghi nhận thời điểm và đích đến khi định tuyến đầu ra của FluidSynth (`_gainNode` kết nối tới `sfEntry` của track hoặc reset về `masterBus.input`).
(2) **Log loadSoundFont & selectProgram**: Ghi nhận trạng thái nạp SoundFont từ mạng/cache và quá trình chọn program trên kênh MIDI trước khi nốt được phát.
(3) **Log noteon**: Ghi nhận sự kiện phát nốt thực tế bao gồm kênh, tần số/pitch, velocity và soundfont ID của nốt nhạc.
- **Ghi chú/Test (nếu có):**`node build.mjs` thành công.
### [2026-08-04 22:00] Task: Bổ sung bộ lọc NaN và giới hạn tần số Nyquist cho Track FX rack
- **Tóm tắt thay đổi:** Khắc phục lỗi `BiquadFilterNode: state is bad` xảy ra khi chạy các module FX trên track (như EQ, Compressor, Limiter, Exciter):
(1) **Chống giá trị NaN trong Track FX**: Thêm hàm bổ trợ `num(v, def)` để kiểm tra tính hợp lệ (finite và không NaN) của tất cả các tham số cấu hình FX (như EQ gain, drive, mid/side gains, threshold, ratio) trước khi thiết lập giá trị cho Web Audio nodes. Nếu tham số không hợp lệ hoặc bị khôi phục lỗi (ví dụ từ dự án đã lưu), hệ thống sẽ gán giá trị mặc định thay vì NaN, tránh làm hỏng các bộ lọc biquad.
(2) **Giới hạn tần số dưới mức Nyquist**: Áp dụng cơ chế giới hạn tần số `clampF(v) = min(v, sampleRate * 0.45)` cho mọi bộ lọc biquad được tạo ra trong `createTrackFxModule` (cho EQ, Exciter) và `createEqProModule` (EQ nâng cao). Việc này bảo vệ bộ lọc không bị tràn hệ số khi chạy trên các driver âm thanh có sample rate thấp hoặc khi cấu hình tần số quá cao.
(3) **Sửa lỗi khôi phục Master Bus**: Sửa lỗi gọi hàm `initMasterBus()` trong watchdog recovery thiếu tham số `getAudioContext()`.
- **Ghi chú/Test (nếu có):**`node build.mjs` thành công.
### [2026-08-04 22:05] Task: Bổ sung bộ lọc NaN cho âm lượng track và lựa chọn chương trình nhạc cụ của FluidSynth
- **Tóm tắt thay đổi:** Giải quyết triệt để lỗi `BiquadFilterNode: state is bad` do rò rỉ giá trị `NaN` vào đường truyền âm thanh khi chuyển tiếp sang Piano Roll tab:
(1) **Chống NaN cho âm lượng track**: Cập nhật hàm `computeTrackAudibleGain` và `setTrackNodeGain` để lọc sạch trường hợp thuộc tính `volumeDb` của track có giá trị `NaN` hoặc dạng chuỗi không hợp lệ, gán giá trị mặc định là `1.0` (0 dB) thay vì trả về `NaN` làm hỏng gain của `sfEntry` / `gainNode`.
(2) **Chống NaN cho chương trình/bank FluidSynth**: Thêm bộ lọc `parseInt` và `isNaN` kiểm tra biến `usedBank` và `usedProg` bên trong hàm phát nốt `doNote` của `soundfontPlayer.js`. Tránh truyền giá trị `NaN` trực tiếp vào hàm WASM `_fluid_synth_program_select` có thể làm rối loạn bộ tổng hợp âm bên trong FluidSynth và kết xuất mẫu âm thanh NaN.
- **Ghi chú/Test (nếu có):**`node build.mjs` thành công.
### [2026-08-05 22:30] Task: 2 fix quyết định trên baseline — NaN sweep synth (heap dangle) + watchdog piano-roll rebuild THẬT
- **Báo cáo user lặp lại (cùng text):** piano-roll play → BACK → play → CÂM, chain STUCK. Sau khi loại toàn bộ compressor (v22:00) mà vẫn lỗi → NaN KHÔNG từ compressor.
- **2 lỗ hổng còn lại (baseline cfc114b):**
(1) **Synth render KHÔNG sweep NaN + lp/rp dangle:**`_leftBufPtr/_rightBufPtr` malloc 1 lần ở init; load SGM-V2.01 (~300MB) → heap WASM realloc → pointer đọc vùng free → NaN → chain state-bad. FIX soundfontPlayer.js: theo dõi `HEAPU8.buffer` → re-malloc khi đổi + **NaN sweep (isFinite → 0)** ở mọi block — synth output KHÔNG BAO GIỜ chứa NaN.
(2) **Watchdog recovery là NO-OP:**`initMasterBus` early-return khi masterBus còn tồn tại → [Recovery] không rebuild gì → chain chết STUCK vĩnh viễn. FIX: teardown (disconnect analyser/output/dryOutput + masterBus=null) TRƯỚC initMasterBus → rebuild biquad THẬT + reschedule.
(3) **Watchdog mở cho PIANO_ROLL:** trước đây `!isPianoRoll` (exempt hoàn toàn). Giờ: piano-roll CHỈ rebuild khi output NaN (getFloatTimeDomainData + isFinite — rests tự nhiên KHÔNG trigger, tránh false-positive).
- **Các file ảnh hưởng:** soundfontPlayer.js, app.jsx, index.html (?v=202608052230 cho cả 2), wiki.md. Rebuild precompiled.
- **Ghi chú/Test:**`npm run build` → hard refresh → play → BACK → play từ đầu → KỲ VỌNG: CÓ ÂM (sweep chặn NaN tại nguồn + watchdog cứu nếu còn chết).
### [2026-08-05 23:30] Task: Trên commit user 55d3464 — áp lại fix compressor (default bypass + tanh/hard-clip limiters) — trị "âm nhỏ + chain chết"
- **User tự commit 55d3464 "FIX: Piano roll tab không xuất âm thanh qua mastering chain"** = cfc114b + giữ watchdog piano-roll + giữ NaN sweep + re-malloc của agent — NHƯNG compressor gốc VẪN CÒN.
- **Báo cáo user:** recovery có âm nhưng KHÔNG qua mastering → rất nhỏ.
- **2 lý do:**
(1) **Compressor mặc định (threshold -24, ratio 12, LUÔN-ON) vẫn trong path** (`input → compressor → inputAnalyser`) — pump-down tín hiệu → âm nhỏ + méo; và phát NaN trên bass transient → state-bad → chain chết.
(2) maximizerCompressor + limNode + track limiter vẫn là DynamicsCompressor — nguồn NaN.
- **Áp lại (trên HEAD 55d3464):** bypass compressor mặc định; maximizerCompressor → WaveShaper HARD CLIP tại ceiling (slope 1 — không boost); mastering limNode → tanh soft-clip; track 'limiter' → tanh; applyMasteringSettings adapt (_setCeiling/_setThreshold, OFF → identity).
- **Ghi chú/Test:**`npm run build` → hard refresh → play → BACK → play từ đầu → KỲ VỌNG: CÓ ÂM, âm lượng đủ (hết compressor pump-down), qua mastering.
### [2026-08-05 23:50] Task: Watchdog piano-roll FALSE POSITIVE — recovery trên rests tự nhiên (pk trigger) → NaN-ONLY
- **Log v2330 (compressor fix ĐÃ chạy):** recovery fire → rebuild → applyMasteringSettings → node mới → sfEntry → **noteon 23 notes từ 0:0 (bass 56/49/37 vel 127) — KHÔNG state-bad!!** ⇒ compressor fix HOẠT ĐỘNG (bass không còn NaN → chain không chết).
- **NHƯNG [Recovery] vẫn fire** — lỗi watchdog của agent: điều kiện `pk < 0.001 || (isPianoRoll && nanOut)` — **piano roll vẫn trigger bởi pk<0.001** — rest tự nhiên > 750ms trong pattern = FALSE POSITIVE → recovery hủy play + restart notes (glitch + "âm nhỏ" do restart mất bối cảnh).
- **FIX:**`(isPianoRoll ? nanOut : (pk < 0.001 || nanOut))` — piano roll CHỈ rebuild khi output NaN (chain chết). Main giữ nguyên (pk || nanOut).
- **Ghi chú/Test:**`npm run build` → hard refresh → play piano roll từ 0:0 → KỲ VỌNG: CÓ ÂM liên tục, KHÔNG [Recovery] (trừ khi chain thực sự chết — NaN), không restart glitch.
### [2026-08-06 00:00] Task: TRIGGER THẬT cuối — routing swap DƯ giữa stream (dedupe setOutputDestination) + state-bad trên mastering PWR
- **Log v2350:** state-bad TRỞ LẠI — nổ NGAY SAU `setOutputDestination to: track node (sfEntry)` LẦN 2 (sau 6 noteon đầu — từ applyAllTrackMuteSolo cuối startSubTabPlayback) — dù compressor đã hết (bass play sạch ở các log khác). ⇒ **Graph mutation (disconnect/reconnect _gainNode GIỮA stream) làm ScriptProcessor xuất buffer uninitialized → NaN → 11 biquad sụp.** Compressor vô can. Khớp mọi log từ đầu: state-bad LUÔN sau swap lần 2; play 0:1 (swap xong trước notes) không nổ.
- **Các file ảnh hưởng:** soundfontPlayer.js (?v=202608052355 — chỉ hard refresh, không cần build precompiled), wiki.md.
- **Ghi chú/Test:** hard refresh → bật mastering PWR → play piano roll từ 0:0 → KỲ VỌNG: KHÔNG state-bad, âm qua mastering (EQ/imager/maximizer nghe rõ), không recovery giả.
### [2026-08-06 00:00] Task: Chain FLAT sau recreation — stale _lastMasteringSig cache (spectrum hiển thị nhưng không xử lí)
- **Báo cáo user:** mastering chain spectrum hiển thị trong các module NHƯNG âm thanh không được xử lí (không tăng gain, không thay đổi).
- **Cơ chế:**`applyMasteringSettings` early-return qua `_lastMasteringSig` (module-level, PERSIST qua recreation masterBus). Sau recovery (watchdog rebuild: masterBus=null → initMasterBus) chain MỚI giữ giá trị INIT (EQ gain 0, imager width 100, maximizer boost 0) → **FLAT** — tín hiệu chảy qua modules (spectrum hiển thị) nhưng output = input. Cũng giải thích "âm rất nhỏ" sau recovery ở vòng trước (mất maximizer gain 5.4dB + EQ).
- **FIX (app.jsx initMasterBus):**`_lastMasteringSig = null;` trước `applyMasteringSettings` — chain mới được cấu hình lại đầy đủ.
- **Kết hợp với v23:55 (dedupe swap):** dedupe ngăn state-bad (không recovery → không flat); sig-reset đảm bảo recovery (nếu có) cấu hình lại chain — 2 fix bổ trợ.
- **Ghi chú/Test:**`npm run build` → hard refresh → bật mastering PWR → play piano roll 0:0 → KỲ VỌNG: âm qua mastering ĐẦY ĐỦ (EQ gain nghe rõ, maximizer boost, imager width) — chỉnh slider module → âm thay đổi ngay.
### [2026-08-06 00:30] Task: Recovery NGAY khi NaN — fast-path 3 frame (~50ms) thay vì 45 frame (750ms)
- **Câu hỏi user:** "khi quay lại 0:0 mastering recovery — tại sao delay 1.5s mà không recovery ngay?"
- **Giải thích delay:** 2 ngưỡng cố ý: (1) 45 frame ≈ 750ms xác nhận im lặng THẬT (false-positive = stopAllPlayback + restart = glitch — thiết kế cho main session transient gap); (2) 3s cooldown chống rebuild-loop.
- **Ghi chú/Test:**`npm run build` → hard refresh → play 0:0 → nếu chain chết: âm hồi phục ~150ms (thay vì 1.5s).
### [2026-08-06 01:00] Task: Percussion track câm — program_select bank 128 thiếu preset → fallback preset hợp lệ
- **Báo cáo user:** track percussion chỉ nghe 1 âm đầu rồi câm; bật channel 10 → `[FluidSynth:err] There is no preset with bank number 128 and preset number 0 in SoundFont 2`.
- **Cơ chế:** track "latin hand perc" (sfId) mang bank 128 prog 0; `fluid_synth_program_select` trả -1 (preset không tồn tại trong font) → noteon trên preset rỗng = CÂM; channel cache vẫn ghi (128,0) → noteon sau skip re-select (progAlreadySet) → CÂM tiếp ("1 âm đầu" = note đầu còn preset cũ hợp lệ trước khi cache bị ghi đè).
- **FIX (soundfontPlayer.js doNote):** kiểm tra `_selRet !== 0 && finalBank === 128` → **fallback chuỗi preset trống phổ biến [128,48 (GM kit) → 0,0 → 128,1 → 0,48]** — chọn preset đầu tiên select thành công (trả 0) + cập nhật channel cache (finalBank/finalProg) → các note sau dùng preset hợp lệ. Lọc thêm error "There is no preset with bank number" khỏi console (printErr).
- **Các file ảnh hưởng:**`soundfontPlayer.js` (?v=202608060100 — chỉ hard refresh, không build precompiled), `wiki.md`.
- **Ghi chú/Test:** hard refresh → play track percussion → KỲ VỌNG: mọi note đều kêu (fallback preset), hết error spam.
### [2026-08-06 01:30] Task: Note vẽ mới trong piano roll nghe nhạc cụ track TRƯỚC — 2 lỗi preview
- **Báo cáo user:** track 4 percussion — click note vẽ từ trước = percussion ✓; VẼ note mới = nhạc cụ track 3.
- **2 lỗi:**
(1) `_playNoteFallback` (soundfontPlayer dòng 664): `prog = _channels[channel].program || prog` — **override program track bằng cache channel** (track 3 cùng channel ghi đè) → oscillator preview mang character track 3. FIX: chỉ override khi `program === undefined`.
(2) **Draw/brush preview dùng `_playNoteFallback` (oscillator beep)** thay vì `playNote` (FluidSynth — nhạc cụ thật) — click note cũ dùng playNote nên đúng. FIX app.jsx: cả 2 chỗ (brush ~7725, draw ~7819) → `playNote` với `resolveTrackInstrumentCtx` (ch/synthEngine của track).
- **Các file ảnh hưởng:**`soundfontPlayer.js` + `app.jsx` + `index.html` (?v=202608060130 cho cả 2), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:**`npm run build` → hard refresh → track percussion → VẼ note mới → KỲ VỌNG: percussion thật (không phải nhạc cụ track trước).
### [2026-08-06 02:00] Task: Percussion piano-roll play "1 âm đầu rồi câm" — quét preset hợp lệ toàn font (cache theo sfId)
- **Báo cáo user:** track 4 percussion — play preview trong piano roll: chỉ 1 âm đầu, các âm sau câm.
- **Cơ chế:** fallback v01:00 chỉ thử 4 preset cố định [128,48],[0,0],[128,1],[0,48] — font "latin hand perc" KHÔNG có preset nào → `finalBank/finalProg` giữ (128,0) → channel cache = (128,0) INVALID → noteon thứ 2: progAlreadySet = true (cache khớp (128,0)) → SKIP re-select → noteon trên preset rỗng = CÂM. Âm đầu = noteon trên preset DEFAULT của font (auto-assign khi load).
- **FIX (soundfontPlayer doNote):** khi `finalBank === 128` → **quét toàn bộ preset font: bank 128 0-127 + bank 0 0-127 (tối đa 256 program_select, probe qua channel 9)** — chọn preset đầu trả 0 → cache `_validPercCache[sfId]` → finalBank/finalProg = preset hợp lệ → select + noteon trên preset ĐÚNG → mọi note kêu. Cache 1 lần/font (lần sau không quét lại).
- **Các file ảnh hưởng:**`soundfontPlayer.js` (?v=202608060200 — chỉ hard refresh, không build), `wiki.md`.
- **Ghi chú/Test:** hard refresh → play track percussion trong piano roll → KỲ VỌNG: MỌI note kêu (không chỉ âm đầu).
### [2026-08-06 03:00] Task: BẢO ĐẢM mastering chain xử lí MỌI track (solo) + preview piano roll khi mastering ON
- **Yêu cầu user:** (1) track solo → luồng âm PHẢI qua mastering chain khi ON (âm to); (2) preview note MIDI trong piano roll PHẢI qua mastering chain khi ON.
- **Cơ chế cũ:** ♪ bypass (trackMidiBypassMap/trackAudioBypassMap) → routeGain=0/dryGain=1 → track bỏ qua chain — kể cả khi chain ON.
- **FIX (app.jsx):**
(1) Helpers: `masteringChainOn()` + `effMidiBypass(track)`/`effAudioBypass(track)` — **chain ON → bypass luôn false (♪ bị override); chain OFF → theo ♪ maps**.
(3) `[masteringSettings]` effect: sau toggle+apply → **re-sync live nodes** (sfRouteGain/sfDryGain + route.routeGain/dryGain theo effBypass) + updateSfRouting — PWR bật/tắt áp ngay lên node đang phát.
(4) Preview piano roll: SF → sfEntry → sfRouteGain (=1 khi chain ON) → masterBus.input → chain ✓.
- **Ghi chú/Test:**`npm run build` → hard refresh → mastering ON → solo track bất kỳ → âm qua chain (to); preview note trong piano roll → qua chain.
### [2026-08-06 03:30] Task: Solo track AI không qua mastering khi bỏ solo track 1 — ép route tại thời điểm routing
- **Báo cáo user:** solo track 1 + solo track AI → cả 2 qua mastering ✓; bỏ solo track 1 → track AI solo KHÔNG qua mastering ✗.
- **Phân tích:** 2 case khác nhau: >1 audible → SF fallback `setOutputDestination(null)` → masterBus.input → chain ✓; 1 audible → SF → node.sfEntry → sfMods → sfRouteGain → chain — nếu sfRouteGain bị 0 (dry — node tạo lúc mastering OFF / state stale) → KHÔNG qua chain. AI track template không có bypass field (sạch) — nên nguyên nhân là ROUTE STALE tại node.
- **FIX (app.jsx updateSfRouting):** ép `sfRouteGain/sfDryGain` theo `effMidiBypass` NGAY TẠI thời điểm routing — cả nhánh PIANO_ROLL + nhánh midiAudible single-track (belt-and-suspenders — không chỉ lúc tạo node). Sửa lỗi gọi effMidiBypass với trackId string → track object.
- **Ghi chú/Test:**`npm run build` → hard refresh → mastering ON → solo track 1 + solo track AI → bỏ solo track 1 → track AI phải VẪN qua mastering. Nếu vẫn lỗi → dán console (tìm `[Bypass]` + `setOutputDestination` + `updateSfRouting error`).
### [2026-08-06 04:00] Task: AI Var clone kế thừa midiChannel track gốc → solo bị nhỏ (CC7 collision)
- **Báo cáo user:** track do USER chèn → solo âm bình thường; track do AI prompt chèn (AI Var) → solo âm NHỎ (nút solo con của track gốc). Yêu cầu kiểm tra quá trình AI clone.
- **Cơ chế (dòng 23042):**`const newTrack = { ...(srcTrack || {}), ... }` — clone AI Var spread TOÀN BỘ track gốc → **kế thừa `midiChannel`** → clone + track gốc DÙNG CHUNG channel. Solo track gốc (hoặc clone) → sync effect gửi `controllerChange(ch, 7, audible ? 100 : 0)` — track bị solo-mute (cùng channel) nhận CC7=0 → **notes của clone (cùng channel) cũng bị CC7=0 → âm NHỎ/CÂM**.
- **FIX (dòng 23046):** thêm `midiChannel: undefined` vào clone — `ensureTrackMidiChannel` (đã có guard) cấp channel RIÊNG (loop 0-15 skip 9). Kiểm tra: chỉ 1 chỗ spread srcTrack (23043) — AI composition dùng template sạch ✓.
- **Ghi chú/Test:**`npm run build` → hard refresh → tạo [AI Var] từ track → solo track AI Var → âm PHẢI bình thường (không nhỏ); solo track gốc → AI Var không bị ảnh hưởng.
### [2026-08-06 04:30] Task: Tab deactive → stop âm của tab đó (piano-roll tiếp tục kêu khi quay main)
- **Báo cáo user:** piano roll tab đang play → quay về MAIN/SECTION-TAB → VẪN nghe âm piano roll. Yêu cầu: tab nào deactive → stop âm tab đó.
- **FIX (app.jsx effect [activeTab] ~14571):**`prevActiveTabRef` lưu tab trước; khi đổi tab → nếu tab CŨ là sub-tab (PIANO_ROLL/section/audio) đang `isPlaying` → `stopAllPlayback()` + set `isPlaying: false` cho tab đó. Tab MỚI không bị ảnh hưởng; MAIN giữ hành vi cũ (mở piano-roll lúc main play → main tiếp tục — handleEditMidiInTab đã xử lý).
- **Ghi chú/Test:**`npm run build` → hard refresh → piano-roll play → bấm tab MAIN → âm piano-roll phải DỪNG ngay; play main → mở piano-roll → main tiếp tục (hành vi cũ).
### [2026-08-06 05:00] Task: Project không mở được khi đăng nhập máy khác/ẩn danh — 2 fix cross-machine
- **Báo cáo user:** login máy khác / browser ẩn danh → không mở được project đã tạo trước đó.
- **Chẩn đoán:** backend cloud key ĐÚNG theo user_id (JWT) ✓; OpenProjectModal có tab Cloud (list per-user) + Local (localStorage MÁY-ĐỊNH XỨ). Vấn đề: (1) `restoreLastSessionProject` đọc `sonic_project_id` từ localStorage — máy mới → trống → không restore gì; (2) project lưu LOCAL (`local_` id — chưa login lúc save) → localStorage → machine-bound.
- **FIX (app.jsx):**
(1) `restoreLastSessionProject`: `!lastId` (máy mới) + có profile (currentUser hoặc `localStorage sonic_user`) → **tự mở project Cloud GẦN NHẤT** (listCloudProjects → [0] → getCloudProject → restore). Local id có sẵn → hành vi cũ.
(2) `handleSaveLocalProject`: đã login → **đồng bộ lên Cloud (fire-and-forget saveCloudProject)** — project mở được từ máy khác.
- **Ghi chú/Test:**`npm run build` → hard refresh → login máy mới/ẩn danh → tự mở project Cloud gần nhất. Lưu local khi login → xuất hiện trong Cloud tab ở máy khác. Project local CŨ (tạo trước fix) → mở trên máy cũ + lưu lại → sync.
### [2026-08-06 05:30] Task: Incognito instrument câm — font KHÔNG còn trên server (chỉ trong IndexedDB browser thường)
- **Báo cáo user:** load project cũ ở browser ẩn danh → instrument không có âm; browser thường → OK.
- **Chẩn đoán:** soundfont (SGM-V2.01, latin hand perc) load từ IndexedDB cache → incognito cache RỖNG → fetch `/api/v1/plugins/soundfonts/download/{sfId}` → **404 — font KHÔNG còn trên server** (upload dir chỉ còn weedsgm3/518e850f; static/soundfonts rỗng; SYSTEM_SF_DIR không tồn tại). Browser thường: IndexedDB đã cache (từ lúc font từng tồn tại server) → không fetch → OK.
- **ĐIỀU KIỆN ĐỦ:** font PHẢI tồn tại trên server — user cần đặt file `SGM-V2.01.sf2/.sf3` + `latin hand perc.sf2/.sf3` vào `app/storage/soundfonts/` (hoặc upload qua UI) → mọi máy/browser fetch được.
- **Ghi chú/Test:** đặt font vào storage/soundfonts → restart backend → hard refresh → incognito load project → instrument có âm.
### [2026-08-06 06:00] Task: Incognito log xác nhận font vẫn 404 + fix crash onMouseMove (guard clip.buffer)
- **Log incognito (v0530):**`soundfont not loaded yet, loading: SGM-V2.01`×17 — load thất bại liên tục = font VẪN không có trên server (404). Kèm `Uncaught TypeError: Cannot read properties of undefined (reading 'duration')` onMouseMove — guard clip.buffer bị mất theo commit user 55d3464.
- **FIX (app.jsx):** re-apply guard `c.buffer` cho 5 chỗ `.buffer.duration` (rightEdgeClip ×2, hoveredClip, clickedClip ×2) — hết crash khi clip không có buffer (audio file load fail ở incognito).
- **ĐIỀU KIỆN CẦN (chưa đủ — user PHẢI thực hiện):** đặt file font (SF2 — export từ cache browser thường hoặc copy từ production /opt/daw_engine/soundfonts) vào `app/storage/soundfonts/` — dev instance KHÔNG có ffmpeg → KHÔNG dùng được .sf3 (cần .sf2). Fix code endpoint + fallback đã vào (v0530) — chỉ có tác dụng khi file tồn tại server-side.
### [2026-08-06 06:30] Task: Note bị DROP khi font không tải được (incognito) → fallback oscillator + cooldown 10s
- **Xác nhận hypothesis user:** "incognito bỏ qua bước FluidSynth WASM" — đúng cơ chế: doNote `if (finalSfId && !_sfHandleMap.has(finalSfId))` → loadSoundFont → `if (ok) doNote();` — **load FAIL (font 404) → note bị DROP âm thầm → WASM không nhận noteon → CÂM.** Incognito: cache rỗng → fetch 404; browser thường: cache có → ok.
- **FIX (soundfontPlayer.js doNote):**
(1) Load fail → **`_playNoteFallback` (oscillator — CÓ ÂM thay vì câm lặng)** + `_sfLoadFailAt[sfId]` timestamp.
(2) **Cooldown 10s**: sau fail, các note tiếp theo chạy thẳng fallback (không spam fetch 404 mỗi note).
- **Các file ảnh hưởng:**`soundfontPlayer.js` (?v=202608060630 — hard refresh, không build), `wiki.md`.
- **Ghi chú/Test:** hard refresh → incognito play track font chưa có → NGHE ĐƯỢC fallback (beep theo pattern — không câm). Vẫn khuyến nghị đặt font thật (SGM-V2.01.sf2...) để có âm thật.
### [2026-08-06 07:00] Task: Bundle mới KHÔNG load ở incognito — index.html cache heuristic (thiếu Cache-Control)
- **Báo cáo user:** re-compile + rebuild docker nhưng incognito vẫn không load bundle mới (URL cũ).
- **Xác minh production (daw.labz.io.vn):** index.html ĐÃ serve `soundfontPlayer?v=202608060630` + `precompiled?v=202608060600` — bundle MỚI NHẤT (precompiled chứa 9 markers fix: nanOut/effMidiBypass/prevActiveTabRef). **Production ĐÚNG** — vấn đề: **incognito dùng index.html CACHED CŨ** (stamp cũ → URL bundle cũ). Server không gửi Cache-Control → browser cache heuristic → HTML cũ.
- **FIX (app/main.py):** index.html (`/`) thêm `Cache-Control: no-cache, no-store, must-revalidate` — HTML luôn mới, bundle JS bust bằng ?v=.
- **Ghi chú/Test:**`npm run build` → hard refresh → mở piano roll → bấm Humanize (nghe velocity/timing đổi) → Transpose +2 (nghe cao hơn 1 tone) → Ctrl+Z hoàn tác.
### [2026-08-06 08:00] Task: Humanize có cường độ (Nhẹ/Vừa/Mạnh) + Transpose theo SCALE (12 tông major/minor)
- **Yêu cầu user:** tùy chỉnh lượng humanize (mạnh/nhẹ) + transpose theo scale major/minor đủ 12 tông.
- **Ghi chú/Test:**`npm run build` → hard refresh → piano roll → Humanize Mạnh vs Nhẹ; Chuyển giọng C major → D minor (map degree — melody giữ hình dạng) → Ctrl+Z.
### [2026-08-06 08:30] Task: Toolbar piano roll 2 hàng — nhóm nút chỉnh sửa note sang hàng mới
- **Yêu cầu user:** thêm hàng toolbar mới, di chuyển nút tính năng tương tự sang hàng mới.
- **Ghi chú/Test:**`npm run build` → hard refresh → mở piano roll → thấy 2 hàng toolbar; nút edit note (Humanize/Transpose/Chuyển giọng) ở hàng 2.
### [2026-08-06 09:30] Task: Fix emoji double-escape — nút "MIDI ghost notes" hiện text \uD83D\uDC7B
- **Báo cáo user:** nút MIDI ghost notes hiện ký tự lạ "\uD83D\uDC7B" (text literal thay vì 👻).
- **Nguyên nhân:** file app.jsx có `"\\uD83D\\uDC7B MIDI ghost notes"` (escape KÉP → runtime render text literal).
- **FIX:** thay bằng emoji thật `"👻 MIDI ghost notes"`. Kiểm tra: các emoji khác (Humanize 🎚, Chuyển giọng 🎵, Session 🌐/Isolated 📋, Nhẹ/Vừa/Mạnh) đều single-escape ✓ — chỉ nút Ghost bị lỗi.
- **Ghi chú/Test:**`npm run build` → hard refresh → mở midi item → dropdown hiện giọng detected (vd D minor) → bấm Chuyển giọng sang giọng khác → Ctrl+Z.
### [2026-08-06 10:30] Task: Toolbar — Đóng sang cuối hàng TRÊN (phải), Lưu + Export MIDI cuối hàng DƯỚI (phải)
- **Yêu cầu user:** move Lưu/Export (đổi tên → Export MIDI) cuối hàng bên phải; Đóng → cuối hàng trên bên phải.
- **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).
- **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.
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.