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

This commit is contained in:
2026-07-18 18:03:48 +07:00
parent 7ee197301d
commit 89f1c14c2e
+81 -27
View File
@@ -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(); } },
]},