fix: maintain item duration in bars when BPM changes

- Add length_bars field to midiItems and sections at creation
- useEffect on bpm recalculates duration = length_bars * secondsPerBar
- Fix section creation: use secondsPerBar instead of hardcoded 4s
- Handle existing items without length_bars (no-op)
This commit is contained in:
2026-07-27 08:03:02 +07:00
parent f98ade5d58
commit d97f82e93f
2 changed files with 37 additions and 18 deletions
+30 -12
View File
@@ -6165,6 +6165,7 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
name: item.name,
startTime: item.start_bar * secondsPerBar,
duration: item.duration_bars * secondsPerBar,
length_bars: item.duration_bars || 4,
notes: (src.notes || []).map(n => ({
id: n.id,
pitch: n.pitch || 60,
@@ -6183,6 +6184,7 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
name: item.name,
start: item.start_bar * secondsPerBar,
duration: item.duration_bars * secondsPerBar,
length_bars: item.duration_bars || 4,
sectionId: secId,
tracks: secContainer ? deserializeTracksList(secContainer.tracks, secondsPerBar, sectionStore) : null
});
@@ -6414,6 +6416,16 @@ const App = () => {
const [bpm, setBpm] = useState(localStorage.getItem('studio_bpm') || '120');
const [draggedClip, setDraggedClip] = useState(null); // { trackId, clickOffset, buffer, name, volume, color }
const [hoveredTrackId, setHoveredTrackId] = useState(null);
// Recalculate item/section durations when BPM changes (maintain bar count)
useEffect(() => {
const bpmVal = parseFloat(bpm) || 120;
const secondsPerBar = (60.0 / bpmVal) * 4;
updateActiveTracks(prev => prev.map(t => ({
...t,
midiItems: (t.midiItems || []).map(m => m.length_bars ? { ...m, duration: m.length_bars * secondsPerBar } : m),
sections: (t.sections || []).map(s => s.length_bars ? { ...s, duration: s.length_bars * secondsPerBar } : s)
})));
}, [bpm]);
const openPanel = id => {
if (id === 'export') setShowExportPanel(true); else if (id === 'ai') setShowAIPanel(true); else if (id === 'python_tools') setShowPythonToolsPanel(true); else if (id === 'selection') setShowSelectionPanel(true);
};
@@ -10589,31 +10601,33 @@ const App = () => {
};
return { ...t, midiItems: updatedItems };
}
const newMidiItem = {
const newMidiItem = {
id: 'midi_rec_' + Date.now(),
name: 'Recorded MIDI',
startTime: recordingStartTimeRef.current,
duration: Math.ceil(totalDurationBeats / 4) * secondsPerBar,
length_bars: Math.ceil(totalDurationBeats / 4),
notes: recordedNotes
};
return {
};
return {
...t,
midiItems: [...(t.midiItems || []), newMidiItem]
};
};
}));
if (recordedNotes.length > 0) {
hasRecordedAnything = true;
hasRecordedAnything = true;
}
} else if (recordedNotes.length > 0) {
} else if (recordedNotes.length > 0) {
hasRecordedAnything = true;
const totalDurationBeats = Math.max(4.0, ...recordedNotes.map(n => n.start_beat + n.duration_beats));
const newMidiItem = {
id: 'midi_rec_' + Date.now(),
name: 'Recorded MIDI',
startTime: recordingStartTimeRef.current,
duration: Math.ceil(totalDurationBeats / 4) * secondsPerBar,
notes: recordedNotes
id: 'midi_rec_' + Date.now(),
name: 'Recorded MIDI',
startTime: recordingStartTimeRef.current,
duration: Math.ceil(totalDurationBeats / 4) * secondsPerBar,
length_bars: Math.ceil(totalDurationBeats / 4),
notes: recordedNotes
};
updateActiveTracks(prev => prev.map(t => {
if (t.id !== trackId) return t;
@@ -11860,11 +11874,13 @@ const App = () => {
const curTracks = activeTracks;
const track = curTracks.find(t => t.id === selectedTrackId);
if (!track) { showToast('Chọn track trước', 'warning'); return; }
const secondsPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
const section = {
id: `sec_${Date.now()}`,
name: 'Section',
start: currentTime,
duration: 4,
duration: 4 * secondsPerBar,
length_bars: 4,
color: track.color || '#06b6d4'
};
updateActiveTracks(prev => prev.map(t => t.id === selectedTrackId ? {
@@ -11885,6 +11901,7 @@ const App = () => {
name: 'MIDI Item',
startTime: currentTime,
duration: 4 * secondsPerBar,
length_bars: 4,
notes: [],
color: '#a78bfa'
};
@@ -13861,6 +13878,7 @@ const App = () => {
name: `${composition_title || 'AI Theme'} - ${aiTrack.track_name}`,
startTime: itemStartTimeSec,
duration: durationSec,
length_bars: total_bars,
notes: aiTrack.notes.map((note, index) => ({
id: `note_ai_${Date.now()}_${index}`,
pitch: note.pitch,
File diff suppressed because one or more lines are too long