FIX: lỗi bypass của SECTION-TAB
This commit is contained in:
+126
-19
@@ -118,11 +118,11 @@ const masteringChainOn = () => !!(window.currentMasteringSettings && window.curr
|
||||
// (solo/preview/play) PHẢI đi qua mastering chain (user requirement: âm phải
|
||||
// qua chain để đủ lớn). 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 → MỌI track qua chain (♪ bị override — user requirement)
|
||||
if (masteringChainOn()) bypass = false;
|
||||
// ⚠️ GỠ 17:00 — override nuốt nút A/♪ (log: audioBypass=true nhưng
|
||||
// routeGain=1) — rules hiện tại: NÚT A/♪ quyết định bypass, không phụ thuộc
|
||||
// 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 (hoặc
|
||||
// chu trình A→B→A) → recursion vô hạn → RangeError: Maximum call stack size
|
||||
// exceeded khi restore project. Visited-set chặn cycle; depth chặn lồng 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 của 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 giữa 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 THẬT (trackAudioBypassMap/trackMidiBypassMap) — KHÔNG qua
|
||||
// effAudioBypass (override mastering-ON ép false → route luôn no-bypass
|
||||
// lúc tạo node → nút A phải bấm 2 lần; stop+play → node tái tạo → mất).
|
||||
// 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 mặc đị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) {
|
||||
|
||||
Reference in New Issue
Block a user