### [2026-08-03] Task: Remove FX Rack / MIDI Events / Selection panels khỏi bottom dock
### [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:** 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.
- **Ghi chú/Test (nếu có):** BUILD OK 977199 bytes, node --check OK, `pytest` 86 passed. Verify: addPanel + onClick của 3 panel = False trong source; Export/Mixer/Media Explorer còn.
- **Ghi chú/Test (nếu có):** BUILD OK 977199 bytes, node --check OK, `pytest` 86 passed. Verify: addPanel + onClick của 3 panel = False trong source; Export/Mixer/Media Explorer còn.
@@ -1573,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.