Interactive audio waveform rendering and playback library for web applications
—
Extensible plugin architecture for adding custom functionality to WaveSurfer with plugin registration, lifecycle management, and built-in plugin utilities.
Register, unregister, and manage plugins throughout the WaveSurfer lifecycle.
interface WaveSurfer {
/**
* Register a wavesurfer.js plugin
* @param plugin - Plugin instance to register
* @returns The registered plugin instance
*/
registerPlugin<T extends GenericPlugin>(plugin: T): T;
/**
* Unregister a wavesurfer.js plugin
* @param plugin - Plugin instance to unregister
*/
unregisterPlugin(plugin: GenericPlugin): void;
/**
* Get all currently active plugins
* @returns Array of active plugin instances
*/
getActivePlugins(): GenericPlugin[];
}
type GenericPlugin = BasePlugin<BasePluginEvents, unknown>;Usage Examples:
import WaveSurfer from "wavesurfer.js";
import Regions from "wavesurfer.js/dist/plugins/regions.esm.js";
import Timeline from "wavesurfer.js/dist/plugins/timeline.esm.js";
// Register plugins during creation
const wavesurfer = WaveSurfer.create({
container: "#waveform",
plugins: [
Regions.create(),
Timeline.create({ height: 30 }),
],
});
// Register plugins after creation
const minimap = Minimap.create({ height: 60 });
wavesurfer.registerPlugin(minimap);
// Unregister specific plugin
wavesurfer.unregisterPlugin(minimap);
// Check active plugins
const activePlugins = wavesurfer.getActivePlugins();
console.log(`${activePlugins.length} plugins active`);
// Find specific plugin
const regionsPlugin = activePlugins.find(p => p.constructor.name === 'RegionsPlugin');
if (regionsPlugin) {
console.log("Regions plugin is active");
}Base class for creating custom plugins with event handling and lifecycle management.
/**
* Base class for wavesurfer plugins with event handling and lifecycle management
*/
class BasePlugin<EventTypes extends BasePluginEvents, Options> {
/**
* Create a plugin instance
* @param options - Plugin configuration options
*/
constructor(options: Options);
/**
* Destroy the plugin and unsubscribe from all events
* Automatically called when WaveSurfer is destroyed
*/
destroy(): void;
/** Reference to the WaveSurfer instance (available after registration) */
protected wavesurfer?: WaveSurfer;
/** Array of event unsubscribe functions for cleanup */
protected subscriptions: (() => void)[];
/** Plugin configuration options */
protected options: Options;
/** Called after plugin is registered and wavesurfer is available */
protected onInit(): void;
}
interface BasePluginEvents {
/** Fired when plugin is destroyed */
destroy: [];
}Usage Examples:
// Custom plugin example
class CustomAnalyzerPlugin extends BasePlugin {
constructor(options = {}) {
super(options);
}
// Called when plugin is registered
protected onInit() {
if (!this.wavesurfer) return;
// Subscribe to wavesurfer events
this.subscriptions.push(
this.wavesurfer.on("ready", () => {
this.analyzeAudio();
}),
this.wavesurfer.on("timeupdate", (time) => {
this.updateAnalysis(time);
})
);
}
private analyzeAudio() {
const audioBuffer = this.wavesurfer?.getDecodedData();
if (audioBuffer) {
console.log("Analyzing audio data...");
// Custom analysis logic
}
}
private updateAnalysis(time) {
// Real-time analysis updates
}
}
// Use custom plugin
const analyzer = new CustomAnalyzerPlugin({ threshold: 0.5 });
wavesurfer.registerPlugin(analyzer);Plugins can emit and listen to events for communication with the main application.
interface BasePlugin<EventTypes, Options> {
/**
* Subscribe to plugin events
* @param event - Event name
* @param listener - Event callback
* @returns Unsubscribe function
*/
on<EventName extends keyof EventTypes>(
event: EventName,
listener: (...args: EventTypes[EventName]) => void
): () => void;
/**
* Subscribe to plugin event once
* @param event - Event name
* @param listener - Event callback
* @returns Unsubscribe function
*/
once<EventName extends keyof EventTypes>(
event: EventName,
listener: (...args: EventTypes[EventName]) => void
): () => void;
/**
* Emit plugin event (protected method for plugin use)
* @param eventName - Event name
* @param args - Event arguments
*/
protected emit<EventName extends keyof EventTypes>(
eventName: EventName,
...args: EventTypes[EventName]
): void;
}Usage Examples:
// Plugin with custom events
interface CustomPluginEvents extends BasePluginEvents {
"analysis-complete": [results: AnalysisResults];
"threshold-exceeded": [value: number, time: number];
}
class CustomAnalyzerPlugin extends BasePlugin<CustomPluginEvents, AnalyzerOptions> {
private analyzeAudio() {
// Perform analysis...
const results = { peak: 0.8, rms: 0.3 };
// Emit custom event
this.emit("analysis-complete", results);
if (results.peak > this.options.threshold) {
this.emit("threshold-exceeded", results.peak, this.wavesurfer.getCurrentTime());
}
}
}
// Listen to plugin events
const analyzer = new CustomAnalyzerPlugin({ threshold: 0.7 });
wavesurfer.registerPlugin(analyzer);
analyzer.on("analysis-complete", (results) => {
console.log("Analysis results:", results);
updateVisualization(results);
});
analyzer.on("threshold-exceeded", (value, time) => {
console.warn(`Threshold exceeded: ${value} at ${time}s`);
showWarning(`High volume detected at ${formatTime(time)}`);
});Configure plugins with type-safe options and runtime updates.
// Plugin options are defined by each plugin implementation
interface PluginOptions {
[key: string]: any;
}
class BasePlugin<EventTypes, Options> {
/** Plugin configuration options */
protected options: Options;
}Usage Examples:
// Plugin with typed options
interface VisualizerOptions {
color?: string;
height?: number;
updateInterval?: number;
smoothing?: boolean;
}
class VisualizerPlugin extends BasePlugin<BasePluginEvents, VisualizerOptions> {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
protected onInit() {
this.createCanvas();
this.startVisualization();
}
private createCanvas() {
this.canvas = document.createElement("canvas");
this.canvas.height = this.options.height || 100;
this.canvas.style.backgroundColor = this.options.color || "#000";
this.ctx = this.canvas.getContext("2d");
this.wavesurfer.getWrapper().appendChild(this.canvas);
}
// Method to update options after creation
updateOptions(newOptions: Partial<VisualizerOptions>) {
this.options = { ...this.options, ...newOptions };
// Apply updates
if (newOptions.color) {
this.canvas.style.backgroundColor = newOptions.color;
}
if (newOptions.height) {
this.canvas.height = newOptions.height;
}
}
}
// Create with options
const visualizer = new VisualizerPlugin({
color: "#1a1a1a",
height: 120,
updateInterval: 60,
smoothing: true,
});
// Update options later
visualizer.updateOptions({
color: "#2a2a2a",
smoothing: false,
});Access WaveSurfer utilities and DOM helpers for plugin development.
interface WaveSurfer {
/** For plugins: get the waveform wrapper div */
getWrapper(): HTMLElement;
/** For plugins: get the scroll container client width */
getWidth(): number;
}
// Static utilities available to plugins
WaveSurfer.BasePlugin: typeof BasePlugin;
WaveSurfer.dom: {
createElement(tagName: string, content?: object, container?: Node): HTMLElement;
};Usage Examples:
class OverlayPlugin extends BasePlugin {
private overlay: HTMLElement;
protected onInit() {
this.createOverlay();
this.positionOverlay();
}
private createOverlay() {
// Use WaveSurfer DOM utilities
this.overlay = WaveSurfer.dom.createElement("div", {
style: {
position: "absolute",
top: "0",
left: "0",
pointerEvents: "none",
background: "rgba(255, 0, 0, 0.3)",
height: "100%",
width: "100px",
},
});
// Add to waveform wrapper
const wrapper = this.wavesurfer.getWrapper();
wrapper.appendChild(this.overlay);
}
private positionOverlay() {
const width = this.wavesurfer.getWidth();
// Position overlay relative to waveform
this.overlay.style.left = `${width * 0.25}px`;
this.overlay.style.width = `${width * 0.5}px`;
}
// Update on resize
private updateOverlay() {
this.positionOverlay();
}
}
// Plugin development utilities
class DeveloperPlugin extends BasePlugin {
protected onInit() {
// Access wrapper for DOM manipulation
const wrapper = this.wavesurfer.getWrapper();
console.log("Wrapper element:", wrapper);
// Monitor width changes
this.subscriptions.push(
this.wavesurfer.on("redraw", () => {
const width = this.wavesurfer.getWidth();
console.log(`Waveform width: ${width}px`);
})
);
}
}Enable plugins to communicate with each other and integrate with external systems.
// Plugins can access other plugins through the WaveSurfer instance
interface WaveSurfer {
getActivePlugins(): GenericPlugin[];
}Usage Examples:
// Plugin that integrates with other plugins
class IntegratorPlugin extends BasePlugin {
private regionsPlugin: any;
private timelinePlugin: any;
protected onInit() {
// Find other plugins
const plugins = this.wavesurfer.getActivePlugins();
this.regionsPlugin = plugins.find(p =>
p.constructor.name === 'RegionsPlugin'
);
this.timelinePlugin = plugins.find(p =>
p.constructor.name === 'TimelinePlugin'
);
if (this.regionsPlugin) {
this.setupRegionIntegration();
}
}
private setupRegionIntegration() {
// Listen to region events
this.regionsPlugin.on("region-created", (region) => {
console.log(`New region: ${region.start}s - ${region.end}s`);
this.syncWithTimeline(region);
});
}
private syncWithTimeline(region) {
if (this.timelinePlugin) {
// Custom integration logic
console.log("Syncing region with timeline");
}
}
}
// Plugin factory pattern for configuration
class PluginFactory {
static createAnalyzerSuite(options = {}) {
return [
new SpectrumAnalyzer(options.spectrum),
new LevelMeter(options.levels),
new PeakDetector(options.peaks),
];
}
}
// Use plugin suite
const analyzerPlugins = PluginFactory.createAnalyzerSuite({
spectrum: { fftSize: 2048 },
levels: { updateRate: 30 },
peaks: { threshold: 0.8 },
});
analyzerPlugins.forEach(plugin => {
wavesurfer.registerPlugin(plugin);
});Handle plugin initialization, cleanup, and state management.
class BasePlugin<EventTypes, Options> {
/** Internal method called by WaveSurfer when plugin is registered */
_init(wavesurfer: WaveSurfer): void;
/** Override this method for plugin initialization logic */
protected onInit(): void;
/** Clean up plugin resources and event listeners */
destroy(): void;
}Usage Examples:
// Plugin with complex lifecycle
class ResourceIntensivePlugin extends BasePlugin {
private worker: Worker;
private animationId: number;
private canvas: HTMLCanvasElement;
protected onInit() {
this.initWorker();
this.initCanvas();
this.startAnimation();
// Clean up on wavesurfer destruction
this.subscriptions.push(
this.wavesurfer.on("destroy", () => {
this.cleanup();
})
);
}
private initWorker() {
this.worker = new Worker("/audio-processor-worker.js");
this.worker.onmessage = (event) => {
this.handleWorkerMessage(event.data);
};
}
private initCanvas() {
this.canvas = document.createElement("canvas");
this.wavesurfer.getWrapper().appendChild(this.canvas);
}
private startAnimation() {
const animate = () => {
this.render();
this.animationId = requestAnimationFrame(animate);
};
animate();
}
private cleanup() {
// Cancel animation
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
// Terminate worker
if (this.worker) {
this.worker.terminate();
}
// Remove canvas
if (this.canvas && this.canvas.parentNode) {
this.canvas.parentNode.removeChild(this.canvas);
}
}
// Override destroy to include custom cleanup
destroy() {
this.cleanup();
super.destroy(); // Call parent cleanup
}
}
// Plugin state management
class StatefulPlugin extends BasePlugin {
private state = {
initialized: false,
active: false,
data: null,
};
protected onInit() {
this.state.initialized = true;
this.subscriptions.push(
this.wavesurfer.on("play", () => {
this.state.active = true;
this.onActivate();
}),
this.wavesurfer.on("pause", () => {
this.state.active = false;
this.onDeactivate();
})
);
}
private onActivate() {
console.log("Plugin activated");
// Start processing
}
private onDeactivate() {
console.log("Plugin deactivated");
// Pause processing
}
// Public method to check state
isActive() {
return this.state.active;
}
}Install with Tessl CLI
npx tessl i tessl/npm-wavesurfer-js