diff --git a/src/components/video-editor/audio/waveform/WaveformGenerator.ts b/src/components/video-editor/audio/waveform/WaveformGenerator.ts index b44c274f..fb8ba2bc 100644 --- a/src/components/video-editor/audio/waveform/WaveformGenerator.ts +++ b/src/components/video-editor/audio/waveform/WaveformGenerator.ts @@ -74,22 +74,36 @@ export class WaveformGenerator { const arrayBuffer = await response.arrayBuffer(); const decoded = await this.audioContext.decodeAudioData(arrayBuffer); - + const adaptivePeakCount = Math.max( + peakCount, + Math.floor(decoded.duration * 500) + ); const channels: Float32Array[] = []; for (let i = 0; i < decoded.numberOfChannels; i++) { // We slice to transfer the underlying buffer to the worker channels.push(decoded.getChannelData(i).slice()); } - const peaks = await this.computePeaksWithWorker(channels, peakCount); + const peaks = await this.computePeaksWithWorker(channels, adaptivePeakCount); + // Robust Normalization: Use 99.5th percentile to avoid being squashed by a single loud spike/pop let max = 0; - for (let i = 0; i < peaks.length; i++) { - if (peaks[i] > max) max = peaks[i]; + const sortedPeaks = [...peaks].sort((a, b) => a - b); + const percentileIndex = Math.floor(sortedPeaks.length * 0.995); + const robustMax = sortedPeaks[percentileIndex] || 0; + + // Fallback to absolute max if the percentile is zero (very quiet file) + if (robustMax === 0) { + for (let i = 0; i < peaks.length; i++) { + if (peaks[i] > max) max = peaks[i]; + } + } else { + max = robustMax; } + if (max > 0) { for (let i = 0; i < peaks.length; i++) { - peaks[i] /= max; + peaks[i] = Math.min(1.0, peaks[i] / max); } } diff --git a/src/components/video-editor/audio/waveform/waveform.worker.ts b/src/components/video-editor/audio/waveform/waveform.worker.ts index 1e43df98..80403a09 100644 --- a/src/components/video-editor/audio/waveform/waveform.worker.ts +++ b/src/components/video-editor/audio/waveform/waveform.worker.ts @@ -24,12 +24,17 @@ workerScope.onmessage = (e: MessageEvent) => { const firstChannel = channels[0]; const result = new Float32Array(samples); const total = firstChannel.length; + const blockSize = total / samples; for (let i = 0; i < samples; i++) { - const start = Math.floor((i * total) / samples); - const end = Math.floor(((i + 1) * total) / samples); + const start = Math.floor(i * blockSize); + const end = Math.min(total, Math.floor((i + 1) * blockSize)); + let max = 0; - for (let j = start; j < end; j++) { + // Ensure we check at least one sample even if blockSize < 1 + const actualEnd = Math.max(start + 1, end); + + for (let j = start; j < actualEnd && j < total; j++) { for (let c = 0; c < channels.length; c++) { const val = Math.abs(channels[c][j]); if (val > max) max = val; diff --git a/src/components/video-editor/timeline/components/waveform/AudioWaveform.tsx b/src/components/video-editor/timeline/components/waveform/AudioWaveform.tsx index 2380d598..1b192651 100644 --- a/src/components/video-editor/timeline/components/waveform/AudioWaveform.tsx +++ b/src/components/video-editor/timeline/components/waveform/AudioWaveform.tsx @@ -75,31 +75,32 @@ function AudioWaveformComponent({ const { peaks: peakData, durationMs } = peaks; if (durationMs <= 0 || peakData.length === 0) return; - const rawVisibleStartMs = segmentStartMs ?? range.start; - const rawVisibleEndMs = segmentEndMs ?? range.end; - const msPerBin = durationMs / peakData.length; - const visibleStartMs = - msPerBin > 0 ? Math.round(rawVisibleStartMs / msPerBin) * msPerBin : rawVisibleStartMs; - const visibleEndMs = - msPerBin > 0 ? Math.round(rawVisibleEndMs / msPerBin) * msPerBin : rawVisibleEndMs; + // Use raw values for smooth zooming/panning (no snapping) + const visibleStartMs = segmentStartMs ?? range.start; + const visibleEndMs = segmentEndMs ?? range.end; const visibleDurationMs = visibleEndMs - visibleStartMs; + if (visibleDurationMs <= 0) return; const midY = height / 2; - ctx.beginPath(); + for (let px = 0; px < width; px++) { const t = visibleStartMs + (px / width) * visibleDurationMs; - const exactIndex = Math.max( - 0, - Math.min(peakData.length - 1, (t / durationMs) * (peakData.length - 1)), - ); + + // If the timeline time is beyond the actual audio duration, we draw nothing (flat line) + if (t < 0 || t > durationMs) continue; + + const exactIndex = (t / durationMs) * (peakData.length - 1); const leftIndex = Math.floor(exactIndex); const rightIndex = Math.min(peakData.length - 1, leftIndex + 1); const mix = exactIndex - leftIndex; + let amplitude = peakData[leftIndex] * (1 - mix) + peakData[rightIndex] * mix; + if (normalize) amplitude = Math.sqrt(Math.max(0, amplitude)); amplitude = Math.max(0, Math.min(1, amplitude * gain)); + const barHeight = amplitude * midY * 0.85; ctx.moveTo(px, midY - barHeight);