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
+42 -31
View File
@@ -801,19 +801,20 @@
const parent = canvas.parentElement;
const scrollContainer = parent ? parent.parentElement : null;
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 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
if (e.altKey && onSpeedChange) {
const clipRightEdge = (duration / speed) * zoom;
const clipRightEdge = (bufDuration / (speed || 1.0)) * zoom;
const tolerance = 8;
if (Math.abs(mouseX - clipRightEdge) <= tolerance) {
isStretchingRef.current = true;
stretchStartRef.current = {
mouseX,
originalDuration: duration,
originalDuration: bufDuration,
originalSpeed: speed
};
canvas.style.cursor = 'ew-resize';
@@ -846,7 +847,7 @@
const handleMouseMove = (moveEvent) => {
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);
};
@@ -872,7 +873,7 @@
const handleMouseMove = (moveEvent) => {
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);
};
@@ -893,7 +894,7 @@
const handleMouseMove = (moveEvent) => {
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);
};
@@ -915,7 +916,7 @@
const scrollContainer = parent ? parent.parentElement : null;
const scrollLeft = scrollContainer ? scrollContainer.scrollLeft : 0;
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);
};
@@ -1321,6 +1322,7 @@
const activeSourcesRef = useRef([]);
const activeTrackNodesRef = useRef({}); // { [trackId]: { gainNode, pannerNode } }
const startOffsetTimeRef = useRef(0);
const startBufferOffsetRef = useRef(0);
const startAudioTimeRef = useRef(0);
const animationFrameIdRef = useRef(null);
const toastTimeoutRef = useRef(null);
@@ -2522,8 +2524,10 @@
}, [zoom, minZoom, maxDuration, activeTab]);
// ── Update Playhead ──
const startSubTabPlayback = (st, offsetTime) => {
const startSubTabPlayback = (st, offsetWallTime) => {
const context = getAudioContext();
const speed = st.speed || 1.0;
const offsetBuffer = offsetWallTime * speed;
const eff = st.buffer.getChannelData(0);
const edBuffer = context.createBuffer(1, eff.length, st.buffer.sampleRate);
const edData = edBuffer.getChannelData(0);
@@ -2554,7 +2558,7 @@
const source = context.createBufferSource();
source.buffer = edBuffer;
source.playbackRate.value = st.speed || 1.0;
source.playbackRate.value = speed;
// Look up track for volume/pan
const subTrack = tracks.find(t => t.id === st.trackId);
@@ -2572,10 +2576,11 @@
gainNode.connect(pannerNode);
pannerNode.connect(context.destination);
source.start(context.currentTime, offsetTime);
source.start(context.currentTime, offsetBuffer);
activeSourcesRef.current = [source];
activeTrackNodesRef.current[st.trackId] = { gainNode, pannerNode, source };
startOffsetTimeRef.current = offsetTime;
startOffsetTimeRef.current = offsetWallTime;
startBufferOffsetRef.current = offsetBuffer;
startAudioTimeRef.current = context.currentTime;
};
@@ -2584,29 +2589,29 @@
const st = subTabs.find(s => s.id === activeTab);
if (!st || !st.isPlaying || !st.buffer) return;
const context = getAudioContext();
const elapsed = context.currentTime - startAudioTimeRef.current;
const speedFactor = st.speed || 1.0;
const updatedTime = startOffsetTimeRef.current + elapsed * speedFactor;
const effectiveDuration = st.buffer.duration / speedFactor;
const elapsed = context.currentTime - startAudioTimeRef.current;
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) {
const start = Math.min(st.selectionStart, st.selectionEnd);
const end = Math.max(st.selectionStart, st.selectionEnd);
if (updatedTime >= end) {
if (bufferPos >= end) {
stopAllPlayback();
setSubTabs(prev => prev.map(s => s.id === activeTab ? {
...s,
currentTime: start,
currentTime: wallTime,
isPlaying: true
} : s));
startSubTabPlayback(st, start);
startSubTabPlayback(st, wallTime);
animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
return;
}
}
if (updatedTime >= effectiveDuration) {
if (bufferPos >= st.buffer.duration) {
stopAllPlayback();
if (isLoopingSelection) {
setSubTabs(prev => prev.map(s => s.id === activeTab ? {
@@ -2622,7 +2627,7 @@
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);
return;
}
@@ -2800,7 +2805,7 @@
stopAllPlayback();
const startOffset = st.currentTime || 0;
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;
}
@@ -4991,15 +4996,21 @@
color={vTrack.color}
name={vTrack.name}
speed={st.speed || 1.0}
onSpeedChange={(newSpeed) => {
setSubTabs(prev => prev.map(s => s.id === st.id ? {
...s,
speed: newSpeed,
label: s.label.replace(/\s\(\d+%\)$/, '') + ` (${Math.round(newSpeed * 100)}%)`
} : s));
const n = activeTrackNodesRef.current[st.trackId];
if (n && n.source) n.source.playbackRate.value = newSpeed;
}}
onSpeedChange={(newSpeed) => {
setSubTabs(prev => prev.map(s => s.id === st.id ? {
...s,
speed: newSpeed,
label: s.label.replace(/\s\(\d+%\)$/, '') + ` (${Math.round(newSpeed * 100)}%)`
} : s));
const n = activeTrackNodesRef.current[st.trackId];
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}
className="absolute bottom-0 left-0 right-0 h-1.5 cursor-ns-resize z-30 hover:bg-purple-500/50 transition-colors" />