### [2026-07-28 07:56] Task: Fix PIANO_ROLL activeTracks + instrument auto-assign trong SECTION-TAB - **Tóm tắt thay đổi:** Fix `activeTracks` useMemo: khi `activeTab` là PIANO_ROLL sub-tab từ SECTION-TAB (`parent_tab_id` bắt đầu `session_`), trả về tracks của section tab thay vì main tracks. Trước đây `activeTracks` luôn trả về main tracks khi activeTab !== session_xxx → dropdown MIDI item rỗng, track name không hiển thị, instrument lookup sai. Fix: check `subTabs` cho PIANO_ROLL type để resolve đúng parent session tracks. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. Cần restart server + clear browser cache. --- ### [2026-07-28 07:47] Task: Sync MIDI item editing + ghost notes từ MAIN vào SECTION-TAB - **Tóm tắt thay đổi:** Fix `handleEditSectionInTab` xoá `midiItems: []` khi clone section tracks. SECTION-TAB giờ giữ nguyên MIDI items từ section data → dropdown MIDI item switcher hoạt động, ghost notes extract từ các track khác trong section, instrument settings được preserve. Piano Roll Tab dùng chung component `PianoRollTabEditor` nên tất cả tính năng (ghost, session sync, CC lane, export, AI) đều hoạt động trên cả MAIN và SECTION-TAB. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` — build passes. Cần lưu section trước (handleSaveSectionTab) để tích luỹ midiItems vào section.tracks, sau đó mở lại section tab để kiểm tra ghost notes + MIDI dropdown. --- ### [2026-07-28 07:13] Task: Instrument selector modal — 2 cột + font 14px - **Tóm tắt thay đổi:** Mở rộng modal `max-w-md` → `max-w-4xl`. Font size từ `text-[10px]`/`text-xs` → `text-sm` (14px). Bố cục 2 cột: trái danh sách soundfont, phải instruments của SF đang chọn. Click SF trái → lọc instrument phải. Nút "All Instruments" để xem tất cả. Search filter cả 2 cột. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` — build passes. Synth button → modal 2 cột → chọn SF trái → instrument phải. --- ### [2026-07-26 18:05] Task: Fix SpessaSynth CDN 404 + AudioWorklet init - **Tóm tắt thay đổi:** Sửa CDN URL từ `@latest/dist/spessasynth_lib.js` (404) → `@4.3.1/dist/index.js` (200). Dùng `WorkletSynthesizer` class + `audioWorklet.addModule(processor.min.js)`, `soundBankManager.addSoundBank()`, `await isReady`. Fallback WorkerSynthesizer nếu AudioWorklet không khả dụng. - **Các file ảnh hưởng:** `app/templates/index.html`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** SpessaSynth CDN load OK (200). Cần kiểm tra browser console. --- ### [2026-07-27 16:16] Task: Tăng chiều cao + fix phím mũi tên cho Copilot prompt + fix Babel build - **Tóm tắt thay đổi:** Tăng rows từ 2 → 4, thêm resize-y. Sửa ArrowUp/ArrowDown: chỉ kích hoạt lịch sử khi con trỏ ở đầu/cuối text. Fix lỗi dư `}` tại line 6795 khiến Babel build failed (pre-existing). Build thành công qua Babel, precompiled.js được sinh ra tự động. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** Kiểm tra prompt input rows=4, resize-y, phím mũi tên di chuyển con trỏ trong multi-line text. Chạy `npm run build` để kiểm tra. --- ### [2026-07-26 20:15] Task: Stabilize client preview (oscillator + FluidSynth server) - **Tóm tắt thay đổi:** Kết luận sau debug: SpessaSynth parser (fromArrayBuffer) không extract được presets từ SF2. Client luôn dùng oscillator (ADSR theo program). Server-side FluidSynth render cho âm thanh chính xác khi Export. Xoá debug logs, clean code. - **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/api/v1/plugins.py`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** Preview qua oscillator luôn có âm thanh. Export WAV dùng FluidSynth với SoundFont thật. --- ### [2026-07-26 17:50] Task: Implement SF3 conversion + SpessaSynth client player - **Tóm tắt thay đổi:** Python SF2→SF3 converter (ffmpeg Ogg Vorbis), startup auto-convert, download endpoint ưu tiên SF3. Client: IndexedDB cache, soundfontPlayer.js dual-mode (SpessaSynth + oscillator fallback), init + loadSF trong app.jsx. - **Các file ảnh hưởng:** `Dockerfile`, `app/core/soundfont_converter.py` (NEW), `app/api/v1/plugins.py`, `app/main.py`, `app/templates/index.html`, `app/static/js/services/soundfontStorage.js` (NEW), `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** DSK 11M→1.1MB (90%), SGM 529M→18MB (97%). SpessaSynth load từ CDN. Fallback oscillator nếu offline. --- ### [2026-07-26 17:25] Task: Fix duplicate Synth + MIDI keyboard silent handler - **Tóm tắt thay đổi:** Xoá nút Synth nhỏ giữa ARM và Monitor (dư thừa). Sửa MIDI keyboard handler: bỏ try/catch rỗng, thêm fallback tìm track armed trong main session, pass synth_engine context. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** ARM track → MIDI keyboard → playNote với instrument đã chọn. Server render dùng đúng SGM/SoundFont; client preview dùng oscillator emulation (giới hạn browser). --- ### [2026-07-26 17:20] Task: MIDI keyboard plays selected instrument on armed main track - **Tóm tắt thay đổi:** Khi ARM track trong MAIN SESSION/SECTION-TAB, nhấn MIDI Keyboard sẽ play note với instrument đã chọn (synth_engine/instrumentProgram). Fallback: nếu không có sub-tab PIANO_ROLL nào đang armed, tìm track đã ARM trong main session và dùng instrument của nó. Audio routing qua track's gainNode nếu có. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** ARM track → chọn SF instrument → bấm MIDI keyboard → nghe âm thanh instrument đã chọn. --- ### [2026-07-26 17:10] Task: Add Drum channel toggle on track panel - **Tóm tắt thay đổi:** Thêm button 🥁 trên track panel (giữa Solo và Record) để chuyển track sang drum channel 9. Khi active: set `is_percussion=true`, `soundfont_bank=128`. Thêm 🥁 indicator cho percussion presets (bank=128) trong instrument selector. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** Click 🥁 → track chuyển drum mode → render chạy channel 9. --- ### [2026-07-26 17:05] Task: Fix FluidSynth API mismatch (silent fallback) - **Tóm tắt thay đổi:** Pyfluidsynth thực tế là low-level CFFI wrapper, không có class `FluidSynth()`. Dùng sai API → tất cả SoundFont render fallback về oscillator (silence). Fix: dùng `new_fluid_synth`, `fluid_synth_sfload`, `fluid_synth_program_select`, `fluid_synth_write_s16_stereo`. Đồng bộ internal clock bằng cách render silence qua `write_s16_stereo` thay vì chỉ advance cursor. - **Các file ảnh hưởng:** `app/core/render_engine.py` - **Ghi chú/Test (nếu có):** DSK BAN-DI (prog=5) và SGM Brass (prog=56) đều có audio riêng biệt. Render peak ~0.04-0.056, spectral profile khác nhau rõ rệt. --- ### [2026-07-26 16:55] Task: Fix SoundFont program selection flow - **Tóm tắt thay đổi:** Sửa `_find_sf2_path` case-insensitive (catalog IDs lowercase vs filename mixed-case), thêm `synth_engine`, `soundfont_id`, `instrument_source` vào serialize/deserialize để project save/load không mất instrument selection. - **Các file ảnh hưởng:** `app/core/render_engine.py`, `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** Verified: `_find_sf2_path('sgm_v2.01')` → `SGM_v2.01.sf2`, render flat fields + synth_engine struct + roundtrip serialization all pass. --- ### [2026-07-26 16:45] Task: Implement VST3/SoundFont engine plan - **Tóm tắt thay đổi:** Fix SF2 path resolution (hardcoded → multi-dir scan), add `synth_engine` struct parsing in render & UI, 3-level fallback chain, pass context to client SoundFont player. - **Các file ảnh hưởng:** `app/core/render_engine.py`, `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/services/aiGateway.js` - **Ghi chú/Test (nếu có):** Verified: catalog API returns 31 condensed instruments, render with flat fields + synth_engine struct + drums (bank=128) + missing SF fallback all succeed. --- ### [2026-07-25 06:57] Task: Fix Piano Roll runtime errors (rollBeats TDZ + handleCCMouseMove) - **Tóm tắt thay đổi:** Sửa lỗi `Cannot access 'rollBeats' before initialization` bằng cách di chuyển khai báo `rollBeats`/`rollBeatsRef` lên trước `totalBeats`. Thêm hàm `handleCCMouseMove` bị thiếu và cập nhật `handleCCMouseDown` hỗ trợ Ctrl+Click paint velocity. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `cd /home/locpham/SonicForgeStudio && npm run build` — build passes. --- ### [2026-07-25 07:25] Task: 4 tính năng mới (Piano Roll zoom fill + Shift+scroll playhead + auto-scroll drag + SoundFont cache) - **Tóm tắt thay đổi:** (1) Piano Roll zoom-out không còn màn hình đen — bars fill toàn bộ viewport. (2) Shift+scroll trong Piano Roll di chuyển playhead và play notes MIDI như fast-forward. (3) Khi drag section/MIDI/clip đến cạnh phải timeline, auto-scroll container. (4) Server-side cache FluidSynth instances + PluginManager singleton + list_soundfont_instruments cache. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/core/vst_engine.py` - **Ghi chú/Test (nếu có):** `npm run build` pass. `vst_engine.py` thêm `load_soundfont_cached`, `release_soundfont`, `get_plugin_manager`, `_SF_INSTRUMENTS_CACHE` — refcount-based cache. --- ### [2026-07-26 09:36] Task: Piano Roll 4 features (SNAP, velocity selected, auto-scroll, Synth button) - **Tóm tắt thay đổi:** (1) SNAP trong MIDI tab — grid snap cho note drawing, selection marquee, loop range. (2) Ctrl+drag velocity — selected notes màu xanh dương, affect only selected; no selection = paint all. (3) Auto-scroll Piano Roll khi brush-drag gần cạnh top/bottom. (4) Synth button trong MIDI tab toolbar — mở instrument selector khi nhấn. Layout fix: thêm `h-full` cho outer div. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. Fix thiếu 1 `)` tại line 5959 return statement (gây build error pre-existing). --- ### [2026-07-26 10:08] Task: Fix auto-scroll continuous + CC velocity batch + flicker - **Tóm tắt thay đổi:** (1) Brush-draw gần cạnh top/bottom giờ dùng `setInterval` 30ms để cuộn liên tục (trước đây cuộn 1 lần per mousemove). Stop interval khi mouseup hoặc chuột rời khỏi threshold. (2) Ctrl+drag velocity cho selected notes — gộp tất cả `setNotes` vào 1 batch call (thay vì 1 call per note), tránh flicker do multi re-render. (3) `handleCCMouseDown` selectedMode cũng batch `setNotes`. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. Thêm ref `brushAutoScrollRef` cho setInterval auto-scroll. Batch `setNotes` trong cả `handleCCMouseDown` và `handleCCMouseMove` để tránh flicker. --- ### [2026-07-26 10:12] Task: Fix brush pitch-based scroll + CC velocity per-note paint - **Tóm tắt thay đổi:** (1) Brush auto-scroll: chuyển từ scroll theo viewport edge (clientY) sang pitch-based — scroll khi note mới được vẽ ở pitch gần rìa visible area (6 note threshold). Dùng `NoteHeight * 2` scroll step, setInterval 30ms. (2) CC velocity selected mode: thay vì paint all selected notes cùng lúc, paint từng note một theo vị trí chuột — tìm note gần cursor beat nhất, chỉ paint note đó nếu đang được selected. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. Thay `lastPainted` trong selectedMode từ array all indices thành array single-node index. --- ### [2026-07-26 10:20] Task: Fix brush scroll speed + CC snap + non-selected flicker - **Tóm tắt thay đổi:** (1) Giảm auto-scroll step từ `NoteHeight * 2` xuống `max(1, floor(NoteHeight * 0.5))` — cuộn chậm hơn để draw note đúng pitch. (2) CC lane `handleCCMouseDown` và `handleCCMouseMove` dùng `getSnapBeat(beat, snapVal)` khi tìm note index — snap đúng grid. (3) Non-selected CC flicker: xóa `paintNote` helper, inline `setNotes` trực tiếp trong mousedown, không gọi setNotes riêng lẻ gây re-render dư thừa. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-25 07:25] Task: Fix auto-scroll + maxDuration tab isolation + 1-bar margin - **Tóm tắt thay đổi:** (1) `maxDuration` dùng `activeTracks` + 4-bar buffer. (2) Cách ly MAIN vs SECTION-TAB. (3) Clip/section/MIDI drag/stretch/resize clamp 1-bar from right. (4) Clip drag + stretched clip dùng `updateActiveTracks`. (5) Stretched clip handler thêm auto-scroll. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass. ### [2026-07-25 07:31] Task: Fix zoom cursor + right edge black + AI Copilot for Piano Roll - **Tóm tắt thay đổi:** (1) Tách `drawWidth` (content) và `cssWidth` (CSS) — canvas coordinate system không còn bị sai khi zoom out. (2) Thêm `min-width` fill viewport, không còn mảng đen phải. (3) Thêm AI Copilot input + handler trong Piano Roll — gõ prompt tạo MIDI notes trực tiếp. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-25 08:08] Task: Fix velocity draw position, shift+scroll play, canvas redraw deps - **Tóm tắt thay đổi:** (1) CC lane velocity stem vẽ tại start note thay vì center. (2) Shift+scroll chỉ play note khi playhead vượt qua start_beat. (3) Thêm `viewWidth`/`viewBeats` vào dependency array grid canvas + CC canvas để auto-redraw khi resize/zoom. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-25 08:31] Task: Fix AI Copilot tab scope + hide bottom panels + context menu Insert + autoscroll - **Tóm tắt thay đổi:** (1) AI Copilot context dùng `activeTracks` thay `tracks` — chỉ tác động tab đang active. (2) Bottom panels mặc định ẩn. (3) Right-click track timeline có 4 mục Insert. (4) Thêm auto-scroll vào stretched clip resize. Sửa `insertSectionAtPlayhead` dùng `updateActiveTracks`. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass. ### [2026-07-25 08:36] Task: Fix auto-scroll + maxDuration tab isolation + 1-bar margin - **Tóm tắt thay đổi:** (1) `maxDuration` dùng `activeTracks` + 4-bar buffer. (2) Cách ly MAIN vs SECTION-TAB. (3) Clip/section/MIDI drag/stretch/resize clamp 1-bar from right. (4) Clip drag + stretched clip dùng `updateActiveTracks`. (5) Stretched clip handler thêm auto-scroll. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass. ### [2026-07-25 08:53] Task: Add timeline left/right margin + dynamic buffer expansion - **Tóm tắt thay đổi:** (1) PADDING_LEFT thay đổi từ 2s thành 2-bar margin ở cả WaveformLane và TempoTrackLane. (2) Thêm state `scrollBufferExtra` + auto-expand 4 bars khi auto-scroll gần đến rìa timeline. (3) `maxDuration` thêm `scrollBufferExtra` + 6 bars static buffer. (4) Clip drag/stretch dùng `activeTracks` + `updateActiveTracks` thay `tracks`/`setTracks`. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass. ### [2026-07-25 09:04] Task: Fix left margin 1-beat, items always visible, bar/timeline sync - **Tóm tắt thay đổi:** (1) PADDING_LEFT từ 2-bar xuống 1-beat ở cả WaveformLane và TempoTrackLane. (2) Drag clamp left ≥ 1 beat, right ≤ maxDuration - 1 bar. (3) Bar numbers + grid vẽ lại đồng bộ với scroll+zoom. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass. ### [2026-07-25 09:08] Task: Fix Piano Roll CC lane always visible + resizable height - **Tóm tắt thay đổi:** (1) `showCC` default `true` — CC lane luôn hiển thị. (2) Thêm `ccHeight` state + resize handle (drag top edge) — cho phép thay đổi độ cao CC lane. (3) Canvas dùng `ccHeight` thay hardcoded 80px. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass. ### [2026-07-25 09:18] Task: Fix brush velocity isolation from CC lane - **Tóm tắt thay đổi:** Thêm `brushVelocityRef` riêng biệt (0.8) — brush không còn bị ảnh hưởng bởi velocity paint từ CC lane. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass. ### [2026-07-25 09:20] Task: Auto-follow dragged item in viewport - **Tóm tắt thay đổi:** (1) Clip drag: sau mỗi update, nếu item ra khỏi vùng 120px viewport edge, tự scroll để item luôn thấy. (2) Section/MIDI drag: tương tự. (3) Section/MIDI resize: theo dõi edge đang kéo. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass. ### [2026-07-25 09:23] Task: Sync bar ruler redraw during drag - **Tóm tắt thay đổi:** (1) TempoTrackLane nhận `canvasRedrawCount` prop + deps — force redraw bar numbers khi drag. (2) Cả 3 drag handler increment `setCanvasRedrawCount` sau auto-follow scroll. (3) Sub-tab TempoTrackLane thêm `viewportWidth` + `scrollLeft` + `canvasRedrawCount` — đồng bộ ruler với timeline. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass. ### [2026-07-25 09:30] Task: Fix bar numbers disappear past bar 12 during drag - **Tóm tắt thay đổi:** TempoTrackLane đọc `scrollLeft` từ DOM (walk parent chain tìm scroll container) thay vì React prop — canvas luôn dùng scroll position thực tế, không lag do async state. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** ### [2026-07-25 09:58] Task: Fix drag speed + bar numbers visibility + snap section items - **Tóm tắt thay đổi:** (1) Xoá auto-follow phải (gây jump), chỉ giữ auto-follow trái 60px — scroll phải nhờ `autoScrollTimeline` mượt. (2) Thêm `PADDING_RIGHT` vào `tEnd` ở TempoTrackLane và WaveformLane — bar numbers + grid hiển thị đến sát rìa phải. (3) `if (localX < 0 || localX > drawWidth) continue;` — skip phần tử ngoài canvas. (4) Buffer 12 bars cho `maxDuration`. (5) Thêm `snapTime` vào section/MIDI item drag. (6) Thêm snap option `'4'` (1 bar). (7) Đồng bộ `PADDING_LEFT = barDuration` ở cả 2 lane. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npx babel app/static/js/app.jsx` — compile pass. ### [2026-07-25 10:08] Task: Merge tempo bar + timebar, beat-based ruler, snap grid - **Tóm tắt thay đổi:** Thay thế seconds-based ruler (0.00s, 1.00s...) + TempoTrackLane bằng TimelineRuler (48px canvas) hiển thị bar:beat grid lines + snap sub-ticks + BPM label. Grid WaveformLane khớp ruler, snapTime() đã dùng grid spacing beat — snap items đúng vị trí ruler. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` — build passes. --- ### [2026-07-25 10:35] Task: Restore time duration labels on TimelineRuler + TempoTrackLane - **Tóm tắt thay đổi:** Thêm time duration labels (formatTimeSimple: "0.00s", "1.00s",...) vào TimelineRuler và TempoTrackLane. Hiển thị dạng màu cam (#ffb464) ở chân ruler, động điều chỉnh interval (1s/2s/5s/10s...) theo zoom level. Time labels đã bị mất từ commit ff44fd4 khi merge time bar + tempo bar thành TimelineRuler. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` — build passes. --- ### [2026-07-25 10:39] Task: Sync TempoTrackLane leadIn + time labels on ruler top - **Tóm tắt thay đổi:** Thêm `leadInMargin` prop vào TempoTrackLane để đồng bộ column-0 (time=0) với vị trí leadIn của TimelineRuler. Thêm CLIP_BUFFER vào TempoTrackLane. Di chuyển time duration labels trong TimelineRuler từ dưới đáy (y=40) lên phía trên (y=11). - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` — build passes. --- ### [2026-07-25 11:09] Task: Fix TempoTrackLane scroll sync during drag auto-scroll - **Tóm tắt thay đổi:** TempoTrackLane/WaveformLane/TimelineRuler hiện đọc scrollLeft trực tiếp từ DOM wrapper (canvases tìm parent scrollable) thay vì React prop. Tránh lệch vị trí khi auto-scroll nhanh trong clip drag do React state batching. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` — build passes. --- ### [2026-07-25 11:04] Task: Fix PADDING_LEFT alignment + right-side bar rendering - **Tóm tắt thay đổi:** Set PADDING_LEFT=0 trong TimelineRuler/WaveformLane/TempoTrackLane để grid column 0 khớp với clip position 0 (trước đây lệch 8px). TempoTrackLane dùng CLIP_BUFFER/zoom cho tEnd thay vì PADDING_LEFT để bar/beat vẽ đủ xa về phía phải khi scroll. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` — build passes. --- ### [2026-07-25 10:58] Task: Fix clip/section snap to column 0 - **Tóm tắt thay đổi:** Xóa `Math.max(beatSec, ...)` trong clip drag (10325), section drag (10495), section resize (10576) — thay bằng `Math.max(0, ...)` để item có thể snap vào column 0. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` — build passes. --- ### [2026-07-25 10:54] Task: Fix TempoTrackLane height + clamp selection at column 0 - **Tóm tắt thay đổi:** Fix TempoTrackLane canvas height = 40px cố định (thay vì parentElement.clientHeight gây tràn). Wrapper `h-10 shrink-0 overflow-hidden` để không đẩy track lanes lệch TCP. Clamp selLeft/selRight/ruler handlers >= 0 để selection không vào vùng âm. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` — build passes. --- ### [2026-07-25 10:44] Task: Zero lead-in, time ruler + tempo track split layout - **Tóm tắt thay đổi:** Xóa leadInMargin (về 0) — time 0 tại mép trái, chỉ còn 8px PADDING_LEFT. TimelineRuler đơn giản thành time-only ruler (40px, chỉ hiển thị time labels). Thêm TempoTrackLane ngay dưới (40px, bars/beats/BPM). Selection overlay top từ 48→80px. Đồng bộ WaveformLane PADDING_LEFT=8/zoom. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` — build passes. --- ### [2026-07-25 10:18] Task: Lead-in margin, bar 0-indexed, grid clip fix - **Tóm tắt thay đổi:** Thêm leadInMargin (1 bar) đẩy timeline content sang phải — bar 0, time 0s tại mép margin. Playhead bắt đầu từ đó. Bar numbering 0-indexed (bar 0 thay vì 1). Sửa CLIP_BUFFER từ 200px → max(400, barWidth+200) để bar number không bị che. Đồng bộ leadInMargin vào tStart/tEnd canvas + mouse handlers (ruler, clip drag, section resize). - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` — build passes. --- ### [2026-07-25 11:32] Task: Redraw canvas on section item duration resize - **Tóm tắt thay đổi:** Thêm `setCanvasRedrawCount(n => n + 1)` trong resize mousemove handler để WaveformLane redraw khi section/MIDI item duration thay đổi. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` — build passes. --- ### [2026-07-25 17:22] Task: Piano roll toolbar - transport, ARM/INPUT, AI bar range - **Tóm tắt thay đổi:** Thêm transport (play/stop/back/next/record), ARM+INPUT select MIDI, AI bar range (bar x-y) vào piano roll toolbar. handleAISend phát hiện piano roll active → MIDI prompt. Thêm selectedMidiInputId + handleMidiInputSelect. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` — build passes. --- ### [2026-07-25 22:05] Task: Fix piano roll selection, playback, marquee drag/resize/move - **Tóm tắt thay đổi:** (1) Fix playback duration — min 16 beats when no notes exist. (2) Selection marquee: green transparent (#22c55e), full-height from top to CC lane. (3) Document-level mousemove/mouseup for marquee drag (không dừng khi ra khỏi grid). (4) Edge resize handles (left/right) cho marquee. (5) Body drag (move) cho marquee. Hiển thị marquee trên cả grid canvas và CC canvas. - **Các file ảnh hưởng:** `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `node --check app/static/js/app.precompiled.js` — syntax OK. --- ### [2026-07-25 22:18] Task: Fix marquee mouseleave, loop reschedule, tempo drag - **Tóm tắt thay đổi:** (1) Separate onMouseLeave from onMouseUp for grid canvas — preserve marquee when mouse leaves canvas. (2) Add MIDI note rescheduling on piano roll loop restart — 2nd+ loop iteration now plays sound. (3) Tempo bar drag already uses document-level listeners — works across columns. - **Các file ảnh hưởng:** `app/static/js/app.precompiled.js` --- ### [2026-07-26 10:24] Task: Fix scroll speed match mouse + CC flicker useLayoutEffect - **Tóm tắt thay đổi:** (1) Brush auto-scroll: thêm direct scroll target — mỗi mousemove tính pitchPixel, nếu gần rìa visible area (2 note margin) thì scroll trực tiếp tới target để tốc độ cuộn = tốc độ chuột. Giữ interval 16ms fallback cho trường hợp chuột ở rìa viewport. (2) CC flicker: đổi `React.useEffect` → `React.useLayoutEffect` cho CC canvas rendering effect — canvas paint trước browser paint, loại bỏ flash khi state thay đổi. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. Direct scroll target: `container.scrollTop = target` trên mỗi mousemove khi pitch ở gần rìa. --- ### [2026-07-26 10:28] Task: Fix velocity flicker - grid canvas useLayoutEffect - **Tóm tắt thay đổi:** Đổi grid canvas effect từ `React.useEffect` → `React.useLayoutEffect`. Nguyên nhân flicker: khi velocity thay đổi, grid canvas (useEffect) vẽ async sau paint → hiển thị velocity fill cũ 1 frame, trong khi CC canvas (useLayoutEffect) đã vẽ sync trước paint → hiển thị stem mới. Sự mismatch giữa 2 canvas tạo flicker. Fix: cả grid + CC canvas đều dùng useLayoutEffect, đồng bộ trước paint. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 10:39] Task: Fix brush shape draw + remove transport + loop repeat sound - **Tóm tắt thay đổi:** (1) Brush drawing: thay vì redistribute notes evenly, append từng note mới tại beat position thực tế (`getSnapBeat(beat, snapVal)`) — notes theo đúng hình dạng brush stroke. (2) Xóa transport controls (back/play/stop/record/forward) khỏi MIDI tab toolbar — chỉ dùng global transport. (3) Loop repeat: thêm `schedulePianoRollMidi` helper — reschedule MIDI notes qua SonicSF mỗi khi loop repeat, fix lỗi notes trong vùng chọn không có âm thanh khi play lặp lại. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 10:44] Task: Fix brush drawing smooth - track pitch + beat delta - **Tóm tắt thay đổi:** Brush drawing: theo dõi cả `lastDrawBeat` và `visitedPitches`. Tạo note mới khi pitch thay đổi HOẶC beat di chuyển >= minBeatStep (25% của grid duration). Dùng raw `beat` thay vì `getSnapBeat` để note theo sát vị trí chuột. Khởi tạo `lastDrawBeat: start` trong draggedNote. Fix lỗi draw bị ngắt quãng do chỉ tạo note khi pitch thay đổi. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 10:47] Task: Fix brush draw only pitch change + CC velocity flicker skip - **Tóm tắt thay đổi:** (1) Brush draw: chỉ tạo note mới khi `pitchChanged` (pitch chưa visited). Nếu cùng pitch, extend `duration_beats` của note cuối cùng trong `brushIds` — không tạo note mới trùng pitch. (2) CC velocity: thêm check `Math.abs(currentVal - val) > 0.001` trước mỗi `setNotes` — skip render khi velocity/pan không thay đổi, loại bỏ flicker cho cả selected + non-selected mode. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. currentVal tính: velocity = note.velocity || 0.8; pan = (note.pan || 0) / 2 + 0.5. ### [2026-07-26 10:49] Task: Fix brush draw per-note duration + noteStartBeats tracking - **Tóm tắt thay đổi:** (1) Thêm `noteStartBeats` array trong `draggedNote` — track start beat của từng note. Khi tạo note mới (pitchChanged), fix duration của note trước đó thành `beat - prevNoteBeat`. Khi extend (cùng pitch), chỉ extend note cuối với `beat - lastNoteBeat` (không extend toàn bộ stroke). (2) Khởi tạo `noteStartBeats: [start]` trong setDraggedNote. Fix lỗi note cuối có duration dài bằng toàn bộ brush stroke. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 10:55] Task: Fix brush draw pitch revisit by comparing lastDrawnPitch - **Tóm tắt thay đổi:** Thay `!visited.includes(snappedPitch)` bằng `snappedPitch !== lastDrawnPitch`. Bug cũ: khi vẽ C→D→C, visited.includes(C) = true → không tạo note mới → note D cũ bị extend. Fix: so sánh với pitch của note cuối cùng, cho phép vẽ tiếp khi pitch thay đổi dù đã visit trước đó. Bỏ `visitedPitches` (không còn dùng). - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 10:58] Task: Fix CC note detection with Y-aware stem matching - **Tóm tắt thay đổi:** Thêm `findCCNoteIndex(beat, mouseY, ccH)` helper — tìm note trong CC lane dựa trên cả X (beat) và Y (vị trí stem top). Khi nhiều note cùng beat (chord), so sánh `mouseY` với stem top của từng note, chọn note có stem gần nhất. Dùng helper này thay thế tất cả `notes.findIndex` và nearest-fallback trong `handleCCMouseDown` và `handleCCMouseMove`. Fix lỗi không detect được note khi velocity gần nhau hoặc cùng beat. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 11:03] Task: Remove MIDI tab loop button + Ctrl+click clear range - **Tóm tắt thay đổi:** (1) Xóa nút loop (repeat) khỏi MIDI tab toolbar — dùng global transport loop button thay thế. (2) Ctrl/Meta+Click trên ruler (tempo track lane) xóa loop range: set loopStartBeat/loopEndBeat = null, selectionStart/selectionEnd = null. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 11:06] Task: Fix MIDI tab real-time instrument change - restart playback - **Tóm tắt thay đổi:** `setTrackInstrumentWithProgram`: khi đổi instrument, kiểm tra nếu có PIANO_ROLL sub-tab với trackId đang playing → gọi `stopAllPlayback()` + `setTimeout` 50ms để `schedulePianoRollMidi` + `startSubTabPlayback` với instrument mới. Fix lỗi MIDI tab không cập nhật realtime khi thay đổi Synth. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 11:07] Task: Fix instrument change breaks loop playback - **Tóm tắt thay đổi:** Khi đổi instrument trong lúc MIDI tab đang play loop, restart code giờ cũng reset `startBufferOffsetRef` và gọi `requestAnimationFrame(updatePlayhead)` để tiếp tục vòng lặp animation. Fix lỗi loop bị ngắt sau khi change instrument. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 11:17] Task: Real-time velocity, ruler play, export MIDI button - **Tóm tắt thay đổi:** (1) Thêm `onRescheduleMidi` prop — khi edit velocity trong CC lane lúc đang play, gọi `window.SonicSF.stopAll()` + `schedulePianoRollMidi` với velocity mới. (2) Click ruler: set `currentTime` + nếu đang play thì `onStop` + `setTimeout` `onPlayPause` để play từ vị trí click. (3) Thêm nút Export cạnh Save — inline MIDI writer, xuất `.mid` file từ notes array. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 11:27] Task: Fix realtime velocity stale notes + ruler click restart - **Tóm tắt thay đổi:** (1) Compute `updatedNotes` synchronously trước khi gọi `setNotes`, pass into `onRescheduleMidi(updatedNotes)`. `schedulePianoRollMidi` nhận tham số `notesOverride` — dùng notes mới thay vì `st.notes` (stale). (2) Ruler click: thay `onStop()` (reset currentTime) bằng `stopAllPlayback()` trực tiếp + `schedulePianoRollMidi` + `startSubTabPlayback` + RAF từ `clickTime`. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 11:32] Task: Fix range auto-loop + gain fade-out crackling - **Tóm tắt thay đổi:** (1) Xóa `setIsLooping(true)` và `isLooping: true` khỏi ruler drag handler — range selection không còn tự động loop, chỉ loop khi nút LOOP global được kích hoạt. (2) Thay `window.SonicSF.stopAll()` abrupt bằng gain fade-out: `linearRampToValueAtTime(0.001, +40ms)` → delay 50-60ms → `stopAll` + reschedule → `linearRampToValueAtTime(volLinear, +15ms)`. Áp dụng cho cả `onRescheduleMidi` và `setTrackInstrumentWithProgram`. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 11:35] Task: Fix ruler click playhead - use onStop+onPlayPause props - **Tóm tắt thay đổi:** Sửa ruler click handler — xóa calls tới `stopAllPlayback`/`schedulePianoRollMidi`/`updatePlayhead` (không có trong scope PianoRollTabEditor). Dùng props: `setSubTabs({currentTime, isPlaying: false})` → `onStop()` → timeout 40ms → `setSubTabs({currentTime})` → `onPlayPause()`. Playhead di chuyển và play tiếp từ vị trí click. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 11:40] Task: Fix ruler cursor+range drag + SonicSF smooth stopAll - **Tóm tắt thay đổi:** (1) Ruler: cursor default, click trong range → drag move (hand cursor → move), click ngoài range → seek/drag tạo selection mới. (2) `soundfontPlayer.js`: `activeOscillators` lưu `{osc, gain}` thay vì `osc`; `stopAll` ramp gain về 0 trong 8ms trước khi `osc.stop(10ms)` — loại bỏ crackling khi realtime update. Cũng xóa `setIsLooping(true)` trong ruler drag cũ. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 11:56] Task: Fix Piano Roll playhead seek + sound cracking - **Tóm tắt thay đổi:** (1) Click ruler khi Piano Roll đang play → seek đến vị trí mới và tiếp tục play (không dừng). (2) ADSR envelope trong `soundfontPlayer.js` dùng `linearRampToValueAtTime` thay `setValueAtTime`/`exponentialRampToValueAtTime` để loại bỏ gain jump gây crackling. `stopAll` ramp gain về 0 trong 20ms trước khi stop oscillator. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 12:20] Task: Fix Piano Roll seek-to-start during playback - **Tóm tắt thay đổi:** Thêm hàm `seekPlaybackTo(time)` xử lý seek khi đang play cho cả sub-tab và main timeline. Nút "Quay lại đầu" (`skip-back`) và "Đầu vùng chọn" (`step-back`) giờ restart playback từ vị trí mới: stop → update refs → reschedule MIDI → restart. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 12:13] Task: Fix Piano Roll loop/stop behavior - **Tóm tắt thay đổi:** (1) Sub-tab loop selection chỉ active khi nút Loop (st.isLooping) bật — không còn bị ảnh hưởng bởi global isLoopingSelection. (2) Stop dừng ngay lập tức: `SonicSF.stopAll()` set gain 0 và stop oscillator tại ctx.currentTime, không ramp. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. ### [2026-07-26 12:31] Task: VST/SoundFont inspection engine + AI schema - **Summary:** New `SoundFontInspector` (sf2utils) scans .sf2 files, generates full/condensed catalog. Added `/soundfonts/catalog` API. Updated AI tool schema with `soundfont_id/bank/program`. Condensed catalog auto-injected into system prompt. Server render now passes bank/program + MIDI channel routing (drums → ch9). Added `DecentSamplerManager` + Pianobook render path. Client `SonicSF`: `applyAITrackInstrument`, `controllerChange`, `programChange`. Catalog cache invalidated on upload. - **Files:** `requirements.txt`, `app/core/soundfont_inspector.py` (new), `app/api/v1/plugins.py`, `app/core/vst_engine.py`, `app/core/render_engine.py`, `app/static/js/services/aiGateway.js`, `app/static/js/services/api.js`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.jsx`, `Dockerfile`, `.gitignore`, `vst_plugins/` (new), `samples/pianobook/` (new) - **Tests:** `python3 -m pytest tests/ -v` → 61 passed, 1 pre-existing failure. Python syntax check OK on all modified files. --- ### [2026-07-26 16:11] Task: Mount daw_assets host dirs into Docker - **Summary:** Created `/home/locpham/daw_assets/{vst3,soundfonts,pianobook}` on host. Mounted them into `web`, `worker`, and `beat` containers at `/opt/daw_engine/vst3`, `/opt/daw_engine/soundfonts`, `/opt/daw_engine/samples/pianobook`. - **Files:** `docker-compose.yml` - **Tests:** N/A --- ### [2026-07-26 16:21] Task: Implement Synth button with VST3 selection - **Summary:** Fixed Synth button to properly select and use system-installed VST3 plugins. Fixed field name mismatch (`instrument_id` vs `instrument`) between UI and render engine. Track auto-switches to MIDI type when VST assigned. SoundFont preset selection now passes `soundfont_bank` + `soundfont_program` to server. Serialize/deserialize includes all instrument fields. - **Files:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/core/render_engine.py` - **Tests:** `pytest` - 61 passed, 1 pre-existing failure. --- ### [2026-07-26 20:45] Task: Fix timeline play — schedule noteOn via setTimeout - **Tóm tắt thay đổi:** SpessaSynth noteOn(ch, pitch, vel, enableReverb) không nhận exactAudioTime. Timeline truyền startTime vào vị trí thứ 4 → SpessaSynth ép kiểu thành enableReverb=true → tất cả notes play ở T=0. Fix: tính delay = startTime - currentTime, dùng setTimeout để schedule noteOn đúng thời điểm. - **Các file ảnh hưởng:** app/static/js/services/soundfontPlayer.js, app/static/js/app.precompiled.js - **Ghi chú/Test (nếu có):** MIDI keyboard play ngay. Timeline play các note đúng thời điểm. --- ### [2026-07-26 21:00] Task: Sync loop button + auto-derive loop boundaries - **Tóm tắt thay đổi:** Nút Loop trên transport controls giờ đồng bộ với sub-tab đang active. Khi bật loop: tự động tính loopEnd từ MIDI notes, section clips, hoặc audio duration. Gán selectionStart=0, selectionEnd=loopEndTime. Main timeline cũng tự derive loop end từ tracks/items/clips. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** Bật Loop → playhead loop từ đầu đến cuối item/clip và ngược lại. ### [2026-07-26 21:15] Task: Fix stop + loop for all tab types - **Tóm tắt thay đổi:** `stopAll` giờ huỷ tất cả `setTimeout` đã schedule cho SpessaSynth notes (cả noteOn lẫn noteOff), gọi `allNotesOff(0-15)`. Loop hoạt động trên MAIN SESSION (qua `isLoopingSelection` + `selLeft/selRight`), SECTION-TAB và PIANO_ROLL (qua `st.isLooping` + `st.selectionStart/End`). - **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** Stop = dừng ngay. Loop = phát từ đầu đến cuối item/clip rồi lặp lại. ### [2026-07-26 21:30] Task: Sticky ruler + tempo lane on track scroll - **Tóm tắt thay đổi:** Wrap TimelineRuler và TempoTrackLane trong `sticky top-0 z-30 bg-[#1a1a1a]`. Khi cuộn tracks lên, ruler và tempo lane vẫn hiển thị dưới toolbar. Áp dụng cho cả MAIN SESSION và sub-tab views. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** Cuộn tracks lên → ruler + tempo lane dính ở top. ### [2026-07-26 22:05] Task: Fix multi-track bank loading (no delete, graceful 404) - **Tóm tắt thay đổi:** Xoá `deleteSoundBank` — SpessaSynth giữ nhiều banks đồng thời. Kiểm tra `soundBankList` trước khi tải lại. 404 từ server không làm mất bank đã load trước đó. - **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js` - **Ghi chú/Test (nếu có):** 3 tracks với 3 SF khác nhau → SpessaSynth giữ cả 3 → noteOn dùng đúng preset từ bank tương ứng. ### [2026-07-27 07:55] Task: Fix item duration when BPM changes - **Tóm tắt thay đổi:** Thêm `length_bars` vào midiItems/sections. `useEffect` trên BPM recalc `duration = length_bars * currentSecondsPerBar`. Fix section hardcoded 4s → `4 * secondsPerBar`. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** Tạo 8 bars item ở BPM 80 → đổi BPM 120 → item vẫn 8 bars. ### [2026-07-27 08:20] Task: Fix TempoTrackLane bar spacing on BPM change - **Tóm tắt thay đổi:** DOM traversal tìm scrollLeft bị gãy do sticky wrapper (tìm nhầm div có `position: sticky` với scrollLeft=0). Xoá DOM lookup, dùng `scrollLeft` prop trực tiếp. Fix cho cả TimelineRuler và TempoTrackLane. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** Thay đổi BPM → bars trên tempo track lane vẽ đúng khoảng cách. ### [2026-07-27 08:25] Task: Fix bar width drift for non-integer BPM - **Tóm tắt thay đổi:** Vòng lặp `t += beatDuration` tích luỹ sai số dấu phẩy động (đặc biệt BPM lẻ như 77, 81). Chuyển sang `t = bn * beatDuration` với bn là số beat nguyên — mỗi bar line tính độc lập, không sai số tích luỹ. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** BPM 77 → tất cả bars đều rộng bằng nhau. ### [2026-07-27 08:40] Task: TimelineRuler draws bar markers aligned with TempoTrackLane - **Tóm tắt thay đổi:** Thêm bar markers vào TimelineRuler dùng cùng `bn * barDuration` với TempoTrackLane. Số bar hiển thị ở giữa ruler. Time markers vẫn giữ phía dưới. Cùng BPM prop → luôn đồng bộ với tempo track. - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** Thay đổi BPM → TimeRuler bars + TempoTrackLane bars luôn đồng nhất. ### [2026-07-27 08:48] Fix: equal bar widths + selection tempo/time mode - **WaveformLane:** Integer beat loop thay vì `t += beatDuration` (fix bar width drift) - **Selection mode:** Nút toggle ♪T (theo tempo - đổi BPM → selection thay đổi) / ⏱T (cố định thời gian) ### [2026-07-27 09:25] Feat: eager soundfont scan at startup - **App load**: fetch full catalog with all instrument presets - **Merge**: gán presets vào từng soundfont trong instrumentSelectorData - **Synth button**: hiển thị presets từ cache, không cần API call per-SF ### [2026-07-27 09:55] Feat: Synth button shows flat instrument list from all SFs - **openInstrumentSelector**: flatten presets from all SFs + soundfont name tag - **Modal**: search bar + flat instrument list + soundfont name prefix - **Fix**: `2ad2922` had extra closing paren in PluginManagerModal (build fix) - **Arch**: loại bỏ hai-bước, click Synth → chọn instrument trực tiếp ### [2026-07-27 11:05] Feat: MIDI keyboard NoteOff sustain + CC/pitch bend realtime - **NoteOff**: durationMs=60000, NoteOff gọi stopNote(ch,pitch) → note tắt ngay - **MIDI channel**: truyền msg.data[0]&0x0F vào playNote/stopNote - **CC64 sustain/CC1 modulation**: forward qua SonicSF.controllerChange - **Pitch Bend**: forward qua SonicSF.pitchBend (14-bit) ### [2026-07-27 11:20] Fix: All Notes Off (CC123) before MIDI NoteOn - **Nguyên nhân:** SoundFonts như saxophone có release envelope dài, noteOff không tắt âm ngay - **Fix:** Gửi `controllerChange(ch, 123, 0)` = All Notes Off trước mỗi NoteOn từ MIDI keyboard - **Kết quả:** Note trước tắt ngay khi play note mới, không còn đè âm ### [2026-07-27 11:45] Fix: lazyInit in playNote + 3 root cause fixes - **Root cause 1:** NoteOn with velocity=0 treated as NoteOff ✅ (cmd === 0x8 || (cmd === 0x9 && vel === 0)) - **Root cause 2:** Same channel extraction for NoteOn+NoteOff ✅ (msg.data[0] & 0x0F) - **Root cause 3:** pitch/channel always integers from msg.data[] ✅ - **Added:** _lazyInit() called from playNote when _initialized false - **Oscillator fallback:** 2000ms instead of 60000ms (quick preview before init) ### [2026-07-27 12:05] Task: Fix Tremolo String continuous sustain after MIDI note-off - **Tóm tắt thay đổi:** Root cause: `stopNote()` used CC 123 (All Notes Off) which merely enters SoundFont's release envelope. Sustained instruments like Tremolo Strings (GM#44) have 2-5s release tails. Fix: replaced CC 123 with CC 120 (All Sound Off) in `stopNote()` and `stopAll()`. CC 120 sets `voice.isActive = false` immediately, bypassing the release envelope entirely, so the note stops instantly on key release. - **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js` - **Ghi chú/Test (nếu có):** Test với MIDI keyboard + Tremolo Strings instrument: note tắt ngay khi release key. CC 120 tác dụng lên toàn bộ channel (kill all voices) — phù hợp với keyboard preview use-case. ### [2026-07-27 12:45] Task: Migrate SpessaSynth → FluidSynth WASM engine - **Tóm tắt thay đổi:** Thay thế SpessaSynth (JS/AudioWorklet) bằng FluidSynth WASM (C++ compiled via Emscripten). CDN `@enikey87/fluidsynth-emscripten@0.1.1` cho dev (localhost), self-host `/static/js/vendor/` cho production. Giải quyết triệt để stuck notes (Tremolo/Saxophone) nhờ FluidSynth xử lý Gen 44 loop mode chuẩn SF2.04. Thêm `fluidsynth-bridge.js` AudioWorklet nhận PCM từ main-thread FluidSynth render loop. Giữ nguyên `window.SonicSF` API surface (0 thay đổi ở app.jsx). - **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html`, `app/static/js/services/fluidsynthLoader.js` (NEW), `app/static/js/worklets/fluidsynth-bridge.js` (NEW), `app/static/js/vendor/libfluidsynth-2.3.0-sf3.js` (NEW), `app/static/js/vendor/libfluidsynth-2.3.0-sf3.wasm` (NEW) - **Ghi chú/Test (nếu có):** Server render (pyfluidsynth) + client preview (FluidSynth WASM) = cùng C++ core → 100% audio parity. Cần clear browser cache. Babel 8 syntax error pre-existing (JSX `{ if(...) { } }` trong app.jsx), không rebuild được app.precompiled.js. --- ### [2026-07-27 15:59] Fix: _activeNotes array — multi-track ARM cùng pitch - **Tóm tắt thay đổi:** Khi 2 track ARM play cùng MIDI pitch, `_activeNotes['0:60']` bị overwrite (track2 ghi đè track1). `stopNote` chỉ stop 1 track. Fix: `_activeNotes[key] = [ch1, ch2, ...]` array. `stopNote` iterate all channels. - **Các file ảnh hưởng:** `soundfontPlayer.js` - **Ghi chú/Test (nếu có):** ARM 2 track, play note → thả phím → cả 2 track stop. --- ### [2026-07-27 15:47] Fix: Multi-track ARM + activeNotes cho stopNote - **Tóm tắt thay đổi:** MIDI handler `find(t=>isArmed)` → `filter(...)` loop all armed tracks. Thêm `_activeNotes[ch:pitch]` map để `stopNote` lookup đúng channel sau khi đổi SoundFont (channel allocation thay đổi). - **Các file ảnh hưởng:** `soundfontPlayer.js`, `app.precompiled.js` - **Ghi chú/Test (nếu có):** ARM 2 track cùng SF khác instrument → cả 2 play. Đổi SF → noteOff vẫn tìm đúng voice. --- ### [2026-07-27 16:05] Fix: program_select thay bank_select+program_change (SF preset search order) - **Tóm tắt thay đổi:** `bank_select` + `program_change` dùng search order qua tất cả SF đã load. Khi track 1 load SF2, preset lookup trên channel 0 tìm thấy preset của SF2 thay vì SF1 (SF2 là last-loaded → priority). Fix: `fluid_synth_program_select(synth, ch, sfHandle, bank, prog)` chỉ định rõ SoundFont ID, không phụ thuộc search order. Track 2 giữ nguyên instrument của SF1. - **Các file ảnh hưởng:** `soundfontPlayer.js` - **Ghi chú/Test (nếu có):** Track 1 SF1→SF2, track 2 SF1 → track 2 không bị ảnh hưởng. --- ### [2026-07-27 12:15] Task: Fix SpessaSynth loop voice not releasing (layer 2) - **Tóm tắt thay đổi:** CC 120 vẫn không đủ vì SpessaSynth 4.3.1 AudioWorklet có bug: looped voices trong MIDI message pipeline xử lý CC 120 sai (`processMessage`). Fix: thêm `noteOn(ch, pitch, 0)` (MIDI noteOff alternate path) + `_synthInstance.post({channelNumber:ch, type:"stopAll", data:1})` gửi lệnh trực tiếp đến worklet qua `handleMessage` — bypass hoàn toàn MIDI pipeline. - **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html` - **Ghi chú/Test (nếu có):** Cần clear cache browser (index.html cache-bust param updated). --- ### [2026-07-27 17:07] Task: MIDI Ghost Notes + Dropdown Item Switcher + Session Sync Mode - **Tóm tắt thay đổi:** Thêm ghost notes cho Piano Roll tab: tất cả MIDI items không được chọn thành ghost notes (25% opacity, dashed border). Dropdown thay thế tab title để chuyển nhanh MIDI item đang edit. Hai chế độ xem: Session Sync (ghost visible, bar labels aligned với session) và Isolated (bar 0, no ghost). - **Các file ảnh hưởng:** `app/static/js/services/ghostNoteExtractor.js`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html` - **Ghi chú/Test (nếu có):** Kiểm tra dropdown list đúng tất cả MIDI items. Switch item → ghost notes của item cũ hiện ra. Nút 🌐 Session/📋 Isolated chuyển chế độ. 👻 Ghost toggle chỉ hoạt động ở Session mode. ### [2026-07-28 07:03] Task: Auto-scan soundfont cho Synth button - **Tóm tắt thay đổi:** Thêm `SoundFontAutoScanner` — background daemon thread quét thư mục `/opt/daw_engine/soundfonts` và `app/storage/soundfonts` mỗi 30s. Dùng `sf_scan_state.json` để track file đã quét (theo size+mtime), chỉ inspect file mới/thay đổi. Catalog dùng scanner cache thay vì regenerate từ đầu mỗi request. `/soundfonts/catalog` API dùng scanner catalog. Upload/delete trigger scan tức thì. Thêm `.gitignore` cho `*.db` và `sf_scan_state.json`. - **Các file ảnh hưởng:** `app/core/soundfont_scanner.py` (NEW), `app/api/v1/plugins.py`, `app/core/soundfont_inspector.py`, `app/main.py`, `.gitignore` - **Ghi chú/Test (nếu có):** `python3 -c "from app.core.soundfont_scanner import SoundFontAutoScanner; s=SoundFontAutoScanner(); print(s.scan_once())"` — scan hoạt động. Condensed catalog dùng scanner's full_catalog tránh duplicate scan. --- - **Tóm tắt thay đổi:** Verify 4 rules: (1) MIDI item nhận instrument từ track cha qua `handleSwitchMidiItem` copy `instrumentProgram` từ track → sub-tab; (2) Section item KHÔNG nhận instrument (reset về null khi clone tracks trong `handleEditSectionInTab`); (3) Track khác không bị ảnh hưởng do `setTrackInstrumentWithProgram` filter by `trackId`; (4) Đổi instrument trong PianoRoll tab → áp dụng ngược lại cho track qua `openInstrumentSelector` → `setTrackInstrumentWithProgram`. Tất cả đều OK, không bug. - **Các file ảnh hưởng:** `app/static/js/app.jsx` (verify lines 4642-4667, 5905, 6763-6807, 8422-8424), `app/core/render_engine.py` (verify lines 95-115) - **Ghi chú/Test (nếu có):** No code changes needed. All 4 rules confirmed working. --- ### [2026-07-27 20:37] Task: Fix cross-track instrument interference (missed channel param) - **Tóm tắt thay đổi:** Sửa 6 vị trí gọi `SonicSF.playNote()` thiếu tham số `channel` + `synthEngine`, khiến FluidSynth mặc định `ch=0` cho mọi track → `program_change` trên channel 0 đè lên nhau giữa các track. Fix: thêm channel computation (từ `track.midiChannel` hoặc track index) + truyền `synth_engine` vào `playNote` tại `startTrackPlayback`, section sub-track MIDI, `playMidiPreviewNote`, piano roll Alt+scroll preview, note click preview, keybed onMouseDown/onMouseEnter. `setTrackInstrumentWithProgram` đã filter đúng `trackId` — lỗi không nằm ở logic set instrument mà ở playback. - **Các file ảnh hưởng:** `app/static/js/app.jsx` (lines 4846, 5121, 5661-5671, 10117-10124, 10179-10201, 10285-10307) - **Ghi chú/Test (nếu có):** `npm run build` pass. Cần test: main session play 2 tracks MIDI khác instrument → mỗi track giữ instrument riêng. Piano Roll preview note → không ảnh hưởng track khác. --- ### [2026-07-27 20:44] Task: Apply md/41_INSTRUMENT.md spec to codebase - **Tóm tắt thay đổi:** Implement spec: (1) Tạo `pianoRollTabService.js` với `getParentTrackByItemId`, `buildActiveScope`, `getTrackMidiChannel`; (2) Thêm `parent_track_id` vào tất cả MIDI items creation (deserialize, recording, AI, API); (3) `handleSwitchMidiItem` dùng `buildActiveScope` pattern — thêm `active_scope` vào sub-tab state; (4) Toolbar Piano Roll hiển thị `(Belongs to: Track X)` và `Synth (Track X): Instrument`; (5) Đồng bộ channel assignment về `trackIndex % 16` (spec §4); (6) Toàn bộ `playNote` calls dùng `SonicPianoRoll.getTrackMidiChannel` thay for-loop channel compute. - **Các file ảnh hưởng:** `app/static/js/services/pianoRollTabService.js` (NEW), `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html` - **Ghi chú/Test (nếu có):** `npm run build` pass. Cần test: multi-item piano roll switch item → scope cập nhật đúng. Parent track label xuất hiện trên toolbar. --- ### [2026-07-27 21:25] Task: Fix _playNoteFluid else branch leaking cross-track SF - **Tóm tắt thay đổi:** Khi track chưa set instrument, `_playNoteFluid` gọi `program_change(ch, 0)`. FluidSynth search program 0 trong ALL loaded SoundFonts → nếu chỉ có SoundFont của Track 2 được load, program 0 của SF đó gán cho channel của Track 1. Fix: bỏ `program_change`, dùng oscillator fallback trực tiếp. Track không instrument → sine wave, không leak instrument từ track khác. - **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. Cần test: Track 2 set instrument, Track 1 không set → Track 1 play sine wave (không phải SF program 0 của Track 2). --- ### [2026-07-27 21:29] Task: Fix multi-track ARM only first track produces sound - **Tóm tắt thay đổi:** ONMIDIMESSAGE handler (`app.jsx:6954-6998`) dùng raw MIDI hardware channel (`msg.data[0] & 0x0F`) thay vì dedicated channel của track, và `find(t => t.isArmed)` chỉ lấy track ARM đầu tiên. Fix: dùng `filter(t => t.isArmed)` + `getTrackMidiChannel` cho từng track — MIDI Note On/Off, CC, Pitch Bend đều route tới ALL armed tracks trên dedicated channel của mỗi track. - **Các file ảnh hưởng:** `app/static/js/app.jsx` (lines 6960-7040), `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. Cần test: ARM cả 2 track, bấm phím MIDI → cả 2 track đều phát ra instrument riêng. NoteOff gửi đúng channel. --- ### [2026-07-27 21:37] Task: Ctrl+drag on ruler for global selection - **Tóm tắt thay đổi:** `handleRulerMouseDown` cũ khi `e.ctrlKey` chỉ clear selection rồi return. Fix: Ctrl+click trên ruler khởi tạo `selectionMode='global'`, set `rulerDragStartRef` và `isDraggingRulerRef=true` → document mousemove handler (đã có sẵn) cập nhật selection range, selection overlay (đã render sẵn) hiển thị vùng chọn. - **Các file ảnh hưởng:** `app/static/js/app.jsx` (line 11133-11142), `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. Cần test: Ctrl+drag trên ruler → selection overlay xuất hiện. Shift+click vẫn extend selection. --- ### [2026-07-27 21:47] Task: Ctrl+drag sweep select + multi-item drag/copy - **Tóm tắt thay đổi:** (1) Ctrl+drag trên empty space (track lane) → sweep select (vùng chọn màu amber), mouseup → chọn tất cả items chạm vùng sweep. (2) Selected items highlight (viền vàng dày). (3) Ctrl+drag trên section/MIDI item → duplicate item. (4) Drag trên selected item → move cả nhóm items đã chọn. (5) Ctrl+drag trên selected item → copy cả nhóm. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** Cần test: Ctrl+drag empty space → sweep overlay + selected items highlight. Click+drag selected item → cả nhóm move. Ctrl+drag selected item → copy nhóm. --- ### [2026-07-28 08:09] Task: Fix Ctrl+Click+Drag không chọn item trong SECTION-TAB - **Tóm tắt thay đổi:** Xoá toggle selection (select/deselect) khỏi Ctrl+Click handler cho section/MIDI items. Trước đây Ctrl+Click vừa chọn item vừa set pending drag → item bị khoá cứng (selected), không thể kéo copy. Fix: Ctrl+Click chỉ set pending drag để copy, không chạm selection state. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. Ctrl+Click+Drag section/MIDI item → copy đến vị trí mới, item không bị selected. --- ### [2026-07-28 09:58] Task: Ctrl+S lưu section, Ctrl+A chọn tất cả items - **Tóm tắt thay đổi:** Thêm `activeTab.startsWith('session_')` branch trong Ctrl+S handler → gọi `handleSaveSectionTab`. Thêm Ctrl+A handler: collect tất cả sections/midiItems/clips từ `activeTracksRef.current` vào `selectedItemIds`. Hoạt động trên MAIN SESSION và SECTION-TAB. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. SECTION-TAB Ctrl+S → lưu section. Ctrl+A → chọn tất cả items. --- ### [2026-07-28 09:55] Task: Remove Insert Section từ context menu trong SECTION-TAB - **Tóm tắt thay đổi:** Thêm `!sessionTabs.some(s => s.id === activeTab)` cho "Insert Section" button trong JSX context menu. Section không thể chứa section item. Data-driven menu (line 14905) đã có check này. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. Right-click trong SECTION-TAB → không thấy "Insert Section". --- ### [2026-07-28 09:50] Task: Program change ở note time, không phải call time - **Tóm tắt thay đổi:** Di chuyển `program_change`/`program_select`/`bank_select` từ synchronous (call time) vào trong `doNote` (note time via setTimeout) trong `_playNoteFluid`. Trước đây tất cả program_changes xảy ra ngay khi `startTrackPlayback` chạy → MIDI items processed trước sections → section's program_change override cuối cùng → ALL notes (section + MIDI) play với section instrument. Fix: mỗi note có program_change tại đúng thời điểm của nó. - **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js` - **Ghi chú/Test (nếu có):** load. TRACK 1: | SECTION ITEM (inst B) | MIDI ITEM (inst A) | → section plays inst B, MIDI item plays inst A. --- ### [2026-07-28 09:30] Task: Thêm _isSectionClone flag — tắt âm thanh mặc định trong SECTION-TAB - **Tóm tắt thay đổi:** Thêm `_isSectionClone: true` vào section cloned tracks. Line 10283 kiểm tra flag: nếu section clone và không instrument → `undefined` (silent); nếu MAIN track và không instrument → `0` (GM Piano). Trước đây section cloned tracks đi qua line 10283 với `instrumentProgram: undefined` → default 0 → play GM Piano. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. SECTION-TAB không instrument → im lặng. MAIN track không instrument → GM Piano. --- ### [2026-07-28 09:27] Task: Silent fallback — không nghe âm thanh mặc định - **Tóm tắt thay đổi:** Thay oscillator fallback bằng silent return trong `_playNoteFluid` (soundfontPlayer.js:395). Khi không có instrument data, không play bất kỳ âm thanh nào (cả oscillator lẫn FluidSynth). - **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js` - **Ghi chú/Test (nếu có):** Section sub-track MIDI items không instrument → im lặng. MAIN track MIDI items không instrument → GM Piano (program 0). --- ### [2026-07-28 09:25] Task: Restore program default 0 cho MAIN track MIDI playback - **Tóm tắt thay đổi:** Restore line 10283 `instrumentProgram !== undefined ? instrumentProgram : 0` — MAIN track MIDI items cần program 0 để play (GM Piano) khi không load instrument. Chỉ section sub-track (line 10393) giữ `subTrack.instrumentProgram` (undefined → oscillator fallback) để tránh load MAIN SoundFont. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. MAIN track MIDI items play GM Piano khi không instrument. Section sub-track MIDI items dùng oscillator fallback. --- ### [2026-07-28 09:21] Task: Revert fix playhead — gây lỗi không play được - **Tóm tắt thay đổi:** Revert 2 changes từ 5d31563: (1) xoá `requestAnimationFrame(updatePlayhead)` thừa trong `handlePlayPause` (effect đã xử lý). (2) xoá `setCurrentTime(0)` trong `handleEditSectionInTab` (gây reset currentTime global). Giữ nguyên các fix instrument isolation + program default. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. Cần kiểm tra Play trên MAIN và SECTION-TAB. --- ### [2026-07-28 09:40] Task: Cả main + section MIDI playback không default program 0 - **Tóm tắt thay đổi:** Thay đổi line 10283 giống line 10393: `instrumentProgram !== undefined ? ... : 0` → `instrumentProgram`. Trước đây main playback cũng default program 0 khi `instrumentProgram` undefined → `_playNoteFluid` gọi `program_change(ch,0)` trên FluidSynth → load program 0 của MAIN SoundFont. Fix: undefined → oscillator fallback. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. MIDI items không instrument → oscillator fallback (không load SoundFont). MIDI items có instrument → FluidSynth play đúng program. --- ### [2026-07-28 09:37] Task: Section sub-track MIDI playback không default program 0 - **Tóm tắt thay đổi:** Line 10393 `subTrack.instrumentProgram !== undefined ? subTrack.instrumentProgram : 0` → `subTrack.instrumentProgram`. Trước đây khi `instrumentProgram` undefined (section clone), program default 0 → `_playNoteFluid` gọi `program_change(ch, 0)` trên FluidSynth → load program 0 của MAIN session's SoundFont. Fix: undefined program → `_playNoteFluid` dùng oscillator fallback (không load SoundFont). - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. Play MAIN session → section MIDI items không load SoundFont. User load instrument trong section tab → program set → FluidSynth play đúng. --- ### [2026-07-28 08:49] Task: Revert Synth button ẩn — track có thể chứa cả section + MIDI - **Tóm tắt thay đổi:** Revert conditional Synth button (815a6fa). Track có section item con VÀ midi item con → Synth vẫn hiển thị. Instrument chỉ áp dụng cho MIDI item, không ảnh hưởng section item (được xử lý bởi data isolation fix trước đó: section clone null hết soundfont fields). - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. Track sections + midiItems → Synth hiển thị, instrument chỉ tác động midiItems. --- ### [2026-07-28 08:36] Task: Synth chỉ hiển thị cho MIDI track, không cho section/audio track - **Tóm tắt thay đổi:** Synth button trong TCP giờ conditional: chỉ hiển thị khi track có midiItems (>0) và KHÔNG có sections. FX button giữ nguyên cho mọi track type. Logic đúng: Synth áp dụng cho MIDI item, FX áp dụng cho section/MIDI/audio clip item. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. Track có sections → ẩn Synth. Track MIDI → hiện Synth. Track audio → ẩn Synth. FX hiện ở mọi track. --- ### [2026-07-28 08:31] Task: Section track auto-load soundfont 1/prog 0 từ MAIN qua `...t` spread - **Tóm tắt thay đổi:** `...t` trong fallback branch spread `soundfont_id`, `soundfont_bank`, `soundfont_program`, `instrument_source` từ parent MAIN track. Dù đã null `instrumentProgram` và `synth_engine`, các field `soundfont_*` vẫn khiến FluidSynth load soundfont khi play. Fix: override tất cả soundfont fields thành null/undefined. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. MAIN loaded SF1/prog5 → mở section tab → section tracks không có SF. --- ### [2026-07-28 08:27] Task: KHÔNG auto-load instrument MAIN vào SECTION-TAB track - **Tóm tắt thay đổi:** Revert kế thừa instrument từ parent MAIN track. Section tracks giữ `instrumentId: null, instrumentProgram: undefined, instrumentName: null, synth_engine: undefined`. Trước đây kế thừa instrument (f2ac70e) → SECTION-TAB auto-load program 0 của SoundFont từ MAIN. User tự chọn instrument trong SECTION-TAB qua Synth button (đã hỗ trợ qua `updateActiveTracks` routing). - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. Load instrument MAIN → mở section tab → section tracks KHÔNG có instrument. --- ### [2026-07-28 08:12] Task: Sync instrument load methods từ MAIN vào SECTION-TAB PIANO_ROLL - **Tóm tắt thay đổi:** Fix `updateActiveTracksRef` — khi `activeTab` là PIANO_ROLL sub-tab có `parent_tab_id` trỏ đến SECTION-TAB, routing instrument update vào `setSessionTabs` thay vì `setTracks` (main). Trước đây đổi instrument trong PIANO_ROLL của SECTION-TAB ghi vào main tracks (vô hiệu). Cùng logic với `activeTracks` useMemo fix trước đó. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Ghi chú/Test (nếu có):** `npm run build` pass. Mở SECTION-TAB → PIANO_ROLL → Synth → chọn instrument → verify instrument được áp dụng cho section track (không phải main track). ---