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:
@@ -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
@@ -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 inaudible→audible
|
||||
// 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 mất â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 mất âm khi play sau đó —
|
||||
// updateSfRouting chạy thừa có thể đặt SF destination sai thời điểm).
|
||||
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 lặng liên tục khi đang play
|
||||
const masterSilenceFramesRef = React.useRef(0);
|
||||
// Cooldown rebuild (ms) — phòng watchdog trigger lặp trong đoạn im tự nhiên
|
||||
const lastMasterRebuildTimeRef = React.useRef(0);
|
||||
const lastRescheduleTimeRef = React.useRef(0);
|
||||
// Re-schedule NGAY (reset cooldown) sau khi THẢ chuột — 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 KHOẢNG PHÁT
|
||||
// (theo lịch) nhưng master output im lặng liên tục ~750ms → graph bị hỏng
|
||||
// (BiquadFilter "state is bad" — node cache dính) → rebuild nodes.
|
||||
// Đoạn im lặng TỰ NHIÊN (intro/rest — mọi source nằm ngoài khoảng phát)
|
||||
// KHÔNG trigger rebuild (cooldown 3s phòng trigger lặp).
|
||||
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 mất 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 RỖNG — không đọc analyser/
|
||||
// activity → hết animation dính + master không full sau khi dừng 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 bởi triggerMidiVuActivity — cả khi play
|
||||
// MIDI item LẪN khi ARM + nhấn phím MIDI keyboard (preview) — nên VU
|
||||
// nhảy trong cả 2 trường hợp (không chặn bởi 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ỉ CHỌN track — KHÔNG tự dừng main play + chuyển sang piano roll
|
||||
// tab (trước đây gây "mất â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
@@ -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
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ✓.
|
||||
|
||||
Reference in New Issue
Block a user