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
+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
};
})();