fix: chỉnh sửa section item dù được lưu lại vẫn là item
This commit is contained in:
+90
-39
@@ -4423,7 +4423,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
const KeybedPixelHeight = (128 - PITCH_START) * NoteHeight;
|
const KeybedPixelHeight = (128 - PITCH_START) * NoteHeight;
|
||||||
const pixelsPerBeat = rollZoom;
|
const pixelsPerBeat = rollZoom;
|
||||||
const timeSigNum = 4;
|
const timeSigNum = 4;
|
||||||
const totalBeats = Math.max((st.duration || 4) * timeSigNum, 64); // at least 64 beats (16 bars) for scrolling
|
const noteMaxBeat = (st.notes || []).reduce((max, n) => Math.max(max, (n.start_beat || 0) + (n.duration_beats || 1)), 0);
|
||||||
|
const totalBeats = Math.max(noteMaxBeat + 16, 64); // at least 64 beats (16 bars) for scrolling
|
||||||
const drawWidth = totalBeats * pixelsPerBeat;
|
const drawWidth = totalBeats * pixelsPerBeat;
|
||||||
|
|
||||||
const [notes, setNotes] = React.useState(st.notes || []);
|
const [notes, setNotes] = React.useState(st.notes || []);
|
||||||
@@ -4548,7 +4549,13 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
|
|
||||||
const delta = e.deltaY < 0 ? 0.05 : -0.05;
|
const delta = e.deltaY < 0 ? 0.05 : -0.05;
|
||||||
|
|
||||||
if (noteUnderCursor) {
|
if (selectedNoteIds.length > 0) {
|
||||||
|
setNotes(prev => prev.map(n => {
|
||||||
|
if (!selectedNoteIds.includes(n.id)) return n;
|
||||||
|
const newVel = Math.max(0.1, Math.min(1.0, (n.velocity ?? 0.8) + delta));
|
||||||
|
return { ...n, velocity: newVel };
|
||||||
|
}));
|
||||||
|
} else if (noteUnderCursor) {
|
||||||
setNotes(prev => prev.map(n => {
|
setNotes(prev => prev.map(n => {
|
||||||
if (n.id !== noteUnderCursor.id) return n;
|
if (n.id !== noteUnderCursor.id) return n;
|
||||||
const newVel = Math.max(0.1, Math.min(1.0, (n.velocity ?? 0.8) + delta));
|
const newVel = Math.max(0.1, Math.min(1.0, (n.velocity ?? 0.8) + delta));
|
||||||
@@ -4558,12 +4565,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
}
|
}
|
||||||
return { ...n, velocity: newVel };
|
return { ...n, velocity: newVel };
|
||||||
}));
|
}));
|
||||||
} else if (selectedNoteIds.length > 0) {
|
|
||||||
setNotes(prev => prev.map(n => {
|
|
||||||
if (!selectedNoteIds.includes(n.id)) return n;
|
|
||||||
const newVel = Math.max(0.1, Math.min(1.0, (n.velocity ?? 0.8) + delta));
|
|
||||||
return { ...n, velocity: newVel };
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -4758,8 +4759,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
setNotes(prev => prev.filter(n => n.id !== clickedNote.id));
|
setNotes(prev => prev.filter(n => n.id !== clickedNote.id));
|
||||||
setSelectedNoteIds(prev => prev.filter(id => id !== clickedNote.id));
|
setSelectedNoteIds(prev => prev.filter(id => id !== clickedNote.id));
|
||||||
showToast('Đã xóa nốt!', 'info');
|
showToast('Đã xóa nốt!', 'info');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// Start erase sweep
|
// Start erase sweep (right-click on empty space only)
|
||||||
notesBeforeDragRef.current = JSON.parse(JSON.stringify(notes));
|
notesBeforeDragRef.current = JSON.parse(JSON.stringify(notes));
|
||||||
setDraggedNote({ mode: 'erase_sweep', visitedPitches: [pitch] });
|
setDraggedNote({ mode: 'erase_sweep', visitedPitches: [pitch] });
|
||||||
return;
|
return;
|
||||||
@@ -4862,13 +4864,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
const clickedNote = notes[clickedNoteIdx];
|
const clickedNote = notes[clickedNoteIdx];
|
||||||
let nextSelectedIds;
|
let nextSelectedIds;
|
||||||
if (!selectedNoteIds.includes(clickedNote.id)) {
|
if (!selectedNoteIds.includes(clickedNote.id)) {
|
||||||
if (e.shiftKey) {
|
nextSelectedIds = [clickedNote.id];
|
||||||
nextSelectedIds = [...selectedNoteIds, clickedNote.id];
|
setSelectedNoteIds(nextSelectedIds);
|
||||||
setSelectedNoteIds(nextSelectedIds);
|
|
||||||
} else {
|
|
||||||
nextSelectedIds = [clickedNote.id];
|
|
||||||
setSelectedNoteIds(nextSelectedIds);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
nextSelectedIds = selectedNoteIds;
|
nextSelectedIds = selectedNoteIds;
|
||||||
}
|
}
|
||||||
@@ -4979,24 +4976,25 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
if (draggedNote.mode === 'draw') {
|
if (draggedNote.mode === 'draw') {
|
||||||
const rawDur = beat - draggedNote.startOffsetBeat;
|
const rawDur = beat - draggedNote.startOffsetBeat;
|
||||||
const newDur = getSnapBeat(Math.max(0.125, rawDur), snapVal);
|
const newDur = getSnapBeat(Math.max(0.125, rawDur), snapVal);
|
||||||
setNotes(prev => prev.map(n => {
|
|
||||||
if (n.id !== draggedNote.drawNoteId) return n;
|
|
||||||
return { ...n, duration_beats: newDur };
|
|
||||||
}));
|
|
||||||
// Brush: track visited pitches and create evenly-spaced notes
|
|
||||||
const visited = draggedNote.visitedPitches || [];
|
const visited = draggedNote.visitedPitches || [];
|
||||||
|
if (visited.length <= 1) {
|
||||||
|
setNotes(prev => prev.map(n => {
|
||||||
|
if (n.id !== draggedNote.drawNoteId) return n;
|
||||||
|
return { ...n, duration_beats: newDur };
|
||||||
|
}));
|
||||||
|
}
|
||||||
if (!visited.includes(pitch)) {
|
if (!visited.includes(pitch)) {
|
||||||
const newPitches = [...visited, pitch];
|
const newPitches = [...visited, pitch];
|
||||||
const totalSpan = Math.max(0.125, beat - draggedNote.initialBeat);
|
const totalSpan = Math.max(0.125, beat - draggedNote.initialBeat);
|
||||||
const perNoteDur = totalSpan / newPitches.length;
|
const perNoteDur = totalSpan / newPitches.length;
|
||||||
const brushIds = draggedNote.brushIds || [];
|
const brushIds = draggedNote.brushIds || [];
|
||||||
setNotes(prev => {
|
setNotes(prev => {
|
||||||
const cleaned = prev.filter(n => !brushIds.includes(n.id));
|
const cleaned = prev.filter(n => !brushIds.includes(n.id) && n.id !== draggedNote.drawNoteId);
|
||||||
const brushNotes = newPitches.map((p, i) => ({
|
const brushNotes = newPitches.map((p, i) => ({
|
||||||
id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8) + '_' + i,
|
id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8) + '_' + i,
|
||||||
pitch: p,
|
pitch: p,
|
||||||
start_beat: draggedNote.initialBeat + i * perNoteDur,
|
start_beat: draggedNote.initialBeat + i * perNoteDur,
|
||||||
duration_beats: perNoteDur * 0.9,
|
duration_beats: Math.max(0.125, perNoteDur * 0.9),
|
||||||
velocity: 0.8,
|
velocity: 0.8,
|
||||||
pan: 0.0
|
pan: 0.0
|
||||||
}));
|
}));
|
||||||
@@ -5010,10 +5008,14 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (draggedNote.mode === 'erase_sweep') {
|
if (draggedNote.mode === 'erase_sweep') {
|
||||||
const erasePitches = draggedNote.visitedPitches || [];
|
const erased = draggedNote.erasedIds || [];
|
||||||
if (!erasePitches.includes(pitch)) {
|
const target = notes.find(n =>
|
||||||
draggedNote.visitedPitches = [...erasePitches, pitch];
|
n.pitch === pitch && beat >= n.start_beat && beat < n.start_beat + n.duration_beats
|
||||||
setNotes(prev => prev.filter(n => !(n.pitch === pitch && beat >= n.start_beat && beat < n.start_beat + n.duration_beats)));
|
);
|
||||||
|
if (target && !erased.includes(target.id)) {
|
||||||
|
draggedNote.erasedIds = [...erased, target.id];
|
||||||
|
setNotes(prev => prev.filter(n => n.id !== target.id));
|
||||||
|
setSelectedNoteIds(prev => prev.filter(id => id !== target.id));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -5021,13 +5023,17 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
const newEnd = getSnapBeat(Math.max(draggedNote.firstStart + 0.125, beat), snapVal);
|
const newEnd = getSnapBeat(Math.max(draggedNote.firstStart + 0.125, beat), snapVal);
|
||||||
const range = draggedNote.originalEnd - draggedNote.firstStart;
|
const range = draggedNote.originalEnd - draggedNote.firstStart;
|
||||||
if (range <= 0) return;
|
if (range <= 0) return;
|
||||||
const scaleFactor = (newEnd - draggedNote.firstStart) / range;
|
const scaleFactor = Math.max(0.01, (newEnd - draggedNote.firstStart) / range);
|
||||||
const ids = draggedNote.selectedNoteIds || [];
|
const ids = draggedNote.selectedNoteIds || [];
|
||||||
const firstStart = draggedNote.firstStart;
|
const firstStart = draggedNote.firstStart;
|
||||||
|
const notesBefore = notesBeforeDragRef.current;
|
||||||
setNotes(prev => prev.map(n => {
|
setNotes(prev => prev.map(n => {
|
||||||
if (!ids.includes(n.id)) return n;
|
if (!ids.includes(n.id)) return n;
|
||||||
const relStart = n.start_beat - firstStart;
|
const orig = notesBefore ? notesBefore.find(o => o.id === n.id) : null;
|
||||||
const relEnd = relStart + n.duration_beats;
|
const origStart = orig ? orig.start_beat : n.start_beat;
|
||||||
|
const origDur = orig ? orig.duration_beats : n.duration_beats;
|
||||||
|
const relStart = origStart - firstStart;
|
||||||
|
const relEnd = relStart + origDur;
|
||||||
return {
|
return {
|
||||||
...n,
|
...n,
|
||||||
start_beat: firstStart + relStart * scaleFactor,
|
start_beat: firstStart + relStart * scaleFactor,
|
||||||
@@ -5449,15 +5455,32 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
|
|||||||
|
|
||||||
const sectionStore = {};
|
const sectionStore = {};
|
||||||
|
|
||||||
|
// Helper: compute length_bars from tracks content
|
||||||
|
const computeLengthBars = (tracksArr, spb) => {
|
||||||
|
let maxSec = 0;
|
||||||
|
(tracksArr || []).forEach(tr => {
|
||||||
|
(tr.clips || []).forEach(c => {
|
||||||
|
const end = (c.startTime || 0) + (c.buffer ? c.buffer.duration / (c.speed || 1.0) : 4);
|
||||||
|
if (end > maxSec) maxSec = end;
|
||||||
|
});
|
||||||
|
(tr.midiItems || []).forEach(m => {
|
||||||
|
const end = (m.startTime || 0) + (m.duration || 4);
|
||||||
|
if (end > maxSec) maxSec = end;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Math.ceil((maxSec || 4) / spb);
|
||||||
|
};
|
||||||
|
|
||||||
// 1. Populate from sessionTabsList (open tabs)
|
// 1. Populate from sessionTabsList (open tabs)
|
||||||
(sessionTabsList || []).forEach(st => {
|
(sessionTabsList || []).forEach(st => {
|
||||||
|
const serializedTracks = serializeTracksList(st.tracks, secondsPerBar);
|
||||||
sectionStore[st.sectionId] = {
|
sectionStore[st.sectionId] = {
|
||||||
id: st.sectionId,
|
id: st.sectionId,
|
||||||
name: st.name,
|
name: st.name,
|
||||||
is_root: false,
|
is_root: false,
|
||||||
length_bars: st.length_bars || 16.0,
|
length_bars: computeLengthBars(st.tracks, secondsPerBar),
|
||||||
auto_compute_length: st.auto_compute_length !== undefined ? st.auto_compute_length : true,
|
auto_compute_length: true,
|
||||||
tracks: serializeTracksList(st.tracks, secondsPerBar),
|
tracks: serializedTracks,
|
||||||
color: st.color || null
|
color: st.color || null
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -5473,7 +5496,7 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
|
|||||||
id: secId,
|
id: secId,
|
||||||
name: s.name,
|
name: s.name,
|
||||||
is_root: false,
|
is_root: false,
|
||||||
length_bars: s.tracks[0]?.length_bars || 16.0,
|
length_bars: computeLengthBars(s.tracks, secondsPerBar),
|
||||||
auto_compute_length: true,
|
auto_compute_length: true,
|
||||||
tracks: serializeTracksList(s.tracks, secondsPerBar),
|
tracks: serializeTracksList(s.tracks, secondsPerBar),
|
||||||
color: s.color || null
|
color: s.color || null
|
||||||
@@ -5518,7 +5541,16 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
|
|||||||
id: "main",
|
id: "main",
|
||||||
name: "MAIN SESSION",
|
name: "MAIN SESSION",
|
||||||
is_root: true,
|
is_root: true,
|
||||||
length_bars: 16.0,
|
length_bars: (() => {
|
||||||
|
let maxBar = 16.0;
|
||||||
|
(mainTracks || []).forEach(t => {
|
||||||
|
(t.items || []).forEach(item => {
|
||||||
|
const end = (item.start_bar || 0) + (item.duration_bars || 4);
|
||||||
|
if (end > maxBar) maxBar = end;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return maxBar;
|
||||||
|
})(),
|
||||||
auto_compute_length: true,
|
auto_compute_length: true,
|
||||||
tracks: mainTracks
|
tracks: mainTracks
|
||||||
},
|
},
|
||||||
@@ -7118,9 +7150,20 @@ const App = () => {
|
|||||||
const bpmVal = parseInt(bpm) || 120;
|
const bpmVal = parseInt(bpm) || 120;
|
||||||
const secondsPerBeat = 60.0 / bpmVal;
|
const secondsPerBeat = 60.0 / bpmVal;
|
||||||
const secondsPerBar = secondsPerBeat * 4;
|
const secondsPerBar = secondsPerBeat * 4;
|
||||||
const durationSec = (tab.length_bars || 16.0) * secondsPerBar;
|
|
||||||
|
|
||||||
const contentTracks = tab.tracks ? tab.tracks.filter(tr => tr.clips?.length > 0 || tr.midiItems?.length > 0) : [];
|
const contentTracks = tab.tracks ? tab.tracks.filter(tr => tr.clips?.length > 0 || tr.midiItems?.length > 0) : [];
|
||||||
|
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);
|
||||||
|
|
||||||
setTracks(prev => prev.map(t => {
|
setTracks(prev => prev.map(t => {
|
||||||
if (!t.sections || t.sections.length === 0) return t;
|
if (!t.sections || t.sections.length === 0) return t;
|
||||||
@@ -8277,7 +8320,10 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
(t.midiItems || []).forEach(m => {
|
(t.midiItems || []).forEach(m => {
|
||||||
max = Math.max(max, m.startTime + (m.duration || 4));
|
max = Math.max(max, (m.startTime || 0) + (m.duration || 4));
|
||||||
|
});
|
||||||
|
(t.sections || []).forEach(s => {
|
||||||
|
max = Math.max(max, (s.start || 0) + (s.duration || 4));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (recordingState === 'RECORDING' || recordingState === 'COUNT_IN') {
|
if (recordingState === 'RECORDING' || recordingState === 'COUNT_IN') {
|
||||||
@@ -12259,8 +12305,13 @@ const App = () => {
|
|||||||
const track = tid && currentTracks.find(t => t.id === String(tid) || t.id === 'track_' + tid);
|
const track = tid && currentTracks.find(t => t.id === String(tid) || t.id === 'track_' + tid);
|
||||||
if (!track) return { success: false, error: 'No track found' };
|
if (!track) return { success: false, error: 'No track found' };
|
||||||
const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{ id: 'default_' + track.id, buffer: track.buffer, startTime: track.startTime || 0, name: track.name }] : []);
|
const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{ id: 'default_' + track.id, buffer: track.buffer, startTime: track.startTime || 0, name: track.name }] : []);
|
||||||
if (clips.length === 0) return { success: false, error: 'Track has no audio clips' };
|
if (clips.length === 0 && (!track.midiItems || track.midiItems.length === 0)) return { success: false, error: 'Track has no audio clips' };
|
||||||
const totalDuration = Math.max(...clips.map(c => (c.startTime || 0) + (c.buffer ? c.buffer.duration / (c.speed || 1.0) : 0)));
|
const beatsPerSec = parseFloat(bpm || 120) / 60;
|
||||||
|
const totalDuration = Math.max(
|
||||||
|
clips.length > 0 ? Math.max(...clips.map(c => (c.startTime || 0) + (c.buffer ? c.buffer.duration / (c.speed || 1.0) : 0))) : 0,
|
||||||
|
...(track.midiItems || []).map(m => (m.startTime || 0) + (m.duration || 4)),
|
||||||
|
...(track.sections || []).map(s => (s.start || 0) + (s.duration || 4))
|
||||||
|
);
|
||||||
const barDur = 60 / parseInt(bpm || 120) * 4;
|
const barDur = 60 / parseInt(bpm || 120) * 4;
|
||||||
const sel = selectionRef.current;
|
const sel = selectionRef.current;
|
||||||
let rawStart, rawEnd;
|
let rawStart, rawEnd;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"original_name": "test.sf2", "uuid": "08692ebf-03e7-4c33-b876-36924024215e", "file": "08692ebf-03e7-4c33-b876-36924024215e.sf2"}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"original_name": "test.sf2", "uuid": "17b3aa85-855e-40fe-a7db-4117681c4497", "file": "17b3aa85-855e-40fe-a7db-4117681c4497.sf2"}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"original_name": "test.sf2", "uuid": "1955f5e5-9986-4b3b-8173-f313b38e4dd9", "file": "1955f5e5-9986-4b3b-8173-f313b38e4dd9.sf2"}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"original_name": "test.sf2", "uuid": "3d0a7b50-a640-4fdf-aa02-b1f034206e86", "file": "3d0a7b50-a640-4fdf-aa02-b1f034206e86.sf2"}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"original_name": "test.sf2", "uuid": "6d1e523e-b9cf-405f-92d0-1b62497554e4", "file": "6d1e523e-b9cf-405f-92d0-1b62497554e4.sf2"}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"original_name": "test.sf2", "uuid": "d790737f-40a2-481a-bc7e-5befaf4a8160", "file": "d790737f-40a2-481a-bc7e-5befaf4a8160.sf2"}
|
||||||
Binary file not shown.
Reference in New Issue
Block a user