fix: sửa lỗi audio clip dán từ clipboard không đúng dung lượng
This commit is contained in:
+81
-27
@@ -277,7 +277,12 @@
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onSelectTrack(track.id);
|
||||
if (onContextMenu) onContextMenu(e, track.id);
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const wrapper = canvasRef.current?.parentElement?.parentElement;
|
||||
const scrollLeft = wrapper ? wrapper.scrollLeft : 0;
|
||||
const x = e.clientX - rect.left + scrollLeft;
|
||||
const time = Math.max(0, x / zoom);
|
||||
if (onContextMenu) onContextMenu(e, track.id, time);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -483,7 +488,7 @@
|
||||
if (ctrl && !alt && e.key === 'm') { e.preventDefault(); handleMergeTracks(); return; }
|
||||
if (ctrl && !alt && e.key === 'c') { e.preventDefault(); handleCopyTrack(); return; }
|
||||
if (ctrl && !alt && e.key === 'x') { e.preventDefault(); handleCutTrack(); return; }
|
||||
if (ctrl && !alt && e.key === 'v') { e.preventDefault(); contextMenuPaste(); return; }
|
||||
if (ctrl && !alt && e.key === 'v') { e.preventDefault(); handlePasteTrack(); return; }
|
||||
if (e.key === 'Delete' || e.key === 'Del') { e.preventDefault(); handleDeleteTrack(); return; }
|
||||
if (!ctrl && !alt && e.key === 's') { e.preventDefault(); handleSplitTrack(selectedTrackId); return; }
|
||||
};
|
||||
@@ -667,10 +672,10 @@
|
||||
};
|
||||
|
||||
// ── Context Menu Handlers ──
|
||||
const handleContextMenu = (e, trackId) => {
|
||||
const handleContextMenu = (e, trackId, clickTime) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, trackId });
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, trackId, time: clickTime || currentTime });
|
||||
};
|
||||
|
||||
const closeContextMenu = () => setContextMenu(null);
|
||||
@@ -755,38 +760,87 @@
|
||||
contextMenuDelete();
|
||||
};
|
||||
|
||||
const contextMenuPaste = () => {
|
||||
if (!clipboardRef.current) {
|
||||
const doPaste = (targetTrackId, pasteTime) => {
|
||||
if (!clipboardRef.current || !clipboardRef.current.buffer) {
|
||||
showToast('Clipboard trống.', 'warning');
|
||||
closeContextMenu();
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const { buffer, name, volume, color } = clipboardRef.current;
|
||||
if (!buffer) {
|
||||
showToast('Clipboard không có dữ liệu âm thanh.', 'warning');
|
||||
closeContextMenu();
|
||||
return;
|
||||
}
|
||||
// Create a new buffer copy
|
||||
const { buffer: clipBuffer, name, volume, color } = clipboardRef.current;
|
||||
const ctx = getAudioContext();
|
||||
const newBuffer = ctx.createBuffer(1, buffer.length, buffer.sampleRate);
|
||||
newBuffer.copyToChannel(buffer.getChannelData(0), 0);
|
||||
|
||||
const targetTrack = tracks.find(t => t.id === targetTrackId);
|
||||
if (targetTrack && targetTrack.buffer) {
|
||||
const targetSr = targetTrack.buffer.sampleRate;
|
||||
const clipSr = clipBuffer.sampleRate;
|
||||
// Resample clipboard to target sample rate if needed
|
||||
let pasteData;
|
||||
let pasteSamples;
|
||||
if (clipSr === targetSr) {
|
||||
const newClipBuffer = ctx.createBuffer(1, clipBuffer.length, clipSr);
|
||||
newClipBuffer.copyToChannel(clipBuffer.getChannelData(0), 0);
|
||||
pasteData = newClipBuffer.getChannelData(0);
|
||||
pasteSamples = clipBuffer.length;
|
||||
} else {
|
||||
pasteSamples = Math.round(clipBuffer.length * targetSr / clipSr);
|
||||
const resampled = ctx.createBuffer(1, pasteSamples, targetSr);
|
||||
const out = resampled.getChannelData(0);
|
||||
const src = clipBuffer.getChannelData(0);
|
||||
const srcLen = clipBuffer.length;
|
||||
for (let i = 0; i < pasteSamples; i++) {
|
||||
const pos = (i / pasteSamples) * srcLen;
|
||||
const idx = Math.floor(pos);
|
||||
const frac = pos - idx;
|
||||
const s0 = src[Math.min(idx, srcLen - 1)];
|
||||
const s1 = src[Math.min(idx + 1, srcLen - 1)];
|
||||
out[i] = s0 + (s1 - s0) * frac;
|
||||
}
|
||||
pasteData = out;
|
||||
}
|
||||
|
||||
const insertSample = Math.floor(pasteTime * targetSr);
|
||||
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));
|
||||
setCurrentTime(pasteTime);
|
||||
showToast('Đã dán vào track.', 'success');
|
||||
return targetTrackId;
|
||||
}
|
||||
if (targetTrack) {
|
||||
// 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);
|
||||
showToast('Đã dán vào track.', 'success');
|
||||
return targetTrackId;
|
||||
}
|
||||
// No matching track — create a new one
|
||||
const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
|
||||
const newId = 'track_pasted_' + Date.now();
|
||||
const newClipBuffer = ctx.createBuffer(1, clipBuffer.length, clipBuffer.sampleRate);
|
||||
newClipBuffer.copyToChannel(clipBuffer.getChannelData(0), 0);
|
||||
setTracks(prev => [...prev, {
|
||||
id: newId,
|
||||
name: `Pasted_${name || 'track'}`,
|
||||
buffer: newBuffer,
|
||||
volume: volume || 0.8,
|
||||
muted: false,
|
||||
solo: false,
|
||||
id: newId, name: `Pasted_${name || 'track'}`, buffer: newClipBuffer,
|
||||
volume: volume || 0.8, muted: false, solo: false,
|
||||
color: color || colors[prev.length % colors.length],
|
||||
markers: [],
|
||||
serverFileId: null,
|
||||
markers: [], serverFileId: null,
|
||||
}]);
|
||||
setSelectedTrackId(newId);
|
||||
setCurrentTime(0);
|
||||
showToast('Đã dán track mới từ clipboard.', 'success');
|
||||
return newId;
|
||||
};
|
||||
|
||||
const handlePasteTrack = () => doPaste(selectedTrackId, currentTime);
|
||||
|
||||
const contextMenuPaste = () => {
|
||||
const result = doPaste(contextMenu.trackId, contextMenu.time || currentTime);
|
||||
closeContextMenu();
|
||||
showToast('Đã dán track từ clipboard.', 'success');
|
||||
};
|
||||
|
||||
const contextMenuMerge = () => {
|
||||
@@ -1961,7 +2015,7 @@
|
||||
{ sep: true },
|
||||
{ label: 'Copy', icon: 'copy', shortcut: 'Ctrl+C', action: () => { handleCopyTrack(); } },
|
||||
{ label: 'Cut', icon: 'scissors', shortcut: 'Ctrl+X', action: () => { handleCutTrack(); } },
|
||||
{ label: 'Paste', icon: 'clipboard', shortcut: 'Ctrl+V', action: contextMenuPaste },
|
||||
{ label: 'Paste', icon: 'clipboard', shortcut: 'Ctrl+V', action: handlePasteTrack },
|
||||
{ sep: true },
|
||||
{ label: 'Delete Track', icon: 'trash-2', shortcut: 'Del', action: () => { handleDeleteTrack(); } },
|
||||
]},
|
||||
|
||||
Reference in New Issue
Block a user