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, name: item.name,
startTime: item.start_bar * secondsPerBar, startTime: item.start_bar * secondsPerBar,
duration: item.duration_bars * secondsPerBar, duration: item.duration_bars * secondsPerBar,
length_bars: item.duration_bars || 4,
notes: (src.notes || []).map(n => ({ notes: (src.notes || []).map(n => ({
id: n.id, id: n.id,
pitch: n.pitch || 60, pitch: n.pitch || 60,
@@ -6183,6 +6184,7 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
name: item.name, name: item.name,
start: item.start_bar * secondsPerBar, start: item.start_bar * secondsPerBar,
duration: item.duration_bars * secondsPerBar, duration: item.duration_bars * secondsPerBar,
length_bars: item.duration_bars || 4,
sectionId: secId, sectionId: secId,
tracks: secContainer ? deserializeTracksList(secContainer.tracks, secondsPerBar, sectionStore) : null tracks: secContainer ? deserializeTracksList(secContainer.tracks, secondsPerBar, sectionStore) : null
}); });
@@ -6414,6 +6416,16 @@ const App = () => {
const [bpm, setBpm] = useState(localStorage.getItem('studio_bpm') || '120'); const [bpm, setBpm] = useState(localStorage.getItem('studio_bpm') || '120');
const [draggedClip, setDraggedClip] = useState(null); // { trackId, clickOffset, buffer, name, volume, color } const [draggedClip, setDraggedClip] = useState(null); // { trackId, clickOffset, buffer, name, volume, color }
const [hoveredTrackId, setHoveredTrackId] = useState(null); 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 => { 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); 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 }; return { ...t, midiItems: updatedItems };
} }
const newMidiItem = { const newMidiItem = {
id: 'midi_rec_' + Date.now(), id: 'midi_rec_' + Date.now(),
name: 'Recorded MIDI', name: 'Recorded MIDI',
startTime: recordingStartTimeRef.current, startTime: recordingStartTimeRef.current,
duration: Math.ceil(totalDurationBeats / 4) * secondsPerBar, duration: Math.ceil(totalDurationBeats / 4) * secondsPerBar,
length_bars: Math.ceil(totalDurationBeats / 4),
notes: recordedNotes notes: recordedNotes
}; };
return { return {
...t, ...t,
midiItems: [...(t.midiItems || []), newMidiItem] midiItems: [...(t.midiItems || []), newMidiItem]
}; };
})); }));
if (recordedNotes.length > 0) { if (recordedNotes.length > 0) {
hasRecordedAnything = true; hasRecordedAnything = true;
} }
} else if (recordedNotes.length > 0) { } else if (recordedNotes.length > 0) {
hasRecordedAnything = true; hasRecordedAnything = true;
const totalDurationBeats = Math.max(4.0, ...recordedNotes.map(n => n.start_beat + n.duration_beats)); const totalDurationBeats = Math.max(4.0, ...recordedNotes.map(n => n.start_beat + n.duration_beats));
const newMidiItem = { const newMidiItem = {
id: 'midi_rec_' + Date.now(), id: 'midi_rec_' + Date.now(),
name: 'Recorded MIDI', name: 'Recorded MIDI',
startTime: recordingStartTimeRef.current, startTime: recordingStartTimeRef.current,
duration: Math.ceil(totalDurationBeats / 4) * secondsPerBar, duration: Math.ceil(totalDurationBeats / 4) * secondsPerBar,
notes: recordedNotes length_bars: Math.ceil(totalDurationBeats / 4),
notes: recordedNotes
}; };
updateActiveTracks(prev => prev.map(t => { updateActiveTracks(prev => prev.map(t => {
if (t.id !== trackId) return t; if (t.id !== trackId) return t;
@@ -11860,11 +11874,13 @@ const App = () => {
const curTracks = activeTracks; const curTracks = activeTracks;
const track = curTracks.find(t => t.id === selectedTrackId); const track = curTracks.find(t => t.id === selectedTrackId);
if (!track) { showToast('Chọn track trước', 'warning'); return; } if (!track) { showToast('Chọn track trước', 'warning'); return; }
const secondsPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
const section = { const section = {
id: `sec_${Date.now()}`, id: `sec_${Date.now()}`,
name: 'Section', name: 'Section',
start: currentTime, start: currentTime,
duration: 4, duration: 4 * secondsPerBar,
length_bars: 4,
color: track.color || '#06b6d4' color: track.color || '#06b6d4'
}; };
updateActiveTracks(prev => prev.map(t => t.id === selectedTrackId ? { updateActiveTracks(prev => prev.map(t => t.id === selectedTrackId ? {
@@ -11885,6 +11901,7 @@ const App = () => {
name: 'MIDI Item', name: 'MIDI Item',
startTime: currentTime, startTime: currentTime,
duration: 4 * secondsPerBar, duration: 4 * secondsPerBar,
length_bars: 4,
notes: [], notes: [],
color: '#a78bfa' color: '#a78bfa'
}; };
@@ -13861,6 +13878,7 @@ const App = () => {
name: `${composition_title || 'AI Theme'} - ${aiTrack.track_name}`, name: `${composition_title || 'AI Theme'} - ${aiTrack.track_name}`,
startTime: itemStartTimeSec, startTime: itemStartTimeSec,
duration: durationSec, duration: durationSec,
length_bars: total_bars,
notes: aiTrack.notes.map((note, index) => ({ notes: aiTrack.notes.map((note, index) => ({
id: `note_ai_${Date.now()}_${index}`, id: `note_ai_${Date.now()}_${index}`,
pitch: note.pitch, pitch: note.pitch,
File diff suppressed because one or more lines are too long