Files
SonicForgeStudio/wiki.md
T

244 KiB
Raw Blame History

[2026-08-03] Task: Fix re-schedule STALE CLOSURE (dùng refs) + persist zoom qua reload

  • Tóm tắt thay đổi:
    1. Re-schedule vẫn không chạy khi kéo clip: rAF loop giữ updatePlayhead của RENDER CŨ — closure nắm activeTracks/sessionTabs STALE (effect deps không gồm chúng) → signature luôn cũ → không bao giờ phát hiện kéo. Fix: re-schedule dùng activeTracksRef.current + sessionTabsRef.current (sync mỗi render); solo check tính lại từ ref (curTracks.some(t => t.solo)).
    2. Zoom persist: zoom (App) khởi tạo từ localStorage.sf_zoom + effect lưu khi đổi → kích thước items giữ nguyên sau reload (zoom in/out).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608037000)
  • Ghi chú/Test (nếu có): BUILD OK 989793 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: Debounce 250ms cũ chờ sig "ổn định" — trong lúc kéo clip liên tục, sig đổi mỗi frame → KHÔNG BAO GIỜ re-schedule → clip kéo đi vẫn phát âm thanh cũ (không cập nhật realtime). Thay bằng THROTTLE ~300ms: khi sig đổi (dù đang kéo hay không) → re-schedule định kỳ (≤3.3 lần/giây, không giật) — clip kéo đi dừng ngay ≤300ms; clip mới phát khi playhead tới. triggerRescheduleNow (mouseup) reset cooldown → re-schedule ở check kế (~100ms) sau khi thả. Giữ mode-aware (solo/loop local/toàn session) + signature đầy đủ.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608036900)
  • Ghi chú/Test (nếu có): BUILD OK 989194 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: triggerRescheduleNow() — set pendingRescheduleRef = { sig hiện tại, time: 0 } → check kế tiếp trong updatePlayhead (~100ms) re-schedule LUÔN (time 0 → điều kiện >250ms thoả) — gọi ở mouseup của drag clip (draggedClipRef) + drag section/midi item (draggedSectionItemRef). Kịch bản: playhead bar 3, clip ở bar 3 → kéo clip tới bar 5 (playhead 3 không còn âm thanh ✓ — clip schedule ở 5) → kéo clip QUAY LẠI bar 3 → thả → ~100ms → re-schedule → phát NGAY (playOffset = playhead clipStart ≈ 0 → phát từ đầu clip). Debounce 250ms chỉ còn dành cho thao tác kéo liên tục (chống giật).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608036800)
  • Ghi chú/Test (nếu có): BUILD OK 989510 bytes, node --check OK, pytest 86 passed. triggerRescheduleNow ×3.

  • Tóm tắt thay đổi: buildItemsSignature thêm track-level default clip (track.bufferdef:startTime:speed) — vị trí clip default lưu ở track.startTime, KHÔNG nằm trong t.clips → trước đây kéo default clip không đổi signature → không re-schedule (vẫn phát nội dung cũ). Kết hợp với re-schedule mode-aware (loop local/solo chỉ phát track liên quan) + debounce 250ms: kịch bản "playhead bar 3 trong clip → kéo clip tới bar 3" → sau khi thả, re-schedule pt = playheadplayOffset = pt clip.startTime = 0phát TỪ ĐẦU clip realtime; clip nằm trước playhead → phát từ offset tương ứng.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608036700)
  • Ghi chú/Test (nếu có): BUILD OK 988860 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: Re-schedule (khi items đổi vị trí lúc play) trước đây luôn gọi startTrackPlayback(pt) → LOOP LOCAL bị phá (phát nhầm các track khác + mất hành vi loop). Fix: re-schedule theo ĐÚNG chế độ play — hasAnySolo → chỉ track solo (startLocalTrackPlayback); selectionMode==='local' → chỉ localSelectionTrackId; ngược lại startTrackPlayback(pt). Cả 2 hàm tính playOffset = pt clip.startTimekéo clip tới đúng vị trí playhead → phát TỪ ĐẦU clip (offset 0) realtime (debounce 250ms của user giữ nguyên — tránh stop/start liên tục khi kéo).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608036600)
  • Ghi chú/Test (nếu có): BUILD OK 988538 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: Khi đang play/loop mà user kéo/thay đổi vị trí item → vẫn phát nội dung cũ (source đã schedule với startTime cũ). Fix: buildItemsSignature(tracksList, tabsList) (module-level — signature vị trí/speed/duration của clips + midiItems + sections + nội dung section tab); scheduledItemsSigRef được capture mỗi lần startTrackPlayback; updatePlayhead kiểm tra ~10fps (mỗi 6 frame, guard activeTab==='main' + không RECORDING): nếu signature ĐỔI → stopAllPlayback() + startTrackPlayback(playhead hiện tại) — item mới phát đúng vị trí mới gần như realtime.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608036500)
  • Ghi chú/Test (nếu có): BUILD OK 987414 bytes, node --check OK, pytest 86 passed. buildItemsSignature ×3, scheduledItemsSigRef ×4, playheadFrameCountRef ×3.

  • Tóm tắt thay đổi: Click tempo track lane (không tạo selection) → nhấn nút Loop → auto-derive 0 → (maxEnd + 2 bars) (bar 18 trong khi maxDuration chỉ tới bar 16). Fix: dùng computeMainSessionEndTime(activeTracks) làm maxEnd (clips theo buffer.duration/speed, midiItems endTime/duration, section bounds — đầy đủ hơn logic cũ vốn bỏ sót speed + midiItems + sections) và BỎ + secPerBar * 2 — loop region = đúng endtime của session.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608036400)
  • Ghi chú/Test (nếu có): BUILD OK 985447 bytes, node --check OK, pytest 86 passed. Không còn maxEnd + secPerBar * 2 trong bundle.

  • Tóm tắt thay đổi: maxDuration = endtime + 12 bars buffer + scrollBufferExtra (dành cho scroll/zoom) — nhưng updatePlayhead dùng nó làm điểm dừng LOOP → loop kéo dài quá duration thật. Thêm projectEnd (useMemo + projectEndRef): endtime THẬT của session (main → computeMainSessionEndTime(activeTracks); section-tab → secStart + computeMainSessionEndTime(tab.tracks); RECORDING → +60s; tối thiểu 1s) — KHÔNG buffer. updatePlayhead (nhánh hết bài) đổi maxDurationRef.currentprojectEndRef.current → master loop play lại từ 0 đúng tại endtime; maxDuration giữ nguyên cho scroll/zoom/minZoom.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608036300)
  • Ghi chú/Test (nếu có): BUILD OK 985502 bytes, node --check OK, pytest 86 passed. projectEndRef ×3, loop stop dùng projectEnd ✓, secPerBar*12 còn (scroll).

  • Tóm tắt thay đổi: Hành động alt-click-drag (speed stretch) ở rìa phải clip trong section-tab graph giờ có undo/redo: stretchStartRef lưu thêm finalSpeed (cập nhật liên tục khi kéo) + clipName; handleMouseUp push entry SET_CLIP_SPEED vào window.UndoRedoEngine (chỉ khi speed thực sự đổi > 0.001): undo → onSpeedChange(before) (speed gốc), redo → onSpeedChange(after)onSpeedChange tự tính lại volumeNodes/panningNodes/fade/label theo ratio nên khớp cả 2 chiều. Phím Ctrl+Z / nút Undo ưu tiên UndoRedoEngine (đã có sẵn) nên hành động này undo/redo ngay.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608036200)
  • Ghi chú/Test (nếu có): BUILD OK 984340 bytes, node --check OK, pytest 86 passed. SET_CLIP_SPEED ×2, finalSpeed ×8 trong bundle.

  • Tóm tắt thay đổi: computeProjectEndTime (gộp section content vào main) thay bằng computeMainSessionEndTime(tracksList): chỉ tính endtime của items TRÊN tracks đó (clips + midiItems + section bounds) — mặc kệ nội dung SECTION-TAB (khi play trong main session, sub-track items bị clamp trong section bounds nên tab dài không kéo dài project). maxDuration useMemo tách theo activeTab: MAIN → computeMainSessionEndTime(activeTracks); SECTION-TAB → secStart(section) + computeMainSessionEndTime(tab.tracks) (tab.sectionId → tìm section start trên main). Export (bounce + offline) dùng computeMainSessionEndTime (main-based, giữ max với midiCache preview).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608036100)
  • Ghi chú/Test (nếu có): BUILD OK 983477 bytes, node --check OK, pytest 86 passed. computeMainSessionEndTime ×5, computeProjectEndTime = 0.

  • Tóm tắt thay đổi: maxDuration (loop stop) + triggerBounceExport + clientSideExport tính end time THIẾU nội dung SECTION-TAB (sub-track clips/MIDI bên trong section + session tabs độc lập) → loop dừng sớm / export cắt cuối bài. Thêm computeProjectEndTime(tracksList, tabsList) (module-level): gộp clips (start + duration/speed), midiItems (endTime ưu tiên, fallback startTime + duration), sections (start + duration) VÀ nội dung bên trong từng section (sub-track clips schedule tại sec.start + local, sub MIDI tại sec.start + item.startTime) + session tabs độc lập. Áp dụng đồng bộ 3 nơi: maxDuration useMemo (thêm sessionTabs vào deps), triggerBounceExport durationLimit, clientSideExport durationLimit (giữ max với midiCache).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608036000)
  • Ghi chú/Test (nếu có): BUILD OK 983950 bytes, node --check OK, pytest 86 passed. computeProjectEndTime ×4 trong bundle.

  • Tóm tắt thay đổi:
    1. Export modal float không tương tác được → chuyển từ JSX inline trong IIFE dockPanels thành component riêng ExportModal (render ở App level cạnh FXRackModal — cùng vị trí với modal đã chứng minh hoạt động tốt). Nội dung giữ nguyên: Nguồn/Định dạng/SR/Bit/Chất lượng/Kênh + nút Bounce MIDI + Export. Loại bỏ mọi nghi vấn stacking-context/pointer-events từ IIFE.
    2. Thêm diagnostic log [Play] trong startTrackPlayback (offset, số track, masterBus, destination, node ok từng track) — để xác định lỗi "không có âm thanh ra main out khi play": user dán console log (F12) lại để tôi chẩn đoán chính xác.
    3. Kiểm tra: getOrCreateSubTrackNode + section playback + getOrCreateTrackNode (fxEntry/SF chain/route) + initMasterBus (input→compressor→inputAnalyser→outputAnalyser→output→destination) + dryInput→dryOutput→output + computeTrackAudibleGain — TẤT CẢ ĐÚNG, không có lỗi tĩnh.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608035900)
  • Ghi chú/Test (nếu có): BUILD OK 982816 bytes, node --check OK, pytest 86 passed. ExportModal ×2 (định nghĩa + render), [Play] log có trong bundle.

  • Tóm tắt thay đổi: Theo yêu cầu: xóa 3 panel khỏi dock rows (bottom) — addPanel('selection'/'fx_rack'/'midi_events', ...) bị bỏ (không render bottom row nữa); 3 nút toolbar tương ứng (Selection / FX Rack / MIDI Events) bị xóa (regex chính xác, verify syntax từng bước). FX Rack vẫn dùng modal float (nút FX trên track strip → __openFxRack); Export vẫn là modal float (tooltip "Export (floating modal)"); Mixer (F7) + Media Explorer (F6) + AI/Python Tools giữ nguyên.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608035800)
  • Ghi chú/Test (nếu có): BUILD OK 977199 bytes, node --check OK, pytest 86 passed. Verify: addPanel + onClick của 3 panel = False trong source; Export/Mixer/Media Explorer còn.

  • Tóm tắt thay đổi: Export panel không còn nằm trong dock rows (bottom row) — bỏ addPanel('export', ...); khi bấm nút Export → modal fixed inset-0 z-[300], overlay đen + blur, panel căn giữa màn hình (max-w-2xl, rounded, header EXPORT + nút X đóng, body scroll max-h 72vh) — nội dung dùng lại renderPanelContent('export'); click overlay → đóng; lucide icons re-render khi mở; tooltip nút cập nhật "Export (floating modal)".
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608035700)
  • Ghi chú/Test (nếu có): BUILD OK 979154 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: Hiện thực hóa ý tưởng "bounce về client + cache khi preview":
    1. Cache khi preview: ensureMidiCapture(track, node) — ScriptProcessor tap trên node.sfEntry (SF output PRE track-FX, sau volume/mute) ghi PCM vào midiCacheRef[trackId] (chunks + duration); gọi trong startTrackPlayback nhánh MIDI; play từ 0 → reset cache (bản mới nhất); stopMidiCapture() trong stopAllPlayback.
    2. Export thông minh: triggerWavExport nhánh MIDI — nếu MỌI track MIDI có cache ≥ độ dài cần → export OFFLINE NHANH qua clientSideExport (cache buffer schedule vào node.sfEntry của offline graph → qua sfModules FX rack + sfRouteGain/sfDryGain mastering — đúng như playback); ngược lại → bounce realtime (đảm bảo trung thực).
    3. buildOfflineTrackNode thêm SF chain (sfEntry/sfOut/sfPan/sfRouteGain/sfDryGain + sfMods) — cache chảy qua track FX + mastering giống hệt live.
    4. durationLimit của offline export gồm cả độ dài MIDI cache.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608035600)
  • Ghi chú/Test (nếu có): BUILD OK 977934 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: triggerWavExport (nút Export) giờ kiểm tra project: nếu CÓ midiItems (track chính hoặc session tabs) → TỰ ĐỘNG gọi triggerBounceExport() (realtime bounce — giữ trung thực MIDI + FX Rack + Mastering Chain) kèm toast thông báo; nếu KHÔNG có MIDI → giữ export offline/server nhanh như cũ. User chỉ cần nhấn Export sau khi mix.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608035500)
  • Ghi chú/Test (nếu có): BUILD OK 973121 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: Export client (OfflineAudioContext) không render được MIDI (FluidSynth = ScriptProcessor, không hỗ trợ offline). Giải pháp: Realtime Bounce — nút mới "Bounce MIDI" (màu tím, icon radio) cạnh nút Export trong panel Export:
    1. triggerBounceExport(): tính duration (clips + midiItems + sections + tail 1.2s) → tạo ScriptProcessor capture tap trên masterBus.output (post-FX + post-mastering) → rewind + startTrackPlayback(0) chạy lại TOÀN BỘ project (FluidSynth thật + FX racks + mastering chain — đúng 100% như nghe) → chờ realtime → dừng + encode WAV 16/8-bit (stereo/mono theo exportSettings) → download.
    2. Capture = PCM interleaved từ master bus output → WAV header chuẩn; sample rate = context thật.
    3. Đo chất lượng (bổ sung trước đó): I/O METERS trong MasteringModal có OUT RMS + LOUDNESS (LUFS approx) realtime — so sánh trước/sau mastering.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608035400)
  • Ghi chú/Test (nếu có): BUILD OK 972502 bytes, node --check OK, pytest 86 passed. Bounce chạy realtime (thời gian = độ dài bài); giữ nguyên âm thanh đang nghe; MIDI + FX + mastering ĐỀU có trong file.

  • Tóm tắt thay đổi: 2 yêu cầu:
    1. Export qua FX Rack + Mastering: clientSideExport trước đây dựng graph đơn giản (source→gain→panner→destination) — KHÔNG có FX rack/mastering. Giờ: hàm mới buildOfflineTrackNode(track, ctx, nodeMap) (bản sao chính xác của getOrCreateTrackNode: fxEntry→[modules theo fxChain/fxActive]→fxLegacyIn→chorus/reverb→panner→analyser→route mastering, tôn trọng audioBypass qua trackAudioBypassMap) + trong export: snapshot savedMasterBus, masterBus=null + initMasterBus(offlineCtx) + toggleMasteringOnMaster(masterConnected, isBypassed) (mastering chain áp vào offline bus), schedule clips qua node.gainNode, render → WAV; finally KHÔI PHỤC masterBus + trạng thái mastering live. Export WAV client giờ NGHE ĐÚNG NHƯ PLAYBACK (track FX + mastering).
    2. Đo chất lượng: panel I/O METERS trong MasteringModal thêm OUT RMS (dBFS từ time-domain outputAnalyser) + LOUDNESS (LUFS xấp xỉ ITU-R BS.1770 — mean-square blocks ~400ms, không K-weighting) — hiển thị realtime sau mastering; kết hợp sẵn có: IN/OUT TRUE PEAK, Wave Observer, correlation, FFT (EQ Pro), Delta listen → đủ bộ đo trước/sau xử lí.
    • LƯU Ý: export client chỉ render AUDIO tracks (clips/sections có buffer) — MIDI items cần FluidSynth (ScriptProcessor không hỗ trợ OfflineAudioContext) → không nằm trong file export; server-side export (multitrack /mix) vẫn dùng cho trường hợp đó.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608035300)
  • Ghi chú/Test (nếu có): BUILD OK 967644 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: Xử lý 2 phản hồi:
    1. Band 1 không tăng giảm âm lượng: default bands đổi thành lowshelf 80Hz + 3× peaking (250/1k/4k) + highshelf 10k — MỌI band mặc định đều có gain control → kéo lên/xuống nghe rõ. (Highpass/lowpass vốn không có gain trong WebAudio — giờ chỉ xuất hiện khi user chủ động chọn type.)
    2. Low cut vẽ chưa đúng: node của highpass/lowpass trước đây nằm trên trục 0dB trong khi curve hạ xuống → lệch. Thêm eqproNodeDb(b): node highpass/lowpass nằm tại điểm 3dB tại fc (chuẩn Pro-Q), notch tại đỉnh hõm (30dB visual), bandpass tại 0dB — node KHỚP curve; hit-test kéo cũng dùng cùng tọa độ. Verify RBJ: highpass 3.01dB@fc, 12.3dB/oct, 24.1dB/2oct; lowpass 3.01@fc; notch 53.5; bandpass 0; peaking +6; highshelf 4 ✓.
    3. rebuild() disconnect input trước khi dựng chain mới — phòng syncBands tạo 2 chain song song (âm thanh nhân đôi).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608035200)
  • Ghi chú/Test (nếu có): BUILD OK 963191 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: Xử lý 2 phản hồi:
    1. Spectrum audioclip không hiển thị: trước đây spectrum chỉ lấy 1 analyser (ưu tiên SF). Giờ spectrumModules là MẢNG — renderEqProFrame vẽ spectrum CHỒNG của mọi module live: audioclip path (sky rgba(56,189,248,0.20)) + soundfont/MIDI path (pink rgba(236,72,153,0.16)) — play clip hay MIDI đều hiện.
    2. Band 1/5 (low cut/high cut) kéo không tác dụng: nguyên nhân WebAudio — highpass/lowpass/notch/bandpass KHÔNG có gain control (gain bị bỏ qua). Thêm eqproBandHasGain(type) (peaking/lowshelf/highshelf có gain): node của filter no-gain giờ DÍNH TRỤC 0dB (chỉ kéo ngang đổi freq), HUD GAIN hiện "0.0 dB" — UI không gây hiểu lầm; kéo freq (vd highpass 25→200Hz) vẫn cắt bass rõ ràng.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608035100)
  • Ghi chú/Test (nếu có): BUILD OK 962906 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: Xử lý 5 phản hồi:
    1. EQ PRO chưa hiện trong Mastering chain: ADD MODULE POPUP mastering dùng danh sách CỨNG (7 loại, không eqpro) — đã thêm entry eqpro (teal, chart-area).
    2. Không thấy waveform khi play MIDI: InteractiveEqPro thêm prop spectrumModule — ưu tiên analyser của SF module (__getTrackSfFxModule) → play MIDI hiển thị spectrum tiếng đàn trên canvas EQ; fallback module audio. 3+5. Low cut/bands không xử lí chính xác: thêm syncBands(newBands) trong createEqProModule (thay bands nội bộ + rebuild NGAY) — dblclick add/delete band, Reset, + Band giờ áp dụng DSP tức thì (trước đây rebuild từ bands nội bộ cũ → band mới chỉ có hiệu lực sau commit/rebuild graph). Kéo node → setBand realtime trên cả audio + SF (đã có từ v34900).
    3. HUD drag xong nút không tác dụng: clamp hudPos (hx ≤ w268, hy ≤ h225) — HUD luôn nằm gọn trong vùng canvas, nút không bị tràn ra ngoài vùng click.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608035000)
  • Ghi chú/Test (nếu có): BUILD OK 961976 bytes, node --check OK, pytest 86 passed; verify createEqProModule: 5 default filters đúng types, highpass freq 200 đúng, inactive→gain 0, amount 50%→gain 3 (mock).

  • Tóm tắt thay đổi: Xử lý 4 phản hồi:
    1. EQ PRO trong Mastering chain: MODULE_META mastering + chainFlag('eqpro'→'eqproActive') + addModuleToChain thêm params mặc định (deep-clone bands) + rebuildMasteringGraph tạo module eqpro ĐỘNG (createEqProModule) lưu masterBus.eqProInstances[mod.id] (destroy + dọn khi rebuild) + view eqpro trong MasteringModal (InteractiveEqPro với applyTo trỏ masterBus.eqProInstances + updateChainEntryParams).
    2. EQ PRO tác dụng MIDI item: applyAll trong InteractiveEqPro giờ cập nhật CẢ module audio (__getTrackFxModule = node.fxMods) lẫn SF (__getTrackSfFxModule mới = node.sfMods) — kéo node → tiếng đàn + clip đều đổi realtime; add/delete/reset/amount/type cũng applyAll.
    3. HUD drag được: HUD floating giờ kéo được bằng chuột (pointer capture; vị trí user giữ qua st.hudPos, tự động clamp trong canvas; bấm node khác → vẫn giữ vị trí đã kéo).
    4. Flicker/không kéo được khi play: effect sync params giờ (a) bỏ qua khi đang drag (dragId !== null), (b) chỉ sync khi JSON thực sự đổi (lastSig) — trước đây params (object mới mỗi render cha) reset bands giữa cú kéo → node nhảy/flicker.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608034900)
  • Ghi chú/Test (nếu có): BUILD OK 960862 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: Tạo module EQ chuyên nghiệp theo spec (FabFilter Pro-Q / iZotope Ozone style) tích hợp FX Rack Panel:
    1. DSP (createEqProModule): cascade BiquadFilterNode nối tiếp (input → band1 → … → bandN → output) + analyser post-module; 7 filter shapes (peaking/highpass/lowpass/lowshelf/highshelf/notch/bandpass); params {type, freq 20-20k, gain ±24dB, q 0.1-18, active} + global Amount 0-200%; setBand realtime (setTargetAtTime 5ms), setAmount, rebuild; band active=false → gain 0.
    2. Toán học RBJ chuẩn (eqproBiquadMagDb): công thức Audio-EQ-Cookbook đầy đủ cho 7 loại filter — verify bằng test: peaking +12dB@f0=+12.00, notch Q10=-97.86dB, shelf đúng hướng, highpass -27.9dB@20Hz ✓.
    3. Log mapping: freq→x = W·log10(f/20)/3, gain→y ±24dB, Q→wing offset = 110/√Q.
    4. InteractiveEqPro UI: canvas rAF render — grid log-freq + dB, realtime FFT spectrum (từ analyser module), band fills màu trong suốt + master white curve (Σ dB); drag center node = freq+gain, drag wings = Q, scroll wheel = Q, double-click canvas = thêm band (tối đa 8), double-click node = xóa; HUD floating (glass): filter type dropdown, readouts FREQ/GAIN/Q, bypass band, delete; toolbar: Band count, Amount slider, Reset, + Band. Khi play → kéo trực tiếp vào module instance live (window.__getTrackFxModule → setBand).
    5. Tích hợp: TRACK_FX_META thêm 'eqpro' (+ nút [+]), TRACK_FX_DEFAULTS.eqpro (deep-clone bands khi add), node lưu fxMods/sfMods (audio + SF instances) để UI truy cập; view 'eqpro' trong FXRackModal; serialize params (bands) qua fx_chain tự động.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608034800)
  • Ghi chú/Test (nếu có): BUILD OK 957341 bytes, node --check OK, pytest 86 passed; verify RBJ math 8/8 case đúng.

  • Tóm tắt thay đổi: __getTrackScopeAnalysers ưu tiên sfAnalyser (chỉ tiếng MIDI) khi track có midiItems → scope chỉ hiển thị MIDI, audioclip/section không xuất hiện. FIX: (1) nối thêm sfOut → scopeSplitter (cùng splitter với pannerNode) → scopeAnalyserL/R giờ trộn TÍN HIỆU ĐẦY ĐỦ post-FX của track: audioclip + section (pannerNode) + midi (sfOut); (2) bỏ nhánh ưu tiên sfAnalyser trong __getTrackScopeAnalysers (luôn dùng scopeAnalyserL/R đã trộn).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608034700)
  • Ghi chú/Test (nếu có): BUILD OK 937588 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: Theo yêu cầu bắt buộc của user: PWR sáng (ON) = mọi items (MIDI/section/audioclip) qua FX Rack Panel; A và ♪ chỉ dùng với Mastering FX Chain (không ảnh hưởng track FX):
    1. Audio path: dryGain của route chuyển từ tap PRE-FX (gainNode) sang POST-FX (analyserNode) — A bypass giờ chỉ bỏ mastering; track FX Rack (fxEntry → modules) vẫn áp dụng cho clip/section bất kể A.
    2. MIDI path: thêm route riêng sfRouteGain/sfDryGain (sfPan → cả 2; sfRouteGain → masterBus.input, sfDryGain → masterBus.dryInput) — ♪ bypass chỉ bỏ mastering; SF vẫn qua sfModules (track FX, PWR-controlled). __setTrackBypass('midi') toggle trực tiếp route này; updateSfRouting bỏ nhánh dryInput (SF LUÔN vào sfEntry); effect sync áp lại khi load/undo.
    3. Tooltip cả 2 strip cập nhật: "A/♪ = Mastering FX Chain... XÁM = bypass mastering (vẫn qua track FX Rack)".
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608034600)
  • Ghi chú/Test (nếu có): BUILD OK 937380 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: User báo: FX Rack ON chỉ MIDI được xử lí, audioclip/section không. NGUYÊN NHÂN THẬT: startLocalTrackPlayback (loop selection) có dòng cũ gainNode.connect(pannerNode) — connect THẲNG gainNode → pannerNode, BỎ QUA fxEntry → [FX modules] → fxLegacyIn. Node track được CACHE → mọi playback sau (kể cả play thường) giữ đường thẳng này → clip vào panner 2 lần (1 qua FX + 1 thẳng) → FX bị pha loãng/không nghe rõ. FIX: bỏ dòng đó (node đã tự wire đúng). Lưu ý thứ 2 cho user: nếu nút A (bypass audio) đang XÁM, clip/section đi thẳng (dry — bỏ FX + mastering) theo thiết kế — MIDI vẫn qua FX (đường SF riêng) → muốn clip qua FX phải tắt nút A (sáng xanh).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608034500)
  • Ghi chú/Test (nếu có): BUILD OK 936122 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: Nút PWR trước đây chỉ toggle state local UI (không tác dụng DSP). Giờ thành state THẬT trên track (fxActive, serialize fx_active, default true):
    1. getOrCreateTrackNode: fxEnabled = track.fxActive !== false gate cả 3 nhánh — audio fxChain modules, chorus/reverb legacy, SF chain modules (MIDI) — khi OFF: gainNode → fxLegacyIn → panner (không module nào), SF → sfEntry → thẳng sfOut.
    2. rebuildTrackFxGraph: gate cả 2 chain (audio + SF) — bấm PWR khi đang play re-route ngay.
    3. Nút PWR (TrackStripConsole): toggle track.fxActive + __rebuildTrackFxGraph — sáng = ON (mọi items qua FX Rack Panel), tối = OFF (bỏ FX). Bỏ state local isFxActive.
    4. Track mới default fxActive: true; deserialize cũ → true (không phá project cũ).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608034400)
  • Ghi chú/Test (nếu có): BUILD OK 935880 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: User (bundle mới nhất, log route ĐÚNG: track1 bypass routeGain=0/dryGain=1) vẫn nghe audioclip qua mastering + tăng gain quá mức. NGUYÊN NHÂN THẬT: patch "SF path độc lập" trước đó nối gainNode.connect(sfEntry) VĨNH VIỄN — clip audio ở gainNode chảy luôn vào đường SF (sfEntry → sfModules → sfOut → sfPan → masterBus.input → mastering) → clip nghe 2 đường cùng lúc (dry + SF path qua mastering) dù bypass bật → tăng gain + vẫn bị mastering xử lý. FIX:
    1. XÓA gainNode.connect(sfEntry) — SF output vào sfEntry TRỰC TIẾP qua setOutputDestination(sfEntry) (updateSfRouting), không đi qua gainNode → clip không bao giờ chảy vào SF chain.
    2. sfEntry.gain mirror audible gain (volume/mute/solo) ở 3 nơi: getOrCreateTrackNode (khởi tạo), applyAllTrackMuteSolo, effect sync [tracks, sessionTabs] — SF vẫn tuân theo fader/mute/solo.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608034300)
  • Ghi chú/Test (nếu có): BUILD OK 935143 bytes, node --check OK, pytest 86 passed. Verify: gainNode.connect(sfEntry) không còn trong bundle.

  • Tóm tắt thay đổi: User yêu cầu rõ: tắt nút A (xám) = phải bypass Mastering — semantic đã đúng (xám = audioBypass=true = dry). Củng cố thêm: (1) setMasteringRoute giờ gán .value TRỰC TIẾP sau cancelScheduledValues (hard switch, không phụ thuộc automation queue — không thể trễ/treo); (2) thêm log [Bypass] node created track <id> initial audioBypass=... routeGain=... dryGain=... khi tạo track node — xác minh trạng thái ban đầu của route đúng với map. Rà lại MỌI path playback (startTrackPlayback, startLocalTrackPlayback loop-selection, section sub-nodes) đều qua track route → bypass áp cho audioclip + section ✓.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608034200)
  • Ghi chú/Test (nếu có): BUILD OK 934318 bytes, node --check OK, pytest 86 passed. Log mới khi bấm A: [Bypass] track <id> audioBypass=true → DRY BUS (bỏ mastering + bỏ track FX).

  • Tóm tắt thay đổi: User dán log: audioBypass=false routeGain.gain=0.000 dryGain.gain=1.000 — giá trị .value ĐẢO ngược so với cờ. Nguyên nhân: setTargetAtTime là automation TRỄ (timeConstant 0.02) — .value đọc ngay sau khi schedule vẫn hiển thị trạng thái CŨ → log gây hiểu lầm (không phải DSP sai). Fix: setMasteringRoute dùng setValueAtTime tại currentTime (áp dụng TỨC THÌ, không ramp) + log mục tiêu rõ ràng: [Bypass] track <id> audioBypass=true → DRY BUS (bỏ mastering + bỏ track FX) / → MASTERING CHAIN (qua FX + mastering) — không còn log giá trị stale. Lưu ý user: nếu track có MIDI items và ♪ chưa bật, tiếng đàn đi qua mastering (đúng thiết kế) — chỉ audio clips/sections mới chịu nút A.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608034100)
  • Ghi chú/Test (nếu có): BUILD OK 934145 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: Log route is not defined do patch trước: trong createMasteringRoute gán route._trackId/route._bypass nhưng biến route KHÔNG tồn tại trong hàm (hàm trả {routeGain, dryGain}; route là tên biến ở caller). Lỗi bắn ra MỖI lần tạo track node (play). Fix: tạo routeObj trước rồi gán _trackId/_bypass lên đó; setMasteringRoute đọc route._trackId hợp lệ (param).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608034000)
  • Ghi chú/Test (nếu có): BUILD OK 934143 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: User báo nút A vẫn đưa tín hiệu qua Mastering panel. Rà toàn bộ chuỗi: clip → gainNode → (bypass) dryGain → masterBus.dryInput → dryOutput → output — bỏ hoàn toàn mastering modules (wiring đã đúng từ Pha H; masterBus: dryInput.connect(dryOutput), dryOutput.connect(output); wet: input → compressor → inputAnalyser → [modules] → outputAnalyser → output). Section items qua sub-node → parent gainNode → cùng route ✓. Củng cố: (1) setMasteringRoute dùng getAudioContext() (loại bỏ nguy cơ return sớm khi audioCtx null) + lưu route._trackId/_bypass; (2) console.log [Bypass] track <id> audioBypass=... routeGain.gain=... dryGain.gain=... → dry bus (bỏ mastering) mỗi lần toggle — user mở DevTools console để xác nhận đường tín hiệu thực tế. Lưu ý: MIDI items (♪ chưa bật) ĐI QUA mastering (masterBus.input → modules) — đúng thiết kế tách A/♪; nếu user test track có MIDI và thấy "vẫn qua mastering", đó là tiếng đàn chứ không phải audio clips.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608033900)
  • Ghi chú/Test (nếu có): BUILD OK 934112 bytes, node --check OK, pytest 86 passed.

  • Tóm tắt thay đổi: Theo yêu cầu user: nút không sáng (xám) = cho phép bypass đang bật; nút sáng = không bypass (xử lý bình thường). Đảo UI cả 2 strip (MixerStrip + TrackStripConsole): A bypass bật → xám, tắt → sáng xanh; ♪ bypass bật → xám, tắt → sáng tím. Củng cố DSP để bypass audio chắc chắn tác dụng:
    1. createMasteringRoute ưu tiên đọc trackAudioBypassMap (sync từ state mỗi render) trước khi fallback track object — tránh stale object.
    2. __setTrackBypass('audio') giờ re-route LIVE mọi section sub-node (<trackId>_sub_<subId>) cùng main node → audioclip item + section item bypass ngay khi bấm, kể cả đang play.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608033800)
  • Ghi chú/Test (nếu có): BUILD OK 933696 bytes, node --check OK, pytest 86 passed. Tooltip nút ghi rõ: "XÁM = bypass đang bật, SÁNG = xử lý bình thường".

[2026-08-03] Task: Fix màu nút bypass A/♪ + tách hoàn toàn đường SF (MIDI không còn lệ thuộc nút A)

  • Tóm tắt thay đổi:
    1. Đường SF độc lập (fix lệ thuộc A): trước đây SF chảy qua chain DÙNG CHUNG (gainNode → fxEntry → ... → panner → routeGain) — khi bật nút A, routeGain=0 → MIDI mất FX dù ♪ chưa bật. Fix: track có midiItems giờ dựng SF chain riêng: gainNode → sfEntry → [module instances RIÊNG (cùng loại/params)] → sfOut → sfPan → masterBus.input — hoàn toàn tách khỏi chuỗi audio. updateSfRouting: ♪ off → SF → sfEntry (FX giữ nguyên dù A bật/tắt); ♪ on → SF → masterBus.dryInput. rebuildTrackFxGraph rebuild cả 2 chain + sync sfPan.pan. Scope rack: track MIDI hiển thị sfAnalyser (post-FX).
    2. Màu nút theo yêu cầu: TrackStripConsole — A/♪ inactive = xám (text-slate-500), active = sáng xanh (A: text-sky-300 bg-sky-500/30) / sáng tím (♪: text-fuchsia-300 bg-fuchsia-500/30). MixerStrip bỏ fallback ?? masteringBypass khỏi visual (state hoàn toàn độc lập: A chỉ đọc audioBypass, ♪ chỉ đọc midiBypass).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608033700)
  • Ghi chú/Test (nếu có): BUILD OK 933049 bytes, node --check OK, pytest 86 passed.

[2026-08-03] Task: Tách nút bypass track thành 2 nút (Audio vs MIDI) + fix loop 0→18 khi click tempo track

  • Tóm tắt thay đổi:
    1. Tách bypass: track giờ có audioBypass + midiBypass (migrate từ masteringBypass cũ — deserialize: audio_bypass ?? mastering_bypass, midi_bypass ?? mastering_bypass; serialize ghi cả 3). UI MixerStrip + TrackStripConsole: thay nút B đơn bằng nút [A] (bypass audio items = clips + sections; route kép dry/route như cũ — trackAudioBypassMap + createMasteringRoute đọc audioBypass ?? masteringBypass) và nút [♪] (bypass MIDI riêng — trackMidiBypassMap + updateSfRouting: khi bật, SF output → masterBus.dryInput trực tiếp, bỏ FX + mastering nhưng giữ CC7 mute/solo). window.__setTrackBypass(trackId, 'audio'|'midi', on); giữ __setTrackMasteringBypass (set cả 2) cho tương thích.
    2. Fix loop: click lên tempo track (hoặc click đơn bất kỳ) tạo selection RỖNG (start==end) → hasSel cũ FALSE → nút loop auto-derive 0→18 ghi đè. Fix: hasSel giờ nhận mọi selection state có tồn tại (selectionStart !== null && selectionEnd !== null — kể cả start==end) → không bao giờ tự derive khi user đã click lane.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608033600)
  • Ghi chú/Test (nếu có): BUILD OK 931110 bytes, node --check OK, pytest 86 passed.

[2026-08-03] Task: Wave Observer + Module Vector Display Canvas trong FX Rack Panel (unified_fx_rack_panel_update.md)

  • Tóm tắt thay đổi: Tích hợp theo spec cập nhật:
    1. Scope analysers per-track (post-FX): getOrCreateTrackNode thêm scopeSplitter + scopeAnalyserL/R (fftSize 2048) nối từ pannerNode (output context, SAU FX chain — đúng sơ đồ spec §III.3); analyserNode chính tăng 256→2048. Expose window.__getTrackScopeAnalysers(trackId) → {L, R, sr}.
    2. Wave Observer UI trong FXRackModal: canvas scope + toolbar đúng spec — Input L/R meters, Channel (Stereo/Left/Right/Mid/Side), Mode (Waveform/Lissajous/Spectrum), Duration slider 0.1-5s (ring buffer history 5s × sampleRate), V.Zoom slider -12..+20 dB, Pause/Resume. Render loop rAF: waveform vẽ L cyan + R amber (stereo), lissajous scatter L vs R, spectrum bar log 20Hz-20kHz.
    3. Interactive Module Vector Display Canvas: canvas EQ response curve trong EQ view — vẽ |H(f)| xấp xỉ (4 band cascaded: lowshelf/peaking/highshelf log-domain) 20Hz-20kHz, cập nhật realtime khi kéo slider band.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608033500)
  • Ghi chú/Test (nếu có): BUILD OK 927915 bytes, node --check OK, pytest 86 passed. Scope chỉ có tín hiệu khi track đang play (analyser tồn tại lúc playback); dừng → flatline + ghi chú.

[2026-08-03] Task: FX Rack Panel tác dụng lên soundfont instrument trên track (route FluidSynth output qua track gainNode)

  • Tóm tắt thay đổi: Trước đây MIDI/soundfont qua FluidSynth (ScriptProcessor → _gainNode gain 0.3 → masterBus.input) bỏ qua hoàn toàn track gainNode + fxEntry → FX Rack (EQ/Comp/Limiter/Exciter/Rebalance) không ảnh hưởng instrument. Fix:
    1. soundfontPlayer.js: thêm setOutputDestination(node) (disconnect + nối _gainNode tới node chỉ định; null → về masterBus.input) + getOutputNode(); biến _pendingOutputDestination áp dụng khi _gainNode được tạo sau (4 nơi tạo gainNode đều tôn trọng).
    2. app.jsx updateSfRouting(): nếu đúng 1 track MIDI audible (có midiItems + computeTrackAudibleGain>0) → setOutputDestination(track.gainNode) → toàn bộ chuỗi track tác dụng: FX Rack chain → fader volume → pan → mute/solo gain → master. Nhiều hơn 1 track MIDI audible → fallback masterBus.input (shared worklet không tách được; CC7 mute vẫn đúng). Expose window.__updateSfRouting.
    3. Gọi updateSfRouting ở 5 điểm: getOrCreateTrackNode (node mới), startTrackPlayback (cuối schedule), applyAllTrackMuteSolo (đổi mute/solo), stopAllPlayback (về masterBus), effect sync [tracks, sessionTabs] (load/undo).
  • Các file ảnh hưởng: app/static/js/services/soundfontPlayer.js (+?v=202608031400 trong index.html), app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608033400)
  • Ghi chú/Test (nếu có): BUILD OK 915831 bytes, balance 0, pytest 86 passed. Giới hạn: >1 track MIDI audible → FX không áp (cần multi-engine để tách, chưa làm).

[2026-08-03] Task: Unified FX Rack Panel theo unified_fx_rack_panel.md (panel FX dùng chung cho track + rebuild graph độc lập)

  • Tóm tắt thay đổi: Cài đặt cơ chế Unified FX Rack Panel (Hệ thống quản lý Module FX dùng chung):
    1. Module factory dùng chung: createTrackFxModule(type, ctx, params) — factory DSP giống mastering, giờ nhận params (EQ g1-g4, Comp threshold/ratio/makeup, Lim ceiling, Exc drive, Rebal mid/side) lưu theo từng chain entry; thêm TRACK_EQ_PRESETS (flat/vocal_clarity/bass_punch/warm_tape/guitar_edge).
    2. Cấu trúc track FX có điểm rebuild: getOrCreateTrackNode wire gainNode → fxEntry → [modules active] → fxLegacyIn → chorus/reverb/direct → pannerNode (lưu fxEntry/fxLegacyIn trên node).
    3. rebuildTrackFxGraph(trackId) (đúng rebuildContextAudioGraph spec §III.2): disconnect fxEntry, nối lại qua modules ACTIVE theo thứ tự hiện tại — chỉ ảnh hưởng track đó, không đụng track khác/master bus; chạy LIVE khi đang play; expose window.__rebuildTrackFxGraph.
    4. FXRackModal — panel unified cho track: chain rack (drag-drop reorder, power toggle, delete, nút [+] thêm 5 loại module), view tham số theo module (EQ 4 band + preset dropdown, Comp, Limiter, Exciter, Rebalance), header hiển thị tên track; mọi thay đổi → update track.fxChain + __rebuildTrackFxGraph ngay.
    5. Nút [FX] trên TrackStripConsole → window.__openFxRack(track.id, track.name) — mở panel bind đúng track (bỏ modal inline cũ). App: state fxRackTarget + render FXRackModal (track lookup + updateTrackProp).
  • SỰ CỐ KHÔI PHỤC FILE: trong lúc edit, app.jsx bị ghi đè nhầm (chỉ còn 2000 dòng). Đã khôi phục từ git HEAD ec5fede (commit chứa toàn bộ fix tới loop-fix — user đã commit) + replay lại các edit unified rack; xác minh bằng build + balance ngoặc.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608033300)
  • Ghi chú/Test (nếu có): node build.mjs BUILD OK 914635 bytes, syntax OK, bracket balance 0, pytest 86 passed.

[2026-08-03] Task: Loop vẫn giãn selection tới bar 18 — mở rộng hasSel + verify bundle serve

  • Tóm tắt thay đổi: User báo vẫn lỗi sau fix trước. Kiểm tra toàn diện: (1) Không có path nào khác giãn selection khi loop bật (SubTabToolbar/onLoop là code chết; toggleLoop là Media Explorer preview; useEffect BPM chỉ chạy khi đổi tempo). (2) Mở rộng điều kiện hasSel trong handler nút loop: ngoài selectionMode==='local', còn nhận selection qua selectionStart/selectionEnd hợp lệ (phòng trường hợp sweep không set 'local'). (3) Verify bằng server thật (uvicorn 9131): file app.precompiled.js?v=202608033200 được serve có chứa hasSel=selectionMode=== + comment fix — chứng minh bundle workspace đã đúng. Nếu user vẫn thấy lỗi → trình duyệt cache hoặc đang chạy deployment khác (không phải workspace).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608033200; sửa double </script> phát sinh khi bump)
  • Ghi chú/Test (nếu có): pytest 86 passed (chưa chạy lại lần này — chỉ đổi frontend).

[2026-08-03] Task: Fix loop button ghi đè vùng chọn thành 0→18 bar (MAIN SESSION/SECTION-TAB)

  • Tóm tắt thay đổi: Khi quét chọn vùng duration rồi bấm nút loop, vùng chọn bị tự đổi thành 0→18 bar. Nguyên nhân: handler nút loop (nhánh main timeline, không có sub-tab active) LUÔN tự derive loop end từ content tracks (maxEnd + 2 bar — content 16 bar → 18) rồi setSelectionStart(0); setSelectionEnd(loopEnd) — ghi đè vùng chọn của user. Fix: tôn trọng selection có sẵn (hasSel = selectionMode==='local' && selLeft/selRight hợp lệ) — chỉ auto-derive khi CHƯA có vùng chọn nào. Áp dụng cho cả MAIN SESSION lẫn SECTION-TAB (cả 2 đều đi qua nhánh main timeline khi không có sub-tab; section-tab dùng chung selection handler nên sweep cũng set selectionMode='local'). Kèm theo: handleTrackLaneMouseDown giờ setSelectionCleared(false) khi bắt đầu chọn vùng mới — nếu user từng Ctrl+click xoá chọn thì vùng mới vẫn loop được.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608033100)
  • Ghi chú/Test (nếu có): node build.mjs OK, syntax OK, pytest 86 passed.

[2026-08-03] Task: Fix React error #310 khi mở Mixer modal (hooks sau early return)

  • Tóm tắt thay đổi: Sau fix isFxActive, mở Mixer modal vẫn lỗi Minified React error #310 ("Rendered more hooks than during the previous render"). Nguyên nhân: khi thêm dynamic module chain, 2 hooks dragChainIndexRef (useRef) + addModuleOpen (useState) được khai báo SAU dòng if (!isOpen) return null; trong MasteringModal → số hooks giữa các render thay đổi khi modal đóng/mở (return null sớm bỏ qua 2 hooks) → React #310. Fix: di chuyển 2 hooks lên TRƯỚC early return (cạnh useEffect [isOpen]).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608032900)
  • Ghi chú/Test (nếu có): verify: hooks tại 9252-9253 trước if (!isOpen) return null; (9255), không còn hook nào sau early return trong MasteringModal; syntax OK; bracket balance 0.

[2026-08-03] Task: Fix "ReferenceError: isFxActive is not defined" khi mở Mixer modal

  • Tóm tắt thay đổi: Khi mở Mixer modal bị lỗi Uncaught ReferenceError: isFxActive is not defined. Nguyên nhân: lúc thêm FX chain editor cho TrackStripConsole, tôi vô tình xóa luôn khai báo const [isFxActive, setIsFxActive] riêng của TrackStripConsole (git HEAD có 2 khai báo: master strip + track strip; sau khi dọn duplicate thì chỉ còn 1 của master strip) → nút FX Power trong TrackStripConsole tham chiếu biến không tồn tại. Fix: khôi phục khai báo isFxActive trong TrackStripConsole (cạnh fxChainOpen).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608032800)
  • Ghi chú/Test (nếu có): verify: 2 khai báo isFxActive (master strip 1154 + track strip 1460), bundle có isFxActive ×5, syntax OK, bracket balance 0.

[2026-08-03] Task: Nâng cấp Mastering panel theo mastering_expand.md (EQ presets + module factory + reorder + track FX reuse)

  • Tóm tắt thay đổi: Làm 4 yêu cầu spec mastering_expand.md:
    1. EQ Presets (§II.1): EQ_PRESET_LIBRARY (flat/vocal_clarity/bass_punch/warm_tape) + dropdown trong EQ view — applyEQPreset ramp freq/gain/Q trên 4 biquad filter + sync knobs/canvas.
    2. Module factory + 5 module mới (§II.2): thêm DSP cho Bus Compressor (DynamicsCompressor + makeup), Brickwall Limiter (ratio 20:1, knee 0), Harmonic Exciter (WaveShaper + HP 2kHz + dry/wet), Master Rebalance (M/S gains qua L/R crossfeed) trong initMasterBus; mỗi module có input/output gain riêng.
    3. Module reordering + dynamic re-route (§II.3): rebuildMasteringGraph(active, chain) — disconnect mọi boundary node rồi nối chuỗi active modules giữa inputAnalyser→outputAnalyser. Chain strip giờ render ĐỘNG từ ozState.chain (drag-drop reorder, power toggle, delete, nút [+] mở modal thêm module 7 loại). toggleMasteringOnMaster dùng chain signature cho idempotency. Migration: project cũ không có chain → default [eq, imager, maximizer] + params mới.
    4. Reuse cho track FX (§II.4): createTrackFxModule(type, ctx) — factory tạo instance DSP giống mastering cho từng track; getOrCreateTrackNode wire track.fxChain (chuỗi module) trước chorus/reverb; serialize/deserialize fx_chain; nút FX trên TrackStripConsole mở modal editor (thêm/xóa/toggle từng module Comp/Limiter/Exciter/M-S/EQ).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608032700)
  • Ghi chú/Test (nếu có): node build.mjs OK, syntax OK, bracket balance 0, pytest 86 passed. Lưu ý: module mới có param riêng (compThreshold/compRatio/compMakeup, limThreshold, excDrive, rebalMid/rebalSide) được lưu trong mastering_settings.

[2026-08-03] Task: Cài đặt Imager module theo imager_spec.md (M/S width + vectorscope + correlation meter)

  • Tóm tắt thay đổi: Làm đúng spec imager_spec.md trong MASTERING PANEL (Mixer F7):
    1. DSP width semantics chuẩn spec: 0% = MONO (S×0), 100% = Original (S×1), 200% = 2× Width (S×2). Giữ cấu trúc 4-band crossover (20-100Hz / 100Hz-1kHz / 1k-6kHz / 6k-20kHz) với matrix L/R crossfeed tương đương M/S (M = (L+R)/2 giữ nguyên, S = (LR)/2 × w/100 — chứng minh: g1=(w+100)/200, g2=(100w)/200 → mid=(g1+g2)=1, side=(g1g2)=w/100).
    2. Migration: project cũ lưu scale 100..+100 (0 = original) → tự +100 mỗi band khi load (0→100, 15→115, 35→135, 50→150), dùng marker imagerScale:'v2' cho project mới. Default mới theo khuyến nghị spec: Band1=0% (MONO maker), Band2=115%, Band3=135%, Band4=150%.
    3. Vectorscope thật (Polar M/S): thay chấm ngẫu nhiên giả bằng trace thật X=(L+R), Y=(LR) từ leftAnalyser/rightAnalyser (post-mastering) — mono → nằm ngang, width tăng → trải dọc.
    4. Phase Correlation meter thật: ρ = Σ(L·R)/√(ΣL²·ΣR²) (1..+1), gauge vẽ zone màu theo spec (+0.5..+1 xanh an toàn, 0..+0.5 vàng cẩn trọng, <0 đỏ nguy hiểm) + hiển thị số "Corr:".
    5. UI: slider range 0-200%, nhãn band theo spec, hint "0% = Mono · 100% = Original · 200% = 2× Width".
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608032600)
  • Ghi chú/Test (nếu có): verify DSP (w=0→side 0, w=100→side 1, w=200→side 2, mid luôn 1), migration (0,15,35,50 → 100,115,135,150; v2 giữ nguyên), slider trong bundle. pytest 86 passed.

[2026-08-03] Task: Mute/Solo realtime cho MIDI items — dùng CC7 (channel volume) của FluidSynth

  • Tóm tắt thay đổi: Mute/unmute/solo chưa realtime với MIDI items vì FluidSynth WASM render toàn bộ channel vào 1 worklet → 1 _gainNode chung (_workletNode.connect(_gainNode)), nên gain node của track không câm được MIDI (chỉ audio clips + section sub-tracks qua track gain mới bị ảnh hưởng). Fix: mỗi track MIDI sở hữu channel riêng (ensureTrackMidiChannel, 0-15 trừ 9) → dùng CC7 (channel volume) của FluidSynth (window.SonicSF.controllerChange(ch, 7, 100|0)) — áp realtime kể cả với notes đang vang. Thêm vào applyAllTrackMuteSolo (nút M/S) + effect sync [tracks, sessionTabs] (load/undo). Khi mute → CC7=0 (notes đang phát câm ngay); unmute → CC7=100 (notes đang phát vang lại + cơ chế becameAudible→restart vẫn giữ). Audio clips/section vẫn qua track gain như cũ.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608032500)
  • Ghi chú/Test (nếu có): node build.mjs OK, syntax OK, verify controllerChange(ch,7,...) ×2 trong bundle; pytest 86 passed.

[2026-08-03] Task: Fix items rớt/dồn sai track khi load-save project (3 bug)

  • Tóm tắt thay đổi: (1) serializeTracksList (app.jsx): chuỗi if/else-if theo trackType chỉ serialize MỘT loại items → track có cả clips + midiItems/sections bị rớt items khi save (mất dữ liệu âm thầm, có thể gây cảm giác "items biến mất/dồn chỗ"). Sửa: serialize ĐỘC LẬP từng loại items có trên track. (2) upgrade_project_json_if_needed (projects.py): hardcode start_bar = startTime/4.0 + duration_bars = 4.0 → vị trí items sai (lệch 2× ở 120bpm, càng lệch khi tempo khác). Sửa: dùng seconds_per_bar = (60/bpm)*4 từ bpm của project. (3) loadAudioBuffersForTracks (app.jsx): merge buffer theo array index → nếu state đổi giữa lúc fetch (race: mở project khác khi buffer-load cũ chưa xong) thì tracks bị xáo trộn, items rơi vào track sai. Sửa: merge theo track ID + clip ID.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/api/v1/projects.py, app/templates/index.html (bump v=202608032400), tests/test_project_upgrade.py (mới)
  • Ghi chú/Test (nếu có): verify: round-trip serialize→deserialize giữ đúng track + vị trí (đã test bằng code bundle thật); track hỗn hợp giờ ra AUDIO_ITEM,MIDI_ITEM; pytest 86 passed (3 test mới cho upgrade). Lưu ý: deserialize/upgrade vốn đã map đúng từng track — nếu user vẫn thấy items dồn track 1 sau khi hard-refresh, cần kiểm tra dữ liệu project cụ thể (và bản bundle trình duyệt đang chạy).

[2026-08-03] Task: Fix unmute không phát lại khi đang play (sources bị loop-restart bỏ qua)

  • Tóm tắt thay đổi: Khi đang play, mute → câm (OK, gain 0) nhưng unmute → không nghe lại được. Nguyên nhân: nếu có loop restart (stopAllPlayback + startTrackPlayback trong updatePlayhead), vòng lặp mới bỏ qua track đang mute/solo (isPlayable check) → sources của track không được tạo lại → chỉ set gain khi unmute không "hồi sinh" được nguồn đã không tồn tại. Fix trong applyAllTrackMuteSolo: theo dõi trackAudibleRef (audibility từng track), khi phát hiện chuyển tiếp inaudible → audible (unmute / tắt solo) và isPlaying + không đang RECORDING → stopAllPlayback() + startTrackPlayback(currentTime) re-schedule lại từ playhead hiện tại (đúng cơ chế toggleTrackSoloEvaluate đã dùng cho solo). Mute thuần (audible→inaudible) vẫn chỉ set gain (tức thì, không gián đoạn track khác). Effect sync [tracks, sessionTabs] giờ cũng cập nhật trackAudibleRef để nhất quán khi load/undo.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608032300)
  • Ghi chú/Test (nếu có): node build.mjs OK, syntax OK, pytest 83 passed.

[2026-08-03] Task: Mute/Solo realtime — tác dụng ngay lên items đang phát của track

  • Tóm tắt thay đổi: Nút M/S ở cả MixerStrip (mixer F7) và TrackStripConsole (track strip) giờ tác dụng realtime: toggle mute/solo lập tức set gain của track node đang phát (crossfade 20ms, không click). Cơ chế: computeTrackAudibleGain(list, track) (mute luôn tắt; nếu có bất kỳ solo → chỉ track solo nghe được; ngược lại theo volumeDb), setTrackNodeGain(node, gain) áp vào gainNode của track node. window.__applyTrackMuteSolo(trackId, patch) được gọi từ: (1) nút M/S của cả 2 strip (patch state + áp ngay), (2) getOrCreateTrackNode khi tạo node (items của track muted/soloed bắt đầu đúng trạng thái), (3) startSubTabPlayback cho chain mới, (4) effect sync [tracks, sessionTabs] với signature muted|solo|volumeDb (chỉ re-apply khi thay đổi — phủ load project/undo/section tab). Vì audio clip source.connect(gainNode) và MIDI note (đường oscillator fallback) đều qua track gainNode → mute/solo có tác dụng lên toàn bộ items của track.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608032200)
  • Ghi chú/Test (nếu có): node build.mjs OK, syntax OK, pytest 83 passed. Giới hạn: MIDI qua FluidSynth WASM render chung 1 worklet → không tách theo track (cùng giới hạn với bypass); đường oscillator fallback thì có tác dụng.

[2026-08-03] Task: Đồng bộ hành vi click+drag item (clip = move như section/MIDI; Alt+click+drag = quét chọn)

  • Tóm tắt thay đổi: (1) Thống nhất: click + drag trên MỌI item (section/MIDI/audio clip) = di chuyển; trước đây audio clip click+drag = quét chọn vùng (gây cảm giác "không đáp ứng"). (2) Hành động cũ của audio clip chuyển sang Alt+click+drag = quét chọn duration (gọi onTrackLaneMouseDown → local sweep select); Alt+right-edge vẫn = time-stretch. (3) Fix "thỉnh thoảng không đáp ứng": mọi item giờ dùng cơ chế pending drag có threshold 5px — click thuần chỉ chọn item (không tạo undo/toast, không vô tình di chuyển do rung chuột), di chuột >5px mới bắt đầu drag. Cơ chế: handleSetPendingDragMove (mới) lưu pendingDragRef với duplicate:false; effect pending-drag route: clip đơn → handleClipDragStartRef (clip machinery), còn lại → handleSectionItemDragStartRef (multiIds đã hỗ trợ clip). handleSetPendingDrag (Ctrl+click copy) giờ lưu duplicate:true. Sửa lookup clip trong nhánh non-duplicate của handleSectionItemDragStart.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608032100)
  • Ghi chú/Test (nếu có): node build.mjs OK, syntax OK, verify duplicate:true/!pdSnap.duplicate trong bundle. pytest 83 passed. Grab tool (kéo nhanh) giữ nguyên.

[2026-08-03] Task: Bypass trên nút Routing Matrix (track strip TCP) + bypass bỏ cả track FX

  • Tóm tắt thay đổi: (1) Nút Routing Matrix trong TrackStripConsole (TCP strip, cạnh M/S) giờ là nút Bypass — tooltip "Bypass: track KHÔNG qua FX + mastering ở Main out", active style xanh khi bật. (2) Thay đổi điểm lấy tín hiệu dry: trong getOrCreateTrackNode, dryGain giờ tap từ gainNode (PRE-FX) thay vì analyserNode (post-FX) → khi bypass, channel bỏ qua cả track FX (chorus/reverb) lẫn mastering chain ở Main out; đường routeGain giữ nguyên (post-FX → mastering) khi không bypass. (3) Xác nhận Mixer Panel đã ở phím F7 (code sẵn: F7 → __toggleMixerRef).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608032000)
  • Ghi chú/Test (nếu có): node build.mjs OK, syntax OK, không còn "Routing Matrix", pytest 83 passed.

[2026-08-03] Task: Fix TDZ "Cannot access 'sessionTabs' before initialization" sau khi thêm nút Bypass

  • Tóm tắt thay đổi: useEffect sync trackMasteringBypassMap đặt ở ~11786 nhưng dependency array [tracks, sessionTabs] được đánh giá ngay tại chỗ gọi — trước khi sessionTabs khai báo (12239) → ReferenceError: Cannot access 'sessionTabs' before initialization khi chạy app. Fix: di chuyển useEffect xuống sau khối khai báo subTabs/sessionTabs/sessionTabsRef (TDZ-safe). window.__setTrackMasteringBypass an toàn vì chỉ truy cập activeTrackNodesRef bên trong body hàm (lúc click).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608031900)
  • Ghi chú/Test (nếu có): node build.mjs OK, verify trong bundle: effect @453168 sau sessionTabs decl @451814. pytest 83 passed. Người dùng chạy npm run build trên workspace là được.

[2026-08-03] Task: Nút Bypass Mastering cho từng track strip trong Mixer Panel

  • Tóm tắt thay đổi: Thêm nút B (Bypass) trong mỗi track strip của Mixer Panel (cạnh M/S). Khi bật ON: âm thanh track đi qua dry bus mới (masterBus.dryInput → dryOutput → output) — bỏ qua toàn bộ chuỗi mastering (EQ / Imager / Maximizer) nhưng vẫn qua master volume + metering ở Main out. Cơ chế: createMasteringRoute() tạo 2 đường gain bù nhau (routeGain → masterBus.input qua mastering, dryGain → dryInput); setMasteringRoute() crossfade 20ms khi toggle (không click). Áp dụng tại: getOrCreateTrackNode (node track chính, toggle live qua node.route), startSubTabPlayback (clip playback). Map trạng thái trackMasteringBypassMap sync từ tracks state qua useEffect; window.__setTrackMasteringBypass toggle ngay cho track đang phát. Lưu/đọc project: mastering_bypass trong serialize/deserialize (schema không chặn additionalProperties nên không cần sửa).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (rebuild), app/templates/index.html (bump v=202608031800)
  • Ghi chú/Test (nếu có): node build.mjs rebuild OK, syntax OK, pytest 83 passed. Giới hạn: track MIDI/SoundFont dùng chung 1 bộ render FluidSynth (1 worklet → 1 gain) nên bypass hiện áp dụng cho track AUDIO (clip) + oscillator fallback; track MIDI dùng FluidSynth WASM chưa tách được theo track (cần refactor renderer) — sẽ làm tiếp nếu cần.

[2026-08-03] Task: Rebuild app.precompiled.js từ app.jsx (fix "handleOpenProject is not defined")

  • Tóm tắt thay đổi: Lỗi Uncaught ReferenceError: handleOpenProject is not defined khi click Open → bundle app.precompiled.js mà trình duyệt tải bị lệch với app.jsx (bundle cũ không chứa định nghĩa hàm ở scope đúng). npm run build không chạy được vì Babel 8 ESM-only (ERR_REQUIRE_ESM). Giải pháp: build lại bundle bằng @babel/standalone@7 qua build.mjs (script mới, mirror đúng lệnh build trong package.json), tạo app.precompiled.js mới (858KB, syntax OK, đủ const handleOpenProject + 3 references, kèm fix restoreLastSessionProject). Đã smoke-test: trích deserializer từ bundle mới chạy với 6 project khôi phục → 6/6 OK, "Rose (autosave 03/08)" đủ 39 MIDI notes. Bump cache-buster lên v=202608031700.
  • Các file ảnh hưởng: app/static/js/app.precompiled.js (rebuild), build.mjs (NEW — rebuild thủ công khi Babel 8 lỗi), app/templates/index.html
  • Ghi chú/Test (nếu có): pytest tests/ → 83 passed. Node portable dùng: /tmp/node-v20.18.0-linux-x64/bin/node (máy không có node hệ thống). Trên máy deployment: copy 3 file (app.jsx, app.precompiled.js, index.html) hoặc chạy node build.mjs rồi hard refresh.

[2026-08-03] Task: Fix auto-restore dự án lỗi "Không tìm thấy dự án" sau khi khôi phục DB

  • Tóm tắt thay đổi: Sau khi DB bị xóa/tạo lại, localStorage.sonic_project_id của trình duyệt vẫn trỏ tới project cũ đã mất → mỗi lần tải trang restoreLastSessionProject gọi API, nhận 404 "Không tìm thấy dự án" và chỉ console.warn vĩnh viễn. Fix: khi restore thất bại, tự xóa sonic_project_id + sonic_project_name khỏi localStorage để lỗi không lặp lại. Bump cache-buster precompiled lên v=202608031600.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Verified qua API: cả 6 project khôi phục mở được (GET /cloud/{id} → 200 + main_session đầy đủ). Người dùng cần hard refresh (Ctrl+Shift+R) để nạp bundle mới, mở "Rose (autosave 03/08)" 1 lần để ghim project hiện tại.

[2026-08-03] Task: Khôi phục các dự án cloud cũ bị mất (git recovery)

  • Tóm tắt thay đổi: Các dự án cloud cũ ('Test' x4, 'Rose' từ 22/07) và project đang làm dở "Rose" (autosave 03/08, 2 track MIDI+AUDIO, 39 notes, bpm 128) đã được khôi phục vào app/storage/sonicforge.db dưới user_id admin hiện tại từ git history (app/storage/sonicforge.db từng được commit trước khi vào .gitignore). Nguyên nhân mất: DB bị xóa/tạo lại nhiều lần trong quá trình dev (test suite test_auth_and_quota xóa DB → admin được re-seed với UUID mới → project cũ gắn với user_id cũ không hiển thị). Đã dọn các project/user test rác, backup DB trước khi khôi phục tại /tmp/sonicforge_db_before_restore.db.
  • Các file ảnh hưởng: app/storage/sonicforge.db (dữ liệu), tools/restore_cloud_projects.py (NEW — script khôi phục cho deployment khác), tools/cloud_projects_backup.json (NEW — 6 project dạng portable)
  • Ghi chú/Test (nếu có): Verified qua API: login admin → GET /api/v1/projects/cloud trả đủ 6 project; mở "Rose (autosave 03/08)" đủ 39 MIDI notes. Trên máy deployment thật (nếu DB khác): python3 tools/restore_cloud_projects.py <duong-dan>/sonicforge.db. Từ giờ test suite không đụng DB dev (xem entry conftest).

[2026-08-03] Task: Cô lập test suite khỏi DB dev + khôi phục mật khẩu admin

  • Tóm tắt thay đổi: (1) DB_PATH trong app/models/user.py giờ có thể override bằng env SONICFORGE_DB_PATH. (2) Thêm tests/conftest.py set env này sang /tmp/sonicforge_test.db trước khi mọi app module được import → pytest không bao giờ sửa DB dev nữa (trước đây test_auth_and_quota seed + đổi mật khẩu admin ngay trong app/storage/sonicforge.db, khiến login admin123 fail). (3) Reset mật khẩu admin DB dev về admin123 (must_change_password=0).
  • Các file ảnh hưởng: app/models/user.py, tests/conftest.py (NEW)
  • Ghi chú/Test (nếu có): pytest tests/ → 83 passed, 5 skipped. Đã verify live: login admin/admin123 → 200, sai mật khẩu → 400. Nếu deployment khác cũng bị dính (chạy test trên cùng DB), reset thủ công: UPDATE users SET hashed_password='<hash_of_admin123>', must_change_password=0 WHERE username='admin' hoặc xóa DB để seed_admin tạo lại.

[2026-08-03] Task: Security audit + bug fixes (bảo mật, bug chức năng, cải thiện)

  • Tóm tắt thay đổi: (1) SSRF /api/v1/ai/proxy: thêm auth bắt buộc + chặn cloud metadata/link-local (169.254.0.0/16), chặn IP private trừ khi host nằm trong danh sách AI provider user đã cấu hình (cho phép Ollama localhost:11434), chặn scheme không phải http/https, không forward header X-Auth-Token lên upstream. (2) Path traversal /plugins/render: output_filename chỉ lấy basename + ép đuôi .wav. (3) Path traversal toàn bộ /audio/*: helper _safe_file_id/_resolve_storage_path (basename + chỉ đọc trong uploads/processed). (4) SECRET_KEY: bỏ hardcode, tự sinh random bền vững lưu app/storage/.secret_key (ưu tiên env SECRET_KEY). (5) media.py (computer/browse/file): yêu cầu auth — login giờ set HttpOnly cookie sf_token, get_current_user nhận token từ Bearer / X-Auth-Token / cookie nên frontend raw fetch vẫn hoạt động. (6) Upload audio + soundfont: streaming theo chunk + giới hạn size (1GB/2GB) thay vì đọc cả file vào RAM. (7) Quota bypass: update_cloud_project giờ kiểm tra quota như save_cloud_project. (8) enforce_password_changed (bắt buộc đổi mật khẩu lần đầu) được wire vào upload/edit/render/ai-scan/ai-cut/python-tool/upload-soundfont. (9) render_engine: fix resample bị bỏ qua (sr != sample_rate trước là no-op → giờ resample_poly), render tôn trọng solo track, cache buffer section theo section_id, thay print → logger. (10) vst_engine: thống nhất FluidSynth API low-level CFFI (high-level Synth()/FluidSynth() không tồn tại trong binding này). (11) Rate-limit login theo IP (10 lần/15 phút), validate độ mạnh password khi register. (12) main.py: on_eventlifespan, CORS allow_credentials=False, xóa stub rỗng. (13) SQLite: WAL + foreign_keys=ON + seed user anonymous placeholder (FK hợp lệ cho project ẩn danh). (14) Pydantic v2 model_dump() thay dict().
  • Các file ảnh hưởng: app/core/auth.py, app/api/v1/auth.py, app/api/v1/ai_proxy.py, app/api/v1/plugins.py, app/api/v1/audio.py, app/api/v1/media.py, app/api/v1/projects.py, app/api/v1/multitrack.py, app/api/v1/user_config.py, app/core/render_engine.py, app/core/vst_engine.py, app/models/user.py, app/main.py, app/static/js/services/aiGateway.js (gửi X-Auth-Token khi gọi proxy), tests/test_security_hardening.py (NEW, 14 test), tests/test_plugin_api.py
  • Ghi chú/Test (nếu có): pytest tests/ → 83 passed, 5 skipped. Đã verify live: proxy không token → 401, metadata → 403; media không token → 401 / có cookie → 200; render output_filename=/tmp/x.wav → path nằm trong PROCESSED_DIR. Lưu ý: nếu triển khai cũ đang chạy, restart server để tạo .secret_key (token cũ sẽ hết hạn vì secret đổi). Login mật khẩu admin mặc định vẫn admin123 (đã khôi phục trong DB dev).

[2026-07-31 07:03] Task: Fix Lucide icons not rendering on new tracks (TCP + SECTION-TAB)

  • Tóm tắt thay đổi: useEffect gọi lucide.createIcons() thiếu activeTracks trong dependency array → khi track được tạo (user/AI/MIDI import/clone section), DOM nodes mới có data-lucide nhưng không được chuyển thành SVG → icon ẩn hoặc hiển thị sai. Fix: thêm activeTracks vào deps để auto-refresh icons sau mỗi lần track list thay đổi.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): Không cần build. Fix áp dụng cho tất cả đường dẫn tạo track: addNewTrack, insertTrackBelow, AI cut/composition/rearrange, MIDI import, section clone.

[2026-07-31 07:09] Task: Allow multiple simultaneous solo buttons (Track TCP)

  • Tóm tắt thay đổi: toggleTrackSoloEvaluate trước đây set solo: false cho tất cả track trừ track được click → chỉ cho phép 1 solo. Fix: toggle solo chỉ trên track được click, giữ nguyên trạng thái solo của track khác. soloedTrackId (single track ID) được thay bằng hasAnySolo (boolean). startTrackPlayback dùng track.solo trực tiếp thay vì so sánh với soloedTrackId. Loop restart với solo: iterate tất cả soloed tracks thay vì chỉ play 1 track. UI highlight các nút solo dùng track.solo.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): Bấm solo track A → active, bấm solo track B → cả A và B đều solo. Audio playback chỉ play track có solo = true.

[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-mdmax-w-4xl. Font size từ text-[10px]/text-xstext-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ả 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-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/soundfontsapp/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 *.dbsf_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 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.

[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 rulerDragStartRefisDraggingRulerRef=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 11:17] Task: AI Copilot Suggestion Chips + Typeahead (spec 44_AI_SUGGEST_PROMT.md)

  • Tóm tắt thay đổi: Thêm suggestion chips bar trên Copilot Prompt: context-aware (MIDI selected → amber rearrange chips, no selection → indigo creation chips). Typeahead dropdown với keyword matching khi gõ ≥2 ký tự, Tab để chọn. getSelectedMidiItemInfo helper. Backend ai_presets.py thêm 4 rearrange presets. promptTemplateManager.js thêm getContextualSuggestions(hasSelectedItem).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/services/promptTemplateManager.js, app/api/v1/ai_presets.py, tests/test_ai_presets.py
  • Ghi chú/Test (nếu có): npx babel compile pass. pytest tests/test_ai_presets.py -v — 7/7 pass.

[2026-07-28 11:02] Task: AI Rearrange Scenario Detection (spec 45_SCENARIAO_AI.md)

  • Tóm tắt thay đổi: Thêm 9 rearrange scenarios vào aiGateway.js (arpeggio, harmonies, syncopation, walking bass, jazz swing, synthwave, cinematic, simplify, passing tones) với keyword mapping → detectRearrangeScenario(). buildRearrangeMessage tự động inject specific technique rules dựa trên scenario detect được. Thêm 9 rearrange presets vào promptTemplateManager.js với category "Rearrange / Variation".
  • Các file ảnh hưởng: app/static/js/services/aiGateway.js, app/static/js/services/promptTemplateManager.js
  • Ghi chú/Test (nếu có): node --check pass. npx babel compile pass. Preset Manager hiển thị 12 presets (3 cũ + 9 rearrange). Gõ "arpeggio" → AI nhận dạng scenario → inject technique "Subdivide sustained chord notes into 0.25-beat arpeggiated steps".

[2026-07-28 10:52] Task: MIDI Rearrange Feature (spec 41_AIMIDI_FIX.md)

  • Tóm tắt thay đổi: Tạo midiExtractor.jsextractSelectedMIDIContext trích xuất notes + note names từ selected MIDI item. Thêm REARRANGE_TOOL_SPEC + buildRearrangeMessage trong aiGateway.js. handleAISend phát hiện selected MIDI item → dùng rearrange flow riêng (source notes → AI → rearrange_midi_melody → A/B track bên dưới source). Handler rearrangeMidiMelody tạo track [AI Rearrange] <title> + MIDI item ngay dưới source track.
  • Các file ảnh hưởng: app/static/js/services/midiExtractor.js (NEW), app/static/js/services/aiGateway.js, app/static/js/services/dawCommandDispatcher.js, app/static/js/app.jsx, app/templates/index.html
  • Ghi chú/Test (nếu có): node --check pass. npx babel compile pass. Cần chọn MIDI item → gõ prompt rearrange → track A/B mới xuất hiện.

[2026-07-28 10:39] Task: AI MIDI Prompt Template & Preset Engine (spec 33_MIDI_AI.md)

  • Tóm tắt thay đổi: Tạo PromptTemplateManager service (keyword matching với scoring, CRUD, star/fav toggle). Backend ai_presets.py CRUD router (JSON file storage, auth isolation). Refactor AIPresetModal dùng service + star toggle + backend API. Piano Roll AI wiring. 7 tests cho matching + backend CRUD + auth isolation.
  • Các file ảnh hưởng: app/static/js/services/promptTemplateManager.js (NEW), app/api/v1/ai_presets.py (NEW), tests/test_ai_presets.py (NEW), app/main.py, app/static/js/services/api.js, app/static/js/app.jsx, app/templates/index.html
  • Ghi chú/Test (nếu có): python3 -m pytest tests/test_ai_presets.py -v — 7 tests pass. Pre-existing 1 test fail (test_api_user_ai_config permission error, unrelated).

[2026-07-28 10:15] Task: Dropdown CC lane — Velocity, Sustain, Modulation, Pitch Bend, Pan

  • Tóm tắt thay đổi: Thay button group (velocity/pan) bằng <select> dropdown với 5 options. Hiển thị mặc định là Velocity. Button toggle CC lane hiển thị short label (Vel/Sus/Mod/Bend/Pan/CC).
  • 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. CC lane dropdown có 5 options, default Velocity.

[2026-07-28 10:10] Task: Ctrl+Click vùng trống không sweep select, chỉ deselect

  • Tóm tắt thay đổi: Thay onSweepSelectStart bằng onClearSelection trong Ctrl+Click vùng không có items. Trước đây gọi handleSweepSelectStart → yellow cursor + mousemove chọn items. Fix: onClearSelection = () => setSelectedItemIds(new Set()) — chỉ clear selection, không sweep.
  • 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 vùng trống → deselect hết, không yellow cursor.

[2026-07-28 10:06] Task: Ctrl+S trong SECTION-TAB chỉ lưu section, không export SF

  • Tóm tắt thay đổi: Di chuyển SECTION-TAB save check từ handler thứ 2 (dead code, shadowed bởi export handler) lên handler đầu tiên. Line 8066: nếu activeTab.startsWith('session_')handleSaveSectionTab, else → handleExportSFS.
  • 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. MAIN Ctrl+S → export SF.

[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 ? ... : 0instrumentProgram. 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 : 0subTrack.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 instrumentProgramsynth_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).

[2026-07-28 18:17] Task: Keyboard shortcuts Ctrl+A select all + Del delete selected items/groups

  • Tóm tắt thay đổi: (1) Ctrl+A đã tồn tại để chọn tất cả items trên timeline. (2) Thêm xử lý phím Delete/Backspace cho selectedItemIds: xóa tất cả section, MIDI item, và audio clip đang được chọn khỏi track, xóa selection, hiện toast thông báo.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): Build có lỗi parse pre-existing (Babel 8) không liên quan đến thay đổi này.

[2026-07-29 09:40] Task: Reset to 33cf804 — clean slate for mixer reimplementation

  • Tóm tắt thay đổi: Hard reset về commit 33cf804, xóa toàn bộ mixer code (MasterChannelStrip, TrackChannelStrip, MixerConsole, AudioMixerGraphManager, setOutputNode) và MIDI keyboard note sustain. Giữ lại TimelineRuler (tempo bar/timebar/lead-in/0-index). Backup mixer panel code lưu trong mixer_panel_backup_*.patch.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/services/soundfontPlayer.js, app/static/js/services/audioMixerGraphManager.js
  • Ghi chú/Test (nếu có): npm run build — build passes. Sẵn sàng cài đặt lại mixer panel display-only.

[2026-07-29 09:46] Task: Install Mixer Panel display-only with F7 toggle

  • Tóm tắt thay đổi: Thêm MixerStrip component (display-only, không audio routing). Mixer panel bottom dock với drag handle resize (80-400px). Track strip hiển thị index, M/S buttons, fader slider, dB readout, track name. Master strip với "MAIN OUT" label. F7 toggle + status bar button + View menu item. onUpdateTrack thay đổi mute/solo/volumeDb trực tiếp.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 09:55] Task: Mixer panel — VU meter, fader scaling, track name color

  • Tóm tắt thay đổi: Master strip thêm fader placeholder + VU meter. MixerStrip fader container items-enditems-stretch để fill chiều cao. Thêm VU meter (volume-dependent color: green/amber/red, height theo volumeDb). Track name dùng track.color thay vì fixed text-zinc-400.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 10:02] Task: Fix stuck MIDI notes when ARM toggled off mid-note

  • Tóm tắt thay đổi: Xóa if (!st.isArmed) return; trong note-off handler. Note-off giờ stopNote trên ALL tracks thay vì chỉ armed tracks — FluidSynth stopNote trên channel không có note đang play là no-op.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 10:08] Task: Fix AI Suggestion list changing on item select/deselect

  • Tóm tắt thay đổi: Xóa filter hasSelItem ? ... khỏi suggestions — luôn hiển thị toàn bộ preset list thay vì chỉ rearrange/favorites khi có item được chọn. Tránh tự động thêm/xóa suggestion khi click chọn/bỏ chọn item.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 10:12] Task: Restore AI suggestion filter by selection + fix stale instance

  • Tóm tắt thay đổi: Restore filter: khi chọn MIDI item chỉ hiện Rearrange/favorites, không chọn thì hiện tất cả. Fix gốc: cache PromptTemplateManager qua aiPromptMgrRef thay vì new mỗi render — tránh thay đổi array reference gây re-render suggestion list.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 10:17] Task: Fix filter including wrong presets in AI suggestion list

  • Tóm tắt thay đổi: Xóa || p.is_favorite khỏi filter khi có MIDI item selected — chỉ lọc theo category === 'Rearrange / Variation', tránh preset favorite bị lọt vào list không đúng.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 10:19] Task: Show all presets always, no filter by selection

  • Tóm tắt thay đổi: Luôn hiển thị promptMgr.presets đầy đủ, không filter theo selection state — tránh preset đầu tiên nhảy vào list khi bỏ chọn item.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 10:42] Task: Fix multi-item drag merging all items into target track

  • Tóm tắt thay đổi: Khi cross-track drag, chỉ item đang được kéo (drag.itemId) được chuyển track — các item được chọn khác giữ nguyên track cũ, chỉ cập nhật vị trí ngang (delta). Fix isDraggedItem check trong multiIds loop.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 10:48] Task: Fix group drag — all items move, clamp to track 0, auto-add track

  • Tóm tắt thay đổi: Restore group drag: tất cả selected items di chuyển cùng nhau. Thêm clamp targetTrackId về track đầu tiên (0). Auto-add track mới khi kéo xuống dưới track cuối cùng.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 10:50] Task: Fix group drag merging items — keep items on original tracks

  • Tóm tắt thay đổi: Xóa toàn bộ cross-track logic (currentTrackMap, crossTracks). MultiIds drag chỉ cập nhật time position (delta) trên từng track — items luôn giữ nguyên track cũ, không gộp.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 10:55] Task: Fix group drag cross-track — each item keeps relative track offset

  • Tóm tắt thay đổi: Khi drag group cross-track, mỗi item tính target track riêng dựa trên srcIdx + crossOffset. Item trên track 1 kéo xuống 2 track → đến track 3. Item trên track 2 kéo xuống 2 track → đến track 4. Không gộp vào 1 track.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 10:58] Task: Fix group drag cross-track stale track index bug

  • Tóm tắt thay đổi: Sửa trkIds dùng prev.map(tr => tr.id) (từ live state trong callback) thay vì activeTracksRef.current. Sửa insert section dùng for loop tìm srcItem đúng thay vì flatMap sai.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 11:00] Task: Fix group drag — use ORIGINAL trackId from drag start

  • Tóm tắt thay đổi: Lưu trackId: t.id vào multiIds lúc bắt đầu drag. Dùng inf.trackId (original) để tính origIdx + crossOffset — không dùng currentTrackMap (live state có thể stale). Item track1 drag n track → track1+n, item track2+n track → track2+n.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 11:04] Task: Fix single-item cross-track drag

  • Tóm tắt thay đổi: Sửa single-item drag path: mỗi track filter drag.itemId riêng, chỉ insert vào targetTrackId. Xóa duplicate setDraggedSectionItem.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 11:06] Task: Fix cross-track drag — compute target track from mouse Y

  • Tóm tắt thay đổi: Thay hoveredTrackIdRef bằng tính toán target track từ mouse Y position + track heights (prev[ti].height). Không phụ thuộc onMouseEnter event có thể không fire khi drag.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 11:07] Task: Auto-add tracks when drag exceeds existing track count

  • Tóm tắt thay đổi: Tính maxTrg từ tất cả items trong multiIds. Nếu prev.length <= maxTrg, tự động thêm track mới vào prev trước khi xử lí. Item track2 cần xuống track3 → auto-add track3.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 11:10] Task: Fix targetTrackId undefined outside closure

  • Tóm tắt thay đổi: Chuyển target track computation ra ngoài updateActiveTracks callback để setDraggedSectionItem thấy targetTrackId. Compute dùng activeTracksRef cho track heights.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 11:12] Task: Fix drag cross-track — use ORIGINAL baseTrackId, never update

  • Tóm tắt thay đổi: Dùng dragBaseTrackId từ multiIds[drag.itemId].trackId (không đổi) thay vì drag.trackId (bị update mỗi mousemove). Xóa setDraggedSectionItem update trackId — không cần thiết. crossOffset ổn định xuyên suốt drag.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 11:16] Task: Rewrite drag handler — separate multi/single paths, pre-compute crossOffset

  • Tóm tắt thay đổi: Tách multiIds path và single path. Pre-compute crossOffset + outTracks ngoài updateActiveTracks. outTracks merge vào prev bằng filter id unique. Mỗi item compute targetTid từ origIdx+crossOffset, filter + upsert trên target track.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 11:27] Task: Fix seed_admin() resetting admin password on every restart

  • Tóm tắt thay đổi: Xóa else branch trong seed_admin() — từng kiểm tra if not verify_password(default_pwd, ...) và reset password admin về admin123 + must_change_password=1 mỗi lần server restart nếu password đã bị đổi. Giờ chỉ create admin khi chưa tồn tại.
  • Các file ảnh hưởng: app/core/auth.py

[2026-07-29 11:34] Task: Fix instrument list not loading on new machine after login

  • Tóm tắt thay đổi: Thêm listPlugins() + getSoundfontCatalog() fetch trong handleAuthSuccess — re-fetch instrument data sau khi login vì useEffect mount chạy trước khi token được set, fail silent.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 11:45] Task: AI SUGGEST auto-update after saving preset in AI Preset Manager

  • Tóm tắt thay đổi: Thêm aiPresetVersion counter, increment khi AIPresetModal đóng. Trong AI panel, so sánh version với window.__aiPresetVersion, nếu khác thì gọi aiPromptMgrRef.current.loadPresets() để reload từ localStorage.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 11:53] Task: Synth button shows track dropdown for quick instrument assign

  • Tóm tắt thay đổi: Synth button TCP giờ mở dropdown list tất cả tracks thay vì mở modal trực tiếp. Chọn track trong dropdown → modal instrument selector mở cho track đó. Không cần đóng modal để click Synth từng track.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 11:57] Task: Fix Synth button — track selector inside instrument modal

  • Tóm tắt thay đổi: Xóa dropdown track riêng (synthTrackDropdownId). Thêm <select> track list trong modal Select Instrument, giữa title và textbox tìm kiếm. Chọn track → mở instrument cho track đó trong cùng modal.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 12:00] Task: Fix missing aiTypeaheadRef definition

  • Tóm tắt thay đổi: Thêm aiTypeaheadRef = useRef(null) — ref được dùng trong typeahead dropdown nhưng chưa được khai báo, gây ReferenceError.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 12:03] Task: Select Instrument no longer auto-closes on instrument pick

  • Tóm tắt thay đổi: Xóa setInstrumentSelectorTrackId(null) khỏi setTrackInstrumentWithProgram — modal không tự đóng khi chọn instrument. Người dùng đổi track trong dropdown, chọn instrument, modal giữ nguyên. Đóng bằng nút ✕ hoặc click outside.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-29 15:39] Task: Implement full Ctrl-Z/Ctrl-Y undo/redo for MAIN SESSION and SECTION-TAB

  • Tóm tắt thay đổi: (1) Tạo UndoRedoEngine service (app/static/js/services/undoRedoEngine.js) — global stack hỗ trợ cross-cutting state changes (BPM, playhead, selection, AI prompt, instrument). (2) Extend handleUndo/handleRedo trong app.jsx để ưu tiên UndoRedoEngine, fallback về track-level undo stack cũ. (3) Áp dụng undo/redo cho: Delete/Create items (section, midi, clip, track), text AI Prompt (native textarea undo stack + engine integration), instrument assignment, playhead position, item drag position, selection changes, BPM/tempo changes. (4) Update undo/redo buttons disabled state + keyboard shortcut indicators. (5) Global Ctrl+Z/Y handler skip INPUT/TEXTAREA focus để text input native undo hoạt động đúng.
  • Các file ảnh hưởng: app/static/js/services/undoRedoEngine.js (NEW), app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-30 12:37] Task: Install Master Strip Console in mixer panel

  • Tóm tắt thay đổi: (1) Thêm CSS styles cho Master Strip Console (strip-bg, fader-track, btn-daw, btn-teal-active, fader-slider) vào app/templates/index.html. (2) Tạo component MasterStripConsole trong app/static/js/app.jsx — thay thế MASTER strip cũ (fader đơn giản) bằng console theo spec md/47_MASTER_STRIP_CONSOLE.md: dual stereo VU meter canvas với dB scales L/R, vertical fader -60~+12dB, button stack (Mono/M/S/Route/FX/Power/Trim/Info), pan knob, dB readout, RMS footer. (3) Tích hợp test signal engine (Play/Stop test audio vào masterBus.input), FX button mở MasteringModal, Power button toggle mastering active/bypass. (4) VU meter animation loop sử dụng masterBus.leftAnalyser/rightAnalyser thay vì synthetic data.
  • Các file ảnh hưởng: app/templates/index.html, app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes (Babel note: deoptimised styling for file >500KB, không phải lỗi).

[2026-07-30 12:53] Task: Install Master Strip + Track Strip Console in mixer panel

  • Tóm tắt thay đổi: (1) Refine MasterStripConsole theo spec md/47: top button "MASTERING PANEL", pan knob pointer-drag (L/R/center label), mute/mono buttons toggle .btn-mute-active/.btn-mono-active, VU meter dùng synthetic post-fader simulation khi muted. (2) Tạo TrackStripConsole từ spec md/48_TRACK_STRIP_COMPONENT.md: w-[145px] với top color accent bar, pan rotary dial, peak dB fader rail + LED VU canvas, button stack (M/S/Route/FX/Power/Auto/Phase Ø), record arm row, track name, color footer index. (3) CSS mới: .strip-bg-track, .fader-track-bg, .btn-solo-active, .btn-arm-active, @keyframes pulse-red, cập nhật .btn-daw:active/fader-slider thumb từ spec. (4) Replace MixerStripTrackStripConsole trong mixer panel.
  • Các file ảnh hưởng: app/templates/index.html, app/static/js/app.jsx
  • Ghi chú/Test (nếu có): npm run build — build passes.

[2026-07-30 21:15] Task: PIANO ROLL draw notes snapped to grid

  • Tóm tắt thay đổi: Note start_beat + duration_beats được snap bằng getSnapBeat trước khi vẽ lên canvas. Áp dụng cho active notes, ghost notes, recording notes.
  • 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. Note hiển thị thẳng hàng với grid lines theo snap value.

[2026-07-30 21:07] Task: Fix PIANO ROLL snap grid — snap values inverted in 3 places

  • Tóm tắt thay đổi: snapVal mapping bị đảo ngược: snap='1' (1 beat) cho q=4.0 (4 beats) → grid vẽ mỗi 4 beat, note snap mỗi 4 beat. Fix: getSnapBeat, getSnapDuration, grid drawing: snap='1'→1.0, '1/2'→0.5, '1/4'→0.25, '1/8'→0.125, '1/16'→0.0625, '1/32'→0.03125, '4'→4.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 pass. Piano roll snap=1 → grid mỗi 1 beat, note snap đúng vị trí.

[2026-07-30 21:02] Task: Fix audio clips lost — draw clip rect even without buffer + store duration in deserialize

  • Tóm tắt thay đổi: WaveformLane từng if (!clip.buffer) return; skip toàn bộ clip (kể cả rect/label). Fix: draw rect/label luôn, chỉ skip waveform khi null buffer. Thêm label "(audio chưa được tải)". Deserialize giờ restore duration trên clip từ duration_bars.
  • 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. Clip không có buffer vẫn hiện rect + label + duration đúng.

[2026-07-30 20:58] Task: Fix snap default + Power button mastering bypass

  • Tóm tắt thay đổi: (1) snapValue mặc định '1' (thay 'free'), snapVal PIANO_ROLL mặc định '1' (thay '1/16'); (2) Power button trong MasterStripConsole: bỏ isFxActive local state dùng chung sai với FX button, đọc masteringSettings.masterConnected trực tiếp → toggle masterConnected + set isBypassed tương ứng.
  • 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 project → snap = 1. Mixer → Power button → mastering bật/tắt đúng.

[2026-07-30 20:52] Task: Fix audio clip items lost after reload

  • Tóm tắt thay đổi: 3 root causes: (1) serverFileId không được lưu vào clip/track khi upload audio — insertSoundClipAtCursor gọi uploadToServer nhưng chỉ lưu vào global serverFileIdMap, không set t.serverFileId; (2) Serialize dùng t.serverFileId (null) → audio_file_url rỗng → Deserialize không lấy được fileId → loadAudioBuffersForTracks không load được buffer; (3) WaveformLane canvas clip.buffer.numberOfChannels crash khi buffer null → clip items không vẽ được. Fix: set serverFileId trên clip + track khi upload; serialize/deserialize server_file_id trong source_data; guard if (!clip.buffer) return trong WaveformLane.
  • 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. Chèn audio clip → lưu project → reload → clip hiển thị + waveform load lại.

[2026-07-30 20:46] Task: Auto-save 5min + Backup 30min + Backup management UI

  • Tóm tắt thay đổi: (1) DB: thêm bảng project_backups + migration cột is_backup/original_id; (2) Backend: POST/POST /cloud/{id}/backup, GET /cloud/{id}/backups, DELETE /cloud/backups/{id}, POST /cloud/backups/cleanup — tự động giới hạn retention (mặc định 10, cấu hình 5-20); (3) Frontend API: 4 methods backup; (4) App: setInterval 5 phút auto-save + 30 phút backup; (5) ProfileModal: nút Backup (hiện số lượng) mở rộng danh sách, xóa từng bản, thanh cấu hình retention slider 5-20, nút dọn dẹp.
  • Các file ảnh hưởng: app/models/user.py, app/api/v1/projects.py, app/static/js/services/api.js, app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): npm run build pass. Cần test: mở dự án Cloud → đợi 30p → backup tự động tạo; slider 5-20 lưu vào localStorage; nút 🧹 DỌN BACKUP giữ lại đúng số lượng.

[2026-07-30 20:36] Task: Fix project load crash — loadAudioBuffersForTracks scoped inside ProfileModal

  • Tóm tắt thay đổi: loadAudioBuffersForTracks defined inside ProfileModal, but restoreLastSessionProject in App component called it → ReferenceError on startup + silent catch → project not restored. Fix: move function to App component, pass as prop to ProfileModal.
  • 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ở lại trang → dự án cũ được khôi phục.

[2026-07-30 20:32] Task: Add "Tạo preset với cấu trúc" button in AI Preset Manager

  • Tóm tắt thay đổi: Thêm handleNewStructured — tạo preset mới với template có cấu trúc (mục: thể loại, bố cục, nhạc cụ, hòa âm, hiệu ứng, MIDI). Button xanh lục "Tạo preset với cấu trúc" bên trái "Quay lại" trong form edit.
  • Các file ảnh hưởng: app/static/js/app.jsx
  • Ghi chú/Test (nếu có): Nhấn "Tạo mới" → form edit hiện button xanh "Tạo preset với cấu trúc" → click → pre-fill template.

[2026-07-30 22:48] Task: Fix audio clip mất âm thanh sau khi mở lại dự án

  • Tóm tắt thay đổi: Sửa 3 lỗi khiến clip audio không tải được buffer sau khi save/reload project: (1) deserializeTracksList thiếu fallback từ audio_file_url khi server_file_id không có trong source_data (2) serializeTracksList không lưu server_file_id ở track level (3) legacy upgrade function trong Python không copy server_file_id vào source_data.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/api/v1/projects.py
  • Ghi chú/Test (nếu có): Mở lại dự án có clip audio → waveform hiển thị + playback có âm thanh.

[2026-07-31 05:42] Task: Cải thiện AI Preset Manager - Category dropdown + Generator modal

  • Tóm tắt thay đổi: (1) Phần DANH MỤC trong form tạo preset đổi từ input text sang dropdown <select> có đầy đủ danh mục từ presets + danh mục đã lưu trong localStorage, cho phép người dùng chọn danh mục có sẵn hoặc nhấn "+ Nhập danh mục mới..." để thêm; danh mục mới được lưu vào midi_prompt_categories trong localStorage để load lần sau. (2) Nút "Tạo preset với cấu trúc" mở modal mới chứa iframe /ai-prompt-generator phục vụ nội dung HTML từ md/49_AI_PROMPT_GENERATOR.md, cho phép người dùng dùng giao diện đầy đủ để tạo preset; đóng modal sẽ tự động refresh danh sách preset từ localStorage.
  • Các file ảnh hưởng: app/main.py, app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): npm run build pass. Backend route /ai-prompt-generator trả về HTML từ md/49_AI_PROMPT_GENERATOR.md. Test: mở AI Preset Manager → Tạo mới → dropdown DANH MỤC hiện danh sách → thêm danh mục mới → lưu → reload modal vẫn giữ danh mục mới. Click "Tạo preset với cấu trúc" → mở iframe generator.

[2026-07-31 06:15] Task: Fix preview layout in AI Prompt Generator modal

  • Tóm tắt thay đổi: Sửa layout right column của generator page: header thêm shrink-0 để không bị co lại; container preview đổi thành flex-1 flex flex-col min-h-0 thay vì overflow-y-auto; #promptOutputText đổi từ h-full sang flex-1 + overflow-y-auto để bottom luôn neo với cạnh modal và nội dung scroll trong element.
  • Các file ảnh hưởng: md/49_AI_PROMPT_GENERATOR.md
  • Ghi chú/Test (nếu có): Refresh trang generator → right column fill full height → content scroll bên trong #promptOutputText.

[2026-07-31 06:35] Task: Apply generator-to-preset-manager transfer spec

  • Tóm tắt thay đổi: Áp dụng spec từ md/50_APPLY_GENERATOR.md vào md/49_AI_PROMPT_GENERATOR.md: (1) Category input chuyển thành <select id="presetCategorySelect">; (2) Thêm nút "Tạo preset có cấu trúc" (xanh lá) bên trái action bar; (3) autoFillPresetModal cập nhật logic infer category từ role/keywords; (4) Thêm handler btnCreateStructuredPreset thay static number bằng template variable; (5) Save handler dùng presetCategorySelect.value, dispatch AI_PRESET_SAVED CustomEvent.
  • Các file ảnh hưởng: md/49_AI_PROMPT_GENERATOR.md
  • Ghi chú/Test (nếu có): Refresh generator → Lưu thành Preset → Category tự chọn dropdown → Tạo preset có cấu trúc → số bars/beats biến thành {{bars}}/{{total_beats}}.

[2026-07-31 06:42] Task: Transfer generator data to parent AI PRESET MANAGER form

  • Tóm tắt thay đổi: Thay vì mở modal standalone trong iframe generator, "Lưu thành Preset" giờ postMessage GENERATOR_PRESET_DATA lên parent window. React AIPresetModal thêm message listener nhận data, auto-populate form (name, category, keywords, bars, bpm, scale, template), set editingPreset='new' và đóng generator modal. Nút "Tạo preset có cấu trúc" trong React form vẫn hoạt động độc lập.
  • Các file ảnh hưởng: md/49_AI_PROMPT_GENERATOR.md, app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): npm run build pass. Flow: generator iframe → Lưu thành Preset → form React tự điền → generator modal đóng.

[2026-07-31 07:37] Task: Fix 3 bugs - panText, cloud project load, auto-restore

  • Tóm tắt thay đổi:
    1. Thêm const [panText, setPanText] = React.useState('center') vào MasterStripConsole để fix Uncaught ReferenceError: panText is not defined.
    2. Fix restoreLastSessionProject: xóa bỏ localStorage.removeItem('sonic_project_id') trong catch block để không xóa project ID khi API lỗi tạm thời → auto-load trên page reload hoạt động lại.
    3. Thêm .catch() vào loadAudioBuffersForTracks trong handleOpenProjectrestoreLastSessionProject để tránh unhandled promise rejection gây gián đoạn load project.
  • 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 reload page và test Open Project + Mixer Panel sau fix.

[2026-07-31 08:01] Task: Fix MIDI note creation duration & add Ctrl+Shift+Click split

  • Tóm tắt thay đổi:
    1. Thêm lastNoteDurationRef để ghi nhớ duration của note vừa tạo → click tạo note mới dùng duration đó thay vì snap duration mặc định.
    2. Ctrl+Shift+Click vào giữa MIDI note → tách note đó thành 2 notes tại vị trí click. Ctrl+Shift+Click vào vùng trống → nhân bản các note đang chọn (giữ hành vi cũ).
  • 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: 1) tạo note → thay đổi duration → tạo note mới → duration được kế thừa. 2) Ctrl+Shift+Click giữa note → note bị cắt thành 2.

[2026-07-31 08:09] Task: Bump precompiled cache version for piano roll split fix

  • Tóm tắt thay đổi: Cập nhật cache-busting parameter app.precompiled.js?v=202607310809 để trình duyệt load bản JS mới nhất có fix Ctrl+Shift+Click split MIDI note.
  • Các file ảnh hưởng: app/templates/index.html
  • Ghi chú/Test (nếu có): Hard reload (Ctrl+F5) để lấy bản JS mới.

[2026-07-31 08:40] Task: Click track name triggers realtime MIDI playback

  • Tóm tắt thay đổi: Click vào track name ở cột trái → tự động play ngay MIDI notes của track đó (nếu có piano roll tab). Dùng schedulePianoRollMidi(prTab, 0) + startSubTabPlayback(prTab, 0) + stopAllPlayback().
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload để load JS mới. Click track có MIDI notes → nghe thấy phát ngay.

[2026-07-31 08:50] Task: Piano roll instrument load + multitrack play select

  • Tóm tắt thay đổi:
    1. handleEditMidiInTab: khi mở piano roll tab bằng double-click MIDI item → lưu instrumentId, synth_engine vào tab, fallback instrumentProgram từ synth_engine.soundfont_program, gọi SonicSF.selectInstrument để load nhạc cụ của track.
    2. Đổi activePlayTrackIds từ single value sang mảng → click nhiều nút tên track ở cột trái piano roll để active nhiều track cùng lúc play multitrack.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Test: double-click MIDI item có nhạc cụ → play đúng nhạc cụ. Click nhiều track name → play multitrack.

[2026-07-31 09:20] Task: Multitrack play select plays immediately

  • Tóm tắt thay đổi: Thêm handlePianoRollRealtimePlay(trackIds) ở App: khi click nút track name trong piano roll → tính ngay ghost layers cho các track được chọn → stopAllPlayback() + SonicSF.stopAll()schedulePianoRollMidi(playTab, offset) → play ngay realtime không cần bấm play.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Click track button → nghe ngay. Click thêm/bớt track → re-schedule lại tức thì.

[2026-07-31 09:32] Task: Drag-drop MIDI file creates new tracks

  • Tóm tắt thay đổi: Thêm handleDropMidiToNewTracks(file) — drop file .mid/.midi vào vùng track container → parse MIDI → tạo track mới cho từng MIDI track và append MIDI item. Container có onDrop xử lý MIDI (hoặc audio → load vào selected track); lane drop cũng route MIDI sang hàm mới.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Kéo file .mid vào vùng track → track mới xuất hiện với MIDI.

[2026-07-31 09:48] Task: Piano roll shows all same-track MIDI items notes as active

  • Tóm tắt thay đổi: ghostNoteExtractor.js: bỏ filter time-overlap cho các MIDI items cùng track (isSameTrack) → tất cả notes của mọi MIDI item trong cùng track đều được gộp vào ghost layer. Render: same-track layers vẽ nổi bật (amber, alpha 0.65 + border) = trạng thái active. Effect sync: same-track layers luôn được đưa vào ghostPlayLayers → play đủ cả track; cross-track vẫn theo activePlayTrackIds.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/static/js/services/ghostNoteExtractor.js
  • Ghi chú/Test (nếu có): Hard reload. Mở MIDI item của track → thấy tất cả notes của các MIDI item cùng track hiển thị amber nổi bật; play → nghe đủ track.

[2026-07-31 09:52] Task: Opened MIDI item notes darker than same-track siblings

  • Tóm tắt thay đổi: Render piano roll: notes của MIDI item đang mở (main layer) đậm hơn — fill rgba(234,179,8,0.5), viền #f59e0b (chọn: xanh rgba(59,130,246,0.5)). Ghost notes của các MIDI items cùng track nhạt hơn (alpha 0.35, fill amber 0.35) → phân biệt item đang active vs tham chiếu.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Mở MIDI item → notes item đó đậm, notes item khác cùng track mờ hơn.

[2026-07-31 09:55] Task: Focused MIDI item notes brighter

  • Tóm tắt thay đổi: Notes của MIDI item được focus (đang mở / được chọn / đang play) sáng hơn: fill rgba(253,224,71,0.6), viền #fde047; khi đang play sáng hơn nữa rgba(254,240,138,0.75) viền #fef08a; chọn: xanh rgba(96,165,250,0.6). Ghost siblings cùng track vẫn mờ (alpha 0.35).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Mở MIDI item → notes sáng; bấm play → sáng hơn; item khác cùng track mờ hơn.

[2026-07-31 10:02] Task: Per-MIDI-item focus brightness in piano roll

  • Tóm tắt thay đổi: Mỗi ghost note có item_id (ghostNoteExtractor.js). Render piano roll tính focusedItemId: selection → item đang mở; đang play → item chứa playhead (st.currentTime trong khoảng startTime..startTime+duration). Notes của item được focus vẽ sáng (fill vàng 0.6/0.7 + viền #fde047, đang play 0.75), notes của các item khác cùng track mờ (alpha 0.3, fill tối #7a6a10). Cross-track giữ ghost 0.25.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/static/js/services/ghostNoteExtractor.js
  • Ghi chú/Test (nếu có): Hard reload. Mở track 3 MIDI items → play: playhead chạy đến item nào item đó sáng, các item khác mờ.

[2026-07-31 10:05] Task: Click note focuses its MIDI item

  • Tóm tắt thay đổi: Thêm state focusItemId trong PianoRollTabEditor. Click vào note chính → focus item đang mở (st.target_id). Click vào ghost note (note của MIDI item khác cùng track) → focus chuyển sang item đó (không vẽ note mới). Reset focus khi đổi st.target_id. Ưu tiên focus: đang play → item theo playhead; có note chọn → item đang mở; còn lại → focusItemId (theo click).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Click note của item 2 (ghost) → item 2 sáng, item 1&3 mờ.

[2026-07-31 10:10] Task: All same-track MIDI items selectable to focus

  • Tóm tắt thay đổi: Cột trái PIANO ROLL: dưới nút tên track (đang mở) hiển thị các nút con cho từng MIDI item trong track đó. Click nút MIDI item → setFocusItemId(im.id) → item đó sáng, các item khác cùng track mờ. Nút item đang focus: vàng đậm; item đang mở: viền vàng; còn lại: xám.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Click nút MIDI item bất kỳ trong track → focus chuyển sang item đó.

[2026-07-31 10:10] Task: Revert left-column MIDI item buttons

  • Tóm tắt thay đổi: Bỏ các nút MIDI items con ở cột trái (không cần). Giữ cơ chế hiện có: các MIDI items cùng track hiển thị sáng/ít sáng theo focus; click vào note mờ (ít sáng) của MIDI item khác → focus chuyển sang item đó.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload.

[2026-07-31 10:15] Task: Ctrl+click dim note selects + focuses item

  • Tóm tắt thay đổi: Trong handleGridMouseDown (ctrl+click, không bấm vào note chính): hit-test trên same-track ghost notes → nếu trúng note mờ → toggle chọn note đó (thêm vào selectedNoteIds, vẽ xanh sáng + viền) và setFocusItemId(item_id) → active MIDI item chứa note. Empty space vẫn mở marquee như cũ.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Ctrl+click note mờ của MIDI item khác → note chuyển xanh + item đó sáng lên.

[2026-07-31 10:13] Task: Selected dim note's MIDI item wins focus

  • Tóm tắt thay đổi: Fix focus resolution khi có selection: duyệt selectedNoteIds từ cuối (note chọn gần nhất) — main note → item đang mở; dim note (same-track) → item_id của nó. Ưu tiên: selection → playhead → focusItemId. GHOST NOTE chỉ là note của MIDI items track khác (alpha 0.25).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Ctrl+click note mờ item 2 → item 2 sáng dù item 1 đang mở.

[2026-07-31 10:19] Task: Click wait-to-active note switches to editing that item

  • Tóm tắt thay đổi: Click thường vào note mờ (wait-to-active) của MIDI item khác cùng track → setFocusItemId + handleSwitchMidiItem(item_id) → item đó trở thành target đang mở (editable), notes load vào editor, các item khác thành mờ. Ctrl+click vẫn giữ select+focus (không switch).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Click note mờ item 2 → item 2 mở ra để chỉnh sửa notes.

[2026-07-31 10:22] Task: Piano roll dropdown lists tracks only

  • Tóm tắt thay đổi: Dropdown trên toolbar PIANO ROLL: thay vì liệt kê tất cả MIDI items (allMidiItems), chỉ liệt kê các track có MIDI items (dedupe theo track id). Chọn track → handleSwitchMidiItem sang MIDI item đầu tiên của track đó. value = st.trackId.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Dropdown chỉ hiện tên các track.

[2026-07-31 10:26] Task: Fix master strip PWR mastering bypass

  • Tóm tắt thay đổi: Nút PWR (Bật/Tắt MASTERING PANEL Bypass) ở Master strip: khi bật ON trước đây set isBypassed: truetoggleMasteringOnMaster(true, true) giữ routing bypass → âm thanh vẫn bypass. Fix: toggle luôn set isBypassed: false + flip masterConnected → bật lên là mastering active (EQ→Imager→Maximizer), tắt là disconnect.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Bật PWR → âm thanh qua mastering (không còn bypass); tắt → bypass/disconnect.

[2026-07-31 10:32] Task: Track VU meter idle when not audible

  • Tóm tắt thay đổi: Track VU meter loop (line ~18446): tính isAudible theo solo/mute (anySolo ? track.solo : !track.muted) — nếu track không phát ra âm thanh thực (muted hoặc bị solo out) thì gán audioPeak=0 và bỏ qua midiVuActivityRef flash → VU meter đứng yên thay vì animation giả khi solo track khác.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Solo track 1 → track 2 (MIDI) VU không còn animation.

[2026-07-31 10:35] Task: Realtime solo toggle during playback

  • Tóm tắt thay đổi: toggleTrackSoloEvaluate: khi toggle solo trong lúc play → stop rồi restart đúng: main timeline gọi startTrackPlayback(currentTime) (đã đổi sang đọc activeTracksRef.current để nhận solo state mới); piano roll subtab đang play → schedulePianoRollMidi + startSubTabPlayback. Trước đây restart thiếu lệnh start nên solo không áp dụng realtime.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Bấm SOLO track trong lúc play → chỉ track đó còn nghe, track khác dừng ngay.

[2026-07-31 10:44] Task: Drop MIDI on existing track loads into that track

  • Tóm tắt thay đổi: Lane onDrop: drop MIDI/audio lên track có sẵn → loadFileOnTrack(track.id, f) (MIDI mở ở track đó; multi-track MIDI: track đầu vào track drop, còn lại tạo track mới). Drop ở vùng trống container vẫn tạo track mới (handleDropMidiToNewTracks).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Hard reload. Kéo file .mid lên track hiện có → MIDI nạp vào track đó.

[2026-07-31 10:47] Task: Fix MIDI playback crackling

  • Tóm tắt thay đổi: soundfontPlayer.js: (1) skip program change nếu channel đã có đúng instrument (_channels[ch] cache) — trước đây mỗi note đều reload soundfont → glitch/crackle; (2) bỏ console.log per-note; (3) render loop: worklet gửi CONSUMED feedback để queueDepth chính xác, QUEUE_TARGET 4→8, interval nhanh hơn → hết underrun (silence gap); (4) stopAll chỉ dùng all_notes_off (release) thay vì all_sounds_off (kill tức thì) → hết pop khi stop. fluidsynth-bridge.js: post CONSUMED sau splice.
  • Các file ảnh hưởng: app/static/js/services/soundfontPlayer.js, app/static/js/worklets/fluidsynth-bridge.js
  • Ghi chú/Test (nếu có): Hard reload. Play MIDI note → nghe mượt, hết lụp bụp.

[2026-07-31 10:50] Task: Fix MIDI crackling v2

  • Tóm tắt thay đổi: soundfontPlayer.js: (1) render loop dùng wall-clock time-based queue accounting thay vì async CONSUMED message (hết race gây underrun); (2) QUEUE_TARGET 8→16 (~170ms buffer chống jank main thread); (3) bỏ 400ms release hack → noteoff đúng duration (FluidSynth tự release mượt, hết chồng voice); (4) synth.verbose=0 + synth.gain=1.0. fluidsynth-bridge.js: bỏ post CONSUMED không còn dùng.
  • Các file ảnh hưởng: app/static/js/services/soundfontPlayer.js, app/static/js/worklets/fluidsynth-bridge.js
  • Ghi chú/Test (nếu có): Hard reload. Play MIDI với instrument → hết lụp bụp.

[2026-07-31 11:25] Task: Media Explorer panel (F6) + mutual exclusion with Mixer (F7)

  • Tóm tắt thay đổi: Tạo MediaExplorerPanel component theo spec md/51_MEDIA_EXPLORER.md (REAPER-style: toolbar, directory tree + file list, transport/preview với canvas visualizer + metadata + status bar), render ở hàng dưới trên status bar (giống Mixer panel, resize được, lưu studio_media_explorer_height). Gán F6 toggle panel với preventDefault+stopPropagation ở cả keydown capture của App (MAIN SESSION/SECTION-TAB) và PianoRollTabEditor. F6/F7 mutual exclusion: bật panel này tự tắt panel kia (window.__toggleMediaExplorerRef / __toggleMixerRef). Bỏ dock placeholder media_explorer cũ. Thêm button status bar "Media Explorer Panel (F6)". Thêm CSS .me-fader-slider + .file-row-selected vào index.html, bump version precompiled.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): npm run build + node --check OK; smoke test jsdom: F6 hiện Media Explorer & tắt Mixer, F7 ngược lại. Hard reload để test thủ công.

[2026-07-31 13:05] Task: My Computer tree truy cập filesystem thật

  • Tóm tắt thay đổi: Backend mới app/api/v1/media.py (+ mount trong app/main.py): GET /api/v1/media/computer liệt kê ổ đĩa/mount point (Windows drive letters / Linux /proc/mounts), GET /api/v1/media/browse?path= liệt kê thư mục con + file audio/MIDI, GET /api/v1/media/file?path= serve file cục bộ để preview (chặn ext không phải media). Frontend MediaExplorerPanel: node "My Computer" click → fetch roots, expand/collapse tree (2 cấp con), click thư mục → liệt kê file vào list, preview audio local qua /media/file, nút Back/Up/Refresh + address bar hiểu folder === 'computer'; metadata hiển thị Local Audio/Local MIDI.
  • Các file ảnh hưởng: app/api/v1/media.py, app/main.py, app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Test trực tiếp: roots 8 mounts, browse /home/locpham 22 dirs; serve wav → content-type audio/wav; chặn /etc/shadow. Smoke jsdom: click My Computer → drive root hiển thị → click drive → list beat.wav/loop.mid, address hiện path. Hard reload để test thủ công.

[2026-08-02 14:15] Task: Fix Window Explorer API không hoạt động

  • Tóm tắt thay đổi: app/api/v1/media.py list_computer_roots: lọc /proc/mounts chỉ giữ filesystem thật (REAL_FS_TYPES: ext4/xfs/btrfs/ntfs/vfat...), bỏ pseudo/docker/systemd mounts (/run/credentials/..., overlay, tmpfs, proc...) trước đó list ra 8 mount rác không duyệt được. Bỏ import windll thừa. Frontend MediaExplorerPanel: bỏ setComputerRoots([]) nuốt lỗi — giờ API fail → fallback về root / + showToast báo lỗi; browseComputerDir fail → toast thay vì im lặng; goComputerParent sửa edge case Windows root (C:\); fileDuration match theo path cho file local; size MIDI local hiện "MIDI TPQN".
  • Các file ảnh hưởng: app/api/v1/media.py, app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Test router riêng: /computer giờ trả đúng / + /boot/efi (bỏ rác), browse root 21 dirs; smoke jsdom: click My Computer → drive render → browse file OK; API fail → fallback root OK. Lưu ý: cần restart server FastAPI để load router mới + hard reload browser.

[2026-08-02 14:30] Task: Media Explorer duyệt toàn bộ folder + files

  • Tóm tắt thay đổi: app/api/v1/media.py /browse: trả về TẤT CẢ file (bỏ lọc media) kèm kind (midi/audio/other). Frontend MediaExplorerPanel: (1) tree My Computer đệ quy sâu vô hạn (renderComputerNode) — click folder chọn + load files, click +/- expand/collapse ở mọi cấp; (2) file list merge dirs + files, click file chọn/preview, click folder vào thư mục; (3) icon theo loại (fa-folder/fa-music/fa-file-audio/fa-file); (4) guard: file kind==='other' không preview; metadata Type hiện "Local File".
  • Các file ảnh hưởng: app/api/v1/media.py, app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Test backend: browse trả subdir + a.wav(audio)/b.mid(midi)/notes.txt+readme.md(other). Smoke jsdom: root → list folder+file, expand /data/data/deep → list loop.mid OK. Hard reload.

[2026-08-02 14:45] Task: My Computer mở explorer client local qua File System Access API

  • Tóm tắt thay đổi: MediaExplorerPanel "My Computer" giờ dùng Web API window.showDirectoryPicker() (Chrome/Edge) để mở Windows Explorer của MÁY CLIENT chọn thư mục, duyệt qua FileSystemDirectoryHandle (handle.entries()) — không còn phụ thuộc server/cloud. Tree đệ quy giữ handle mỗi node, computerMode='client' + clientRoot. Preview audio/MIDI đọc trực tiếp handle.getFile() → arrayBuffer → decodeAudioData (helper readLocalFileBuffer), không cần URL server. Back/Up/Refresh hoạt động cả 2 mode. Trình duyệt không hỗ trợ FS API → fallback về server API cũ.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom mock showDirectoryPicker + handle tree: click My Computer → picker → drive C: hiển thị → browse Users/readme.txt → expand Usersme OK. Chỉ hoạt động Chrome/Edge (FS Access API). Hard reload.

[2026-08-02 14:55] Task: My Computer client liệt kê folder + files bên phải

  • Tóm tắt thay đổi: Fix file list folder rows gọi browseComputerDir(f.path) (chuỗi) → đổi thành browseComputerDir(f) (entry object) để client mode duyệt đúng qua handle. Khi nhấn My Computer (client-side, showDirectoryPicker): picker chọn thư mục → tree trái hiện root, file list phải merge dirs + files (folder fa-folder + file theo kind), click folder trong list → vào thư mục.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom mock FS handle: picker → root C: → list Users(folder)+readme.txt → click Users row → vào root/Users → me hiển thị OK. Hard reload.

[2026-08-02 16:40] Task: Media Explorer panel — resize cây thư mục

  • Tóm tắt thay đổi: MediaExplorerPanel directory tree bỏ class w-44 cố định → width động qua state treeWidth (default 176px, min 110 max 420, ref treeWidthRef). Thêm drag handle w-1.5 cursor-ew-resize bên phải tree (mousedown/mousemove/mouseup trên document, startTreeResize). File list bên phải tự co giãn theo flex-1.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom: kéo handle +80px → tree width 176→256px OK, clamp 110420. Hard reload.

[2026-08-02 16:50] Task: Media Explorer preview không cập nhật realtime khi chọn file

  • Tóm tắt thay đổi: MediaExplorerPanel: (1) handleSelect giờ clear ngay setPeaks(null) + setAudioBuffer(null) + stopMediaPlayback() khi chọn file mới → canvas xóa dữ liệu cũ ngay; (2) thêm selectTokenRef guard async race: loadWaveformplaySelected check token sau mỗi await, kết quả cũ (file trước) không ghi đè file mới chọn; (3) gọi drawCanvas qua effect phụ thuộc [peaks, audioBuffer, selected] → vẽ lại realtime.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom: click file → row selected + metadata MIDI events hiển thị; switch nhanh liên tục → file cuối đúng được chọn, không bị race. Hard reload.

[2026-08-02 17:15] Task: Media Explorer — persist tree width, canvas realtime, Synth instrument cho MIDI

  • Tóm tắt thay đổi: (1) Tree width lưu vào localStorage['studio_media_explorer_tree_width'] khi kéo resize (onUp), khởi tạo đọc lại — reload vẫn giữ độ rộng. (2) Canvas cập nhật realtime khi click file: clear peaks/audioBuffer/midiNotes ngay khi chọn + selectTokenRef chống race async; MIDI real vẽ piano-roll nốt thật (màu xám theo pitch 48-84). (3) Nút Synth cạnh Auto-Play: dropdown chọn SoundFont + preset (load qua listPlugins+listSoundfontInstruments), lưu vào localStorage['studio_media_explorer_synth']; preview MIDI file thật play qua window.SonicSF.playNote với instrument đã chọn (parseMidiFile expose ra window.parseMidiFile vì nằm trong App scope).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom: tree resize → localStorage 276; Synth dropdown hiện Piano/Grand → chọn persisted; click tune.mid → SonicSF.playNote gọi với program 0. Hard reload.

[2026-08-02 17:18] Task: Fix MIDI auto-play không stop khi click file midi khác

  • Tóm tắt thay đổi: stopMediaPlayback (MediaExplorerPanel) giờ gọi window.SonicSF.stopAll() trước khi play file mới → hủy timers note-on/note-off đã schedule của file MIDI trước (trước đây chỉ stop audio source + cancel rAF, nốt MIDI cũ vẫn kêu). Đảm bảo khi click file midi khác, bản cũ dừng ngay.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom: play a.mid (4 nốt) → click b.mid → stopAll gọi, b.mid play 4 nốt pitch 72, không overlap. Hard reload.

[2026-08-02 17:24] Task: Fix MIDI auto-play đọc nhầm file cũ khi click file mới

  • Tóm tắt thay đổi: playMidiPreview (MediaExplorerPanel) thiếu re-check token sau await window.SonicSF.selectInstrument(...) — khi click file A rồi nhanh tới file B, invocation cũ (A) chờ selectInstrument xong rồi mới schedule notes, ghi đè lên file B mới chọn → auto-play nghe file cũ nhưng canvas hiển thị file mới. Thêm re-check selectTokenRef.current !== token ngay sau await (giống pattern đã có ở audio path sau decodeAudioData).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom 2 kịch bản race: (1) A read chậm 200ms + B nhanh 10ms → chỉ B play; (2) A nhanh + B chậm → chỉ B play (A bị stopAll). Canvas luôn = file cuối click. Hard reload.

[2026-08-02 17:35] Task: Fix canvas "No file selected" lần đầu + Tempo control

  • Tóm tắt thay đổi: (1) Fix canvas hiển thị sai lần đầu khi click file MIDI: startCanvasClock rAF tick giữ closure cũ của drawCanvas (đọc selected cũ = null) và vẽ đè "No file selected" mỗi frame. Chuyển drawCanvas/fileDuration/clock sang đọc state mới nhất qua refs (selectedRef, peaksRef, audioBufferRef, midiNotesRef, midiTotalRef, isPlayingRef, isPausedRef, currentTimeRef, tempoRef). (2) Thêm Tempo control (BPM, +/- input, min 40 max 300) cạnh nút Synth — ảnh hưởng tốc độ preview MIDI (playMidiPreview dùng tempoRef thay synthInst.bpm), ruler canvas, footer status; lưu localStorage['studio_media_explorer_tempo'].
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom: click a.mid lần đầu → canvas vẽ ruler (0,1,2..) không còn "No file selected", audio play; Tempo 120 → +1 → localStorage 121. Hard reload.

[2026-08-02 18:05] Task: Canvas scroll + giữ state folder/files khi toggle + restore session

  • Tóm tắt thay đổi: (1) drawCanvas scroll khi content rộng hơn khung: pxPerBeat 42 / pxPerSec 50, playhead di chuyển tới giữa khung (min(w/2, t*pxPerSec)) rồi dừng, content scroll trái với offset max(0, min(contentW-w, t*pxPerSec - w/2)); ruler scroll đồng bộ. (2) Panel giờ luôn mounted (display:none khi ẩn) thay vì showMediaExplorer && → toggle F6 giữ nguyên folder/files/tree/selection. (3) Session persistence localStorage['studio_media_explorer_session_v1'] (folder, mode, path, roots, tree, files, selected — bỏ handle) + lưu FileSystemDirectoryHandle vào IndexedDB sonicforge_media_explorer/root_handle để client mode restore được handle thật sau reload; restoreSession + re-browse lại folder đã load → cây tree + file list hiển thị đúng thư mục cũ, tên thư mục loaded luôn hiện trong tree.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom: toggle F6 off/on → tree+files+selection giữ nguyên; reload (fresh DOM + IndexedDB stub) → folder MyMusic, files a.mid/b.mid, selection restore. Scroll math: t=10s → offset 350, playhead 150 (center), t=29s clamp 1200. Hard reload.

[2026-08-02 18:50] Task: Media Explorer — bars MIDI, synth realtime, loop, icons, lưu folder path

  • Tóm tắt thay đổi: (1) parseMidiFile trả thêm totalBeats/bars/bpm/ticksPerBeat → metadata + footer + ruler canvas hiển thị bars đúng (thay vì suy từ duration). (2) Thêm synthInstRefselectSynthInst re-schedule preview MIDI ngay khi đổi instrument đang phát (realtime). (3) MIDI preview hỗ trợ loop: loopTimerRef setInterval re-schedule notes mỗi vòng khi isLooping; toggleLoop cũng re-schedule khi đang preview; stopMediaPlayback clear interval. (4) Thêm FontAwesome CDN vào index.html → icon Stop/Play/Pause/Loop + Back/Forward/Up/Refresh hiển thị. (5) openMyComputer restore thư mục đã mở từ session (click My Computer → hiện content folder trong ô File, không mở picker lại nếu đã có session + handle IndexedDB).
  • Các file ảnh hưởng: app/templates/index.html, app/static/js/app.jsx, app/static/js/app.precompiled.js
  • Ghi chú/Test (nếu có): Smoke jsdom: metadata Bars/Beats/BPM hiển thị, footer "Bar X/Y"; đổi synth Strings→prog 48 re-schedule OK; loop bật→notes 64→96 (re-schedule vòng 2); icons đủ; folder restore khi click My Computer OK. Hard reload.

[2026-08-02 19:30] Task: Fix canvas audio + drag & drop Media Explorer → timeline

  • Tóm tắt thay đổi: (1) Fix canvas audio hiển thị sai: waveform server trả duration nhưng không lưu → dur=0 nên ruler/playhead/time sai. Thêm state audioDuration (set ở loadWaveform local/server + playSelected, clear ở handleSelect), fileDuration ưu tiên dùng; bar width waveform sửa theo contentW thay vì w để scroll đúng. (2) Drag & drop file từ Media Explorer vào timeline MAIN SESSION + SECTION-TAB: file row draggable + onDragStart set window.__mediaExplorerDragFile; App thêm resolveMediaExplorerDropFile() chuyển entry → real File (client handle getFile(), server file_id fetch download, local path fetch /media/file); drop handlers trên track lane + wrapper timeline xử lý cả drag ME lẫn drag OS file (MIDI → new track / audio → load vào track).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom: wav client → metadata Duration 3.00s (trước là 0); drag file row → payload text/plain=name + __mediaExplorerDragFile set → drop vào track lane → resolve thành File + load (uploadToServer gọi). Hard reload.

[2026-08-02 19:36] Task: Fix canvas audioclip giãn đầy khung preview

  • Tóm tắt thay đổi: drawCanvas branch audio: bỏ contentW = Math.max(w, dur*pxPerSec) (giãn đầy frame) → contentW = max(1, dur*pxPerSec) với pxPerBeat=42, pxPerSec = 42*bpm/60 (cùng tỉ lệ tempo với MIDI). Clip ngắn hiển thị đúng chiều rộng theo tempo (2s@120 → 168px), clip dài scroll; playhead theo tỉ lệ tempo (t*pxPerSec, khóa giữa khi scroll). Ruler cùng scale (bỏ stretch Math.max(w, ...)). Thêm audioDuration vào deps effect redraw.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Probe drawCanvas: 2s@120 → contentW=168, waveform maxX 167.8 < 300 (không giãn); 10s → 840 scroll; 2s@240 → 336 (rộng hơn theo tempo). Hard reload.

[2026-08-02 19:56] Task: My Computer auto-load cây + Favorites thư mục

  • Tóm tắt thay đổi: (1) Khi mở My Computer client-side (pick xong root), tự động browse + expand tối đa 10 thư mục con cấp 1 (browseComputerDir/browseClientDir giờ trả về {dirs, files} để auto-expand) → cây hiển thị cấu trúc ổ đĩa/thư mục ngay. (2) Favorites: node "Favorites" ngay dưới My Computer (hiện khi có favorite, icon fa-star); chuột phải folder trong cây (hoặc trong Favorites) → context menu "Thêm vào Favorites"/"Gỡ khỏi Favorites" + "Mở thư mục"; lưu vào localStorage['studio_media_explorer_favorites_v1'] [{path, name}]; click favorite mở nhanh folder (tìm handle trong computerTree, hoặc walk lại từ clientRoot qua getDirectoryHandle theo segments path, fallback browseComputerDir); node folder có favorite hiện icon star.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom: pick root → C: + Users + Program Files auto-load; right-click Users → menu → favorite saved localStorage + node Favorites hiện; click favorite → mở root/Users. Hard reload.

[2026-08-02 19:65] Task: My Computer auto-scan + playhead chạy tới cuối khi hết scroll

  • Tóm tắt thay đổi: (1) openMyComputer bỏ showDirectoryPicker khi click node My Computer — giờ tự động quét server API (/api/v1/media/computer liệt kê ổ đĩa máy local + /browse expand ổ đĩa → expand thư mục con cấp 1), cây hiển thị ngay không cần hộp thoại; vẫn restore session/handle IndexedDB nếu có. (2) Playhead trong drawCanvas (MIDI + audio): công thức max(0, min(w, t*pxPerSec - offset)) — khi content rộng hơn frame, playhead chạy tới giữa rồi khóa ở giữa trong khi content scroll trái; khi offset đạt max (contentW-w) thì playhead tiếp tục chạy với tốc độ play tới cuối canvas.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke: click My Computer → picker NOT called, DATA/HOME + Music/user auto-load; playhead math: big clip t=1→84, t=3→150 (center), t=5→150 locked, t=10→300 (end). Hard reload.

[2026-08-02 19:75] Task: Fix My Computer client-side tự động quét cây

  • Tóm tắt thay đổi: openMyComputer ưu tiên client-side: load root handle từ IndexedDB → tự động quét cây thư mục client (C:, Users, Program Files...) mà KHÔNG gọi showDirectoryPicker (đã gỡ hẳn). Fix bug cũ: if (savedHandle && ... && !computerRoots) luôn false vì computerRoots vừa được set bởi server API → client handle không bao giờ restore được → gây ra hộp thoại picker. Giờ: (1) nếu có client handle → auto-scan client tree + expand; (2) không có → fallback server API /api/v1/media/computer tự động quét ổ đĩa máy local. Mọi path đều không mở hộp thoại.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom: IndexedDB có client handle C: → click My Computer → picker NOT called, cây C:/Users/Program Files auto-load. Không handle → server API Root (/) auto-load, picker NOT called. Hard reload (browser cache cũ có thể vẫn hiện picker). Commit.

[2026-08-02 19:85] Task: Fix My Computer hiển thị nhầm Favorites thay vì cây hệ thống

  • Tóm tắt thay đổi: openMyComputer bỏ block restore session cũ (đọc studio_media_explorer_session_v1 từ localStorage ghi đè computerRoots/computerTree bằng dữ liệu favorites/stale → khi click My Computer hiện thư mục favorite cũ thay vì cây client). Giờ click My Computer luôn: (1) load client root handle từ IndexedDB → auto-scan cây client thật (C:, Users, Program Files...) hoặc (2) fallback server API scan ổ đĩa local. Không còn ghi đè bằng session cũ.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom: seed session cũ chứa OLD_FAV + favorites → click My Computer → picker NOT called, hiện C:/Users/Program Files (client tree), OLD_FAV không hiển thị. Hard reload.

[2026-08-02 19:95] Task: My Computer luôn hiển thị cây hệ thống client, Favorites node riêng

  • Tóm tắt thay đổi: restoreSession giờ CHỈ khôi phục favorites + folder/mode/rootName — BỎ khôi phục computerRoots/computerPath/computerFiles/computerTree/selected (nguyên nhân click My Computer vẫn mở thư mục cũ từ session). Mount effect chỉ nối lại FileSystemDirectoryHandle (clientRoot) không browse folder cũ. Click My Computer luôn: load client handle → auto-scan cây hệ thống client (C:, Users, Windows...) hoặc fallback server API scan ổ đĩa. Favorites node hiển thị sát dưới node My Computer (đã có từ trước).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom: seed session OLD_STUFF + favorite Users → click My Computer → picker NOT called, cây client C:/Users/Windows hiển thị, OLD_STUFF không còn, Favorites node + favorite Users có icon star. Hard reload.

[2026-08-02 20:01] Task: Fix My Computer chỉ hiện 1 thư mục xanh không có ổ đĩa/thư mục

  • Tóm tắt thay đổi: openMyComputer: nếu có client root handle đã lưu (IndexedDB) nhưng quyền đọc bị thu hồi sau reload (handle.entries() throw/empty) thì trước đây vẫn return → chỉ hiện node gốc (icon xanh) không có con. Giờ: (1) gọi queryPermission/requestPermission trước khi quét; (2) chỉ giữ client path nếu browsed.dirs.length > 0; (3) nếu quét client lỗi/rỗng → fallback server API scan ổ đĩa + expand thư mục con.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Smoke jsdom 2 kịch bản: (1) handle bị revoke (entries throw, permission denied) → fallback server: 2 drives (fa-hard-drive) + 5 folder, DATA/HOME/Music/Docs hiển thị; (2) handle khỏe (granted) → cây client C: + Users/me/Windows. Hard reload.

[2026-08-02 20:10] Task: Click My Computer PHẢI hiện cây thư mục ổ đĩa + thư mục client-side

  • Tóm tắt thay đổi: openMyComputer refactor thành helper useClientRoot: (1) client handle đã lưu (IndexedDB) + permission granted → auto-scan cây client; (2) KHÔNG có handle/không có quyền → gọi window.showDirectoryPicker() để cấp quyền đọc thư mục client rồi hiện cây client (lần đầu sẽ hiện picker 1 lần); (3) không hỗ trợ API/người dùng hủy → fallback server scan /api/v1/media/computer (ổ đĩa máy local). Bỏ điều kiện browsed.dirs.length để luôn giữ client mode sau khi chọn. Fix cha-con: browseClientDir gắn parent: handle cho từng thư mục con, browseComputerDir giữ entry.parent khi node chưa có trong tree → nút Up (↑) điều hướng đúng trong cây client.
  • 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 (babel) thành công, grep bundle chứa showDirectoryPicker/useClientRoot/parent:handle. Smoke: không handle → click My Computer → picker hiện, chọn thư mục → cây client expand. Có handle → không picker, cây client auto-load.

[2026-08-02 20:56] Task: Fix "Unable to preventDefault inside passive event listener invocation"

  • Tóm tắt thay đổi: Media Explorer preview canvas dùng React onWheel={handleCanvasWheel}e.preventDefault() — React gắn wheel passive tại root nên Chrome log lỗi "Unable to preventDefault inside passive event listener invocation" và page vẫn scroll. Thay bằng native listener: useEffect + canvasRef + addEventListener('wheel', h, { passive: false }) qua handleCanvasWheelRef (luôn gọi handler mới nhất), bỏ prop onWheel khỏi <canvas>. Giờ block scroll thật + hết lỗi console.
  • 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 OK; bundle: handleCanvasWheelRef có mặt, onWheel:handleCanvasWheel đã bỏ. Scroll chuột trên canvas preview (có file selected) → zoom hoạt động, không lỗi passive. Lưu ý: các onWheel tương tự ở MixerStrip/MasterStrip/TrackStripConsole (pan dial, fader) vẫn còn cảnh báo này — chưa sửa.

[2026-08-02 21:40] Task: Instrument Sonatina soundfont không phát âm thanh

  • Tóm tắt thay đổi: Nguyên nhân gốc: FluidSynth WASM (soundfontPlayer) KHÔNG decode được mẫu Ogg Vorbis/SF3 — mọi sample có bit OGG_VORBIS bị từ chối ("unknown flags... unsupported compression") → chọn instrument từ soundfont .sf3 (Sonatina đang ở dạng SF3 do startup convert) → câm hoàn toàn. Ngoài ra converter SF2→SF3 cũ còn 2 bug: (1) thiếu chunk id 'smpl' khi dựng lại file → sfload -1; (2) offset đọc shdr sai (đọc [end,loopstart,loopend,rate] thay vì [start,end,...]) → smpl rỗng. Fix: (1) sf3_to_sf2() + _decode_sample_ogg() chuyển SF3→SF2 (giải nén OGG về PCM, đúng semantics end-exclusive, loop absolute, xóa bit OGG) — đã verify phát âm thanh cả native lẫn WASM; (2) endpoint download ưu tiên .sf2, nếu chỉ có .sf3 thì chuyển SF3→SF2 on-demand (cache vào upload dir); (3) vô hiệu startup convert SF2→SF3 (main.py) — client không chơi được SF3; (4) client loadSoundFont nếu cache IndexedDB cũ chứa buffer hỏng thì xóa cache + tải lại; (5) sửa _sf2_to_sf3_python (smpl id, shdr offset đúng, end-exclusive, version 3.0, bit OGG, loop relative) + gate _sf3_plays_audio xóa SF3 không phát được.
  • Các file ảnh hưởng: app/core/soundfont_converter.py, app/api/v1/plugins.py, app/main.py, app/static/js/services/soundfontPlayer.js
  • Ghi chú/Test (nếu có): Test Node/WASM + native: SF2 gốc rms 0.007 (OK); SF3 convert mới sfload=1 nhưng câm (WASM từ chối OGG); SF3→SF2 convert ra file 52.37MB phát rms 0.007-0.02 (native + WASM). Test endpoint mô phỏng: upload SF3-only → download trả SF2 phát được. pytest tests/test_vst_engine.py 12 passed. Lưu ý: scipy thiếu nên test_plugin_api không import được (môi trường dev).

[2026-08-03 07:55] Task: Media Explorer preview - loop liên tục vùng chọn + margin 2px quanh canvas

  • Tóm tắt thay đổi: (1) Loop preview giờ chạy liên tục vô hạn cho đến khi nhấn Stop: startCanvasClock đọc refs (isLoopingRef/selStartRef/selEndRef) thay vì closure cũ nên việc bật loop giữa lúc đang play được phản ánh ngay, playhead wrap đúng theo loopStartSec (trừ offset gốc), không còn tự stopMediaPlayback() khi hết selection; playMidiPreview dùng isLoopingRef.current khi lập lịch interval (trước đây closure isLooping cũ → bật loop không tạo interval) và hủy interval khi tắt loop; toggleLoop sync isLoopingRef ngay + cập nhật loopStart/loopEnd cho audio đang phát theo selection hiện tại; playSelected dùng refs cho loop points/startOffset. (2) Container render canvas thêm p-0.5 (2px) để quét chọn vùng không vượt ra ngoài khung preview.
  • 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 (babel) thành công. Smoke: chọn file audio → quét chọn 1 đoạn → bật Loop → phát liên tục vùng chọn đến khi nhấn Stop (playhead wrap đúng, selection overlay vẫn hiển thị khi rAF redraw nhờ drawSelStart/drawSelEnd). MIDI: bật loop khi đang preview → interval reschedule vùng chọn.

[2026-08-03 10:15] Task: Fix MIDI ARM không phát âm thanh sau khi load instrument

  • Tóm tắt thay đổi: Sửa soundfontPlayer.js để phím MIDI trên track ARM phát đúng instrument: (1) _playNoteFluid không còn để state mặc định của channel (bank 0, program 0, không sfId) che mất synth_engine của track → finalSfId luôn đúng; (2) tự load soundfont lười (lazy) ngay trong đường phát note nếu font chưa vào _sfHandleMap (track pick nhanh từ dropdown không gọi selectInstrument → trước đây rơi vào bank_select/program_change trên synth không có soundfont → câm lặng); (3) selectInstrument không còn đổi hướng channel khi đã truyền channel tường minh (sửa lỗi 2 track dùng chung instrument bị giật channel). Bump version cache soundfontPlayer.js trong index.html.
  • Các file ảnh hưởng: app/static/js/services/soundfontPlayer.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Test logic bằng harness node /tmp/kilo/test_sonicsf_fix.js (mô phỏng FluidSynth): S1 lazy-load + program_select + noteon, S2 channel cấu hình sẵn không sfload lại, S3 program-only fallback, S4 no-instrument vẫn silent, S5 2 track cùng instrument giữ channel riêng — ALL PASSED. Cần hard-refresh trình duyệt để nạp soundfontPlayer.js bản mới.

[2026-08-03 10:30] Task: Fix ARM nhiều track - instrument sai theo track (channel state che mất synth_engine)

  • Tóm tắt thay đổi: _playNoteFluid trước đây ưu tiên state channel (_channels[ch].sfId) làm nguồn instrument cho note → khi ARM nhiều track hoặc track đổi instrument qua dropdown nhanh, note bị phát theo instrument cũ/khác đang "dính" trên channel (leftover state hoặc 2 track trùng channel) → sai instrument từng track. Sửa: synth_engine của track là nguồn quyết định; channel state chỉ là cache (chỉ dùng khi note không có engine). Khi note mang engine khác với channel đang giữ, tự động program_select lại đúng instrument trước noteon; vẫn lazy-load soundfont nếu chưa nạp. Bump version cache soundfontPlayer.js trong index.html.
  • Các file ảnh hưởng: app/static/js/services/soundfontPlayer.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Harness node /tmp/kilo/test_multi_track.js (mô phỏng routing ARM như app.jsx): (A) 3 track 3 instrument distinct channel → noteon đúng, không re-select thừa; (B) 3 track quick-pick (không midiChannel, channel đang giữ instrument cũ) → mỗi track re-select đúng Q1/Q2/Q3 trước noteon; (C) 2 track trùng channel 0 → track 2 re-select program 40 trước noteon. Harness test_sonicsf_fix.js (single-track S1-S5) vẫn ALL PASSED. Hard refresh trình duyệt để nạp bản mới.

[2026-08-03 10:40] Task: Fix lag khi ARM sau reload + hết âm thanh sau vài lần nhấn MIDI

  • Tóm tắt thay đổi: (1) Dedup load soundfont: loadSoundFont trước đây không chặn các lệnh gọi đồng thời — nhấn phím MIDI liên tục (hoặc nhiều track ARM) kích hoạt lazy-load cùng lúc → SGM-V2.01 bị sfload 4 lần (handle 1,2,3,4 trong log), mỗi bản ~52MB trong heap WASM 256MB → cạn bộ nhớ, các lần load sau fail → câm sau vài lần nhấn + mỗi note chờ load (lag). Thêm _loadPromises[sfId] dedup (1 font = 1 lần load, cache cả kết quả fail) + _sfloadSeq cho filename temp duy nhất tránh trùng tên khi load 2 font khác nhau đồng thời. (2) Preload instrument khi mở/khôi phục dự án: thêm preloadTrackInstruments(tracks) gọi selectInstrument cho từng track có soundfont ngay sau khi setTracks trong handleOpenProjectrestoreLastSessionProject → nhấn MIDI key đầu tiên không còn lag (font đã nạp sẵn).
  • Các file ảnh hưởng: app/static/js/services/soundfontPlayer.js, app/static/js/app.jsx, app/static/js/app.precompiled.js (build lại), app/templates/index.html
  • Ghi chú/Test (nếu có): npm run build (babel) OK, bundle chứa preloadTrackInstruments. Harness node /tmp/kilo/test_dedup.js: 8 lần loadSoundFont cùng 1 font đồng thời → chỉ 1 sfload, 1 lần đọc cache; load lại font đã nạp → 0 sfload; 2 font khác nhau đồng thời → đúng 2 sfload — ALL PASSED. test_sonicsf_fix.js + test_multi_track.js vẫn ALL PASSED. Hard refresh trình duyệt.

[2026-08-03 11:00] Task: ARM multitrack - mỗi track dùng channel MIDI riêng, không làm đổi instrument track khác

  • Tóm tắt thay đổi: FluidSynth có 16 channel; nếu 2 track trùng channel thì ARM track này sẽ program_select đè instrument của track kia. Thêm cơ chế channel riêng cho từng track trong app.jsx: trackMidiChannelsRef + ensureTrackMidiChannel/assignTrackMidiChannel cấp channel ổn định, duy nhất (0-15, bỏ qua 9 - slot percussion cổ điển), tự sửa khi 2 track trùng channel được lưu. Áp dụng ở mọi nơi tính channel: setTrackInstrumentWithProgram, setTrackInstrument (dropdown nhanh), preloadTrackInstruments, routing ARM note-on (armedTracks + piano-roll sub-tab), note-off/CC/PitchBend (bỏ fallback index % 16 — tránh dừng nhầm note của track khác). Lưu midi_channel khi save project + restore; reset map channel khi mở/khôi phục dự án. Kết hợp doNote cũ (synth_engine quyết định + re-select trước noteon) → mỗi track luôn phát đúng instrument của nó dù ARM nhiều track, chord/hợp âm đồng thời không còn đổi âm sắc nhau.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (build lại), app/templates/index.html
  • Ghi chú/Test (nếu có): npm run build OK, bundle chứa assignTrackMidiChannel (8 chỗ). Harness node /tmp/kilo/test_channels.js: 3 track → channel 0,1,2 duy nhất; gọi lại ổn định; track mới lấy channel trống; track lưu trùng channel được cấp lại; track lưu channel duy nhất được giữ; percussion giữ 9 + track trống khác lấy channel riêng — ALL PASSED. 3 harness cũ (single/multi/dedup) vẫn PASS. Hard refresh.

[2026-08-03 11:30] Task: Fix instrument sau làm đổi thông số instrument trước + ReferenceError preloadTrackInstruments + warning channel 9

  • Tóm tắt thay đổi: (1) sfload reset_presets 1→0 trong soundfontPlayer._tryLoadSFL: trước đây mỗi lần load soundfont mới, FluidSynth reset preset của MỌI channel về preset 0 của font mới → load instrument sau làm đổi âm sắc (decay/loop...) của instrument trước dù channel đã program_select riêng; giờ channel giữ nguyên instrument, doNote/selectInstrument tự program_select rõ ràng. (2) ReferenceError preloadTrackInstruments: hàm bị đặt nhầm bên trong ProfileModal (không cùng scope với restoreLastSessionProject trong App) → lỗi khi reload. Chuyển preloadTrackInstruments lên module-level (self-contained, chỉ warm font cache). Đồng thời sửa lỗi tiềm ẩn của handleOpenProject trong ProfileModal: truyền thêm props App-scoped (setAppWarningModal, bpm, setBpm, setMasteringSettings, setSessionTabs, setSubTabs, trackMidiChannelsRef) — trước đây click mở dự án trong modal sẽ ReferenceError. (3) "No preset found on channel 9 [bank=128 prog=0]": lọc warning benign trong printErr (font không có preset bank 128 thì note chỉ câm, không phải lỗi) + reset=0 giảm nguồn sinh ra nó.
  • Các file ảnh hưởng: app/static/js/services/soundfontPlayer.js, app/static/js/app.jsx, app/static/js/app.precompiled.js (build lại), app/templates/index.html
  • Ghi chú/Test (nếu có): npm run build OK, bundle syntax OK. Harness node /tmp/kilo/test_reset.js: sfload reset=0, load B không re-select channel A, noteon ch0/ch1 không kèm program_select thừa — PASSED. 5 harness còn lại (single/multi/dedup/channels) vẫn PASS. Deprecation ScriptProcessorNode giữ nguyên (đổi sang AudioWorklet có rủi ro crackle, cần test audio thật). Hard refresh.

[2026-08-03 11:50] Task: Play MIDI item luôn dùng instrument của track chứa item (kể cả item duplicate)

  • Tóm tắt thay đổi: Rà soát mọi đường phát MIDI trong app.jsx để chắc chắn playNote luôn nhận channel riêng của track (assignTrackMidiChannel) + synth_engine của track đó: (1) fix bug startLocalTrackPlayback (local selection loop) — trước đây gọi playNote KHÔNG truyền channel/synth_engine → rơi về channel 0, phát nhầm instrument của track đang giữ channel 0; giờ truyền lcCh + track.synth_engine. (2) Đồng nhất các đường còn lại (transport startTrackPlayback, section sub-track, schedulePianoRollMidi, playMidiPreviewNote, piano-roll canvas/click/keybed/brush preview, ghost layers) từ getTrackMidiChannelassignTrackMidiChannel để mọi path dùng đúng dedicated channel của track. Vì playback lấy synth_engine/instrumentProgram từ track CHỨA item (item không lưu instrument, duplicate chỉ copy notes) nên khi duplicate/move MIDI item sang track khác, item sẽ tự động phát instrument đã load ở track đó.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (build lại), app/templates/index.html
  • Ghi chú/Test (nếu có): npm run build OK, bundle syntax OK, getTrackMidiChannel không còn trong app.jsx (0), assignTrackMidiChannel có 19 chỗ. Bundle chứa lcCh=assignTrackMidiChannel(track,tracks) + playNote truyền lcCh,track.synth_engine. 5 harness (single/multi/dedup/channels/reset) vẫn PASS. Hard refresh.

[2026-08-03 12:10] Task: Fix ReferenceError assignTrackMidiChannel + bật AudioWorklet thay ScriptProcessor

  • Tóm tắt thay đổi: (1) ReferenceError assignTrackMidiChannel is not defined: allocator channel (trackMidiChannelsRef/ensureTrackMidiChannel/assignTrackMidiChannel) đặt bên trong App, nhưng các sub-component piano roll (PianoRollTabEditor — canvas preview, click note, draw/paint brush, keybed) là module-level → không thấy hàm → lỗi khi render/play. Chuyển allocator lên module-level (đầu file, trước PianoRollTabEditor), App dùng chung bản đó (bỏ khai báo useRef cục bộ trong App; reset trackMidiChannelsRef.current = {} khi load dự án vẫn dùng chung object). (2) Deprecation ScriptProcessorNode: bật AudioWorklet fluidsynth-bridge làm mặc định (_useScriptNode = false), chỉ fallback ScriptProcessor khi new AudioWorkletNode/addModule thất bại → hết warning deprecation; _startRenderLoop (queue 16 block × 512 sample ~186ms headroom, interval 2× frame rate) vốn đã được thiết kế cho worklet.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (build lại), app/static/js/services/soundfontPlayer.js, app/templates/index.html
  • Ghi chú/Test (nếu có): npm run build OK, bundle syntax OK, bundle load smoke test (stub React/ReactDOM/window) OK. Harness test_sonicsf_fix.js với mock AudioWorkletNode + performance: workletCreated=1 scriptProcCreated=0 (worklet được dùng), S1-S5 vẫn PASS; trước đó (thiếu mock performance) xác nhận fallback ScriptProcessor hoạt động. 4 harness còn lại vẫn PASS. Hard refresh.

[2026-08-03 12:30] Task: Fix crash AudioWorklet - "Cannot set properties of undefined (setting '0')" khi output mono

  • Tóm tắt thay đổi: FluidSynthBridge.process luôn ghi stereo out[0]/out[1]; khi thiết bị/context xuất mono (chỉ 1 channel), out[1]undefinedout[1][i] = ... ném TypeError, dừng audio. Fix: (1) fluidsynth-bridge.js xử lý mọi số channel output (vòng lặp theo out.length, chẵn=lấy L, lẻ=lấy R, trường hợp cạn queue ghi 0 cho từng channel); (2) soundfontPlayer.js tạo AudioWorkletNode với outputChannelCount: [2], channelCount: 2, channelCountMode: 'explicit' để ép stereo (FluidSynth render stereo) thay vì để device quyết định.
  • Các file ảnh hưởng: app/static/js/worklets/fluidsynth-bridge.js, app/static/js/services/soundfontPlayer.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Harness node /tmp/kilo/test_worklet.js mô phỏng process(): (1) output mono 1 channel → mirror left, không crash; (2) stereo → L/R đúng; (3) queue cạn → silence. Tất cả PASS. 5 harness khác vẫn PASS. Worklet + soundfontPlayer node --check OK. Không cần build bundle (2 file served trực tiếp). Hard refresh.

[2026-08-03 12:45] Task: Fix không có âm thanh khi play MIDI item - quay lại ScriptProcessor làm renderer mặc định

  • Tóm tắt thay đổi: Sau khi bật AudioWorklet làm mặc định, audio không phát khi play MIDI item trên track (môi trường user bị 2 lỗi liên tiếp: crash mono rồi câm). ScriptProcessor là đường đã được xác nhận hoạt động ổn định (mọi test trước đó). Quay lại _useScriptNode = true (ScriptProcessor mặc định) để đảm bảo âm thanh; giữ code AudioWorklet (đã mono-safe) + thêm cache-busting ?v=202608031240 cho URL addModule để khi bật lại không bị cache bản cũ. Deprecation warning quay lại nhưng chỉ là cảnh báo cosmetic.
  • Các file ảnh hưởng: app/static/js/services/soundfontPlayer.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Harness node /tmp/kilo/test_render.js (path ScriptProcessor): workletCreated=0, ScriptProcessor được tạo, playNote → noteon, onaudioprocess render ra audio peak 0.25 — ALL PASSED. test_sonicsf_fix.js cập nhật assertion ScriptProcessor mặc định → rc=0. 6 harness còn lại PASS (test_worklet rc báo 1 do glitch shell nhưng result file ghi ALL TESTS PASSED). Không cần build bundle (file served trực tiếp). Hard refresh.

[2026-08-03 13:00] Task: Fix piano roll tab không phát âm thanh instrument của track

  • Tóm tắt thay đổi: schedulePianoRollMidi cộng sessionBeatOffset (vị trí tuyệt đối của item trong project, đơn vị beat) vào start_beat của mọi note → note được lên lịch ở thời điểm tuyệt đối của project, trong khi playhead/tab piano roll là item-relative (0 = đầu item). Item đặt sau vị trí 0 (vd 10s) → nhấn play nghe không có âm thanh trong 10s (note được schedule ở now+10s), sau đó mới kêu → cảm giác "không có âm thanh instrument". Fix: bỏ sessionBeatOffset ở cả loop note chính và loop ghost (ghost relative_start_beat vốn đã item-relative) — note phát theo vị trí tương đối item, khớp playhead; item ở vị trí 0 không đổi hành vi. sessionSyncMode chỉ là chế độ hiển thị ghost/context, không liên quan timing phát.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (build lại), app/templates/index.html
  • Ghi chú/Test (nếu có): npm run build OK, bundle syntax OK, hết sessionBeatOffset (0 chỗ). Harness node /tmp/kilo/test_pr_timing.js mô phỏng timing mới: item ở project 10s, nhấn play mới → note đầu phát ngay +0s (trước đây +10s), note 2 ở +2s; resume 1.2s → bỏ note đầu, schedule note sau; item vị trí 0 không đổi — ALL PASSED. 6 harness còn lại PASS. Hard refresh.

[2026-08-03 13:15] Task: Fix mất soundfont khi ARM + BiquadFilterNode state is bad

  • Tóm tắt thay đổi: (1) Master chain bị NaN: applyMasteringSettings chạy mỗi lần getAudioContext(); nếu window.currentMasteringSettings bị thiếu field/NaN (vd project lưu cũ, slider kéo cực hạn) thì eqLowFilter.gain.setTargetAtTime(undefined/NaN) → biquad "state is bad" → master chain im lặng → mọi audio (kể cả MIDI/ARM) biến mất ("mất soundfont"). Thêm clamp() chặn mọi tham số EQ (±24dB), imager (±100), maximizer, ceiling — NaN/thiếu → 0 (trung tính), cực hạn → giới hạn an toàn. (2) toggleMasteringOnMaster trở nên idempotent (chỉ disconnect/reconnect khi trạng thái đổi) — trước đây gọi lại liên tục từ getAudioContext() gây reconnect nhanh → biquad mất ổn định. (3) loadSoundFont không cache lỗi vĩnh viễn: lỗi tải thoáng qua (mạng 503, áp lực bộ nhớ) trước đây bị giữ trong _loadPromises → instrument câm vĩnh viễn tới khi reload; giờ xóa cache lỗi để note kế tiếp thử lại và tự hồi phục.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (build lại), app/static/js/services/soundfontPlayer.js, app/templates/index.html
  • Ghi chú/Test (nếu có): npm run build OK, bundle syntax OK. Harness node /tmp/kilo/test_retry.js: clamp undefined/NaN → 0, cực hạn → giới hạn; load font lỗi 503 lần 1 → lần 2 retry thành công (2 fetches) → note phát được — ALL PASSED. 8 harness còn lại PASS. Deprecation ScriptProcessorNode vẫn còn (giữ ScriptProcessor vì ổn định, worklet từng gây lỗi thiết bị). Hard refresh.

[2026-08-03 13:30] Task: Hết ScriptProcessor deprecation (bật AudioWorklet) + hết BiquadFilterNode state is bad (signature guard)

  • Tóm tắt thay đổi: (1) Deprecation: bật lại AudioWorklet làm renderer mặc định (_useScriptNode = false) — worklet giờ đã mono-safe + ép stereo outputChannelCount:[2] + URL addModule có cache-busting ?v= nên luôn nạp bản đã sửa; fallback ScriptProcessor khi tạo worklet thất bại. Trước đây "mất tiếng khi transport" là do worklet cũ (pre-mono-fix) bị cache. (2) BiquadFilterNode: state is bad: applyMasteringSettings được gọi mỗi lần getAudioContext() (stopAll/play/VU…), liên tục setTargetAtTime vào EQ biquad = "fast parameter automation" → Chromium báo state bad dù giá trị hợp lệ. Thêm signature guard: chỉ áp dụng khi giá trị thực sự đổi → bỏ chùm automation lặp, giữ clamp chống NaN. toggleMasteringOnMaster vẫn idempotent.
  • Các file ảnh hưởng: app/static/js/services/soundfontPlayer.js, app/static/js/app.jsx, app/static/js/app.precompiled.js (build lại), app/templates/index.html
  • Ghi chú/Test (nếu có): npm run build OK. Harness test_render.js (worklet mặc định): workletCreated=1 scriptProcCreated=0, noteon=1, 26 PCM frames posted, peak 0.25 — ALL PASSED. test_sonicsf_fix.js cập nhật assertion worklet → PASS. 8 harness còn lại PASS. Hard refresh (Ctrl+F5) — lần đầu sẽ tải worklet mới. Nếu nghe crackle/mất tiếng khi phát, báo lại để quay lại ScriptProcessor.

[2026-08-03 13:40] Task: Fix Lỗi không có âm thanh - quay lại ScriptProcessor (quyết định cuối)

  • Tóm tắt thay đổi: Bật AudioWorklet lần 2 vẫn làm mất tiếng trên máy user. Nguyên nhân thực sự: worklet là push model (setInterval trên main thread) — khi main thread bận (load font/WASM decode/UI), vòng lặp nghẽn → worklet hết frame → câm. ScriptProcessor là pull-based (onaudioprocess do audio thread gọi) nên không bao giờ đói dữ liệu. Quyết định cuối: giữ ScriptProcessor làm renderer mặc định vĩnh viễn (_useScriptNode = true); warning deprecation chỉ cosmetic. Worklet (đã mono-safe + stereo-forced + cache-busting) giữ lại như opt-in để test sau.
  • Các file ảnh hưởng: app/static/js/services/soundfontPlayer.js, app/templates/index.html
  • Ghi chú/Test (nếu có): Harness test_render.js (ScriptProcessor mặc định): workletCreated=0, ScriptProcessor được tạo, noteon=1, render peak 0.25 — ALL PASSED. test_sonicsf_fix.js assertion ScriptProcessor → PASS. 8 harness còn lại PASS. Hard refresh (Ctrl+F5) — âm thanh hoạt động lại.

[2026-08-03 13:45] Task: Fix mất âm thanh khi nhấp đôi MIDI item vào Piano Roll

  • Tóm tắt thay đổi: handleEditMidiInTab (nhấp đôi MIDI item → mở PIANO ROLL tab) preload instrument bằng channel cũ trkIdx % 16, trong khi MỌI đường phát (schedulePianoRollMidi, transport, ARM) dùng dedicated channel (assignTrackMidiChannel). Channel lệch nhau → tab-open selectInstrument cấu hình nhầm channel (có thể đè instrument của track khác nếu trùng channel 0-15), khiến âm thanh trong/pianô sau khi mở tab không còn đúng. Sửa: dùng assignTrackMidiChannel(track, allTrks) cho preload khi mở tab → khớp đúng channel phát.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (build lại), app/templates/index.html
  • Ghi chú/Test (nếu có): npm run build OK, bundle syntax OK. Harness node /tmp/kilo/test_dbl.js mô phỏng toàn bộ flow double-click (init → tab-open selectInstrument → schedulePianoRollMidi playNote → ScriptProcessor render): noteon đúng channel, render peak 0.25 (không câm) — ALL PASSED. 9 harness còn lại PASS. Nếu vẫn mất âm thanh sau hard refresh, cần xác định rõ: câm toàn cục hay chỉ play trong piano roll, và có console error gì.

[2026-08-03 14:00] Task: Fix câm toàn cục sau nhấp đôi MIDI item + BiquadFilterNode state is bad (triệt để)

  • Tóm tắt thay đổi: User xác nhận: sau nhấp đôi MIDI item vào Piano Roll, câm TOÀN CỤC (main cũng hết tiếng) + lỗi BiquadFilterNode: state is bad vẫn còn. Nguyên nhân: master chain (EQ/imager biquad) bị toggleMasteringOnMaster + applyMasteringSettings gọi lại mỗi lần getAudioContext() (play, stopAll, double-click, VU...) — burst "fast parameter automation" vào biquad làm Chromium báo state bad, và nếu routing master bị ngắt giữa chừng → câm toàn cục. Fix triệt để: (1) gỡ mastering khỏi getAudioContext — master chain chỉ được áp dụng 1 lần khi initMasterBus tạo (nếu có currentMasteringSettings) + qua React effect khi settings đổi; (2) time constant 0.01 → 0.05 + cancelScheduledValues trước mỗi setTargetAtTime EQ — hết tích tụ automation event khi kéo slider; (3) toggleMasteringOnMaster bọc try/catch, luôn reconnect lại routing hợp lệ dù lỗi — không bao giờ để inputAnalyser bị ngắt không nối (gây câm toàn cục).
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (build lại), app/templates/index.html
  • Ghi chú/Test (nếu có): npm run build OK, bundle syntax OK, toggleMasteringOnMaster còn 3 chỗ (def + initMasterBus + effect). 9 harness (single/multi/dedup/reset/channels/render/pr_timing/retry/dbl) vẫn PASS. Hard refresh (Ctrl+F5). Nếu lỗi biquad vẫn hiện khi bật MasteringModal và kéo EQ, đó có thể là false-positive Chromium (parallel biquad) — báo tôi để tôi đổi cấu trúc imager/EQ.

[2026-08-03 14:15] Task: Fix câm toàn cục - IN peak có, OUT peak không (master chain chết)

  • Tóm tắt thay đổi: User chẩn đoán: mở Mastering modal, IN peak có tín hiệu nhưng OUT peak trống → tín hiệu chết TRONG master chain. Khắc phục triệt để 3 nguyên nhân có thể làm chain câm: (1) WaveShaper curve = null: một số engine xuất CÂM khi curve null (identity) — đổi luôn sang identity table Float32Array([-1,1]) (passthrough chủ động, không bao giờ null) ở cả init và khi maximizer tắt. (2) Tần số filter vượt Nyquist: eqHighFilter 10000Hz / imager crossover 6000Hz trên thiết bị sample rate thấp (8/11/16kHz) → hệ số biquad NaN → BiquadFilterNode: state is bad → chain câm. Thêm clampF(v) = min(v, sampleRate*0.45) cho mọi biquad. (3) Watchdog an toàn: trong Mastering modal, nếu masteringActive mà IN peak > 0.01 còn OUT peak < 0.001 (chain hỏng) → tự toggleMasteringOnMaster(false) về routing trực tiếp để âm thanh KHÔNG BAO GIỜ bị câm toàn cục.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (build lại), app/templates/index.html
  • Ghi chú/Test (nếu có): npm run build OK, bundle chứa maxFilterFreq, Chain broken, Float32Array([-1,1]). 9 harness vẫn PASS. Hard refresh (Ctrl+F5) → thử play (main + piano roll) + bật mastering. Nếu OUT peak vẫn trống, watchdog sẽ tự bypass và log [Mastering] Chain broken... — báo tôi message đó.

[2026-08-03 14:30] Task: Media Explorer - auto tempo theo MIDI file, gõ tempo tay, focus folder cha, điều hướng tree bằng phím mũi tên

  • Tóm tắt thay đổi: (1) Auto set tempo: click MIDI file → handleSelect set tempo từ metadata f.bpm; playMidiPreview sau khi parse set tempo theo midiResult[0].bpm (clamp 40-300) trước khi schedule → preview phát đúng tempo file. (2) Gõ tempo tay: input tempo dùng tempoText (string) cho phép gõ tự do (trước đây clamp 40-300 ngay khi gõ chặn việc nhập số < 40), commit khi hợp lệ hoặc blur/Enter; commitTempo còn re-schedule MIDI preview đang phát theo tempo mới. (3) Focus folder cha: click file → expand các node cha + centerTreeNodeInPane cuộn tree pane (ref treePaneRef) để folder cha hiện GIỮA ô tree. (4) Phím mũi tên: khi panel active (window.mediaExplorerActive) và ở computer mode, ArrowUp/Down di chuyển cursor qua node hiển thị (dùng computerPathRef/computerTreeRef/computerRootsRef để tránh stale closure trong keydown []), ArrowRight expand/load, ArrowLeft collapse hoặc về thư mục cha; browseComputerDirRef tránh stale browseComputerDir.
  • Các file ảnh hưởng: app/static/js/app.jsx, app/static/js/app.precompiled.js (build lại), app/templates/index.html
  • Ghi chú/Test (nếu có): npm run build OK, bundle chứa commitTempo/navigateTreeTo/centerTreeNodeInPane/getVisibleTreePaths/treePaneRef/tempoText. Harness node /tmp/kilo/test_tree.js mô phỏng flatten tree + Up/Down/Left logic — ALL PASSED. 9 harness còn lại PASS. Hard refresh.