FIX: khi click vào track number thì bị mất âm thanh, cho phép lưu màu của track

This commit is contained in:
2026-08-04 16:43:44 +07:00
parent c047934fc4
commit 0272912cff
6 changed files with 227 additions and 58 deletions
+1
View File
@@ -45,6 +45,7 @@
"id": { "type": "string" },
"name": { "type": "string" },
"type": { "type": "string", "enum": ["AUDIO", "MIDI", "SECTION"] },
"color": { "type": ["string", "null"], "default": null },
"volume_db": { "type": "number", "default": 0.0 },
"pan": { "type": "number", "minimum": -1.0, "maximum": 1.0, "default": 0.0 },
"mute": { "type": "boolean", "default": false },
+113 -39
View File
@@ -1777,7 +1777,7 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
};
// Track Strip Console Component (from md/48_TRACK_STRIP_COMPONENT.md)
const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs }) => {
const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs, style }) => {
var vol = track.volumeDb != null ? track.volumeDb : 0;
var trackColor = track.color || '#06b6d4';
var isMuted = track.muted;
@@ -1827,7 +1827,8 @@ const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs }) => {
};
return React.createElement("div", {
className: "flex flex-col items-stretch w-[145px] shrink-0 strip-bg-track rounded-md text-slate-300 select-none shadow-2xl border border-slate-900 overflow-hidden"
style: style || undefined,
className: "flex flex-col items-stretch w-[145px] shrink-0 strip-bg-track rounded-md text-slate-300 select-none shadow-2xl border border-slate-900 overflow-y-auto"
},
/* 1. Top Track Color Accent Bar */
React.createElement("div", {
@@ -1870,7 +1871,7 @@ const TrackStripConsole = ({ track, index, onUpdateTrack, trackVuRefs }) => {
),
/* 3. Center Area: Peak dB + Fader + VU + Button Stack */
React.createElement("div", { className: "flex-1 p-1 flex gap-1 justify-between items-stretch min-h-0" },
React.createElement("div", { className: "flex-1 p-1 flex gap-1 justify-between items-stretch min-h-[170px]" },
/* Left Fader & VU Column */
React.createElement("div", {
className: "flex-1 flex flex-col justify-between items-center bg-black/60 p-1 rounded border border-slate-800/80"
@@ -2363,9 +2364,9 @@ const WaveformLane = ({
const secWidth = sec.duration * zoom;
if (secStartLocal + secWidth < 0 || secStartLocal > drawWidth) return;
var isSecSelected = selectedItemIds && selectedItemIds.has(sec.id);
ctx.fillStyle = isSecSelected ? 'rgba(245, 158, 11, 0.35)' : (sec.color ? sec.color + '44' : 'rgba(6, 182, 212, 0.25)');
ctx.fillStyle = isSecSelected ? 'rgba(245, 158, 11, 0.35)' : ((sec.color || track.color || '#06b6d4') + '44');
ctx.fillRect(secStartLocal, 2, secWidth, height - 4);
ctx.strokeStyle = isSecSelected ? '#f59e0b' : (sec.color || '#06b6d4');
ctx.strokeStyle = isSecSelected ? '#f59e0b' : (sec.color || track.color || '#06b6d4');
ctx.lineWidth = isSecSelected ? 2.5 : 1;
ctx.setLineDash(isSecSelected ? [] : [4, 4]);
ctx.strokeRect(secStartLocal, 2, secWidth, height - 4);
@@ -2455,12 +2456,12 @@ const WaveformLane = ({
}
var isMidiSelected = selectedItemIds && selectedItemIds.has(midi.id);
ctx.fillStyle = isMidiSelected ? 'rgba(245, 158, 11, 0.35)' : (isRecordingItem ? 'rgba(239, 68, 68, 0.2)' : '#a78bfa33');
ctx.fillStyle = isMidiSelected ? 'rgba(245, 158, 11, 0.35)' : (isRecordingItem ? 'rgba(239, 68, 68, 0.2)' : (track.color || '#a78bfa') + '33');
ctx.fillRect(midiStartLocal, 2, midiWidth, height - 4);
ctx.strokeStyle = isMidiSelected ? '#f59e0b' : (isRecordingItem ? '#ef4444' : '#a78bfa');
ctx.strokeStyle = isMidiSelected ? '#f59e0b' : (isRecordingItem ? '#ef4444' : (track.color || '#a78bfa'));
ctx.lineWidth = isMidiSelected ? 2.5 : 1.5;
ctx.strokeRect(midiStartLocal, 2, midiWidth, height - 4);
ctx.fillStyle = isRecordingItem ? '#fca5a5' : '#c4b5fd';
ctx.fillStyle = isRecordingItem ? '#fca5a5' : (track.color || '#a78bfa');
ctx.font = 'bold 9px sans-serif';
ctx.fillText(isRecordingItem ? '[Ghi MIDI...]' : (midi.name || 'MIDI'), Math.max(midiStartLocal + 4, 4), 14);
@@ -2480,7 +2481,7 @@ const WaveformLane = ({
const ny = 18 + (1.0 - pitchFrac) * (height - 26);
const nh = Math.max(6, (height - 26) / (pitchMax - pitchMin) * 4);
ctx.fillStyle = isRecordingItem ? '#10b981' : '#a78bfa';
ctx.fillStyle = isRecordingItem ? '#10b981' : (track.color || '#a78bfa');
ctx.fillRect(Math.max(noteStartLocal, midiStartLocal + 2), ny, Math.max(2, nw), nh);
});
}
@@ -8733,6 +8734,7 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
id: t.id,
name: t.name,
type: trackType,
color: t.color || null,
volume_db: t.volumeDb || 0.0,
pan: t.pan || 0.0,
mute: t.muted || false,
@@ -14387,6 +14389,9 @@ const App = () => {
// Last-known audibility per track id (used to detect inaudibleaudible
// transitions that need a playback re-schedule).
const trackAudibleRef = useRef({});
// Audio-relevant signature (mute/solo/vol/bypass/items count) đi màu/
// rename KHÔNG trigger re-sync/re-route (tránh mt âm khi play).
const trackAudioSyncSigRef = useRef(null);
// Keep the mastering-bypass routing map + realtime mute/solo in sync with the
// tracks state (loads, undo, AI ops). Placed AFTER the tracks/subTabs/
@@ -14399,6 +14404,12 @@ const App = () => {
trackMidiBypassMap[t.id] = !!t.midiBypass;
});
const list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : all;
// Audio-relevant signature: đi MÀU/rename track (không liên quan audio)
// KHÔNG đưc trigger re-sync/re-route (tránh mt âm khi play sau đó
// updateSfRouting chy tha có th đt SF destination sai thi đim).
const audioSig = list.map(t => (t.muted ? '1' : '0') + (t.solo ? '1' : '0') + ':' + (t.volumeDb ?? 0) + ':' + (t.audioBypass ? '1' : '0') + (t.midiBypass ? '1' : '0') + ':' + (t.midiItems || []).length + ':' + (t.clips || []).length + ':' + (t.sections || []).length).join('|');
if (trackAudioSyncSigRef.current === audioSig) return;
trackAudioSyncSigRef.current = audioSig;
list.forEach(t => {
// state SF mastering route (sfRouteGain/sfDryGain), live on existing nodes
const sn = activeTrackNodesRef.current[t.id];
@@ -17576,6 +17587,10 @@ const App = () => {
const scheduledItemsSigRef = React.useRef('');
const playheadFrameCountRef = React.useRef(0);
const pendingRescheduleRef = React.useRef(null);
// Master-silence watchdog counter: đếm frame im lng liên tc khi đang play
const masterSilenceFramesRef = React.useRef(0);
// Cooldown rebuild (ms) phòng watchdog trigger lp trong đon im t nhiên
const lastMasterRebuildTimeRef = React.useRef(0);
const lastRescheduleTimeRef = React.useRef(0);
// Re-schedule NGAY (reset cooldown) sau khi TH chut check kế tiếp trong
// updatePlayhead (~100ms) re-schedule luôn.
@@ -17630,6 +17645,49 @@ const App = () => {
}
}
}
// Master-silence watchdog: đang play + có source ĐANG TRONG KHONG PHÁT
// (theo lch) nhưng master output im lng liên tc ~750ms graph b hng
// (BiquadFilter "state is bad" node cache dính) rebuild nodes.
// Đon im lng T NHIÊN (intro/rest mi source nm ngoài khong phát)
// KHÔNG trigger rebuild (cooldown 3s phòng trigger lp).
if (isPlaying && activeTabRef.current === 'main' && masterBus && masterBus.analyser && activeSourcesRef.current.length > 0) {
try {
const ctxNow = getAudioContext().currentTime;
const anyPlaying = activeSourcesRef.current.some(s => typeof s.startTime === 'number' && ctxNow >= s.startTime && ctxNow <= s.startTime + (s.buffer ? s.buffer.duration : 0) + 0.1);
if (anyPlaying) {
const d = new Uint8Array(128);
masterBus.analyser.getByteTimeDomainData(d);
let pk = 0;
for (let i = 0; i < d.length; i++) { const v = Math.abs(d[i] - 128) / 128; if (v > pk) pk = v; }
if (pk < 0.001) {
masterSilenceFramesRef.current++;
const sinceRebuild = performance.now() - (lastMasterRebuildTimeRef.current || 0);
if (masterSilenceFramesRef.current > 45 && sinceRebuild > 3000) {
masterSilenceFramesRef.current = 0;
lastMasterRebuildTimeRef.current = performance.now();
console.warn('[Recovery] Master silent while sources active — rebuilding audio graph');
try {
const pt = currentTimeRef.current;
stopAllPlayback();
Object.keys(activeTrackNodesRef.current).forEach(k => { const n = activeTrackNodesRef.current[k]; try { if (n && n.gainNode && n.gainNode.disconnect) n.gainNode.disconnect(); } catch (e) {} });
activeTrackNodesRef.current = {};
try { initMasterBus(); } catch (e) { console.warn('[Recovery] initMasterBus error:', e); }
setIsPlaying(true);
startTrackPlayback(pt);
} catch (e) { console.warn('[Recovery] rebuild error:', e); }
}
} else {
masterSilenceFramesRef.current = 0;
}
} else {
masterSilenceFramesRef.current = 0;
}
} catch (e) { masterSilenceFramesRef.current = 0; }
} else {
masterSilenceFramesRef.current = 0;
}
if (recordingStateRef.current === 'RECORDING') {
const audioCtx = getAudioContext();
const lookahead = 0.1; // 100ms
@@ -20526,6 +20584,14 @@ const App = () => {
...t,
color
} : t));
// Lưu màu NGAY (flush không ch debounce 2s tránh mt màu khi reload nhanh)
try {
if (window.SonicStorage) {
const nextTracks = (tracks || []).map(t => t.id === trackId ? { ...t, color } : t);
window.SonicStorage.scheduleTempAutoSave(() => serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, nextTracks, subTabs, sessionTabs, masteringSettings));
if (window.SonicStorage.flushTempAutoSave) window.SonicStorage.flushTempAutoSave();
}
} catch (e) {}
setUndoStack(prev => {
const next = [...prev, {
action_type: 'RECOLOR',
@@ -23743,6 +23809,22 @@ STRICT CONSTRAINTS:
const masterVUAnimRef = useRef(null);
useEffect(() => {
function tick() {
try {
// Khi STOP (không play, không recording): v VU RNG không đc analyser/
// activity hết animation dính + master không full sau khi dng play.
const isRecActive = activeAudioRecordersRef.current && Object.keys(activeAudioRecordersRef.current).length > 0;
if (!isPlaying && !isRecActive) {
setMasterVU(0);
setMasterMeterPeak(0);
Object.keys(trackVuRefs.current).forEach(k => {
const c = trackVuRefs.current[k];
if (!c) return;
if (k.endsWith('_mixer')) drawMixerVuMeter(c, 0);
else drawVuMeter(c, -60);
});
masterVUAnimRef.current = requestAnimationFrame(tick);
return;
}
// 1. Master VU Meter
if (masterBus && masterBus.analyser) {
const data = new Uint8Array(128);
@@ -23785,7 +23867,10 @@ STRICT CONSTRAINTS:
}
}
let midiPeak = isPlaying && isAudible ? (midiVuActivityRef.current[trackId] || 0) : 0;
// midiVuActivityRef đưc set bi triggerMidiVuActivity c khi play
// MIDI item LN khi ARM + nhn phím MIDI keyboard (preview) nên VU
// nhy trong c 2 trưng hp (không chn bi isPlaying).
let midiPeak = isAudible ? (midiVuActivityRef.current[trackId] || 0) : 0;
if (midiPeak > 0) {
midiVuActivityRef.current[trackId] = midiPeak * 0.90;
if (midiVuActivityRef.current[trackId] < 0.01) {
@@ -23811,6 +23896,9 @@ STRICT CONSTRAINTS:
}
});
} catch (e) {
console.warn('VU tick error:', e);
}
masterVUAnimRef.current = requestAnimationFrame(tick);
}
masterVUAnimRef.current = requestAnimationFrame(tick);
@@ -25461,14 +25549,9 @@ STRICT CONSTRAINTS:
},
className: `shrink-0 relative flex flex-col justify-between p-2.5 bg-[#1e1e1e] border-r border-zinc-900 cursor-pointer border-l-4 border-b border-[#141414] overflow-hidden ${isSelected ? 'border-cyan-500 bg-[#252525]' : 'border-transparent hover:bg-zinc-800/20'}`,
onClick: () => {
// Ch CHN track KHÔNG t dng main play + chuyn sang piano roll
// tab (trưc đây gây "mt âm thanh khi click track number").
setSelectedTrackId(track.id);
var prTab = subTabs.find(function(s) { return s.type === 'PIANO_ROLL' && s.trackId === track.id; });
if (prTab && prTab.notes && prTab.notes.length > 0) {
stopAllPlayback();
setSubTabs(function(prev) { return prev.map(function(s) { return s.id === prTab.id ? Object.assign({}, s, { isPlaying: true }) : s; }); });
schedulePianoRollMidi(prTab, 0);
startSubTabPlayback(prTab, 0);
}
}
}, /*#__PURE__*/React.createElement("div", {
className: "flex items-start justify-between"
@@ -25477,20 +25560,17 @@ STRICT CONSTRAINTS:
}, /*#__PURE__*/React.createElement("span", {
className: "text-xs font-bold text-zinc-500 font-mono"
}, (idx + 1).toString().padStart(2, '0')), /*#__PURE__*/React.createElement("label", {
onClick: e => {
e.stopPropagation();
const el = e.currentTarget.querySelector('input');
if (el) el.click();
},
className: "cursor-pointer"
className: "cursor-pointer relative block"
}, /*#__PURE__*/React.createElement("input", {
type: "color",
value: track.color || '#0f766e',
onClick: e => e.stopPropagation(),
onMouseDown: e => e.stopPropagation(),
onChange: e => {
e.stopPropagation();
updateTrackColor(track.id, e.target.value);
},
className: "w-0 h-0 opacity-0 absolute pointer-events-none"
className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer"
}), /*#__PURE__*/React.createElement("div", {
className: "w-3 h-3 rounded-full border border-zinc-600 hover:scale-110 transition-transform",
style: {
@@ -25678,21 +25758,18 @@ STRICT CONSTRAINTS:
}, "Any MIDI Keyboard"), midiDevices.map(d => /*#__PURE__*/React.createElement("option", {
key: d.id,
value: `MIDI_KEYBOARD:${d.id}`
}, d.name || `MIDI Input ${d.id.slice(0, 5)}`)))), track.isArmed && lastMidiNote && (lastMidiNote.length === 0 || Date.now() - lastMidiNote.time < 3000) && /*#__PURE__*/React.createElement("span", {
className: "text-[9px] font-mono text-emerald-400 ml-0.5 truncate max-w-[60px] shrink-0",
title: "MIDI Note:velocity:length"
}, `${midiPitchToName(lastMidiNote.pitch)}:${lastMidiNote.velocity}:${lastMidiNote.length > 0 ? lastMidiNote.length.toFixed(2) + 's' : '...'}`)), track.isArmed && /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1 mt-0.5"
}, d.name || `MIDI Input ${d.id.slice(0, 5)}`)))), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-0.5 w-16 shrink-0 ml-1"
}, /*#__PURE__*/React.createElement("span", {
className: "w-8 text-right text-zinc-500 text-[9px]"
}, "VU:"), /*#__PURE__*/React.createElement("canvas", {
className: "text-zinc-500 text-[9px]"
}, "VU"), /*#__PURE__*/React.createElement("canvas", {
ref: el => {
if (el) trackVuRefs.current[track.id] = el; else delete trackVuRefs.current[track.id];
},
width: 100,
height: 4,
className: "flex-1 bg-[#18181b] rounded h-1"
}))), /*#__PURE__*/React.createElement("div", {
})))), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1.5 mt-1",
onClick: e => e.stopPropagation()
}, /*#__PURE__*/React.createElement("input", {
@@ -26083,20 +26160,17 @@ STRICT CONSTRAINTS:
}, /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-2"
}, /*#__PURE__*/React.createElement("label", {
onClick: e => {
e.stopPropagation();
const el = e.currentTarget.querySelector('input');
if (el) el.click();
},
className: "cursor-pointer"
className: "cursor-pointer relative block"
}, /*#__PURE__*/React.createElement("input", {
type: "color",
value: vTrack.color || '#0f766e',
onClick: e => e.stopPropagation(),
onMouseDown: e => e.stopPropagation(),
onChange: e => {
e.stopPropagation();
updateTrackColor(vTrack.id, e.target.value);
},
className: "w-0 h-0 opacity-0 absolute pointer-events-none"
className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer"
}), /*#__PURE__*/React.createElement("div", {
className: "w-3 h-3 rounded-full border border-zinc-600 hover:scale-110 transition-transform",
style: {
File diff suppressed because one or more lines are too long
+20 -2
View File
@@ -60,11 +60,13 @@
}
let autoSaveTimer = null;
let lastGetProjectStateCallback = null;
function scheduleTempAutoSave(getProjectStateCallback) {
if (getProjectStateCallback) lastGetProjectStateCallback = getProjectStateCallback;
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(async () => {
try {
const state = getProjectStateCallback();
const state = lastGetProjectStateCallback ? lastGetProjectStateCallback() : null;
if (!state || (!state.tracks && !state.main_session)) return;
const dataJson = JSON.stringify(state);
localStorage.setItem('sonic_temp_project', dataJson);
@@ -76,10 +78,26 @@
}
}, 2000);
}
// Lưu NGAY (bỏ debounce 2s) — dùng cho thay đổi cần bền vững tức thì (đổi màu track)
async function flushTempAutoSave() {
if (autoSaveTimer) { clearTimeout(autoSaveTimer); autoSaveTimer = null; }
try {
const state = lastGetProjectStateCallback ? lastGetProjectStateCallback() : null;
if (!state || (!state.tracks && !state.main_session)) return;
const dataJson = JSON.stringify(state);
localStorage.setItem('sonic_temp_project', dataJson);
if (window.SonicAPI && localStorage.getItem('sonic_token')) {
await window.SonicAPI.saveTempProject(dataJson).catch(() => {});
}
} catch (e) {
console.warn("Flush temp project warning:", e);
}
}
window.SonicStorage = {
exportProjectToSFS,
importProjectFromSFSFile,
scheduleTempAutoSave
scheduleTempAutoSave,
flushTempAutoSave
};
})();
+2 -2
View File
@@ -14,7 +14,7 @@
<script src="/static/js/services/fluidsynthLoader.js?v=202607271245"></script>
<script src="/static/js/services/api.js?v=202607271016"></script>
<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/storage.js?v=202608038200"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608031400"></script>
<script src="/static/js/services/aiGateway.js?v=202608037200"></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=202608037600" defer></script>
<script src="/static/js/app.precompiled.js?v=202608038600" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+57 -1
View File
@@ -1,4 +1,60 @@
### [2026-08-03] Task: Fix CÂM TOÀN CỤC — EQ PRO setTargetAtTime 0.005 gây "BiquadFilterNode: state is bad"
### [2026-08-03] Task: Save project MẤT màu track — serializeTracksList thiếu color + schema
- **Tóm tắt thay đổi:** Save project (Cloud/.sfs) → reload → mất màu track. Nguyên nhân: **`serializeTracksList` (8733-8757) KHÔNG serialize `color`** (serializeSafe có color nhưng chỉ là helper temp autosave không dùng; serializeProjectToSchema dùng serializeTracksList) → data lưu server/.sfs không có màu → deserialize (8825 có `color: t.color`) nhận null → mất. Fix:
1. `serializeTracksList`: thêm **`color: t.color || null`** vào track-level fields.
2. `app/models/project_schema.json`: thêm **`color: { type: ["string","null"], default: null }`** vào Track properties (schema validate cho phép + khớp).
(deserialize đã có `color: t.color || default` ✓; validate_project_data không strip color ✓)
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/models/project_schema.json`, `app/templates/index.html` (bump v=202608038600)
- **Ghi chú/Test (nếu có):** BUILD OK 1006266 bytes, node --check OK, `pytest` 86 passed. serialize color ✓, schema color ✓.
---
- **Tóm tắt thay đổi:** Click TCP header (số track "01") → container onClick cũ: nếu track có PIANO ROLL tab với notes → `stopAllPlayback()` + `startSubTabPlayback`**DỪNG main play** (nghe như "mất âm") — đây cũng là cơ chế lỗi "đổi màu → mất âm" trước (click bubble tới container). Fix:
1. Container onClick **chỉ `setSelectedTrackId`** — bỏ toàn bộ auto-play piano roll tab (không phá main playback).
2. Color input thêm `onClick`/`onMouseDown` **stopPropagation** (click chấm màu không bubble → không trigger container select).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608038500)
- **Ghi chú/Test (nếu có):** BUILD OK 1006246 bytes, node --check OK, `pytest` 86 passed. Số track còn ✓, vTrack nguyên vẹn ✓, hết auto-play ✓.
---
- **Tóm tắt thay đổi:** Bản 38300 watchdog rebuild cứu câm — NHƯNG trigger SAI khi nhạc đang ở đoạn im lặng tự nhiên (intro/rest >750ms): rebuild → loop vô hạn → âm không qua main out + master VU đứng im (đúng triệu chứng user báo). Fix:
1. **`anyPlaying` guard**: chỉ đếm im lặng khi có **source ĐANG TRONG KHOẢNG PHÁT** (`ctxNow ∈ [source.startTime, startTime+duration]` — so với audioCtx.currentTime) — đoạn lặng tự nhiên (mọi source ngoài khoảng) → KHÔNG rebuild.
2. **Cooldown 3s** (`lastMasterRebuildTimeRef`) — sau rebuild, 3s mới được rebuild tiếp (phòng lặ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=202608038400)
- **Ghi chú/Test (nếu có):** BUILD OK 1006318 bytes, node --check OK, `pytest` 86 passed. anyPlaying ✓, cooldown 3s ✓.
---
- **Tóm tắt thay đổi:** User: nhấn TCP đổi màu → warning `BiquadFilterNode: state is bad` + CÂM ngay sau đó. Warning này từ fast automation (EQ PRO 0.005 — đã sửa setValueAtTime) nhưng **node bị cache dính** (activeTrackNodesRef) → graph hỏng vĩnh viễn → câm. Fix: **master-silence watchdog trong updatePlayhead**: khi đang play + có sources hoạt động nhưng masterBus.analyser im lặng liên tục **>45 frame (~750ms)** → **TỰ ĐỘNG rebuild**: disconnect + xóa toàn bộ track nodes → `initMasterBus()` (graph mới sạch) → `startTrackPlayback(playhead)` → hết câm tự phục hồi (log `[Recovery]`). Reset counter khi có tín hiệu/không play. (Lưu ý: bundle cũ vẫn gây warning — cần hard refresh để EQ PRO setValueAtTime có hiệu lự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=202608038300)
- **Ghi chú/Test (nếu có):** BUILD OK 1005657 bytes, node --check OK, `pytest` 86 passed. Watchdog ✓, pendingRescheduleRef nguyên vẹn ✓.
---
- **Tóm tắt thay đổi:** 3 fix (sau khi đổi track color):
1. **VU dính animation dù stop + Master VU full**: VU render loop (rAF) chạy mỗi frame bất kể play/stop + không try/catch (exception → loop chết → VU dính giá trị cuối, master full). Fix: **guard `!isPlaying && !recording` → vẽ VU RỖNG (master 0, track -60)** + bọc **try/catch** quanh tick (exception → log, loop vẫn sống).
2. **Màu track không lưu khi reload**: autosave temp debounce 2s — reload nhanh sau đổi màu → mất. Fix: storage.js thêm **`flushTempAutoSave()`** (bỏ debounce — lưu localStorage + saveTempProject NGAY) + `updateTrackColor` gọi schedule + flush với state tracks mới.
3. **Câm sau đổi màu** (chưa tái hiện được): guard audioSig (bản trước) + nếu còn — console `[Play]` logs + `VU tick error` sẽ lộ nguyên nhân.
- **Các file ảnh hưởng:** `app/static/js/services/storage.js` (flushTempAutoSave), `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608038200 cả storage.js)
- **Ghi chú/Test (nếu có):** BUILD OK 1004289 bytes, node --check OK, `pytest` 86 passed.
---
- **Tóm tắt thay đổi:** Đổi màu track → `setTracks` → effect sync (mute/solo/route) chạy → `updateSfRouting()` chạy thừa → SF destination có thể bị đặt sai thời điểm (node chưa tồn tại → setOutputDestination(null)) → play sau đó mất âm thanh. Fix: thêm **`trackAudioSyncSigRef`** — signature audio-relevant (muted/solo/volumeDb/audioBypass/midiBypass + số lượng midiItems/clips/sections) — đổi MÀU/rename (không liên quan audio) → signature giống → **skip toàn bộ re-sync/re-route**; mọi thay đổi audio thực sự (mute/solo/volume/bypass/items) vẫn sync bình thườ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=202608038100)
- **Ghi chú/Test (nếu có):** BUILD OK 1003238 bytes, node --check OK, `pytest` 86 passed.
---
- **Tóm tắt thay đổi:** TrackStripConsole trước set `height = track.height` ngay trong component — nhưng component CHỈ được dùng ở Mixer panel (F7) → mixer strip bị thu nhỏ bằng track.height (140) trong row cao hơn. Fix: bỏ height cố định — `style: style || undefined` → mixer strip **stretch full row** (items-stretch của container); TCP header panel trái không dùng component này (riêng, đã neo autoHeight).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608038000)
- **Ghi chú/Test (nếu có):** BUILD OK 1002528 bytes, node --check OK, `pytest` 86 passed.
---
- **Tóm tắt thay đổi:** 3 yêu cầu:
1. **Color picker "A user gesture is required"**: input type=color cũ có `pointer-events-none` + label onClick gọi `el.click()` (JS-click bị Chrome chặn). Fix: bỏ el.click — **input phủ label** (`absolute inset-0 w-full h-full opacity-0 cursor-pointer`) → click TRỰC TIẾP vào input (user gesture hợp lệ) — áp cả track header TCP + Sub-Tab editor (vTrack).
2. **Items đổi màu theo track.color**: MIDI items trước dùng màu tím cố định #a78bfa — giờ `(track.color || '#a78bfa')` cho fill/stroke/text/notes; sections fallback `sec.color || track.color` (sec.color riêng vẫn ưu tiên); clips đã theo track.color sẵn.
3. **Mixer strip chỉ 1/2 row** — do fix trước set height track.height TRONG TrackStripConsole (áp cả mixer). KHẮC PHỤC: (đã kiểm tra — height vẫn còn trong component — cần xem lại nếu user còn báo; bản này giữ nguyên vì TCP header dùng chung công thứ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=202608037900)
- **Ghi chú/Test (nếu có):** BUILD OK 1002593 bytes, node --check OK, `pytest` 86 passed. el.click() = 0 ✓, vTrack nguyên vẹn ✓, items/sections theo track.color ✓.
---
- **Tóm tắt thay đổi:** VU meter chỉ nhảy khi play MIDI item — không nhảy khi ARM + nhấn phím MIDI. Nguyên nhân: VU render loop (rAF) tính `midiPeak = isPlaying && isAudible ? midiVuActivityRef[...] : 0``triggerMidiVuActivity` ĐÃ được gọi khi phím MIDI (13658) nhưng bị guard `isPlaying` chặn. Fix: bỏ `isPlaying``midiPeak = isAudible ? midiVuActivityRef[...] : 0` (velocity đã normalize 0-1 trong triggerMidiVuActivity; decay 0.9/frame giữ nguyên) → VU nhảy khi ARM + phím MIDI lẫn khi play item.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037800)
- **Ghi chú/Test (nếu có):** BUILD OK 1002683 bytes, node --check OK, `pytest` 86 passed.
---
- **Tóm tắt thay đổi:** 3 yêu cầu TCP:
1. **Chiều cao TCP = chiều cao timeline row (neo)**: TrackStripConsole nhận `style` prop + tự set `height = track.height || (isArmed ? 164 : 140)` (CÙNG công thức timeline row) — resize track → TCP đổi theo, không lệch; bỏ `overflow-hidden``overflow-y-auto` + center `min-h-[170px]`**các nút (M/S/A/♪/FX/PWR...) không bị che** (đủ chỗ / cuộn được).
2. **Xóa label MIDI note** (khi ARM + nhấn phím MIDI hiện `pitch:velocity:length` sát ô input dropdown).
3. **VU meter lên bên phải, CÙNG HÀNG với input dropdown** (`In: [select] [VU ▓▓▓]` — hiện mọi lúc, không chỉ khi ARM).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037700)
- **Ghi chú/Test (nếu có):** BUILD OK 1002496 bytes, node --check OK, `pytest` 86 passed. Label note đã xóa ✓, VU cùng hàng ✓, TCP neo height ✓.
---
- **Tóm tắt thay đổi:** User báo play + ARM MIDI preview đều không có âm thanh; console: `BiquadFilterNode: state is bad, probably due to unstable filter caused by fast parameter automation` — đúng cảnh báo cũ trong code (fast automation → master routing broken → CÂM toàn cục). Thủ phạm: EQ PRO `setBand`/`setAmount` dùng `setTargetAtTime(..., now, 0.005)` — automation 5ms quá nhanh → Chromium đánh dấu filter unstable vĩnh viễn (node cache dính). **Fix: đổi toàn bộ sang `setValueAtTime(x, now)`** (tức thì, không automation ramp → không flag) — 4 chỗ (freq/Q/gain trong setBand + gain trong setAmount). Hard refresh → module EQ PRO mới (filter mới) → hết câm.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037600)
- **Ghi chú/Test (nếu có):** BUILD OK 1002801 bytes, node --check OK, `pytest` 86 passed. Không còn setTargetAtTime 0.005 (EQ PRO) — setValueAtTime ✓.