kv-music/js/waveform.js
2026-02-13 21:02:00 +01:00

105 lines
3.3 KiB
JavaScript

// js/waveform.js
export class WaveformGenerator {
constructor() {
// Use OfflineAudioContext to prevent creating unnecessary OS audio streams
// decodeAudioData doesn't require a real-time AudioContext
this.audioContext = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, 1, 44100);
this.cache = new Map();
}
async getWaveform(url, trackId) {
if (this.cache.has(trackId)) {
return this.cache.get(trackId);
}
try {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
const peaks = this.extractPeaks(audioBuffer);
const result = { peaks, duration: audioBuffer.duration };
this.cache.set(trackId, result);
return result;
} catch (error) {
console.error('Waveform generation failed:', error);
return null;
}
}
extractPeaks(audioBuffer) {
const { length, duration } = audioBuffer;
const numPeaks = Math.min(Math.floor(4 * duration), 1000);
const peaks = new Float32Array(numPeaks);
const chanData = audioBuffer.getChannelData(0); // Use first channel
const step = Math.floor(length / numPeaks);
const stride = 8; // Check every 8th sample for speed
for (let i = 0; i < numPeaks; i++) {
let max = 0;
const start = i * step;
const end = start + step;
for (let j = start; j < end; j += stride) {
const datum = chanData[j];
if (datum > max) {
max = datum;
} else if (-datum > max) {
max = -datum;
}
}
peaks[i] = max;
}
// Normalize peaks so the highest peak is 1.0
let maxPeak = 0;
for (let i = 0; i < numPeaks; i++) {
if (peaks[i] > maxPeak) maxPeak = peaks[i];
}
if (maxPeak > 0) {
for (let i = 0; i < numPeaks; i++) {
peaks[i] /= maxPeak;
}
}
return peaks;
}
drawWaveform(canvas, peaks) {
if (!canvas || !peaks) return;
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
ctx.clearRect(0, 0, width, height);
const step = width / peaks.length;
const centerY = height / 2;
ctx.fillStyle = '#000'; // Mask color (opaque part)
ctx.beginPath();
// Draw top half
ctx.moveTo(0, centerY);
for (let i = 0; i < peaks.length; i++) {
const peak = peaks[i];
const barHeight = Math.max(1.5, peak * height * 0.9);
ctx.lineTo(i * step, centerY - barHeight / 2);
}
// Draw bottom half (backwards)
for (let i = peaks.length - 1; i >= 0; i--) {
const peak = peaks[i];
const barHeight = Math.max(1.5, peak * height * 0.9);
ctx.lineTo(i * step, centerY + barHeight / 2);
}
ctx.closePath();
ctx.fill();
}
// Removed drawRoundedRect as it's no longer used for continuous paths
}
export const waveformGenerator = new WaveformGenerator();