wavesurfer.js
Version:
Audio waveform player
214 lines (213 loc) • 7.55 kB
JavaScript
/**
* State-driven event emission utilities
*
* Automatically emit events when reactive state changes.
* Ensures events are always in sync with state and removes manual emit() calls.
*/
import { effect } from './store.js';
/**
* Setup automatic event emission from state changes
*
* This function subscribes to all relevant state signals and automatically
* emits corresponding events when state changes. This ensures:
* - Events are always in sync with state
* - No manual emit() calls needed
* - Can't forget to emit an event
* - Clear event sources (state changes)
*
* @example
* ```typescript
* const { state } = createWaveSurferState()
* const wavesurfer = new WaveSurfer()
*
* const cleanup = setupStateEventEmission(state, wavesurfer)
*
* // Now state changes automatically emit events
* state.isPlaying.set(true) // → wavesurfer.emit('play')
* ```
*
* @param state - Reactive state to observe
* @param emitter - Event emitter to emit events on
* @returns Cleanup function that removes all subscriptions
*/
export function setupStateEventEmission(state, emitter) {
const cleanups = [];
// ============================================================================
// Play/Pause Events
// ============================================================================
// Emit play/pause events when playing state changes
cleanups.push(effect(() => {
const isPlaying = state.isPlaying.value;
emitter.emit(isPlaying ? 'play' : 'pause');
}, [state.isPlaying]));
// ============================================================================
// Time Update Events
// ============================================================================
// Emit timeupdate when current time changes
cleanups.push(effect(() => {
const currentTime = state.currentTime.value;
emitter.emit('timeupdate', currentTime);
// Also emit audioprocess when playing
if (state.isPlaying.value) {
emitter.emit('audioprocess', currentTime);
}
}, [state.currentTime, state.isPlaying]));
// ============================================================================
// Seeking Events
// ============================================================================
// Emit seeking event when seeking state changes to true
cleanups.push(effect(() => {
const isSeeking = state.isSeeking.value;
if (isSeeking) {
emitter.emit('seeking', state.currentTime.value);
}
}, [state.isSeeking, state.currentTime]));
// ============================================================================
// Ready Event
// ============================================================================
// Emit ready when state becomes ready
let wasReady = false;
cleanups.push(effect(() => {
const isReady = state.isReady.value;
if (isReady && !wasReady) {
wasReady = true;
emitter.emit('ready', state.duration.value);
}
}, [state.isReady, state.duration]));
// ============================================================================
// Finish Event
// ============================================================================
// Emit finish when playback ends (reached duration and stopped)
let wasPlayingAtEnd = false;
cleanups.push(effect(() => {
const isPlaying = state.isPlaying.value;
const currentTime = state.currentTime.value;
const duration = state.duration.value;
// Check if we're at the end
const isAtEnd = duration > 0 && currentTime >= duration;
// Emit finish when we were playing at end and now stopped
if (wasPlayingAtEnd && !isPlaying && isAtEnd) {
emitter.emit('finish');
}
// Track if we're playing at the end
wasPlayingAtEnd = isPlaying && isAtEnd;
}, [state.isPlaying, state.currentTime, state.duration]));
// ============================================================================
// Zoom Events
// ============================================================================
// Emit zoom when zoom level changes
cleanups.push(effect(() => {
const zoom = state.zoom.value;
if (zoom > 0) {
emitter.emit('zoom', zoom);
}
}, [state.zoom]));
// Return cleanup function
return () => {
cleanups.forEach((cleanup) => cleanup());
};
}
/**
* Setup custom event emission from signal changes
*
* This is a lower-level utility for setting up custom event emission
* from any signal. Useful when you need more control over event emission logic.
*
* @example
* ```typescript
* const volumeSignal = signal(1)
*
* const cleanup = setupSignalEventEmission(
* volumeSignal,
* emitter,
* (volume) => ['volume', volume]
* )
* ```
*
* @param signal - Signal to observe
* @param emitter - Event emitter
* @param getEventData - Function that returns [eventName, ...args]
* @returns Cleanup function
*/
export function setupSignalEventEmission(signal, emitter, getEventData) {
return effect(() => {
const value = signal.value;
const [eventName, ...args] = getEventData(value);
emitter.emit(eventName, ...args);
}, [signal]);
}
/**
* Setup event emission with debouncing
*
* Useful for high-frequency events like scroll or timeupdate.
*
* @example
* ```typescript
* const cleanup = setupDebouncedEventEmission(
* state.scrollPosition,
* emitter,
* (pos) => ['scroll', pos],
* 100 // debounce 100ms
* )
* ```
*
* @param signal - Signal to observe
* @param emitter - Event emitter
* @param getEventData - Function that returns [eventName, ...args]
* @param debounceMs - Debounce delay in milliseconds
* @returns Cleanup function
*/
export function setupDebouncedEventEmission(signal, emitter, getEventData, debounceMs) {
let timeoutId = null;
const cleanup = effect(() => {
const value = signal.value;
// Clear previous timeout
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
// Set new timeout
timeoutId = setTimeout(() => {
const [eventName, ...args] = getEventData(value);
emitter.emit(eventName, ...args);
timeoutId = null;
}, debounceMs);
}, [signal]);
// Return cleanup that also clears pending timeout
return () => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
cleanup();
};
}
/**
* Setup conditional event emission
*
* Only emit events when a condition is met.
*
* @example
* ```typescript
* // Only emit finish event when playing stops at end
* const cleanup = setupConditionalEventEmission(
* state.isPlaying,
* emitter,
* (isPlaying) => !isPlaying && state.currentTime.value >= state.duration.value,
* () => ['finish']
* )
* ```
*
* @param signal - Signal to observe
* @param emitter - Event emitter
* @param condition - Function that returns true when event should emit
* @param getEventData - Function that returns [eventName, ...args]
* @returns Cleanup function
*/
export function setupConditionalEventEmission(signal, emitter, condition, getEventData) {
return effect(() => {
const value = signal.value;
if (condition(value)) {
const [eventName, ...args] = getEventData(value);
emitter.emit(eventName, ...args);
}
}, [signal]);
}