4 Commits

5 changed files with 2214 additions and 253 deletions
+1806 -201
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+24 -4
View File
@@ -22,6 +22,7 @@
let _loadedFonts = {};
let _activeOscillators = {};
let _gainNode = null;
let _pendingOutputDestination = null;
let _scheduledNotes = [];
let _loadPromises = {};
let _sfloadSeq = 0;
@@ -31,7 +32,7 @@
if (!_gainNode) {
_gainNode = _audioCtx.createGain();
_gainNode.gain.value = 0.3;
_gainNode.connect(window.masterBus ? window.masterBus.input : _audioCtx.destination);
_gainNode.connect(_pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination));
}
return _audioCtx;
}
@@ -40,7 +41,7 @@
if (!_gainNode) {
_gainNode = ctx.createGain();
_gainNode.gain.value = 0.3;
_gainNode.connect(window.masterBus ? window.masterBus.input : ctx.destination);
_gainNode.connect(_pendingOutputDestination || (window.masterBus ? window.masterBus.input : ctx.destination));
}
return ctx;
}
@@ -53,7 +54,7 @@
if (!_gainNode) {
_gainNode = window.__sharedAudioCtx.createGain();
_gainNode.gain.value = 0.3;
_gainNode.connect(window.__sharedAudioCtx.destination);
_gainNode.connect(_pendingOutputDestination || window.__sharedAudioCtx.destination);
}
return window.__sharedAudioCtx;
};
@@ -61,6 +62,25 @@
const SonicSF = {
loadedFonts: _loadedFonts,
// Route the shared FluidSynth output through a per-track node (e.g. the
// track's gainNode) so the track's FX chain / fader / pan affect the
// soundfont instrument. Pass null to restore the default master-bus route.
setOutputDestination: function (node) {
try {
if (_gainNode) {
_gainNode.disconnect();
_gainNode.connect(node || (window.masterBus ? window.masterBus.input : ((_audioCtx || window.__sharedAudioCtx).destination)));
} else {
_pendingOutputDestination = node || null;
}
} catch (e) {
console.warn('[SonicSF] setOutputDestination error:', e);
}
},
getOutputNode: function () {
return _gainNode;
},
init: async function (audioContext) {
if (_initialized && _fluidModule) return;
if (_initPromise) return _initPromise;
@@ -81,7 +101,7 @@
if (!_gainNode) {
_gainNode = _audioCtx.createGain();
_gainNode.gain.value = 0.3;
_gainNode.connect(window.masterBus ? window.masterBus.input : _audioCtx.destination);
_gainNode.connect(_pendingOutputDestination || (window.masterBus ? window.masterBus.input : _audioCtx.destination));
}
console.log("[SonicSF] AudioCtx state:", _audioCtx.state, "sampleRate:", _audioCtx.sampleRate);
+2 -2
View File
@@ -16,7 +16,7 @@
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
<script src="/static/js/services/storage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608031340"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608031400"></script>
<script src="/static/js/services/aiGateway.js?v=202607271016"></script>
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
@@ -24,7 +24,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608033200" defer></script>
<script src="/static/js/app.precompiled.js?v=202608035800" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+171
View File
@@ -1,3 +1,174 @@
### [2026-08-03] Task: Remove FX Rack / MIDI Events / Selection panels khỏi bottom dock
- **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).
4. **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)