waveform-renderer
Version:
High-performance audio waveform visualization library for the web. Create customizable, interactive waveform renderers with TypeScript support and zero dependencies.
836 lines (825 loc) • 24.9 kB
JavaScript
var WaveformRenderer = (function(exports) {
//#region src/constants/default.ts
const DEFAULT_OPTIONS = {
amplitude: 1,
backgroundColor: "#CCCCCC",
barWidth: 2,
borderColor: "#000000",
borderRadius: 0,
borderWidth: 0,
color: "#000000",
gap: 1,
minPixelRatio: 1,
position: "center",
progress: 0,
debug: false,
smoothing: true,
progressLine: {
color: "#FF0000",
heightPercent: 1,
position: "center",
style: "solid",
width: 2
}
};
//#endregion
//#region src/events/EventEmitter.ts
var EventEmitter = class {
events = /* @__PURE__ */ new Map();
off(event, callback) {
const callbacks = this.events.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index !== -1) callbacks.splice(index, 1);
if (callbacks.length === 0) this.events.delete(event);
}
}
on(event, callback) {
if (!this.events.has(event)) this.events.set(event, []);
const callbacks = this.events.get(event);
callbacks.push(callback);
}
once(event, callback) {
const onceCallback = (args) => {
this.off(event, onceCallback);
callback(args);
};
this.on(event, onceCallback);
}
removeAllListeners(event) {
if (event) this.events.delete(event);
else this.events.clear();
}
emit(event, args) {
const callbacks = this.events.get(event);
if (callbacks) callbacks.forEach((callback) => callback(args));
}
hasListeners(event) {
const callbacks = this.events.get(event);
return callbacks ? callbacks.length > 0 : false;
}
};
//#endregion
//#region src/utils/canvas.ts
/**
* Calculates the vertical position and height for a bar based on the render mode
*/
function calculateBarDimensions(peak, canvasHeight, amplitude, position) {
const height = peak * canvasHeight * amplitude;
switch (position) {
case "bottom": return {
height,
y: canvasHeight - height
};
case "top": return {
height,
y: 0
};
case "center":
default: return {
height,
y: (canvasHeight - height) / 2
};
}
}
/**
* Calculates the vertical position for a progress line based on the render mode
*/
function calculateLineDimensions(lineHeight, canvasHeight, position) {
switch (position) {
case "bottom": return {
endY: canvasHeight,
startY: canvasHeight - lineHeight
};
case "top": return {
endY: lineHeight,
startY: 0
};
case "center":
default: return {
endY: (canvasHeight + lineHeight) / 2,
startY: (canvasHeight - lineHeight) / 2
};
}
}
/**
* Draws a progress line on the canvas
*/
function drawProgressLine(ctx, x, canvasHeight, options) {
const { color, heightPercent, position, style, width } = options;
const lineHeight = canvasHeight * heightPercent;
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.lineCap = "round";
const { endY, startY } = calculateLineDimensions(lineHeight, canvasHeight, position);
if (style !== "solid") {
const [dashSize, gapSize] = style === "dashed" ? [8, 4] : [2, 2];
ctx.setLineDash([dashSize, gapSize]);
}
ctx.beginPath();
ctx.moveTo(x, startY);
ctx.lineTo(x, endY);
ctx.stroke();
ctx.restore();
}
/**
* Resizes the canvas accounting for device pixel ratio
*/
function resizeCanvas(canvas, devicePixelRatio) {
const rect = canvas.getBoundingClientRect();
const width = rect.width * devicePixelRatio;
const height = rect.height * devicePixelRatio;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
return {
height: rect.height,
width: rect.width
};
}
/**
* Configures the canvas context with the specified settings
*/
function setupCanvasContext(ctx, smoothing = true) {
ctx.imageSmoothingEnabled = smoothing;
if (smoothing) ctx.imageSmoothingQuality = "high";
}
//#endregion
//#region src/utils/peaks.ts
/**
* Calculates peaks from an AudioBuffer
*/
function getPeaksFromAudioBuffer(audioBuffer, numberOfPeaks) {
const channelData = audioBuffer.getChannelData(0);
const peaks = Array.from({ length: numberOfPeaks });
const samplesPerPeak = Math.floor(channelData.length / numberOfPeaks);
for (let i = 0; i < numberOfPeaks; i++) {
const start = i * samplesPerPeak;
const end = start + samplesPerPeak;
let max = 0;
for (let j = start; j < end; j++) {
const absolute = Math.abs(channelData[j]);
if (absolute > max) max = absolute;
}
peaks[i] = max;
}
return normalizePeaks(peaks);
}
/**
* Normalizes an array of peak values to a range of -1 to 1
*/
function normalizePeaks(peaks) {
let maxPeak = 1;
for (let i = 0; i < peaks.length; i++) {
const peak = Math.abs(peaks[i]);
if (peak > maxPeak) maxPeak = peak;
}
for (let i = 0; i < peaks.length; i++) peaks[i] = peaks[i] / maxPeak;
return peaks;
}
/**
* Ensures progress value is between 0 and 1
*/
function normalizeProgress(progress) {
return Math.max(0, Math.min(1, progress));
}
//#endregion
//#region src/cache-manager.ts
var CacheManager = class {
cache = null;
invalidate() {
if (this.cache) this.cache.staticWaveformPath = void 0;
}
clear() {
this.cache = null;
}
getCache(canvas, devicePixelRatio, peaks, options) {
const currentCanvasWidth = canvas.width / devicePixelRatio;
const currentCanvasHeight = canvas.height / devicePixelRatio;
const currentOptionsHash = this.createOptionsHash(options);
const currentPeaksHash = this.createPeaksHash(peaks);
if (this.isCacheValid(currentCanvasWidth, currentCanvasHeight, currentOptionsHash, currentPeaksHash)) return this.cache;
this.cache = this.buildCache(currentCanvasWidth, currentCanvasHeight, peaks, options, currentOptionsHash, currentPeaksHash);
return this.cache;
}
createStaticPath(cache, borderRadius) {
if (cache.staticWaveformPath) return cache.staticWaveformPath;
const path = new Path2D();
for (const bar of cache.bars) if (borderRadius > 0 && typeof path.roundRect === "function") path.roundRect(bar.x, bar.y, bar.width, bar.height, borderRadius);
else path.rect(bar.x, bar.y, bar.width, bar.height);
cache.staticWaveformPath = path;
return path;
}
isValid() {
return this.cache !== null;
}
isCacheValid(canvasWidth, canvasHeight, optionsHash, peaksHash) {
return this.cache !== null && this.cache.canvasWidth === canvasWidth && this.cache.canvasHeight === canvasHeight && this.cache.lastOptionsHash === optionsHash && this.cache.lastPeaksHash === peaksHash;
}
buildCache(canvasWidth, canvasHeight, peaks, options, optionsHash, peaksHash) {
const { amplitude = 1, barWidth, borderWidth = 0, gap = 0, position } = options;
const initialOffset = borderWidth;
const availableWidth = canvasWidth - borderWidth * 2 * initialOffset;
const singleUnitWidth = barWidth + borderWidth * 2 + gap;
const totalBars = Math.max(1, Math.floor(availableWidth / singleUnitWidth));
const step = peaks.length / totalBars;
const bars = Array.from({ length: totalBars });
for (let i = 0; i < totalBars; i++) {
const peakIndex = Math.floor(i * step);
const peakValue = Math.abs(peaks[peakIndex] || 0);
const x = initialOffset + i * singleUnitWidth;
const { height, y } = calculateBarDimensions(peakValue, canvasHeight, amplitude, position);
bars[i] = {
x,
y,
width: barWidth,
height,
peakValue
};
}
return {
canvasWidth,
canvasHeight,
totalBars,
step,
singleUnitWidth,
bars,
lastOptionsHash: optionsHash,
lastPeaksHash: peaksHash
};
}
createOptionsHash(options) {
const { amplitude, barWidth, borderWidth, gap, position, borderRadius } = options;
return `${amplitude}-${barWidth}-${borderWidth}-${gap}-${position}-${borderRadius}`;
}
createPeaksHash(peaks) {
return `${peaks.length}-${peaks[0] || 0}-${peaks[peaks.length - 1] || 0}`;
}
};
//#endregion
//#region src/debug-system.ts
var DebugSystem = class {
enabled = false;
debugInfo;
renderTimes = [];
lastFrameTime = 0;
constructor() {
this.debugInfo = this.createInitialDebugInfo();
}
enable() {
this.enabled = true;
this.log("Debug mode enabled");
}
disable() {
this.enabled = false;
}
isEnabled() {
return this.enabled;
}
log(message, data) {
if (!this.enabled) return;
const timestamp = performance.now().toFixed(2);
const prefix = `[WaveformRenderer Debug ${timestamp}ms]`;
if (data) console.log(prefix, message, data);
else console.log(prefix, message);
}
updateRenderMetrics(renderTime) {
if (!this.enabled) return;
this.debugInfo.performance.totalRenders++;
this.debugInfo.performance.lastRenderTime = renderTime;
this.renderTimes.push(renderTime);
if (this.renderTimes.length > 60) this.renderTimes.shift();
this.debugInfo.performance.averageRenderTime = this.renderTimes.reduce((sum, time) => sum + time, 0) / this.renderTimes.length;
const now = performance.now();
if (this.lastFrameTime > 0) {
const deltaTime = now - this.lastFrameTime;
this.debugInfo.performance.fps = Math.round(1e3 / deltaTime);
}
this.lastFrameTime = now;
}
updateCacheMetrics(buildTime) {
if (!this.enabled) return;
this.debugInfo.performance.cacheBuilds++;
this.debugInfo.performance.lastCacheBuildTime = buildTime;
}
updateState(canvas, peaksCount, barsRendered, cacheValid, dirtyFlags) {
if (!this.enabled) return;
const rect = canvas.getBoundingClientRect();
this.debugInfo.state = {
canvasSize: {
width: rect.width,
height: rect.height
},
peaksCount,
barsRendered,
cacheValid,
dirtyFlags: { ...dirtyFlags }
};
}
incrementSeeks() {
this.debugInfo.events.totalSeeks++;
}
incrementResizes() {
this.debugInfo.events.totalResizes++;
}
incrementErrors() {
this.debugInfo.events.totalErrors++;
}
getInfo() {
return JSON.parse(JSON.stringify(this.debugInfo));
}
reset() {
this.debugInfo.performance.totalRenders = 0;
this.debugInfo.performance.averageRenderTime = 0;
this.debugInfo.performance.cacheBuilds = 0;
this.debugInfo.events.totalSeeks = 0;
this.debugInfo.events.totalResizes = 0;
this.debugInfo.events.totalErrors = 0;
this.renderTimes = [];
this.log("Debug counters reset");
}
logPerformanceSummary() {
if (!this.enabled || this.debugInfo.performance.totalRenders % 60 !== 0) return;
this.log("Performance summary", {
totalRenders: this.debugInfo.performance.totalRenders,
averageRenderTime: this.debugInfo.performance.averageRenderTime.toFixed(2) + "ms",
fps: this.debugInfo.performance.fps,
cacheBuilds: this.debugInfo.performance.cacheBuilds
});
}
createInitialDebugInfo() {
return {
performance: {
lastRenderTime: 0,
averageRenderTime: 0,
totalRenders: 0,
fps: 0,
cacheBuilds: 0,
lastCacheBuildTime: 0
},
state: {
canvasSize: {
width: 0,
height: 0
},
peaksCount: 0,
barsRendered: 0,
cacheValid: false,
dirtyFlags: {
peaks: true,
options: true,
size: true,
progress: true
}
},
events: {
totalSeeks: 0,
totalResizes: 0,
totalErrors: 0
}
};
}
};
//#endregion
//#region src/event-handler.ts
var EventHandlerManager = class {
canvas;
resizeObserver;
callbacks;
resizeTimeout;
resizeDebounceDelay = 150;
constructor(canvas, callbacks) {
this.canvas = canvas;
this.callbacks = callbacks;
this.resizeObserver = new ResizeObserver(this.handleResize);
this.resizeObserver.observe(this.canvas);
this.attachEventListeners();
}
destroy() {
this.detachEventListeners();
this.resizeObserver.disconnect();
if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
}
attachEventListeners() {
this.canvas.addEventListener("click", this.handleClick);
this.canvas.addEventListener("touchstart", this.handleTouch);
}
detachEventListeners() {
this.canvas.removeEventListener("click", this.handleClick);
this.canvas.removeEventListener("touchstart", this.handleTouch);
}
handleClick = (event) => {
event.preventDefault();
try {
const progress = this.calculateProgressFromEvent(event);
this.callbacks.onSeek(progress);
} catch (e) {
this.handleError(e);
}
};
handleTouch = (event) => {
event.preventDefault();
if (!event.changedTouches[0]) return;
try {
const progress = this.calculateProgressFromTouch(event.changedTouches[0]);
this.callbacks.onSeek(progress);
} catch (e) {
this.handleError(e);
}
};
handleResize = () => {
if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
this.resizeTimeout = window.setTimeout(() => {
try {
const rect = this.canvas.getBoundingClientRect();
this.callbacks.onResize({
height: rect.height,
width: rect.width
});
} catch (e) {
this.handleError(e);
}
}, this.resizeDebounceDelay);
};
calculateProgressFromEvent(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
return normalizeProgress(x / rect.width);
}
calculateProgressFromTouch(touch) {
const rect = this.canvas.getBoundingClientRect();
const x = touch.clientX - rect.left;
return normalizeProgress(x / rect.width);
}
handleError(e) {
const error = e instanceof Error ? e : new Error("An unknown error occurred");
this.callbacks.onError(error);
}
};
//#endregion
//#region src/rendering-engine.ts
var RenderingEngine = class {
ctx;
callbacks;
customRenderer;
hooks = {};
constructor(ctx, callbacks) {
this.ctx = ctx;
this.callbacks = callbacks;
}
setCustomRenderer(renderer) {
this.customRenderer = renderer;
}
setHooks(hooks) {
this.hooks = { ...hooks };
}
clearHooks() {
this.hooks = {};
}
render(cache, options, staticPath) {
this.callbacks.onRenderStart();
try {
this.hooks.beforeRender?.(this.ctx, cache, options);
if (this.customRenderer?.render(this.ctx, cache, options, staticPath)) {
this.callbacks.onRenderComplete();
return;
}
const { backgroundColor, color, progress } = options;
this.ctx.clearRect(0, 0, cache.canvasWidth, cache.canvasHeight);
if (this.shouldUseFallbackRendering(options, staticPath)) this.renderWithFallback(cache, backgroundColor, options);
else this.renderWithPath(staticPath, backgroundColor, options);
this.hooks.afterBackground?.(this.ctx, cache, options);
if (progress > 0) {
if (this.shouldUseFallbackRendering(options, staticPath)) this.renderProgressWithFallback(cache, color, progress, options);
else this.renderProgressWithPath(staticPath, color, progress, cache.canvasWidth, options);
this.hooks.afterProgress?.(this.ctx, cache, options, progress);
}
if (options.progressLine && progress > 0) {
const x = cache.canvasWidth * progress;
drawProgressLine(this.ctx, x, cache.canvasHeight, options.progressLine);
}
this.hooks.afterComplete?.(this.ctx, cache, options);
this.callbacks.onRenderComplete();
} catch (error) {
throw error;
}
}
shouldUseFallbackRendering(options, staticPath) {
return options.borderRadius > 0 && (!staticPath || typeof Path2D.prototype.roundRect !== "function");
}
renderWithPath(path, backgroundColor, options) {
this.ctx.fillStyle = backgroundColor;
this.ctx.fill(path);
if (options.borderWidth > 0) {
this.ctx.strokeStyle = options.borderColor;
this.ctx.lineWidth = options.borderWidth;
this.ctx.stroke(path);
}
}
renderProgressWithPath(path, color, progress, canvasWidth, options) {
this.ctx.save();
const progressWidth = canvasWidth * progress;
this.ctx.beginPath();
this.ctx.rect(0, 0, progressWidth, canvasWidth);
this.ctx.clip();
this.ctx.fillStyle = color;
this.ctx.fill(path);
if (options.borderWidth > 0) {
this.ctx.strokeStyle = options.borderColor;
this.ctx.stroke(path);
}
this.ctx.restore();
}
renderWithFallback(cache, color, options) {
this.renderBarsWithFallback(cache.bars, color, options);
}
renderProgressWithFallback(cache, color, progress, options) {
this.ctx.save();
const progressWidth = cache.canvasWidth * progress;
this.ctx.beginPath();
this.ctx.rect(0, 0, progressWidth, cache.canvasHeight);
this.ctx.clip();
this.renderBarsWithFallback(cache.bars, color, options);
this.ctx.restore();
}
renderBarsWithFallback(bars, color, options) {
const { borderColor, borderRadius, borderWidth = 0 } = options;
this.ctx.fillStyle = color;
if (borderWidth > 0) {
this.ctx.strokeStyle = borderColor;
this.ctx.lineWidth = borderWidth;
}
this.ctx.beginPath();
for (const bar of bars) if (borderRadius > 0 && typeof this.ctx.roundRect === "function") this.ctx.roundRect(bar.x, bar.y, bar.width, bar.height, borderRadius);
else this.ctx.rect(bar.x, bar.y, bar.width, bar.height);
this.ctx.fill();
if (borderWidth > 0) this.ctx.stroke();
}
};
//#endregion
//#region src/renderer.ts
var WaveformRenderer = class extends EventEmitter {
canvas;
ctx;
devicePixelRatio;
cacheManager;
debugSystem;
eventHandler;
renderingEngine;
isDestroyed = false;
options;
peaks = [];
dirtyFlags = {
peaks: true,
options: true,
size: true,
progress: true
};
frameRequest;
lastRenderTime = 0;
minRenderInterval = 16;
constructor(canvas, peaks, options = {}) {
super();
const startTime = performance.now();
try {
this.validateInputs(canvas, peaks);
this.canvas = canvas;
this.ctx = this.getCanvasContext(canvas);
this.peaks = normalizePeaks(peaks);
this.options = this.mergeOptions(options);
this.devicePixelRatio = Math.max(window.devicePixelRatio || 1, this.options.minPixelRatio);
this.cacheManager = new CacheManager();
this.debugSystem = new DebugSystem();
this.renderingEngine = new RenderingEngine(this.ctx, {
onRenderStart: () => this.emit("renderStart", void 0),
onRenderComplete: () => this.emit("renderComplete", void 0)
});
this.eventHandler = new EventHandlerManager(this.canvas, {
onSeek: (progress) => this.handleSeek(progress),
onResize: (dimensions) => this.handleResize(dimensions),
onError: (error) => this.handleError(error)
});
if (this.options.debug) this.debugSystem.enable();
this.setupCanvas();
this.scheduleRender();
const initTime = performance.now() - startTime;
this.debugSystem.log(`Initialized in ${initTime.toFixed(2)}ms`);
requestAnimationFrame(() => this.emit("ready", void 0));
} catch (e) {
this.handleError(e);
}
}
destroy() {
if (this.isDestroyed) return;
this.debugSystem.log("Destroying renderer");
this.emit("destroy", void 0);
this.isDestroyed = true;
this.eventHandler.destroy();
this.cancelPendingRender();
this.cacheManager.clear();
}
setOptions(options) {
if (this.isDestroyed) return;
const startTime = performance.now();
const oldOptions = this.options;
this.options = this.mergeOptions(options, this.options);
if (options.debug !== void 0) if (options.debug) this.debugSystem.enable();
else this.debugSystem.disable();
this.updateDirtyFlags(oldOptions, this.options);
this.setupContext();
this.scheduleRender();
const setOptionsTime = performance.now() - startTime;
this.debugSystem.log(`setOptions completed in ${setOptionsTime.toFixed(2)}ms`);
}
setPeaks(peaks) {
if (this.isDestroyed) return;
const startTime = performance.now();
try {
if (!Array.isArray(peaks) || peaks.length === 0) throw new Error("Peaks array must not be empty");
this.peaks = normalizePeaks([...peaks]);
this.dirtyFlags.peaks = true;
this.cacheManager.invalidate();
this.scheduleRender();
const setPeaksTime = performance.now() - startTime;
this.debugSystem.log(`setPeaks completed in ${setPeaksTime.toFixed(2)}ms, ${peaks.length} peaks`);
} catch (e) {
this.handleError(e);
}
}
setProgress(progress) {
if (this.isDestroyed) return;
try {
const normalizedProgress = normalizeProgress(progress);
if (Math.abs(this.options.progress - normalizedProgress) < .001) return;
this.options.progress = normalizedProgress;
this.dirtyFlags.progress = true;
this.emit("progressChange", normalizedProgress);
this.scheduleRender();
this.debugSystem.log(`Progress set to ${(normalizedProgress * 100).toFixed(1)}%`);
} catch (e) {
this.handleError(e);
}
}
setProgressLineOptions(options) {
if (this.isDestroyed) return;
try {
if (options) this.options.progressLine = {
...DEFAULT_OPTIONS.progressLine,
...this.options.progressLine,
...options
};
else this.options.progressLine = null;
this.dirtyFlags.options = true;
this.scheduleRender();
this.debugSystem.log(`Progress line options ${options ? "updated" : "disabled"}`);
} catch (e) {
this.handleError(e);
}
}
setDebug(enabled) {
if (enabled) this.debugSystem.enable();
else this.debugSystem.disable();
this.options.debug = enabled;
}
getDebugInfo() {
return this.debugSystem.getInfo();
}
resetDebugCounters() {
this.debugSystem.reset();
}
setCustomRenderer(renderer) {
this.renderingEngine.setCustomRenderer(renderer);
this.debugSystem.log(`Custom renderer ${renderer ? "set" : "cleared"}`);
}
setRenderHooks(hooks) {
this.renderingEngine.setHooks(hooks);
this.debugSystem.log("Render hooks configured");
}
clearRenderHooks() {
this.renderingEngine.clearHooks();
this.debugSystem.log("Render hooks cleared");
}
validateInputs(canvas, peaks) {
if (!canvas) throw new Error("Canvas element is required");
if (!Array.isArray(peaks) || peaks.length === 0) throw new Error("Peaks array is required and must not be empty");
}
getCanvasContext(canvas) {
const context = canvas.getContext("2d");
if (!context) throw new Error("Could not get 2D context from canvas");
return context;
}
mergeOptions(newOptions, baseOptions) {
const base = baseOptions || DEFAULT_OPTIONS;
return {
...base,
...newOptions,
progressLine: newOptions.progressLine !== void 0 ? newOptions.progressLine ? {
...DEFAULT_OPTIONS.progressLine,
...base.progressLine,
...newOptions.progressLine
} : null : base.progressLine
};
}
updateDirtyFlags(oldOptions, newOptions) {
const layoutKeys = [
"amplitude",
"backgroundColor",
"barWidth",
"borderColor",
"borderRadius",
"borderWidth",
"color",
"gap",
"position"
];
const hasLayoutChanges = layoutKeys.some((key) => oldOptions[key] !== newOptions[key]);
if (hasLayoutChanges) {
this.dirtyFlags.options = true;
this.cacheManager.invalidate();
this.debugSystem.log("Layout-affecting options changed, invalidating cache");
}
const progressChanged = oldOptions.progress !== newOptions.progress;
if (progressChanged && !hasLayoutChanges) {
this.dirtyFlags.progress = true;
this.debugSystem.log("Progress changed");
}
}
setupCanvas() {
resizeCanvas(this.canvas, this.devicePixelRatio);
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.scale(this.devicePixelRatio, this.devicePixelRatio);
this.setupContext();
}
setupContext() {
setupCanvasContext(this.ctx, this.options.smoothing);
}
scheduleRender() {
if (this.frameRequest) return;
this.frameRequest = requestAnimationFrame(() => {
this.frameRequest = void 0;
this.render();
});
}
cancelPendingRender() {
if (this.frameRequest) {
cancelAnimationFrame(this.frameRequest);
this.frameRequest = void 0;
}
}
render() {
if (this.isDestroyed) return;
const now = performance.now();
if (now - this.lastRenderTime < this.minRenderInterval) {
this.scheduleRender();
return;
}
this.lastRenderTime = now;
const renderStartTime = performance.now();
try {
const cache = this.cacheManager.getCache(this.canvas, this.devicePixelRatio, this.peaks, this.options);
const staticPath = this.cacheManager.createStaticPath(cache, this.options.borderRadius);
this.renderingEngine.render(cache, this.options, staticPath);
this.dirtyFlags = {
peaks: false,
options: false,
size: false,
progress: false
};
const renderTime = performance.now() - renderStartTime;
this.debugSystem.updateRenderMetrics(renderTime);
this.debugSystem.updateState(this.canvas, this.peaks.length, cache.totalBars, this.cacheManager.isValid(), this.dirtyFlags);
this.debugSystem.logPerformanceSummary();
} catch (e) {
this.handleError(e);
}
}
handleSeek(progress) {
this.debugSystem.incrementSeeks();
this.debugSystem.log(`Seek to ${(progress * 100).toFixed(1)}%`);
this.emit("seek", progress);
}
handleResize(dimensions) {
this.debugSystem.incrementResizes();
this.debugSystem.log(`Canvas resized to ${dimensions.width}x${dimensions.height}`);
this.emit("resize", dimensions);
this.dirtyFlags.size = true;
this.cacheManager.invalidate();
this.setupCanvas();
this.scheduleRender();
}
handleError(e) {
this.debugSystem.incrementErrors();
const error = e instanceof Error ? e : new Error("An unknown error occurred");
this.debugSystem.log("Error occurred", error.message);
console.error("WaveformRenderer error:", e);
this.emit("error", error);
}
};
//#endregion
exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
exports.WaveformRenderer = WaveformRenderer;
exports.getPeaksFromAudioBuffer = getPeaksFromAudioBuffer;
return exports;
})({});