feat: sync loop button with sub-tab + auto-derive loop boundaries

- Transport Loop button now syncs isLooping with active sub-tab
- Auto-computes loopEnd from midi notes, section clips, or audio duration
- Sub-tab selectionStart=0, selectionEnd=computed boundary on loop enable
- Main timeline also auto-derives loop end from track items/clips
- All three tab types: MAIN SESSION, SECTION-TAB, PIANO ROLL
This commit is contained in:
2026-07-26 21:03:17 +07:00
parent 5ab93953e7
commit bdaa5594bd
2 changed files with 53 additions and 2 deletions
+50 -1
View File
@@ -14709,7 +14709,56 @@ const App = () => {
}))), /*#__PURE__*/React.createElement("div", {
className: "w-[1px] h-5 bg-zinc-800 mx-0.5"
}), /*#__PURE__*/React.createElement("button", {
onClick: () => setIsLoopingSelection(prev => !prev),
onClick: () => {
setIsLoopingSelection(prev => !prev);
// Sync loop state with active sub-tab (piano roll / audio editor)
const activeSub = activeTab && subTabs.find(s => s.id === activeTab && ['PIANO_ROLL', 'AUDIO_CLIP_EDITOR', 'SECTION_EDITOR'].includes(s.type));
if (activeSub) {
const newLoop = !activeSub.isLooping;
setSubTabs(prev => prev.map(s => s.id === activeTab ? { ...s, isLooping: newLoop } : s));
if (newLoop) {
const bpmVal = parseInt(bpm) || 120;
const beatSec = 60.0 / bpmVal;
let maxEnd = 0;
if (activeSub.type === 'PIANO_ROLL') {
(activeSub.notes || []).forEach(n => {
const end = (n.start_beat || 0) + (n.duration_beats || 1);
if (end > maxEnd) maxEnd = end;
});
} else if (activeSub.type === 'SECTION_EDITOR') {
(activeSub.clips || []).forEach(c => {
const end = (c.startTime || 0) + (c.duration || 0);
if (end > maxEnd) maxEnd = end;
});
} else if (activeSub.type === 'AUDIO_CLIP_EDITOR') {
const dur = activeSub.buffer?.duration || 0;
if (dur > maxEnd) maxEnd = dur;
}
const loopEndTime = activeSub.type === 'PIANO_ROLL' ? Math.max(maxEnd, 16) * beatSec + 1.0 : Math.max(maxEnd, 1);
setSubTabs(prev => prev.map(s => s.id === activeTab ? { ...s, selectionStart: 0, selectionEnd: loopEndTime } : s));
}
} else {
// Main timeline: auto-derive loop end from tracks
const bpmVal = parseInt(bpm) || 120;
const secPerBar = (60.0 / bpmVal) * 4;
let maxEnd = 0;
activeTracks.forEach(t => {
(t.clips || []).forEach(c => {
const end = (c.startTime || 0) + (c.duration || 0);
if (end > maxEnd) maxEnd = end;
});
(t.items || []).forEach(it => {
const end = (it.start || 0) + (it.duration || 4);
if (end > maxEnd) maxEnd = end;
});
});
if (maxEnd > 0) {
const loopEnd = maxEnd + secPerBar * 2;
setSelectionStart(0);
setSelectionEnd(loopEnd);
}
}
},
className: `w-7 h-7 flex items-center justify-center rounded border transition ${isLoopingSelection ? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500' : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'}`,
title: isLoopingSelection ? (selLeft !== null && selRight !== null ? "Loop vùng chọn" : "Loop timeline") : "Bật loop"
}, /*#__PURE__*/React.createElement("span", {