FIX: sửa lỗi lưu thay đổi trong SECTION-TAB thì cũng tạo duration trên MAIN SESSION

This commit is contained in:
2026-08-07 11:08:39 +07:00
parent c1aa1eb4d4
commit 173a1f227b
4 changed files with 187 additions and 60 deletions
+82 -37
View File
@@ -6923,7 +6923,18 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
const [activeRollTool, setActiveRollTool] = React.useState('select');
const [renderTick, setRenderTick] = React.useState(0);
const [ccMode, setCcMode] = React.useState('velocity');
const [rollZoom, setRollZoom] = React.useState(60); // local horizontal zoom factor
const [rollZoom, setRollZoom] = React.useState(() => {
// Persist zoom piano roll qua phiên (localStorage) — user yêu cầu giữ kích
// thước zoom ở phiên làm việc trước.
try {
const v = parseFloat(localStorage.getItem('sf_pr_zoom'));
if (isFinite(v) && v >= 15 && v <= 250) return v;
} catch (e) { }
return 60;
}); // local horizontal zoom factor
React.useEffect(() => {
try { localStorage.setItem('sf_pr_zoom', String(rollZoom)); } catch (e) { }
}, [rollZoom]);
const [aiBarStart, setAiBarStart] = React.useState(0);
const [aiBarEnd, setAiBarEnd] = React.useState(4);
@@ -9264,7 +9275,7 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
time_signature_numerator: 4,
time_signature_denominator: 4,
sample_rate: 44100,
zoom: window.__currentZoom || 1.0
zoom: window.__sfTimelineZoom || window.__currentZoom || 1.0
},
main_session: {
id: "main",
@@ -13917,6 +13928,10 @@ const App = () => {
});
React.useEffect(() => {
try { localStorage.setItem('sf_zoom', String(zoom)); } catch (e) { }
// Zoom TIMELINE (App) — serialize project dùng cái này (window.__currentZoom
// là zoom của Media Explorer — lưu nhầm làm project load ra zoom 1.0 →
// timeline thu nhỏ ở góc — user phải zoom in mới về đúng).
window.__sfTimelineZoom = zoom;
}, [zoom]);
const [isLoopingSelection, setIsLoopingSelection] = useState(false);
const [beginBar, setBeginBar] = useState(0);
@@ -14856,6 +14871,10 @@ const App = () => {
const prevTab = prevActiveTabRef.current;
prevActiveTabRef.current = activeTab;
updateSfRouting();
// Log chẩn đoán zoom khi đổi tab (user: zoom reset khi chuyển tab)
if (prevTab !== activeTab) {
console.log('[Zoom] đổi tab', prevTab, '→', activeTab, '| zoom=', zoom, '| minZoom=', (minZoom || 0).toFixed(2));
}
// Tab DEACTIVE → stop âm của tab đó (user requirement): rời khỏi sub-tab
// (PIANO_ROLL/section/audio tab) đang play → stop playback của tab đó.
// Tab mới hoạt động bình thường; MAIN giữ hành vi cũ (mở piano-roll lúc
@@ -15204,7 +15223,9 @@ const App = () => {
restoredBpm = result.bpm;
restoredSessionTabs = result.sessionTabs;
restoredSubTabs = result.subTabs;
if (result.zoom && setZoom) setZoom(result.zoom);
// Bỏ qua zoom ≤ 1 (data cũ lưu 1.0 — lỗi __currentZoom của Media
// Explorer) — áp dụng mới giữ zoom hiện tại/localStorage — hết thu nhỏ.
if (result.zoom > 1 && setZoom) setZoom(result.zoom);
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
} else {
restoredTracks = (parsed.tracks || []).map(function(t) {
@@ -16644,9 +16665,6 @@ const App = () => {
const handleSaveSectionTab = async (tabId) => {
const tab = sessionTabs.find(s => s.id === tabId);
if (!tab) return;
const bpmVal = parseInt(bpm) || 120;
const secondsPerBeat = 60.0 / bpmVal;
const secondsPerBar = secondsPerBeat * 4;
// Upload buffer-only clips (chưa có serverFileId — upload sớm thất bại/
// race) TRƯỚC khi ghi vào section item → serialized AUDIO_ITEM có file →
@@ -16683,19 +16701,6 @@ const App = () => {
}
return nextTr;
}) : [];
let maxEndTime = 0;
(contentTracks || []).forEach(tr => {
(tr.clips || []).forEach(c => {
const end = (c.startTime || 0) + (c.buffer ? c.buffer.duration / (c.speed || 1.0) : 4);
if (end > maxEndTime) maxEndTime = end;
});
(tr.midiItems || []).forEach(m => {
const end = (m.startTime || 0) + (m.duration || 4);
if (end > maxEndTime) maxEndTime = end;
});
});
const durationSec = Math.max(maxEndTime, 4 * secondsPerBar);
// Đảm bảo mọi clip có serverFileId (upload buffer-only trước khi lưu)
await ensureClipsUploaded(contentTracks);
@@ -16708,7 +16713,11 @@ const App = () => {
return {
...s,
name: tab.name,
duration: durationSec,
// ⚠️ KHÔNG ghi đè duration: MAIN SESSION chỉ play ĐÚNG duration
// của section item (user đã resize thủ công) — mặc kệ SECTION-TAB
// dài bao nhiêu. Trước đây duration = durationSec (theo content
// tab) → item tự giãn/thu lại mỗi lần lưu → mất kích thước user
// set. Play MAIN đã clamp theo sec.duration (19154/19191).
tracks: contentTracks
};
})
@@ -17981,6 +17990,34 @@ const App = () => {
let max;
if (activeTab === 'main') {
max = computeMainSessionEndTime(activeTracks);
// Content-min CHỈ khi nút LOOP BẬT (user: projectEnd chỉ kích hoạt khi
// LOOP bật — play loop liên tục tại content end): content NGẮN hơn item
// → loop ngắn đúng content; content DÀI hơn item → loop tại item end.
// LOOP TẮT → play 1 lần tới END ITEMS (computeMainSessionEndTime —
// không cắt sớm theo content).
if (isLoopingSelection) {
(activeTracks || []).forEach(_t => (_t.sections || []).forEach(_s => {
const _tab = sessionTabs.find(st => st.sectionId === _s.sectionId || st.sectionId === _s.id);
let _contentEnd = 0;
if (_tab) {
(_tab.tracks || []).forEach(_tr => {
(_tr.clips || []).forEach(_c => {
const _e = (_c.startTime || 0) + (_c.buffer ? _c.buffer.duration / (_c.speed || 1.0) : 4);
if (_e > _contentEnd) _contentEnd = _e;
});
(_tr.midiItems || []).forEach(_m => {
const _e = (_m.startTime || 0) + (_m.duration || 4);
if (_e > _contentEnd) _contentEnd = _e;
});
});
}
if (_contentEnd > 0) {
const _itemEnd = (_s.start || 0) + (_s.duration || 0);
const _realEnd = (_s.start || 0) + Math.min(_s.duration || 0, _contentEnd);
if (_realEnd < _itemEnd && max <= _itemEnd + 0.001) max = _realEnd;
}
}));
}
} else {
const tab = sessionTabs.find(st => st.id === activeTab);
const tabMax = tab ? computeMainSessionEndTime(tab.tracks || []) : 0;
@@ -17997,7 +18034,7 @@ const App = () => {
max = Math.max(max, currentTime + 60);
}
return Math.max(1, max);
}, [activeTab, activeTracks, sessionTabs, recordingState, currentTime]);
}, [activeTab, activeTracks, sessionTabs, recordingState, currentTime, isLoopingSelection]);
const projectEndRef = useRef(projectEnd);
projectEndRef.current = projectEnd;
const minZoom = useMemo(() => {
@@ -18007,7 +18044,15 @@ const App = () => {
return Math.max(zoom * (maxDuration + leadInMargin), viewportWidth);
}, [zoom, maxDuration, viewportWidth, leadInMargin]);
useEffect(() => {
// ⚠️ KHÔNG ép minZoom khi activeTab là SUB-TAB (PIANO ROLL/audio):
// maxDuration tính theo activeTab=sub-tab → chỉ còn 12 bars buffer →
// minZoom LỚN → ép zoom lên (log: vào midi_ → ép 32.78→37.54) → quay lại
// MAIN/SECTION zoom giữ giá trị bị ép (KHÔNG khôi phục) = "zoom reset khi
// chuyển tab". Timeline không hiển thị ở sub-tab → ép vô nghĩa.
const _tabNow = activeTabRef.current;
if (_tabNow !== 'main' && !(_tabNow && _tabNow.startsWith('session_'))) return;
if (zoom < minZoom) {
console.log('[Zoom] ép lên minZoom=', minZoom.toFixed(2), '(zoom=', zoom.toFixed(2), ') tab=', activeTab, 'maxDuration=', maxDuration.toFixed(2));
setZoom(minZoom);
}
}, [minZoom]);
@@ -18680,6 +18725,7 @@ const App = () => {
// Hết bài: dừng/loop lại tại ENDTIME THẬT của session (projectEnd — không
// buffer 12 bars như maxDuration dùng cho scroll/zoom).
if (updatedTime >= projectEndRef.current) {
console.log('[Stop] projectEnd reached — updatedTime=', updatedTime.toFixed(2), 'projectEnd=', projectEndRef.current.toFixed(2), 'isLoopingSelection=', isLoopingSelection);
if (recordingStateRef.current === 'RECORDING') {
setCurrentTime(updatedTime);
animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
@@ -19197,7 +19243,7 @@ const App = () => {
// Capture items signature at schedule time — updatePlayhead so sánh với
// signature này để phát hiện items đổi vị trí giữa lúc play (re-schedule).
scheduledItemsSigRef.current = buildItemsSignature(allPlayTracks, sessionTabs);
console.log('[Play] offset=' + offsetTime + ' tracks=' + allPlayTracks.length + ' masterBus=' + !!masterBus + ' dest=' + (context.destination ? 'ok' : 'MISSING'));
console.log('[Play] offset=' + offsetTime + ' tracks=' + allPlayTracks.length + ' masterBus=' + !!masterBus + ' dest=' + (context.destination ? 'ok' : 'MISSING') + ' projectEnd=' + (projectEndRef.current || 0).toFixed(2));
const hasSolo = allPlayTracks.some(t => t.solo);
allPlayTracks.forEach(track => {
const isPlayable = hasSolo ? track.solo : !track.muted;
@@ -24980,7 +25026,9 @@ STRICT CONSTRAINTS:
restoredBpm = result.bpm;
restoredSessionTabs = result.sessionTabs;
restoredSubTabs = result.subTabs;
if (result.zoom && setZoom) setZoom(result.zoom);
// Bỏ qua zoom ≤ 1 (data cũ lưu 1.0 — lỗi __currentZoom của Media
// Explorer) — áp dụng mới giữ zoom hiện tại/localStorage.
if (result.zoom > 1 && setZoom) setZoom(result.zoom);
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
} else {
restoredTracks = (parsed.tracks || []).map(function (t) {
@@ -25784,22 +25832,19 @@ STRICT CONSTRAINTS:
setSubTabs(prev => prev.map(s => s.id === activeTab ? { ...s, selectionStart: 0, selectionEnd: loopEndTime } : s));
}
} else {
// Main timeline / section tab: if the user already selected a loop
// region, KEEP it — only auto-derive (0 → content end) when no
// selection exists yet. Previously this always overwrote the
// selection with 0 → (contentEnd + 2 bars).
// Main timeline / section tab: nếu user đã chọn vùng loop riêng →
// giữ nguyên (loop vùng chọn — nhánh updatePlayhead selLeft/
// selRight). KHÔNG auto-derive selection: LOOP bật → loop tại
// projectEnd (content end khi LOOP — 0515/0520) — nhánh
// updatePlayhead `updatedTime >= projectEndRef` loop lại từ 0 —
// tránh selection 0→end items ép loop toàn bài (sai ý "loop tại
// projectEnd").
const hasSel = (selectionMode === 'local' && selLeft !== null && selRight !== null && selRight > selLeft)
|| (selectionStart !== null && selectionEnd !== null);
if (!hasSel) {
// Auto-derive loop region = ĐÚNG endtime của session (items trên
// track main: clips theo buffer.duration/speed, midiItems, section
// bounds) — KHÔNG cộng thêm bars buffer (loop không được dài hơn
// duration hiện có).
const maxEnd = computeMainSessionEndTime(activeTracks);
if (maxEnd > 0) {
setSelectionStart(0);
setSelectionEnd(maxEnd);
}
if (hasSel) {
// giữ selection — loop vùng chọn
} else {
// không selection → loop tại projectEnd (không tạo selection)
}
}
},
File diff suppressed because one or more lines are too long