diff --git a/app/templates/index.html b/app/templates/index.html index 8c52b3a..3690e5b 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -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; + }} />