fix: sửa lỗi audio clip dán từ clipboard không đúng duration

This commit is contained in:
2026-07-18 20:02:21 +07:00
parent e0742d013c
commit 2dbd5828ae
3 changed files with 515 additions and 67 deletions
+258 -67
View File
@@ -174,6 +174,7 @@
localSelRight,
onTrackLaneMouseDown,
onContextMenu,
onClipDragStart,
}) => {
const canvasRef = useRef(null);
@@ -212,12 +213,10 @@
const sampleRate = track.buffer.sampleRate;
const totalSamples = data.length;
const duration = totalSamples / sampleRate;
const visibleStart = 0;
const visibleEnd = Math.max(1, width / zoom);
const startSample = Math.floor(visibleStart * sampleRate);
const endSample = Math.min(totalSamples, Math.ceil(visibleEnd * sampleRate));
const samplesToDraw = endSample - startSample;
const pixelsPerSample = samplesToDraw > 0 ? width / samplesToDraw : 1;
const xStart = (track.startTime || 0) * zoom;
const wClip = duration * zoom;
const xEnd = xStart + wClip;
// Draw markers
if (markers && markers.length > 0) {
@@ -230,13 +229,18 @@
});
}
// Peak waveform drawing
// Peak waveform drawing only within clip bounds
ctx.strokeStyle = isSelected ? '#06b6d4' : '#6ee7b7';
ctx.lineWidth = 1;
for (let px = 0; px < width; px++) {
const sampleIdx = startSample + Math.floor(px / pixelsPerSample);
const chunkSize = Math.max(1, Math.floor(1 / pixelsPerSample));
const drawXStart = Math.max(0, Math.floor(xStart));
const drawXEnd = Math.min(width, Math.ceil(xEnd));
const samplesPerPixel = sampleRate / zoom;
for (let px = drawXStart; px < drawXEnd; px++) {
const timeInClip = (px - xStart) / zoom;
const sampleIdx = Math.floor(timeInClip * sampleRate);
const chunkSize = Math.max(1, Math.floor(samplesPerPixel));
const chunkStart = Math.max(0, sampleIdx - Math.floor(chunkSize / 2));
const chunkEnd = Math.min(totalSamples, chunkStart + chunkSize);
@@ -288,6 +292,18 @@
const x = e.clientX - rect.left + scrollLeft;
const time = Math.max(0, x / zoom);
onSelectTrack(track.id);
// Check for Alt+Click drag clip
const isOverClip = track.buffer && time >= (track.startTime || 0) && time < (track.startTime || 0) + track.buffer.duration;
if (isOverClip && e.altKey) {
e.preventDefault();
e.stopPropagation();
if (onClipDragStart) {
onClipDragStart(track.id, time - (track.startTime || 0));
}
return;
}
onPlayheadSet(time);
if (onTrackLaneMouseDown) {
onTrackLaneMouseDown(track.id, time, e);
@@ -429,9 +445,11 @@
const App = () => {
// ── State Definitions ──
const [tracks, setTracks] = useState([
{ id: '1', name: 'Track 01', buffer: null, volume: 0.8, muted: false, solo: false, color: '#0f766e', markers: [], serverFileId: null },
{ id: '2', name: 'Track 02', buffer: null, volume: 0.8, muted: false, solo: false, color: '#1d4ed8', markers: [], serverFileId: null },
{ id: '1', name: 'Track 01', buffer: null, startTime: 0, volume: 0.8, muted: false, solo: false, color: '#0f766e', markers: [], serverFileId: null },
{ id: '2', name: 'Track 02', buffer: null, startTime: 0, volume: 0.8, muted: false, solo: false, color: '#1d4ed8', markers: [], serverFileId: null },
]);
const [draggedClip, setDraggedClip] = useState(null); // { trackId, clickOffset, buffer, name, volume, color }
const [hoveredTrackId, setHoveredTrackId] = useState(null);
// BMP for Tempo Track - LOOP_EDITOR_2.md §6
const [bpm, setBpm] = useState(localStorage.getItem('studio_bpm') || '120');
const [selectedTrackId, setSelectedTrackId] = useState('1');
@@ -527,6 +545,7 @@
markers: JSON.parse(JSON.stringify(track.markers || [])),
// buffer is captured via reference copy for undo; we store a clone for redo
buffer: track.buffer,
startTime: track.startTime || 0,
};
};
@@ -576,7 +595,7 @@
if (ctrl && e.key === 'z' && !e.shiftKey) { e.preventDefault(); handleUndoRef.current(); return; }
if (ctrl && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) { e.preventDefault(); handleRedoRef.current(); return; }
if (ctrl && !alt && e.key === 'o') { e.preventDefault(); showToast('Open Project dialog','info'); return; }
if (ctrl && !alt && e.key === 'n') { e.preventDefault(); setTracks([{ id:'1', name:'Track 01', buffer:null, volume:0.8, muted:false, solo:false, color:'#0f766e', markers:[], serverFileId:null }, { id:'2', name:'Track 02', buffer:null, volume:0.8, muted:false, solo:false, color:'#1d4ed8', markers:[], serverFileId:null }]); setSelectedTrackId('1'); showToast('New project created','info'); return; }
if (ctrl && !alt && e.key === 'n') { e.preventDefault(); setTracks([{ id:'1', name:'Track 01', buffer:null, startTime:0, volume:0.8, muted:false, solo:false, color:'#0f766e', markers:[], serverFileId:null }, { id:'2', name:'Track 02', buffer:null, startTime:0, volume:0.8, muted:false, solo:false, color:'#1d4ed8', markers:[], serverFileId:null }]); setSelectedTrackId('1'); showToast('New project created','info'); return; }
if (ctrl && !alt && e.key === 's') { e.preventDefault(); showToast('Project saved','success'); return; }
if ((ctrl && alt && e.key === 's') || (ctrl && e.shiftKey && e.key === 's')) { e.preventDefault(); showToast('Save As dialog','info'); return; }
if (ctrl && !alt && e.key === 'i') { e.preventDefault(); addNewTrack(); return; }
@@ -648,8 +667,11 @@
return;
}
const sr = t.buffer.sampleRate;
const startSample = Math.max(0, Math.floor(selLeft * sr));
const endSample = Math.min(t.buffer.length, Math.floor(selRight * sr));
const trackStart = t.startTime || 0;
const relSelLeft = Math.max(0, selLeft - trackStart);
const relSelRight = Math.max(0, selRight - trackStart);
const startSample = Math.max(0, Math.floor(relSelLeft * sr));
const endSample = Math.min(t.buffer.length, Math.floor(relSelRight * sr));
const len = endSample - startSample;
if (len < 100) {
showToast('Khoảng chọn quá ngắn.', 'warning');
@@ -723,8 +745,9 @@
// Crossfade merge into original track (§2.2)
const sr = track.buffer.sampleRate;
const origData = track.buffer.getChannelData(0);
const startSample = Math.floor(subTab.startTime * sr);
const endSample = Math.floor(subTab.endTime * sr);
const trackStart = track.startTime || 0;
const startSample = Math.floor((subTab.startTime - trackStart) * sr);
const endSample = Math.floor((subTab.endTime - trackStart) * sr);
const crossfadeLen = Math.min(100, Math.floor(0.01 * sr)); // 10ms
const mergedBuffer = ctx.createBuffer(1, track.buffer.length, sr);
@@ -831,8 +854,11 @@
const sr = t.buffer.sampleRate;
const data = t.buffer.getChannelData(0);
if (selLeft !== null && selRight !== null && selRight > selLeft) {
const startSample = Math.floor(selLeft * sr);
const endSample = Math.min(data.length, Math.floor(selRight * sr));
const trackStart = t.startTime || 0;
const relSelLeft = Math.max(0, selLeft - trackStart);
const relSelRight = Math.max(0, selRight - trackStart);
const startSample = Math.floor(relSelLeft * sr);
const endSample = Math.min(data.length, Math.floor(relSelRight * sr));
const len = endSample - startSample;
if (len > 0) {
const ctx = getAudioContext();
@@ -857,7 +883,7 @@
contextMenuDelete();
};
const doPaste = (targetTrackId, pasteTime) => {
const doPaste = (targetTrackId, pasteTime) => {
if (!clipboardRef.current || !clipboardRef.current.buffer) {
showToast('Clipboard trống.', 'warning');
return null;
@@ -894,15 +920,32 @@
pasteData = out;
}
const insertSample = Math.floor(pasteTime * targetSr);
const targetStart = targetTrack.startTime || 0;
const relativePasteTime = pasteTime - targetStart;
let newStartTime = targetStart;
let combined;
const origLen = targetTrack.buffer.length;
const newLen = Math.max(origLen, insertSample + pasteSamples);
const combined = ctx.createBuffer(1, newLen, targetSr);
const combinedData = combined.getChannelData(0);
const origData = targetTrack.buffer.getChannelData(0);
for (let i = 0; i < origLen; i++) combinedData[i] = origData[i];
for (let i = 0; i < pasteSamples; i++) combinedData[insertSample + i] = pasteData[i];
setTracks(p => p.map(t => t.id === targetTrackId ? { ...t, buffer: combined } : t));
if (relativePasteTime >= 0) {
const insertSample = Math.floor(relativePasteTime * targetSr);
const newLen = Math.max(origLen, insertSample + pasteSamples);
combined = ctx.createBuffer(1, newLen, targetSr);
const combinedData = combined.getChannelData(0);
for (let i = 0; i < origLen; i++) combinedData[i] = origData[i];
for (let i = 0; i < pasteSamples; i++) combinedData[insertSample + i] = pasteData[i];
} else {
newStartTime = pasteTime;
const prependSamples = Math.floor((targetStart - pasteTime) * targetSr);
const newLen = Math.max(prependSamples + origLen, pasteSamples);
combined = ctx.createBuffer(1, newLen, targetSr);
const combinedData = combined.getChannelData(0);
for (let i = 0; i < origLen; i++) combinedData[prependSamples + i] = origData[i];
for (let i = 0; i < pasteSamples; i++) combinedData[i] = pasteData[i];
}
setTracks(p => p.map(t => t.id === targetTrackId ? { ...t, buffer: combined, startTime: newStartTime } : t));
setCurrentTime(pasteTime);
showToast('Đã dán vào track.', 'success');
return targetTrackId;
@@ -911,8 +954,8 @@
// Track exists but no buffer — use clipboard sample rate
const newClipBuffer = ctx.createBuffer(1, clipBuffer.length, clipBuffer.sampleRate);
newClipBuffer.copyToChannel(clipBuffer.getChannelData(0), 0);
setTracks(p => p.map(t => t.id === targetTrackId ? { ...t, buffer: newClipBuffer, name: `Pasted_${name || t.name}`, volume: volume || t.volume, color: color || t.color } : t));
setCurrentTime(0);
setTracks(p => p.map(t => t.id === targetTrackId ? { ...t, buffer: newClipBuffer, startTime: pasteTime, name: `Pasted_${name || t.name}`, volume: volume || t.volume, color: color || t.color } : t));
setCurrentTime(pasteTime);
showToast('Đã dán vào track.', 'success');
return targetTrackId;
}
@@ -923,12 +966,13 @@
newClipBuffer.copyToChannel(clipBuffer.getChannelData(0), 0);
setTracks(prev => [...prev, {
id: newId, name: `Pasted_${name || 'track'}`, buffer: newClipBuffer,
startTime: pasteTime,
volume: volume || 0.8, muted: false, solo: false,
color: color || colors[prev.length % colors.length],
markers: [], serverFileId: null,
}]);
setSelectedTrackId(newId);
setCurrentTime(0);
setCurrentTime(pasteTime);
showToast('Đã dán track mới từ clipboard.', 'success');
return newId;
};
@@ -948,14 +992,17 @@
return;
}
const ctx = getAudioContext();
const maxDur = Math.max(...activeTracks.map(t => t.buffer.duration));
const maxDur = Math.max(...activeTracks.map(t => (t.startTime || 0) + t.buffer.duration));
const sr = activeTracks[0].buffer.sampleRate;
const merged = ctx.createBuffer(1, Math.ceil(maxDur * sr), sr);
const mergedData = merged.getChannelData(0);
activeTracks.forEach(t => {
const data = t.buffer.getChannelData(0);
const startSample = Math.floor((t.startTime || 0) * sr);
for (let i = 0; i < data.length; i++) {
mergedData[i] += data[i] * t.volume;
if (startSample + i < mergedData.length) {
mergedData[startSample + i] += data[i] * t.volume;
}
}
});
let maxPeak = 0;
@@ -970,7 +1017,7 @@
const newId = 'track_merged_' + Date.now();
const names = activeTracks.map(t => t.name).join('+').slice(0, 30);
setTracks(prev => [...prev, {
id: newId, name: `Merged_${names}.wav`, buffer: merged,
id: newId, name: `Merged_${names}.wav`, buffer: merged, startTime: 0,
volume: 0.8, muted: false, solo: false,
color: colors[prev.length % colors.length], markers: [], serverFileId: null,
}]);
@@ -984,14 +1031,22 @@
const at = tracks.filter(t => t.buffer && !t.muted);
if (at.length < 2) { showToast('Cần 2+ tracks để merge.','warning'); return; }
const actx = getAudioContext();
const maxDur = Math.max(...at.map(t => t.buffer.duration));
const maxDur = Math.max(...at.map(t => (t.startTime || 0) + t.buffer.duration));
const sr = at[0].buffer.sampleRate;
const mb = actx.createBuffer(1, Math.ceil(maxDur * sr), sr);
const mdata = mb.getChannelData(0);
at.forEach(t => { const d = t.buffer.getChannelData(0); for (let i=0;i<d.length;i++) mdata[i] += d[i] * t.volume; });
at.forEach(t => {
const d = t.buffer.getChannelData(0);
const startSample = Math.floor((t.startTime || 0) * sr);
for (let i = 0; i < d.length; i++) {
if (startSample + i < mdata.length) {
mdata[startSample + i] += d[i] * t.volume;
}
}
});
let mp = 0; for (let i=0;i<mdata.length;i++) { const a=Math.abs(mdata[i]); if (a>mp) mp=a; }
if (mp > 1.0) for (let i=0;i<mdata.length;i++) mdata[i] /= mp;
setTracks(p => [...p, { id:'merged_'+Date.now(), name:'Merged_mix.wav', buffer:mb, volume:0.8, muted:false, solo:false, color:['#0f766e','#1d4ed8'][p.length%2], markers:[], serverFileId:null }]);
setTracks(p => [...p, { id:'merged_'+Date.now(), name:'Merged_mix.wav', buffer:mb, startTime: 0, volume:0.8, muted:false, solo:false, color:['#0f766e','#1d4ed8'][p.length%2], markers:[], serverFileId:null }]);
showToast('Merged all unmuted tracks.','success');
};
const handleCopyTrack = () => {
@@ -1001,8 +1056,11 @@
const data = t.buffer.getChannelData(0);
// If selection exists, copy only the selected region
if (selLeft !== null && selRight !== null && selRight > selLeft) {
const startSample = Math.floor(selLeft * sr);
const endSample = Math.min(data.length, Math.floor(selRight * sr));
const trackStart = t.startTime || 0;
const relSelLeft = Math.max(0, selLeft - trackStart);
const relSelRight = Math.max(0, selRight - trackStart);
const startSample = Math.floor(relSelLeft * sr);
const endSample = Math.min(data.length, Math.floor(relSelRight * sr));
const len = endSample - startSample;
if (len > 0) {
const ctx = getAudioContext();
@@ -1025,8 +1083,11 @@
const data = t.buffer.getChannelData(0);
// If selection exists, cut only the selected region
if (selLeft !== null && selRight !== null && selRight > selLeft) {
const startSample = Math.floor(selLeft * sr);
const endSample = Math.min(data.length, Math.floor(selRight * sr));
const trackStart = t.startTime || 0;
const relSelLeft = Math.max(0, selLeft - trackStart);
const relSelRight = Math.max(0, selRight - trackStart);
const startSample = Math.floor(relSelLeft * sr);
const endSample = Math.min(data.length, Math.floor(relSelRight * sr));
const len = endSample - startSample;
if (len > 0) {
// Copy selection to clipboard
@@ -1060,6 +1121,20 @@
};
// ── Server Health Check ──
const [viewportWidth, setViewportWidth] = useState(1200);
useEffect(() => {
const wrapper = timelineWrapperRef.current;
if (!wrapper) return;
const observer = new ResizeObserver(entries => {
for (let entry of entries) {
setViewportWidth(entry.contentRect.width);
}
});
observer.observe(wrapper);
return () => observer.disconnect();
}, []);
useEffect(() => {
fetch(API_BASE_URL)
.then(r => { if (r.ok) setServerStatus('connected'); else setServerStatus('error'); })
@@ -1070,7 +1145,10 @@
const maxDuration = useMemo(() => {
let max = 10;
tracks.forEach(t => {
if (t.buffer) max = Math.max(max, t.buffer.duration);
if (t.buffer) {
const trackStart = t.startTime || 0;
max = Math.max(max, trackStart + t.buffer.duration);
}
});
return max;
}, [tracks]);
@@ -1078,9 +1156,19 @@
const maxDurationRef = useRef(maxDuration);
maxDurationRef.current = maxDuration;
const timelineWidth = useMemo(() => Math.max(zoom * maxDuration, 1200), [zoom, maxDuration]);
const minZoom = useMemo(() => {
return Math.max(20, viewportWidth / maxDuration);
}, [viewportWidth, maxDuration]);
const minZoom = useMemo(() => Math.max(20, 1200 / maxDuration), [maxDuration]);
const timelineWidth = useMemo(() => {
return Math.max(zoom * maxDuration, viewportWidth);
}, [zoom, maxDuration, viewportWidth]);
useEffect(() => {
if (zoom < minZoom) {
setZoom(minZoom);
}
}, [minZoom]);
const playheadLeftPos = useMemo(() => currentTime * zoom, [currentTime, zoom]);
@@ -1268,8 +1356,17 @@
source.connect(gainNode);
gainNode.connect(context.destination);
if (offsetTime < track.buffer.duration) {
source.start(0, offsetTime);
const trackStart = track.startTime || 0;
const trackDuration = track.buffer.duration;
const trackEnd = trackStart + trackDuration;
if (offsetTime < trackStart) {
const delay = trackStart - offsetTime;
source.start(context.currentTime + delay, 0);
activeSourcesRef.current.push(source);
} else if (offsetTime < trackEnd) {
const playOffset = offsetTime - trackStart;
source.start(context.currentTime, playOffset);
activeSourcesRef.current.push(source);
}
});
@@ -1290,8 +1387,17 @@
source.connect(gainNode);
gainNode.connect(context.destination);
if (offsetTime < track.buffer.duration) {
source.start(0, offsetTime);
const trackStart = track.startTime || 0;
const trackDuration = track.buffer.duration;
const trackEnd = trackStart + trackDuration;
if (offsetTime < trackStart) {
const delay = trackStart - offsetTime;
source.start(context.currentTime + delay, 0);
activeSourcesRef.current.push(source);
} else if (offsetTime < trackEnd) {
const playOffset = offsetTime - trackStart;
source.start(context.currentTime, playOffset);
activeSourcesRef.current.push(source);
}
};
@@ -1424,6 +1530,85 @@
};
}, [zoom, maxDuration]);
const draggedClipRef = useRef(null);
draggedClipRef.current = draggedClip;
const hoveredTrackIdRef = useRef(null);
hoveredTrackIdRef.current = hoveredTrackId;
const handleClipDragStart = (trackId, clickOffset) => {
const track = tracks.find(t => t.id === trackId);
if (!track || !track.buffer) return;
const beforeSnap = captureTrackSnapshot(trackId);
setDraggedClip({
trackId: trackId,
clickOffset: clickOffset,
buffer: track.buffer,
name: track.name,
volume: track.volume,
color: track.color,
beforeSnap: beforeSnap
});
};
useEffect(() => {
const handleMouseMove = (e) => {
const drag = draggedClipRef.current;
if (!drag) return;
const wrapper = timelineWrapperRef.current;
if (!wrapper) return;
const rect = wrapper.getBoundingClientRect();
const scrollLeft = wrapper.scrollLeft;
const mouseX = e.clientX - rect.left + scrollLeft;
const time = mouseX / zoom;
const newStart = Math.max(0, time - drag.clickOffset);
const targetTrackId = hoveredTrackIdRef.current || drag.trackId;
setTracks(prev => prev.map(t => {
if (t.id === drag.trackId && drag.trackId !== targetTrackId) {
return { ...t, buffer: null, startTime: 0 };
}
if (t.id === targetTrackId) {
return {
...t,
buffer: drag.buffer,
startTime: newStart,
name: drag.name,
volume: drag.volume,
color: drag.color
};
}
return t;
}));
if (drag.trackId !== targetTrackId) {
setDraggedClip(prev => ({ ...prev, trackId: targetTrackId }));
}
};
const handleMouseUp = () => {
const drag = draggedClipRef.current;
if (!drag) return;
const afterSnap = captureTrackSnapshot(drag.trackId);
pushAction('MOVE_CLIP', drag.trackId, drag.beforeSnap, afterSnap);
setDraggedClip(null);
showToast('Đã di chuyển clip.', 'success');
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [zoom]);
const handleSelectRange = (start, end, reset) => {
const maxLen = maxDuration;
const cleanStart = Math.max(0, Math.min(maxLen, start));
@@ -1665,20 +1850,20 @@
showToast(`Đã nạp sóng âm tổng hợp: ${type.toUpperCase()}`, 'success');
};
// ── Add Track ──
const addNewTrack = () => {
const newId = (tracks.length + 1).toString();
const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
const selectColor = colors[tracks.length % colors.length];
setTracks(prev => [...prev, {
id: newId, name: `Track ${newId}`, buffer: null,
volume: 0.8, muted: false, solo: false,
color: selectColor, markers: [], serverFileId: null
}]);
showToast(`Đã thêm Track ${newId}.`, 'info');
setTimeout(() => lucide.createIcons(), 200);
};
// ── Add Track ──
const addNewTrack = () => {
const newId = (tracks.length + 1).toString();
const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
const selectColor = colors[tracks.length % colors.length];
setTracks(prev => [...prev, {
id: newId, name: `Track ${newId}`, buffer: null, startTime: 0,
volume: 0.8, muted: false, solo: false,
color: selectColor, markers: [], serverFileId: null
}]);
showToast(`Đã thêm Track ${newId}.`, 'info');
setTimeout(() => lucide.createIcons(), 200);
};
// ── Server-side Export ──
const triggerWavExport = async () => {
@@ -1705,8 +1890,8 @@
muted: false,
clips: [{
clip_id: `clip_${t.id}`,
start_time_seconds: 0,
end_time_seconds: t.buffer.duration,
start_time_seconds: t.startTime || 0,
end_time_seconds: (t.startTime || 0) + t.buffer.duration,
loop_count: 1,
apply_zero_crossing: true,
fade_in_ms: 0,
@@ -1770,9 +1955,9 @@
try {
const targetRate = parseInt(exportSettings.sampleRate);
const bitDepth = parseInt(exportSettings.bitDepth);
const durationLimit = Math.max(...activeTracks.map(t => t.buffer.duration));
const durationLimit = Math.max(...activeTracks.map(t => (t.startTime || 0) + t.buffer.duration));
const offlineCtx = new OfflineAudioContext(1, targetRate * durationLimit, targetRate);
const offlineCtx = new OfflineAudioContext(1, Math.ceil(targetRate * durationLimit), targetRate);
activeTracks.forEach(t => {
const source = offlineCtx.createBufferSource();
@@ -1781,7 +1966,8 @@
gain.gain.setValueAtTime(t.volume, 0);
source.connect(gain);
gain.connect(offlineCtx.destination);
source.start(0);
const trackStart = t.startTime || 0;
source.start(trackStart);
});
const renderedBuffer = await offlineCtx.startRendering();
@@ -2044,7 +2230,9 @@
const track = tracks.find(t => t.id === trackId);
if (!track || !track.buffer) return;
const cutTime = findZeroCrossing(track.buffer, currentTime);
const trackStart = track.startTime || 0;
const relCurrentTime = Math.max(0, currentTime - trackStart);
const cutTime = findZeroCrossing(track.buffer, relCurrentTime);
const sr = track.buffer.sampleRate;
const cutSample = Math.floor(cutTime * sr);
const originalData = track.buffer.getChannelData(0);
@@ -2071,6 +2259,7 @@
id: 'track_split_' + Date.now(),
name: `${track.name} (Part 2)`,
buffer: b2,
startTime: trackStart + (cutSample / sr),
markers: [],
serverFileId: null,
};
@@ -2405,12 +2594,14 @@
loadFileOnTrack(track.id, e.dataTransfer.files[0]);
}
}}
onMouseEnter={() => setHoveredTrackId(track.id)}
>
<WaveformLane track={track} zoom={zoom} timelineWidth={timelineWidth}
onSelectRange={handleSelectRange} onPlayheadSet={setCurrentTime}
isSelected={isSelected} onSelectTrack={setSelectedTrackId} markers={track.markers}
onTrackLaneMouseDown={handleTrackLaneMouseDown}
onContextMenu={handleContextMenu}
onClipDragStart={handleClipDragStart}
selectionMode={selectionMode}
localSelectionTrackId={localSelectionTrackId}
localSelLeft={localSelectionStart !== null && localSelectionEnd !== null ? Math.min(localSelectionStart, localSelectionEnd) : null}