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) {
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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=202608061400" defer></script>
<script src="/static/js/app.precompiled.js?v=202608061830" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+70
View File
@@ -2104,3 +2104,73 @@
(3) (Đã có) AI Var clone `midiChannel: undefined` (23288) + `ensureTrackMidiChannel` tự cấp channel riêng khi trùng (30: `!inUse.has(track.midiChannel)`) — collision được xử lý khi assign.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061400), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: tạo AI Var mới → kéo volume từng track → âm đổi theo track; mute track trước → track sau vẫn đúng volume. Nếu track AI Var CŨ (tạo trước fix midiChannel) vẫn dùng chung channel → xóa + tạo lại.
### [2026-08-06 14:30] Task: Nút A (audio bypass) trên section strip không auto-áp — sub-node thiếu route
- **Báo cáo user:** nút A xám (bypass on) nhưng audio không bypass — phải bấm 2 lần mới có tác dụng.
- **Nguyên nhân:** sub-node section (key `<parent>_sub_<subId>`) chỉ có gainNode → parent — **KHÔNG có route (routeGain/dryGain)**`__setTrackBypass` scan node theo `indexOf(trackId + '_sub_')` (sai key — sub-node key bắt đầu bằng PARENT id) → nút A không áp được; node play section không có gì để toggle.
- **FIX (app.jsx):**
(1) `getOrCreateSubTrackNode`: thêm **route riêng** cho sub-track — `routeGain` (mastering path → parent) + `dryGain` (→ masterBus.dryInput) — gain khởi tạo theo `effAudioBypass(subTrack)` — nút A áp ngay từ lúc tạo node.
(2) `__setTrackBypass` (audio): scan `k === trackId || k.endsWith('_sub_' + trackId)` (đúng key sub-node của track đó) → `setMasteringRoute` realtime.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061430), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: nút A track audioclip → xám = bypass ngay (nghe hết mastering); sáng = qua mastering.
### [2026-08-06 15:00] Task: Nút A section — route tạo node dùng effAudioBypass bị override mastering-ON → bypass mất khi stop+play
- **Báo cáo user:** A mặc định tắt → play không bypass; bấm A ON rồi OFF mới bypass; stop + play lại → NOT BYPASS.
- **Nguyên nhân:** sub-node route tạo bằng `effAudioBypass(subTrack)` — hàm này override `false` khi mastering ON (yêu cầu v03:00) → route luôn no-bypass lúc TẠO node → phải bấm A 2 lần (__setTrackBypass set route trực tiếp); stop → node bị xóa/tái tạo → route lại ép false → mất bypass.
- **FIX (app.jsx getOrCreateSubTrackNode):** dùng **map THẬT** `trackAudioBypassMap[subTrack.id]` (trạng thái nút A) — không qua effAudioBypass → route tạo theo nút A; stop+play → node tái tạo → route đúng trạng thái nút A.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061500), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: bật A (xám) → play → bypass ngay; stop + play lại → VẪN bypass; tắt A (sáng) → qua mastering.
### [2026-08-06 15:30] Task: SECTION-TAB audioclip PHẢI bypass mặc định — addNewTrack context-aware (A xám)
- **Yêu cầu user:** nút A mặc định tắt nhưng audioclip item PHẢI bypass khi play; stop + play lại không được mất bypass.
- **FIX (app.jsx addNewTrack 21440):** `audioBypass: isSectionCtx ? true : false` — activeTab `session_` → track mới trong SECTION mặc định **BYPASS** (nút A xám, audio raw); MAIN giữ false (qua mastering).
- **Kết hợp vòng trước:** sub-node route dùng map THẬT (không override mastering-ON) → route đúng nút A; stop+play → node tái tạo → route đúng trạng thái.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061530), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: tạo track mới + drop audioclip → play → BYPASS ngay (A xám); stop + play lại → VẪN bypass. Track CŨ (tạo trước fix, audioBypass false) → bấm A 1 lần (xám) → từ đó bypass đúng.
### [2026-08-06 16:00] Task: SECTION-TAB bypass rules — nút A (audio) + nút ♪ (midi) áp đúng cho sub-node
- **Yêu cầu user:** nút A tắt → bypass Audioclip; A bật → không bypass; nút ♪ tắt → midi bypass; ♪ bật → midi không bypass.
- **FIX (app.jsx):**
(1) `getOrCreateSubTrackNode`: subBypass theo LOẠI item của sub-track — có CLIPS → `trackAudioBypassMap` (nút A); có MIDI → `trackMidiBypassMap` (nút ♪) — route tạo đúng trạng thái nút.
(2) `__setTrackBypass` nhánh 'midi': **scan sub-node** (`endsWith('_sub_'+trackId)`) → `setMasteringRoute` — nút ♪ trên section strip toggle route sub-node MIDI ngay.
- **Kết hợp:** sub-node route (routeGain→parent / dryGain→masterBus.dryInput) — bypass → dry (raw); không bypass → qua mastering.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061600), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → section: track audioclip — A tắt → bypass, A bật → qua mastering; track MIDI — ♪ tắt → bypass, ♪ bật → qua mastering. Stop+play lại → trạng thái giữ nguyên.
### [2026-08-06 16:30] Task: ReferenceError handleOpenProject — hàm chỉ tồn tại trong ProfileModal, App render reference sai scope
- **Báo cáo user:** bundle 061600 không mở/reload lại dự án — console: `Uncaught ReferenceError: handleOpenProject is not defined`.
- **Nguyên nhân:** `handleOpenProject` (5624) nằm BÊN TRONG component ProfileModal — nhưng OpenProjectModal (File → Open Project) nhận `onOpenCloud` closure ở **App render** (27727: `handleOpenProject(projId, projName)`) — reference ngoài scope → ReferenceError khi bấm Mở.
- **FIX (app.jsx):** thêm **App-level `handleOpenProject`** (trước render) — logic: getCloudProject → deserializeProjectFromSchema → setTracks/mastering/sessionTabs/subTabs + localStorage — giống ProfileModal. ProfileModal giữ bản riêng (tự scope).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061630), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → File → Open Project → bấm MỞ → dự án nạp + reload lại đúng.
### [2026-08-06 17:00] Task: Restore project stack overflow — guard cycle trong deserializeTracksList (SECTION_ITEM tự trỏ)
- **Báo cáo user:** reload page → `restoreLastSessionProject failed: RangeError: Maximum call stack size exceeded`.
- **Nguyên nhân:** `deserializeTracksList` (9023) → SECTION_ITEM (9059) → gọi lại `deserializeTracksList(secContainer.tracks,...)` — section item trỏ về section ĐANG deserialize (hoặc chu trình A→B→A) → recursion VÔ HẠN → stack overflow.
- **FIX (app.jsx deserializeTracksList):**
(1) **Visited-set**: truyền `visitedSections` xuống; SECTION_ITEM trỏ về section có trong set (cycle) → `tracks: null` (không đệ quy tiếp).
(2) **Depth guard**: `curDepth > 12 → return []` — chặn lồng sâu bất thường.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061700), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → reload page → dự án restore bình thường (section tự trỏ → deserialize an toàn, tracks null thay vì crash).
### [2026-08-06 17:30] Task: GỠ override mastering-ON — nút A/♪ quyết định bypass (rules SECTION-TAB)
- **Báo cáo user:** rules bypass vẫn chưa áp được — log: `[Bypass] node created track 1 initial audioBypass= true routeGain= 1 dryGain= 0` — bypass TRUE nhưng routeGain=1 (INVERTED).
- **Nguyên nhân:** `createMasteringRoute` dòng 142 `if (masteringChainOn()) bypass = false;` + `effAudioBypass/effMidiBypass` override mastering-ON (yêu cầu v03:00) → khi mastering ON, route LUÔN ép qua chain → nút A/♪ bị nuốt.
- **FIX (app.jsx):** GỠ override ở 3 chỗ: `createMasteringRoute` + `effMidiBypass` + `effAudioBypass`**NÚT A/♪ quyết định bypass, không phụ thuộc mastering ON** (rules hiện tại của user thắng yêu cầu cũ v03:00).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061730), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → SECTION-TAB: A tắt (xám) → routeGain=0/dryGain=1 (bypass) ngay lần play đầu; A bật → qua mastering. Log `[Bypass] node created ... audioBypass= true routeGain= 0 dryGain= 1` ✓.
### [2026-08-06 18:00] Task: Copy/Paste notes giữa các PIANO ROLL TAB (clipboard)
- **Yêu cầu user:** copy midi note từ PIANO ROLL TAB này sang tab khác.
- **FIX (app.jsx):**
(1) App: state `pianoRollClipboard` + prop `onCopyNotes`/`clipboardNotes` cho PianoRollTabEditor (deep-copy khi copy).
(2) Editor toolbar (hàng 2): nút **📋 Copy** (toàn bộ notes → clipboard + toast) + **📥 Paste** (notes clipboard → append cuối tab hiện tại, sinh id MỚI `note_cp_*`, `pushToUndo` → Ctrl+Z hoàn tác).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061800), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → tab A: Copy → mở tab B → Paste → notes xuất hiện; Ctrl+Z hoàn tác. Verify: 0 double-escape emoji.
### [2026-08-06 18:30] Task: NotSupportedError createBuffer 0 frames — guard Math.max(1) cho mọi createBuffer
- **Báo cáo user:** `Uncaught NotSupportedError: Failed to execute 'createBuffer' ... frames (0) less than or equal to minimum bound (0)`.
- **Nguyên nhân:** các op audio (sub-tab cut/copy/paste, normalize/fade/gain) tính `len`/`data.length` = 0 (vùng chọn rỗng hoặc buffer 0 frame) → `createBuffer(..., 0, ...)` → throw.
- **FIX (app.jsx — guard `Math.max(1, ...)`):** sub-tab Cut (15637/15644), Copy (15672), Paste (15727), clonedBuffer ×3 (15554/15577/15601). Chỗ 11802 đã guard sẵn.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061830), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** `npm run build` → hard refresh → thao tác cut/copy audio vùng chọn rỗng → hết crash.