FIX: lỗi bypass của SECTION-TAB

This commit is contained in:
2026-08-06 18:19:04 +07:00
parent f1e914fa62
commit 93dc13b17a
4 changed files with 231 additions and 34 deletions
+126 -19
View File
@@ -118,11 +118,11 @@ const masteringChainOn = () => !!(window.currentMasteringSettings && window.curr
// (solo/preview/play) PHI đi qua mastering chain (user requirement: âm phi
// qua chain đ đ ln). Chain OFF theo maps như cũ.
const effMidiBypass = (track) => {
if (masteringChainOn()) return false;
// G override mastering-ON (17:00) nút quyết đnh bypass (user rules)
return trackMidiBypassMap[track.id] !== undefined ? !!trackMidiBypassMap[track.id] : !!(track.midiBypass ?? track.masteringBypass);
};
const effAudioBypass = (track) => {
if (masteringChainOn()) return false;
// G override mastering-ON (17:00) nút A quyết đnh bypass (user rules)
return trackAudioBypassMap[track.id] !== undefined ? !!trackAudioBypassMap[track.id] : !!(track.audioBypass ?? track.masteringBypass);
};
@@ -139,7 +139,9 @@ function createMasteringRoute(ctx, track, bus) {
bypass = !!(track.audioBypass ?? track.masteringBypass);
}
// Mastering chain ON MI track qua chain ( b override user requirement)
if (masteringChainOn()) bypass = false;
// G 17:00 override nut nút A/ (log: audioBypass=true nhưng
// routeGain=1) rules hin ti: NÚT A/ quyết đnh bypass, không ph thuc
// mastering ON.
const routeGain = ctx.createGain();
const dryGain = ctx.createGain();
const masterDest = bus ? bus.input : ctx.destination;
@@ -6882,7 +6884,7 @@ const AIPresetModal = ({ isOpen, onClose }) => {
}))) : null);
};
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClose, onUpdateNotes, onSaveNotes, setSubTabs, onPlayPause, onStop, isPlaying, playPreviewNote, showToast, midiDevices, recordingState, recTempMidiNotes, onRecord, selectedMidiInputId, onMidiInputSelect, activeMidiPitches, onInstrumentSelect, onRescheduleMidi, onSeekPlayhead, snapValue, onSnapChange, onRealtimePlay }) => {
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClose, onUpdateNotes, onSaveNotes, setSubTabs, onPlayPause, onStop, isPlaying, playPreviewNote, showToast, midiDevices, recordingState, recTempMidiNotes, onRecord, selectedMidiInputId, onMidiInputSelect, activeMidiPitches, onInstrumentSelect, onRescheduleMidi, onSeekPlayhead, snapValue, onSnapChange, onRealtimePlay, onCopyNotes, clipboardNotes }) => {
const [activeRollTool, setActiveRollTool] = React.useState('select');
const [renderTick, setRenderTick] = React.useState(0);
const [ccMode, setCcMode] = React.useState('velocity');
@@ -8594,7 +8596,25 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
onChange: function(e) { setHumanizeStrength(parseFloat(e.target.value)); },
className: "px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer",
title: "Cường độ humanize"
}, React.createElement("option", { key: "l", value: 0.05 }, "Nh\u1EB9"), React.createElement("option", { key: "m", value: 0.10 }, "V\u1EEBa"), React.createElement("option", { key: "s", value: 0.18 }, "M\u1EA1nh")), React.createElement("div", {
}, React.createElement("option", { key: "l", value: 0.05 }, "Nh\u1EB9"), React.createElement("option", { key: "m", value: 0.10 }, "V\u1EEBa"), React.createElement("option", { key: "s", value: 0.18 }, "M\u1EA1nh")), React.createElement("button", {
onClick: function() {
if (notes && notes.length) {
if (onCopyNotes) onCopyNotes(notes);
showToast('Đã copy ' + notes.length + ' nốt vào clipboard — mở tab khác + Paste.', 'success');
} else { showToast('Không có nốt để copy.', 'warning'); }
},
className: "px-2 py-1 rounded text-xs bg-cyan-900/40 text-cyan-300 border border-cyan-700/60 hover:bg-cyan-800/50 transition",
title: "Copy toàn bộ notes vào clipboard (paste sang PIANO ROLL TAB khác)"
}, "\uD83D\uDCCB Copy"), React.createElement("button", {
onClick: function() {
if (!clipboardNotes || !clipboardNotes.length) { showToast('Clipboard trống — bấm Copy trước.', 'warning'); return; }
pushToUndo(notes);
setNotes(prev => [...(prev || []), ...clipboardNotes.map(function(n) { return { ...n, id: 'note_cp_' + Date.now() + '_' + Math.floor(Math.random() * 100000) }; })]);
showToast('Đã paste ' + clipboardNotes.length + ' nốt từ clipboard.', 'success');
},
className: "px-2 py-1 rounded text-xs bg-teal-900/40 text-teal-300 border border-teal-700/60 hover:bg-teal-800/50 transition",
title: "Paste notes từ clipboard vào tab này (append cuối)"
}, "\uD83D\uDCE5 Paste"), React.createElement("div", {
key: "transpose", className: "flex items-center gap-1"
}, React.createElement("input", {
key: "in", type: "number", step: 1, min: -24, max: 24, value: transposeSemis,
@@ -9020,7 +9040,13 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
});
};
const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore, visitedSections, depth) => {
// Guard CHU TRÌNH: SECTION_ITEM tr v section đang đưc deserialize (hoc
// chu trình ABA) recursion vô hn RangeError: Maximum call stack size
// exceeded khi restore project. Visited-set chn cycle; depth chn lng sâu.
const visited = visitedSections || new Set();
const curDepth = depth || 0;
if (curDepth > 12) return [];
return (schemaTracks || []).map(t => {
const clips = [];
const sections = [];
@@ -9060,6 +9086,7 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
const src = item.source_data || {};
const secId = src.referenced_section_id;
const secContainer = sectionStore ? sectionStore[secId] : null;
const isCycle = secContainer ? visited.has(secId) : false;
sections.push({
id: item.id,
name: item.name,
@@ -9067,7 +9094,7 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
duration: item.duration_bars * secondsPerBar,
length_bars: item.duration_bars || 4,
sectionId: secId,
tracks: secContainer ? deserializeTracksList(secContainer.tracks, secondsPerBar, sectionStore) : null
tracks: (secContainer && !isCycle) ? deserializeTracksList(secContainer.tracks, secondsPerBar, sectionStore, new Set(visited).add(secId), curDepth + 1) : null
});
}
});
@@ -14077,9 +14104,9 @@ const App = () => {
trackAudioBypassMap[trackId] = !!on;
trackMasteringBypassMap[trackId] = !!on;
// Live re-route the main track node AND every section sub-node of this
// track (keys "<trackId>_sub_<subId>") so section items bypass instantly.
// track (keys "<parent>_sub_<trackId>") so section items bypass instantly.
Object.keys(activeTrackNodesRef.current).forEach(function(k) {
if (k === trackId || k.indexOf(trackId + '_sub_') === 0) {
if (k === trackId || k.endsWith('_sub_' + trackId)) {
const n = activeTrackNodesRef.current[k];
if (n && n.route) setMasteringRoute(n.route, !!on);
}
@@ -14092,6 +14119,13 @@ const App = () => {
if (node && node.sfRouteGain && node.sfDryGain) {
setMasteringRoute({ routeGain: node.sfRouteGain, dryGain: node.sfDryGain, _trackId: trackId }, !!on);
}
// SECTION: nút cũng toggle route ca sub-node MIDI (key <parent>_sub_<trackId>)
Object.keys(activeTrackNodesRef.current).forEach(function(k) {
if (k.endsWith('_sub_' + trackId)) {
const sn = activeTrackNodesRef.current[k];
if (sn && sn.route) setMasteringRoute(sn.route, !!on);
}
});
try { if (updateSfRoutingRef.current) updateSfRoutingRef.current(); } catch (e) { }
}
};
@@ -14627,6 +14661,7 @@ const App = () => {
const [subTabGainVal, setSubTabGainVal] = useState(100);
const [subTabPitchVal, setSubTabPitchVal] = useState(0);
const [subTabs, setSubTabs] = useState([]); // [{id, label, trackId, startTime, endTime, buffer, effects, currentTime, selectionStart, selectionEnd, isPlaying}, ...]
const [pianoRollClipboard, setPianoRollClipboard] = useState(null); // notes copy gia các PIANO ROLL TAB
const [sessionTabs, setSessionTabs] = useState([]); // [{id, name, tracks}, ...]
const [tabContextMenu, setTabContextMenu] = useState(null); // { x, y, tabId, tabType }
const handleSetTabColor = (tabId, tabType, color) => {
@@ -15516,7 +15551,7 @@ const App = () => {
}
if (maxVal === 0) return st;
const scale = 1.0 / maxVal;
const clonedBuffer = ctx.createBuffer(1, data.length, st.buffer.sampleRate);
const clonedBuffer = ctx.createBuffer(1, Math.max(1, data.length), st.buffer.sampleRate);
const clonedData = clonedBuffer.getChannelData(0);
clonedData.set(data);
for (let i = startSample; i < endSample; i++) {
@@ -15539,7 +15574,7 @@ const App = () => {
const startSample = Math.floor(left * st.buffer.sampleRate);
const endSample = Math.floor(right * st.buffer.sampleRate);
const scale = Math.pow(10, gainDb / 20);
const clonedBuffer = ctx.createBuffer(1, data.length, st.buffer.sampleRate);
const clonedBuffer = ctx.createBuffer(1, Math.max(1, data.length), st.buffer.sampleRate);
const clonedData = clonedBuffer.getChannelData(0);
clonedData.set(data);
for (let i = startSample; i < endSample; i++) {
@@ -15563,7 +15598,7 @@ const App = () => {
const endSample = Math.floor(right * st.buffer.sampleRate);
const durationSamples = endSample - startSample;
if (durationSamples <= 0) return st;
const clonedBuffer = ctx.createBuffer(1, data.length, st.buffer.sampleRate);
const clonedBuffer = ctx.createBuffer(1, Math.max(1, data.length), st.buffer.sampleRate);
const clonedData = clonedBuffer.getChannelData(0);
clonedData.set(data);
if (type === 'in') {
@@ -15599,14 +15634,14 @@ const App = () => {
const data = st.buffer.getChannelData(0);
const startSample = Math.floor(left * sr);
const endSample = Math.floor(right * sr);
const len = endSample - startSample;
const len = Math.max(1, endSample - startSample); // guard 0 frames createBuffer crash
const cutBuffer = ctx.createBuffer(1, len, sr);
cutBuffer.copyToChannel(data.subarray(startSample, endSample), 0);
clipboardRef.current = {
buffer: cutBuffer,
name: 'Subtab Clip'
};
const newBuffer = ctx.createBuffer(1, data.length - len, sr);
const newBuffer = ctx.createBuffer(1, Math.max(1, data.length - len), sr);
const newData = newBuffer.getChannelData(0);
let idx = 0;
for (let i = 0; i < startSample; i++) newData[idx++] = data[i];
@@ -15634,7 +15669,7 @@ const App = () => {
const data = st.buffer.getChannelData(0);
const startSample = Math.floor(left * sr);
const endSample = Math.floor(right * sr);
const len = endSample - startSample;
const len = Math.max(1, endSample - startSample); // guard 0 frames createBuffer crash
const copyBuffer = ctx.createBuffer(1, len, sr);
copyBuffer.copyToChannel(data.subarray(startSample, endSample), 0);
clipboardRef.current = {
@@ -15689,7 +15724,7 @@ const App = () => {
const data = st.buffer.getChannelData(0);
const insertTime = st.currentTime || 0;
const insertSample = Math.floor(insertTime * sr);
const newBuffer = ctx.createBuffer(1, data.length + clipBuf.length, sr);
const newBuffer = ctx.createBuffer(1, Math.max(1, data.length + clipBuf.length), sr);
const newData = newBuffer.getChannelData(0);
let idx = 0;
for (let i = 0; i < insertSample; i++) newData[idx++] = data[i];
@@ -18730,8 +18765,29 @@ const App = () => {
const volLinear = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
gainNode.gain.setValueAtTime(volLinear, context.currentTime);
const parentNode = getOrCreateTrackNode(track, context);
gainNode.connect(parentNode);
node = { gainNode, pannerNode: null };
// Route RIÊNG cho sub-track (nút A/ trên section strip): mastering path
// parent; dry path masterBus.dryInput bypass áp đúng track section.
// Dùng map THT (trackAudioBypassMap/trackMidiBypassMap) KHÔNG qua
// effAudioBypass (override mastering-ON ép false route luôn no-bypass
// lúc to node nút A phi bm 2 ln; stop+play node tái to mt).
// RULES: sub-track có CLIPS nút A (audioBypass); có MIDI nút (midiBypass).
const subHasClips = (subTrack.clips && subTrack.clips.length > 0) || !!subTrack.buffer;
const subBypass = subHasClips
? (trackAudioBypassMap[subTrack.id] !== undefined ? !!trackAudioBypassMap[subTrack.id] : !!(subTrack.audioBypass ?? subTrack.masteringBypass))
: (trackMidiBypassMap[subTrack.id] !== undefined ? !!trackMidiBypassMap[subTrack.id] : !!(subTrack.midiBypass ?? subTrack.masteringBypass));
const routeGain = context.createGain();
const dryGain = context.createGain();
routeGain.gain.value = subBypass ? 0 : 1;
dryGain.gain.value = subBypass ? 1 : 0;
gainNode.connect(routeGain);
gainNode.connect(dryGain);
routeGain.connect(parentNode);
if (window.masterBus && window.masterBus.dryInput) {
dryGain.connect(window.masterBus.dryInput);
} else {
dryGain.connect(parentNode);
}
node = { gainNode, pannerNode: null, route: { routeGain: routeGain, dryGain: dryGain, _trackId: subTrack.id } };
activeTrackNodesRef.current[subKey] = node;
}
return node.gainNode;
@@ -21423,6 +21479,9 @@ const App = () => {
const rearrangeNewId = (curTracks.length + 1).toString();
const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
const selectColor = colors[curTracks.length % colors.length];
// SECTION-TAB: audioclip item mc đnh BYPASS mastering (raw) nút A xám;
// MAIN session: qua mastering (A sáng). User requirement 15:00.
const isSectionCtx = activeTabRef.current && activeTabRef.current.startsWith('session_');
updateActiveTracks(prev => [...prev, {
id: rearrangeNewId,
name: `Track ${rearrangeNewId}`,
@@ -21433,7 +21492,7 @@ const App = () => {
muted: false,
solo: false,
masteringBypass: false,
audioBypass: false,
audioBypass: isSectionCtx ? true : false,
midiBypass: false,
fxActive: true,
color: selectColor,
@@ -24464,6 +24523,52 @@ STRICT CONSTRAINTS:
return () => { if (masterVUAnimRef.current) cancelAnimationFrame(masterVUAnimRef.current); };
}, [isPlaying]);
const handleOpenProject = async (projectId, projectName) => {
setAppWarningModal({
title: "Mở dự án",
message: "Bạn có muốn mở dự án này? Các thay đổi chưa lưu trên workspace hiện tại sẽ bị mất.",
isAlert: false,
onConfirm: async () => {
try {
const proj = await window.SonicAPI.getCloudProject(projectId);
const parsed = JSON.parse(proj.data_json);
let restoredTracks = [];
let restoredBpm = bpm;
let restoredSessionTabs = [];
let restoredSubTabs = [];
if (parsed.main_session) {
const result = deserializeProjectFromSchema(parsed);
restoredTracks = result.tracks;
restoredBpm = result.bpm;
restoredSessionTabs = result.sessionTabs;
restoredSubTabs = result.subTabs;
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
} else {
restoredTracks = (parsed.tracks || []).map(function (t) {
const rest = Object.assign({}, t); delete rest.height;
return Object.assign({}, rest, { buffer: null, channelInfo: t.channelInfo || null, clips: t.clips || [], serverFileId: t.serverFileId || null });
});
}
setTracks(restoredTracks);
loadAudioBuffersForTracks(restoredTracks).catch(function (err) { console.warn('loadAudioBuffersForTracks error:', err); });
trackMidiChannelsRef.current = {};
preloadTrackInstruments(restoredTracks).catch(function (err) { console.warn('preloadTrackInstruments error:', err); });
setBpm(restoredBpm.toString());
setSelectedTrackId(restoredTracks[0]?.id || '1');
setProjectName(proj.name);
setCurrentProjectId(proj.id);
if (restoredSessionTabs.length > 0) setSessionTabs(restoredSessionTabs);
if (restoredSubTabs.length > 0) setSubTabs(restoredSubTabs);
localStorage.setItem('sonic_project_name', proj.name);
localStorage.setItem('sonic_project_id', proj.id);
showToast('Đã nạp dự án "' + proj.name + '" thành công!', "success");
} catch (e) {
showToast(e.message || "Lỗi khi nạp dự án", "error");
}
}
});
};
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
className: "h-full w-full flex flex-col bg-[#1e1e1e]"
}, /*#__PURE__*/React.createElement("header", {
@@ -26641,6 +26746,8 @@ STRICT CONSTRAINTS:
activeMidiPitches: activeMidiPitches,
onInstrumentSelect: (trackId) => { openInstrumentSelector(trackId); },
onRealtimePlay: handlePianoRollRealtimePlay,
onCopyNotes: (n) => setPianoRollClipboard(JSON.parse(JSON.stringify(n || []))),
clipboardNotes: pianoRollClipboard,
onRescheduleMidi: (updatedNotes) => {
const playingSub = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL' && s.isPlaying);
if (playingSub) {