105 lines
3.3 KiB
JavaScript
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();
|