Files
SonicForgeStudio/wiki.md
T
3dtours b42190e6bc feat: implement md/41_INSTRUMENT.md spec
- PianoRollTabService: getParentTrackByItemId, buildActiveScope,
  getTrackMidiChannel helpers
- parent_track_id on all MIDI items for reverse track lookup
- handleSwitchMidiItem uses active_scope pattern
- Toolbar shows parent track context labels
- Channel assignment simplified to trackIndex % 16
- All playNote calls use unified getTrackMidiChannel
2026-07-27 20:48:14 +07:00

52 KiB

[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ả handleCCMouseDownhandleCCMouseMove để 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 handleCCMouseDownhandleCCMouseMove 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.useEffectReact.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.useEffectReact.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ả lastDrawBeatvisitedPitches. 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 handleCCMouseDownhandleCCMouseMove. 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)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ả onRescheduleMidisetTrackInstrumentWithProgram.
  • 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-27 20:28] Task: Verify instrument assignment rules for MAIN SESSION

  • 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 openInstrumentSelectorsetTrackInstrumentWithProgram. 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)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.