FIX: chỉnh sửa mute và unmute realtime, sửa lỗi save dự án và chèn chung các items vào một track

This commit is contained in:
2026-08-03 17:22:19 +07:00
parent a9da813cb1
commit 8fc1c2641b
6 changed files with 418 additions and 91 deletions
+42
View File
@@ -1,3 +1,45 @@
### [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)