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.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSelectTrack(track.id);
|
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 === 'm') { e.preventDefault(); handleMergeTracks(); return; }
|
||||||
if (ctrl && !alt && e.key === 'c') { e.preventDefault(); handleCopyTrack(); 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 === '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 (e.key === 'Delete' || e.key === 'Del') { e.preventDefault(); handleDeleteTrack(); return; }
|
||||||
if (!ctrl && !alt && e.key === 's') { e.preventDefault(); handleSplitTrack(selectedTrackId); return; }
|
if (!ctrl && !alt && e.key === 's') { e.preventDefault(); handleSplitTrack(selectedTrackId); return; }
|
||||||
};
|
};
|
||||||
@@ -667,10 +672,10 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── Context Menu Handlers ──
|
// ── Context Menu Handlers ──
|
||||||
const handleContextMenu = (e, trackId) => {
|
const handleContextMenu = (e, trackId, clickTime) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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);
|
const closeContextMenu = () => setContextMenu(null);
|
||||||
@@ -755,38 +760,87 @@
|
|||||||
contextMenuDelete();
|
contextMenuDelete();
|
||||||
};
|
};
|
||||||
|
|
||||||
const contextMenuPaste = () => {
|
const doPaste = (targetTrackId, pasteTime) => {
|
||||||
if (!clipboardRef.current) {
|
if (!clipboardRef.current || !clipboardRef.current.buffer) {
|
||||||
showToast('Clipboard trống.', 'warning');
|
showToast('Clipboard trống.', 'warning');
|
||||||
closeContextMenu();
|
return null;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const { buffer, name, volume, color } = clipboardRef.current;
|
const { buffer: clipBuffer, 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 ctx = getAudioContext();
|
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 colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
|
||||||
const newId = 'track_pasted_' + Date.now();
|
const newId = 'track_pasted_' + Date.now();
|
||||||
|
const newClipBuffer = ctx.createBuffer(1, clipBuffer.length, clipBuffer.sampleRate);
|
||||||
|
newClipBuffer.copyToChannel(clipBuffer.getChannelData(0), 0);
|
||||||
setTracks(prev => [...prev, {
|
setTracks(prev => [...prev, {
|
||||||
id: newId,
|
id: newId, name: `Pasted_${name || 'track'}`, buffer: newClipBuffer,
|
||||||
name: `Pasted_${name || 'track'}`,
|
volume: volume || 0.8, muted: false, solo: false,
|
||||||
buffer: newBuffer,
|
|
||||||
volume: volume || 0.8,
|
|
||||||
muted: false,
|
|
||||||
solo: false,
|
|
||||||
color: color || colors[prev.length % colors.length],
|
color: color || colors[prev.length % colors.length],
|
||||||
markers: [],
|
markers: [], serverFileId: null,
|
||||||
serverFileId: null,
|
|
||||||
}]);
|
}]);
|
||||||
setSelectedTrackId(newId);
|
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();
|
closeContextMenu();
|
||||||
showToast('Đã dán track từ clipboard.', 'success');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const contextMenuMerge = () => {
|
const contextMenuMerge = () => {
|
||||||
@@ -1961,7 +2015,7 @@
|
|||||||
{ sep: true },
|
{ sep: true },
|
||||||
{ label: 'Copy', icon: 'copy', shortcut: 'Ctrl+C', action: () => { handleCopyTrack(); } },
|
{ label: 'Copy', icon: 'copy', shortcut: 'Ctrl+C', action: () => { handleCopyTrack(); } },
|
||||||
{ label: 'Cut', icon: 'scissors', shortcut: 'Ctrl+X', action: () => { handleCutTrack(); } },
|
{ 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 },
|
{ sep: true },
|
||||||
{ label: 'Delete Track', icon: 'trash-2', shortcut: 'Del', action: () => { handleDeleteTrack(); } },
|
{ label: 'Delete Track', icon: 'trash-2', shortcut: 'Del', action: () => { handleDeleteTrack(); } },
|
||||||
]},
|
]},
|
||||||
|
|||||||
Reference in New Issue
Block a user