fix: chỉnh sửa tốc độ của audio clip realtime chính xác

This commit is contained in:
2026-07-19 17:27:06 +07:00
parent b39bfbc1bc
commit b1f9659f06
+33 -22
View File
@@ -801,19 +801,20 @@
const parent = canvas.parentElement; const parent = canvas.parentElement;
const scrollContainer = parent ? parent.parentElement : null; const scrollContainer = parent ? parent.parentElement : null;
const scrollLeft = scrollContainer ? scrollContainer.scrollLeft : 0; const scrollLeft = scrollContainer ? scrollContainer.scrollLeft : 0;
const duration = buffer.duration; const bufDuration = buffer.duration;
const wallDuration = bufDuration / (speed || 1.0);
const mouseX = e.clientX - rect.left + scrollLeft; const mouseX = e.clientX - rect.left + scrollLeft;
const startTime = Math.max(0, Math.min(duration, mouseX / zoom)); const startTime = Math.max(0, Math.min(wallDuration, mouseX / zoom));
// Alt+Click near right edge → speed stretch // Alt+Click near right edge → speed stretch
if (e.altKey && onSpeedChange) { if (e.altKey && onSpeedChange) {
const clipRightEdge = (duration / speed) * zoom; const clipRightEdge = (bufDuration / (speed || 1.0)) * zoom;
const tolerance = 8; const tolerance = 8;
if (Math.abs(mouseX - clipRightEdge) <= tolerance) { if (Math.abs(mouseX - clipRightEdge) <= tolerance) {
isStretchingRef.current = true; isStretchingRef.current = true;
stretchStartRef.current = { stretchStartRef.current = {
mouseX, mouseX,
originalDuration: duration, originalDuration: bufDuration,
originalSpeed: speed originalSpeed: speed
}; };
canvas.style.cursor = 'ew-resize'; canvas.style.cursor = 'ew-resize';
@@ -846,7 +847,7 @@
const handleMouseMove = (moveEvent) => { const handleMouseMove = (moveEvent) => {
const currentX = moveEvent.clientX - rect.left + scrollLeft; const currentX = moveEvent.clientX - rect.left + scrollLeft;
const ct = Math.max(0, Math.min(duration, currentX / zoom)); const ct = Math.max(0, Math.min(wallDuration, currentX / zoom));
onPlayheadSet(ct); onPlayheadSet(ct);
}; };
@@ -872,7 +873,7 @@
const handleMouseMove = (moveEvent) => { const handleMouseMove = (moveEvent) => {
const currentX = moveEvent.clientX - rect.left + scrollLeft; const currentX = moveEvent.clientX - rect.left + scrollLeft;
const ct = Math.max(0, Math.min(duration, currentX / zoom)); const ct = Math.max(0, Math.min(wallDuration, currentX / zoom));
onPlayheadSet(ct); onPlayheadSet(ct);
}; };
@@ -893,7 +894,7 @@
const handleMouseMove = (moveEvent) => { const handleMouseMove = (moveEvent) => {
const currentX = moveEvent.clientX - rect.left + scrollLeft; const currentX = moveEvent.clientX - rect.left + scrollLeft;
const ct = Math.max(0, Math.min(duration, currentX / zoom)); const ct = Math.max(0, Math.min(wallDuration, currentX / zoom));
onSelectRange(startTime, ct); onSelectRange(startTime, ct);
}; };
@@ -915,7 +916,7 @@
const scrollContainer = parent ? parent.parentElement : null; const scrollContainer = parent ? parent.parentElement : null;
const scrollLeft = scrollContainer ? scrollContainer.scrollLeft : 0; const scrollLeft = scrollContainer ? scrollContainer.scrollLeft : 0;
const x = e.clientX - rect.left + scrollLeft; const x = e.clientX - rect.left + scrollLeft;
const clickTime = Math.max(0, Math.min(buffer.duration, x / zoom)); const clickTime = Math.max(0, Math.min(buffer.duration / (speed || 1.0), x / zoom));
onContextMenu(e, clickTime); onContextMenu(e, clickTime);
}; };
@@ -1321,6 +1322,7 @@
const activeSourcesRef = useRef([]); const activeSourcesRef = useRef([]);
const activeTrackNodesRef = useRef({}); // { [trackId]: { gainNode, pannerNode } } const activeTrackNodesRef = useRef({}); // { [trackId]: { gainNode, pannerNode } }
const startOffsetTimeRef = useRef(0); const startOffsetTimeRef = useRef(0);
const startBufferOffsetRef = useRef(0);
const startAudioTimeRef = useRef(0); const startAudioTimeRef = useRef(0);
const animationFrameIdRef = useRef(null); const animationFrameIdRef = useRef(null);
const toastTimeoutRef = useRef(null); const toastTimeoutRef = useRef(null);
@@ -2522,8 +2524,10 @@
}, [zoom, minZoom, maxDuration, activeTab]); }, [zoom, minZoom, maxDuration, activeTab]);
// ── Update Playhead ── // ── Update Playhead ──
const startSubTabPlayback = (st, offsetTime) => { const startSubTabPlayback = (st, offsetWallTime) => {
const context = getAudioContext(); const context = getAudioContext();
const speed = st.speed || 1.0;
const offsetBuffer = offsetWallTime * speed;
const eff = st.buffer.getChannelData(0); const eff = st.buffer.getChannelData(0);
const edBuffer = context.createBuffer(1, eff.length, st.buffer.sampleRate); const edBuffer = context.createBuffer(1, eff.length, st.buffer.sampleRate);
const edData = edBuffer.getChannelData(0); const edData = edBuffer.getChannelData(0);
@@ -2554,7 +2558,7 @@
const source = context.createBufferSource(); const source = context.createBufferSource();
source.buffer = edBuffer; source.buffer = edBuffer;
source.playbackRate.value = st.speed || 1.0; source.playbackRate.value = speed;
// Look up track for volume/pan // Look up track for volume/pan
const subTrack = tracks.find(t => t.id === st.trackId); const subTrack = tracks.find(t => t.id === st.trackId);
@@ -2572,10 +2576,11 @@
gainNode.connect(pannerNode); gainNode.connect(pannerNode);
pannerNode.connect(context.destination); pannerNode.connect(context.destination);
source.start(context.currentTime, offsetTime); source.start(context.currentTime, offsetBuffer);
activeSourcesRef.current = [source]; activeSourcesRef.current = [source];
activeTrackNodesRef.current[st.trackId] = { gainNode, pannerNode, source }; activeTrackNodesRef.current[st.trackId] = { gainNode, pannerNode, source };
startOffsetTimeRef.current = offsetTime; startOffsetTimeRef.current = offsetWallTime;
startBufferOffsetRef.current = offsetBuffer;
startAudioTimeRef.current = context.currentTime; startAudioTimeRef.current = context.currentTime;
}; };
@@ -2584,29 +2589,29 @@
const st = subTabs.find(s => s.id === activeTab); const st = subTabs.find(s => s.id === activeTab);
if (!st || !st.isPlaying || !st.buffer) return; if (!st || !st.isPlaying || !st.buffer) return;
const context = getAudioContext(); const context = getAudioContext();
const elapsed = context.currentTime - startAudioTimeRef.current;
const speedFactor = st.speed || 1.0; const speedFactor = st.speed || 1.0;
const updatedTime = startOffsetTimeRef.current + elapsed * speedFactor; const elapsed = context.currentTime - startAudioTimeRef.current;
const effectiveDuration = st.buffer.duration / speedFactor; const wallTime = startOffsetTimeRef.current + elapsed;
const bufferPos = startBufferOffsetRef.current + elapsed * speedFactor;
// Loop sub-tab selection // Loop sub-tab selection (bufferPos is buffer-time)
if (st.selectionStart !== null && st.selectionEnd !== null && st.selectionStart !== st.selectionEnd) { if (st.selectionStart !== null && st.selectionEnd !== null && st.selectionStart !== st.selectionEnd) {
const start = Math.min(st.selectionStart, st.selectionEnd); const start = Math.min(st.selectionStart, st.selectionEnd);
const end = Math.max(st.selectionStart, st.selectionEnd); const end = Math.max(st.selectionStart, st.selectionEnd);
if (updatedTime >= end) { if (bufferPos >= end) {
stopAllPlayback(); stopAllPlayback();
setSubTabs(prev => prev.map(s => s.id === activeTab ? { setSubTabs(prev => prev.map(s => s.id === activeTab ? {
...s, ...s,
currentTime: start, currentTime: wallTime,
isPlaying: true isPlaying: true
} : s)); } : s));
startSubTabPlayback(st, start); startSubTabPlayback(st, wallTime);
animationFrameIdRef.current = requestAnimationFrame(updatePlayhead); animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
return; return;
} }
} }
if (updatedTime >= effectiveDuration) { if (bufferPos >= st.buffer.duration) {
stopAllPlayback(); stopAllPlayback();
if (isLoopingSelection) { if (isLoopingSelection) {
setSubTabs(prev => prev.map(s => s.id === activeTab ? { setSubTabs(prev => prev.map(s => s.id === activeTab ? {
@@ -2622,7 +2627,7 @@
return; return;
} }
setSubTabs(prev => prev.map(s => s.id === activeTab ? { ...s, currentTime: updatedTime } : s)); setSubTabs(prev => prev.map(s => s.id === activeTab ? { ...s, currentTime: wallTime } : s));
animationFrameIdRef.current = requestAnimationFrame(updatePlayhead); animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
return; return;
} }
@@ -2800,7 +2805,7 @@
stopAllPlayback(); stopAllPlayback();
const startOffset = st.currentTime || 0; const startOffset = st.currentTime || 0;
startSubTabPlayback(st, startOffset); startSubTabPlayback(st, startOffset);
setSubTabs(prev => prev.map(s => s.id === activeTab ? { ...s, isPlaying: true } : s)); setSubTabs(prev => prev.map(s => s.id === activeTab ? { ...s, isPlaying: true, currentTime: startOffset } : s));
} }
return; return;
} }
@@ -4999,6 +5004,12 @@
} : s)); } : s));
const n = activeTrackNodesRef.current[st.trackId]; const n = activeTrackNodesRef.current[st.trackId];
if (n && n.source) n.source.playbackRate.value = newSpeed; if (n && n.source) n.source.playbackRate.value = newSpeed;
// Reset time refs to prevent playhead jump when speed changes mid-playback
const ctx = getAudioContext();
const elapsed = ctx.currentTime - startAudioTimeRef.current;
startBufferOffsetRef.current = startBufferOffsetRef.current + elapsed * (st.speed || 1.0);
startOffsetTimeRef.current = startOffsetTimeRef.current + elapsed;
startAudioTimeRef.current = ctx.currentTime;
}} }}
/> />
<div onMouseDown={handleSubTabResizeMouseDown} <div onMouseDown={handleSubTabResizeMouseDown}