Skip to main content

Event System

Understanding the comprehensive event system that powers real-time audio monitoring and reactive programming patterns.

What is the Event System?​

The AudioQ package provides a rich event system that allows you to react to audio playback changes, queue modifications, and progress updates in real-time. Events are channel-specific and fire automatically during audio operations.

import { 
onAudioStart,
onAudioComplete,
onAudioProgress,
onQueueChange,
offAudioStart,
offAudioComplete,
offAudioProgress,
offQueueChange
} from 'audioq';

// React to audio starting
const cleanupStart = onAudioStart(0, (info) => {
console.log(`Started playing: ${info.fileName}`);
});

// React to audio completing
const cleanupComplete = onAudioComplete(0, (info) => {
console.log(`Finished: ${info.fileName}`);
console.log(`Remaining in queue: ${info.remainingInQueue}`);
});

// Track playback progress
const cleanupProgress = onAudioProgress(0, (info) => {
console.log(`Progress: ${Math.round(info.progress * 100)}%`);
});

// Monitor queue changes
const cleanupQueue = onQueueChange(0, (snapshot) => {
console.log(`Queue now has ${snapshot.totalItems} items`);
});

Core Event Types​

Audio Start Events​

Fired when audio begins playing on a channel:

import { onAudioStart, AudioStartInfo } from 'audioq';

onAudioStart(0, (info: AudioStartInfo) => {
console.log('Audio started:', {
fileName: info.fileName,
duration: `${Math.round(info.duration / 1000)} seconds`,
volume: `${Math.round(info.volume * 100)}%`,
currentTime: info.currentTime
});

// Example: Update UI
updateNowPlaying(info.fileName);
setProgressBarMax(info.duration);
});

function updateNowPlaying(fileName: string): void {
const element = document.getElementById('now-playing');
if (element) {
element.textContent = `Now Playing: ${fileName}`;
}
}

function setProgressBarMax(duration: number): void {
const progressBar = document.getElementById('progress') as HTMLProgressElement;
if (progressBar) {
progressBar.max = duration;
}
}

Audio Complete Events​

Fired when audio finishes playing (naturally or interrupted):

import { onAudioComplete, AudioCompleteInfo } from 'audioq';

onAudioComplete(0, (info: AudioCompleteInfo) => {
console.log('Audio completed:', {
fileName: info.fileName,
channelNumber: info.channelNumber,
remainingInQueue: info.remainingInQueue,
src: info.src
});
});

Audio Progress Events​

Fired continuously during audio playback for real-time progress tracking:

import { onAudioProgress, AudioInfo } from 'audioq';

onAudioProgress(0, (info: AudioInfo) => {
const percentage = Math.round(info.progress * 100);
const currentSeconds = Math.floor(info.currentTime / 1000);
const totalSeconds = Math.floor(info.duration / 1000);

// Update progress display
updateProgressDisplay(percentage, currentSeconds, totalSeconds);

// Trigger events at specific progress points
if (info.progress >= 0.25 && info.progress < 0.26) {
console.log('25% complete');
onQuarterProgress(info.fileName);
}

if (info.progress >= 0.5 && info.progress < 0.51) {
console.log('50% complete');
onHalfwayProgress(info.fileName);
}

if (info.progress >= 0.75 && info.progress < 0.76) {
console.log('75% complete');
onThreeQuarterProgress(info.fileName);
}
});

function updateProgressDisplay(percentage: number, current: number, total: number): void {
// Update progress bar
const progressBar = document.getElementById('progress') as HTMLProgressElement;
if (progressBar) {
progressBar.value = percentage;
}

// Update time display
const timeDisplay = document.getElementById('time-display');
if (timeDisplay) {
timeDisplay.textContent = `${current}s / ${total}s (${percentage}%)`;
}
}

function onQuarterProgress(fileName: string): void {
console.log(`${fileName} is 25% complete`);
}

function onHalfwayProgress(fileName: string): void {
console.log(`${fileName} is 50% complete - halfway point reached`);
}

function onThreeQuarterProgress(fileName: string): void {
console.log(`${fileName} is 75% complete - almost finished`);
}

Queue Change Events​

Fired when the queue is modified (items added, removed, or reordered):

import { onQueueChange, QueueSnapshot } from 'audioq';

onQueueChange(0, (snapshot: QueueSnapshot) => {
console.log('Queue changed:', {
snapshot.totalItems,
snapshot.currentIndex,
snapshot.isPaused,
snapshot.channelNumber,
snapshot.items,
snapshot.volume,
});

// Update queue display
updateQueueDisplay(snapshot);

// Handle queue state changes
if (snapshot.totalItems === 0) {
onQueueEmpty();
} else if (snapshot.totalItems === 1) {
const currentItem = snapshot.items[0];
onLastItem(currentItem ? currentItem.fileName : null);
} else if (snapshot.totalItems > 10) {
onQueueOverloaded(snapshot.totalItems);
}
});

function updateQueueDisplay(snapshot: QueueSnapshot): void {
const queueList = document.getElementById('queue-list');
if (!queueList) return;

queueList.innerHTML = '';
snapshot.items.forEach((item, index) => {
const listItem = document.createElement('li');
listItem.className = item.isCurrentlyPlaying ? 'playing' : 'queued';
listItem.textContent = `${item.position}. ${item.fileName}`;
queueList.appendChild(listItem);
});
}

function onQueueEmpty(): void {
console.log('Queue is now empty');
// Maybe show "Add music" prompt
}

function onLastItem(fileName: string | null): void {
console.log(`Last item in queue: ${fileName}`);
// Maybe start preloading next playlist
}

function onQueueOverloaded(count: number): void {
console.warn(`Queue has ${count} items - may impact performance`);
}

Event Management Patterns​

Event Handler Classes​

Organize event handling with dedicated classes:

class AudioEventManager {
private channel: number;
private isSetup: boolean = false;

constructor(channel: number = 0) {
this.channel = channel;
}

setupAllEventHandlers(): void {
if (this.isSetup) return;

this.setupStartHandler();
this.setupCompleteHandler();
this.setupProgressHandler();
this.setupQueueHandler();

this.isSetup = true;
console.log(`Event handlers setup for channel ${this.channel}`);
}

private setupStartHandler(): void {
onAudioStart(this.channel, (info) => {
console.log(`[${this.channel}] Started: ${info.fileName}`);
this.onAudioStarted(info);
});
}

private setupCompleteHandler(): void {
onAudioComplete(this.channel, (info) => {
console.log(`[${this.channel}] Completed: ${info.fileName}`);
this.onAudioCompleted(info);
});
}

private setupProgressHandler(): void {
onAudioProgress(this.channel, (info) => {
this.onAudioProgress(info);
});
}

private setupQueueHandler(): void {
onQueueChange(this.channel, (snapshot) => {
console.log(`[${this.channel}] Queue changed: ${snapshot.totalItems} items`);
this.onQueueChanged(snapshot);
});
}

// Override these methods in subclasses
protected onAudioStarted(info: AudioStartInfo): void {
// Default implementation
}

protected onAudioCompleted(info: AudioCompleteInfo): void {
// Default implementation
}

protected onAudioProgress(info: AudioInfo): void {
// Default implementation - called frequently
}

protected onQueueChanged(snapshot: QueueSnapshot): void {
// Default implementation
}

// Clean up all event listeners
cleanupAlternative(): void {
if (!this.isSetup) return;

console.log(`Cleaning up all event listeners for channel ${this.channel} using off* methods`);

// Remove all event listeners for this channel
offAudioStart(this.channel);
offAudioComplete(this.channel);
offAudioProgress(this.channel);
offQueueChange(this.channel);

this.cleanupFunctions = [];
this.isSetup = false;
}
}

// Gaming-specific event handler
class GameAudioEventManager extends AudioEventManager {
protected onAudioStarted(info: AudioStartInfo): void {
// Update game UI
this.updateGameUI(`Playing: ${info.fileName}`);

// Log for analytics
this.logGameAudioEvent('start', info.fileName);
}

protected onAudioCompleted(info: AudioCompleteInfo): void {
console.log(`Game audio completed: ${info.fileName} on channel ${info.channelNumber}`);

this.logGameAudioEvent('complete', info.fileName);
}

private updateGameUI(message: string): void {
// Update game interface
console.log(`Game UI: ${message}`);
}

private logGameAudioEvent(type: string, fileName: string): void {
// Send to analytics
console.log(`Analytics: ${type} - ${fileName}`);
}
}

Cross-Channel Event Coordination​

Coordinate events across multiple channels:

class MultiChannelEventCoordinator {
private musicChannel: number = 0;
private sfxChannel: number = 1;
private voiceChannel: number = 2;

setupCrossChannelCoordination(): void {
this.setupVoiceDucking();
this.setupMusicTransitions();
this.setupSfxLimiting();
}

private setupVoiceDucking(): void {
// Duck music and SFX when voice starts
onAudioStart(this.voiceChannel, (info) => {
console.log('Voice started - ducking other channels');
// Use the proper ducking API instead of individual channel adjustments
setVolumeDucking({
priorityChannel: this.voiceChannel,
priorityVolume: 1.0,
duckingVolume: 0.2
});
});

// Restore volumes when voice ends
onAudioComplete(this.voiceChannel, (info) => {
console.log('Voice ended - restoring channel volumes');
// No need to manually restore - ducking system handles this automatically
});
}

private setupMusicTransitions(): void {
onAudioComplete(this.musicChannel, (info) => {
console.log('Music track completed');
this.considerNextTrack();
});
}

private setupSfxLimiting(): void {
onQueueChange(this.sfxChannel, (snapshot) => {
if (snapshot.totalItems > 5) {
console.warn('Too many SFX queued - consider throttling');
this.throttleSfx();
}
});
}

private considerNextTrack(): void {
// Logic to decide next music track
console.log('Considering next music track...');
}

private throttleSfx(): void {
// Logic to limit SFX queue
console.log('Throttling SFX to prevent overload');
}

// Clean up all event listeners
cleanupAlternative(): void {
console.log('Cleaning up all cross-channel event handlers using off* methods');

// Remove all event listeners for all channels
offAudioStart(this.musicChannel);
offAudioStart(this.sfxChannel);
offAudioStart(this.voiceChannel);

offAudioComplete(this.musicChannel);
offAudioComplete(this.sfxChannel);
offAudioComplete(this.voiceChannel);

offAudioProgress(this.musicChannel);
offAudioProgress(this.sfxChannel);
offAudioProgress(this.voiceChannel);

offQueueChange(this.musicChannel);
offQueueChange(this.sfxChannel);
offQueueChange(this.voiceChannel);

this.cleanupFunctions = [];
}
}

State Machine with Events​

Use events to drive complex state machines:

enum AudioState {
STOPPED = 'stopped',
LOADING = 'loading',
PLAYING = 'playing',
PAUSED = 'paused',
COMPLETING = 'completing'
}

class AudioStateMachine {
private state: AudioState = AudioState.STOPPED;
private channel: number;

constructor(channel: number = 0) {
this.channel = channel;
this.setupStateMachine();
}

private setupStateMachine(): void {
onAudioStart(this.channel, () => {
this.transitionTo(AudioState.PLAYING);
});

onAudioComplete(this.channel, (info) => {
if (info.remainingInQueue > 0) {
this.transitionTo(AudioState.LOADING);
} else {
this.transitionTo(AudioState.STOPPED);
}
});

onAudioProgress(this.channel, (info) => {
if (this.state === AudioState.PLAYING) {
if (info.progress > 0.9) {
this.transitionTo(AudioState.COMPLETING);
}
}
});

// Listen for pause events
onQueueChange(this.channel, (snapshot) => {
const info = getCurrentAudioInfo(this.channel);

// If there's audio in the queue but it's paused
if (snapshot.totalItems > 0 && snapshot.isPaused) {
this.transitionTo(AudioState.PAUSED);
}
// If there was a pause but now we're playing again
else if (this.state === AudioState.PAUSED && snapshot.totalItems > 0 && !snapshot.isPaused) {
this.transitionTo(AudioState.PLAYING);
}
});
}

// Public methods to control audio state
pauseAudio(): void {
if (this.state === AudioState.PLAYING) {
pauseChannel(this.channel);
// State will be updated via the onQueueChange handler
}
}

resumeAudio(): void {
if (this.state === AudioState.PAUSED) {
resumeChannel(this.channel);
// State will be updated via the onQueueChange handler
}
}

private transitionTo(newState: AudioState): void {
const oldState = this.state;
this.state = newState;

console.log(`State transition: ${oldState} → ${newState}`);

// Handle state-specific logic
switch (newState) {
case AudioState.STOPPED:
this.onStopped();
break;
case AudioState.PLAYING:
this.onPlaying();
break;
case AudioState.PAUSED:
this.onPaused();
break;
case AudioState.COMPLETING:
this.onCompleting();
break;
}
}

private onStopped(): void {
console.log('Audio system is stopped');
// Maybe start background music
}

private onPlaying(): void {
console.log('Audio is playing');
// Update UI to show play state
}

private onPaused(): void {
console.log('Audio is paused');
// Update UI to show pause state
}

private onCompleting(): void {
console.log('Audio is near completion');
// Maybe preload next track or show completion animation
}

getCurrentState(): AudioState {
return this.state;
}

getDisplayState(): string {
const info = getCurrentAudioInfo(this.channel);
if (!info) return 'Stopped';

return info.isPlaying ? 'Playing' : info.isPaused ? 'Paused' : 'Stopped';
}
}

// Usage example
const audioMachine = new AudioStateMachine(0);
await queueAudio('./music/track1.mp3', 0);

// Later, pause the audio
audioMachine.pauseAudio();
console.log(`Current state: ${audioMachine.getDisplayState()}`); // "Paused"

// Resume playback
audioMachine.resumeAudio();
console.log(`Current state: ${audioMachine.getDisplayState()}`); // "Playing"

// Real-world example: Audio Player UI
class AudioPlayerUI {
private stateMachine: AudioStateMachine;
private channel: number;

constructor(channel: number = 0) {
this.channel = channel;
this.stateMachine = new AudioStateMachine(channel);
this.setupUI();
}

private setupUI(): void {
// Update UI based on state changes
onQueueChange(this.channel, () => {
this.updateUIControls();
});

onAudioProgress(this.channel, (info) => {
this.updateProgressBar(info.progress);
});
}

private updateUIControls(): void {
const state = this.stateMachine.getCurrentState();
const displayState = this.stateMachine.getDisplayState();

console.log(`Updating UI for state: ${displayState}`);

// Update play/pause button
const playPauseButton = document.getElementById('playPauseButton');
if (playPauseButton) {
if (state === AudioState.PLAYING) {
playPauseButton.innerHTML = '⏸️'; // Pause icon
playPauseButton.setAttribute('aria-label', 'Pause');
} else if (state === AudioState.PAUSED) {
playPauseButton.innerHTML = '▶️'; // Play icon
playPauseButton.setAttribute('aria-label', 'Play');
} else {
playPauseButton.innerHTML = '▶️'; // Play icon
playPauseButton.setAttribute('aria-label', 'Play');
playPauseButton.disabled = state === AudioState.STOPPED;
}
}

// Update status text
const statusElement = document.getElementById('playerStatus');
if (statusElement) {
statusElement.textContent = displayState;

// Add appropriate status class
statusElement.className = 'status';
statusElement.classList.add(`status-${state.toLowerCase()}`);
}
}

private updateProgressBar(progress: number): void {
const progressBar = document.getElementById('progressBar');
if (progressBar) {
const percent = Math.round(progress * 100);
progressBar.style.width = `${percent}%`;
progressBar.setAttribute('aria-valuenow', percent.toString());
}
}

// Public UI event handlers
handlePlayPauseClick(): void {
const state = this.stateMachine.getCurrentState();

if (state === AudioState.PLAYING) {
this.stateMachine.pauseAudio();
} else if (state === AudioState.PAUSED) {
this.stateMachine.resumeAudio();
} else if (state === AudioState.STOPPED) {
// Maybe start playing from the beginning or load a default track
await queueAudio('./music/default.mp3', this.channel);
}
}

handleStopClick(): void {
stopAllAudioInChannel(this.channel);
// State machine will update via events
}
}

// Usage
const playerUI = new AudioPlayerUI(0);

// Connect to DOM events
document.getElementById('playPauseButton')?.addEventListener('click', () => {
playerUI.handlePlayPauseClick();
});

document.getElementById('stopButton')?.addEventListener('click', () => {
playerUI.handleStopClick();
});

Advanced Event Patterns​

Event Aggregation​

Collect and analyze events across time periods:

class AudioEventAggregator {
private events: { type: string; timestamp: number; data: any }[] = [];
private maxEvents: number = 1000;

constructor() {
this.setupEventCollection();
}

private setupEventCollection(): void {
// Collect all events from channel 0
onAudioStart(0, (info) => {
this.recordEvent('audioStart', info);
});

onAudioComplete(0, (info) => {
this.recordEvent('audioComplete', info);
});

onQueueChange(0, (snapshot) => {
this.recordEvent('queueChange', { totalItems: snapshot.totalItems });
});
}

private recordEvent(type: string, data: any): void {
this.events.push({
type,
timestamp: Date.now(),
data
});

// Keep only recent events
if (this.events.length > this.maxEvents) {
this.events = this.events.slice(-this.maxEvents);
}
}

getEventsSince(timestampMs: number): any[] {
return this.events.filter(event => event.timestamp >= timestampMs);
}

getEventStats(periodMs: number = 60000): {
starts: number;
completions: number;
queueChanges: number;
} {
const since = Date.now() - periodMs;
const recentEvents = this.getEventsSince(since);

const starts = recentEvents.filter(e => e.type === 'audioStart').length;
const completions = recentEvents.filter(e => e.type === 'audioComplete').length;
const queueChanges = recentEvents.filter(e => e.type === 'queueChange').length;

return { starts, completions, queueChanges };
}
}

Event-Driven Analytics​

class AudioAnalytics {
private sessionStart: number = Date.now();
private playbackStats: Map<string, { count: number; completions: number }> = new Map();

constructor() {
this.setupAnalytics();
}

private setupAnalytics(): void {
onAudioStart(0, (info) => {
this.trackAudioStart(info);
});

onAudioComplete(0, (info) => {
this.trackAudioComplete(info);
});

onAudioProgress(0, (info) => {
this.trackProgress(info);
});
}

private trackAudioStart(info: AudioStartInfo): void {
console.log(`📊 Analytics: Started ${info.fileName}`);

// Initialize or increment play count
const stats = this.playbackStats.get(info.fileName) || { count: 0, totalDuration: 0 };
stats.count++;
this.playbackStats.set(info.fileName, stats);
}

private trackAudioComplete(info: AudioCompleteInfo): void {
console.log(`📊 Analytics: Completed ${info.fileName}`);

// Update completion stats
const stats = this.playbackStats.get(info.fileName);
if (stats) {
stats.completions = (stats.completions || 0) + 1;
this.playbackStats.set(info.fileName, stats);
}

// Track remaining queue
console.log(`📊 ${info.fileName} completed. ${info.remainingInQueue} items remaining in queue.`);
}

private trackProgress(info: AudioInfo): void {
// Track milestone progress (only log once per milestone)
const milestones = [0.25, 0.5, 0.75];
for (const milestone of milestones) {
if (info.progress >= milestone && info.progress < milestone + 0.01) {
console.log(`📊 Analytics: ${info.fileName} reached ${milestone * 100}%`);
}
}
}

getSessionReport(): {
sessionDuration: number;
filesPlayed: string[];
mostPlayedFile: string | null;
totalCompletions: number;
} {
const sessionDuration = Date.now() - this.sessionStart;
const filesPlayed = Array.from(this.playbackStats.keys());

let mostPlayedFile: string | null = null;
let maxCount = 0;
let totalCompletions = 0;

for (const [fileName, stats] of this.playbackStats) {
if (stats.count > maxCount) {
maxCount = stats.count;
mostPlayedFile = fileName;
}
totalCompletions += stats.completions || 0;
}

return {
sessionDuration,
filesPlayed,
mostPlayedFile,
totalCompletions
};
}
}

Event Cleanup and Management​

Event Handler Cleanup​

Properly manage event handler lifecycle:

class EventHandlerManager {
private cleanupFunctions: Array<() => void> = [];

addEventHandler(
channel: number,
eventType: 'start' | 'complete' | 'progress' | 'queueChange',
handler: any
): void {
let cleanup: () => void;

switch (eventType) {
case 'start':
cleanup = onAudioStart(channel, handler);
break;
case 'complete':
cleanup = onAudioComplete(channel, handler);
break;
case 'progress':
cleanup = onAudioProgress(channel, handler);
break;
case 'queueChange':
cleanup = onQueueChange(channel, handler);
break;
default:
throw new Error(`Unknown event type: ${eventType}`);
}

this.cleanupFunctions.push(cleanup);
this.channels.add(channel);
}

removeAllEventHandlers(): void {
console.log(`Cleaning up event handlers for ${this.channels.size} channels`);

for (const channel of this.channels) {
offAudioStart(channel);
offAudioComplete(channel);
offAudioProgress(channel);
offQueueChange(channel);
}

this.cleanupFunctions = [];
}

removeChannelEventHandlers(channel: number): void {
console.log(`Removing all event handlers for channel ${channel}`);

// Remove all event handlers for a specific channel
offAudioStart(channel);
offAudioComplete(channel);
offAudioProgress(channel);
offQueueChange(channel);

this.channels.delete(channel);
}

getHandlerCount(): number {
return this.cleanupFunctions.length;
}
}

// Usage
const eventManager = new EventHandlerManager();

// Add handlers
eventManager.addEventHandler(0, 'start', (info) => {
console.log(`Started: ${info.fileName}`);
});

eventManager.addEventHandler(0, 'complete', (info) => {
console.log(`Completed: ${info.fileName}`);
});

eventManager.addEventHandler(1, 'start', (info) => {
console.log(`Channel 1 started: ${info.fileName}`);
});

// Later, clean up when component unmounts or page unloads
window.addEventListener('beforeunload', () => {
eventManager.removeAllEventHandlers();

// OR Remove handlers for a specific channel
// eventManager.removeChannelEventHandlers(0);
});

Performance-Aware Event Handling​

Handle high-frequency events efficiently:

class PerformantEventHandler {
private lastProgressUpdate: number = 0;
private progressThrottle: number = 100; // Update every 100ms max
private channel: number;

constructor(channel: number = 0) {
this.channel = channel;
}

setupOptimizedEventHandlers(): void {
const channel = this.channel;

onAudioStart(channel, (info) => {
this.handleAudioStart(info);
});

onAudioComplete(channel, (info) => {
this.handleAudioComplete(info);
});

onAudioProgress(channel, (info) => {
this.handleThrottledProgress(info);
});

onQueueChange(channel, (snapshot) => {
this.handleQueueChange(snapshot);
});
}

private handleAudioStart(info: AudioStartInfo): void {
// Immediate response for start events
console.log(`Quick start: ${info.fileName}`);
}

private handleAudioComplete(info: AudioCompleteInfo): void {
// Immediate response for completion events
console.log(`Quick complete: ${info.fileName}`);
}

private handleThrottledProgress(info: AudioInfo): void {
const now = Date.now();

if (now - this.lastProgressUpdate >= this.progressThrottle) {
this.lastProgressUpdate = now;

// Only update UI at reasonable intervals
this.updateProgressUI(info);
}
}

private updateProgressUI(info: AudioInfo): void {
const percentage = Math.round(info.progress * 100);
console.log(`Progress: ${percentage}%`);
}

private handleQueueChange(snapshot: QueueSnapshot): void {
// Queue changes are less frequent but important
console.log(`Queue: ${snapshot.totalItems} items`);
}

cleanup(): void {
console.log(`Cleaning up event handlers for channel ${this.channel}`);

// Remove all event handlers for this channel
offAudioStart(this.channel);
offAudioComplete(this.channel);
offAudioProgress(this.channel);
offQueueChange(this.channel);
}
}

// Usage
const handler = new PerformantEventHandler(0);
handler.setupOptimizedEventHandlers();

// Later, when done with this handler
handler.cleanup();

Event Listener Removal​

All event listeners can be removed using corresponding "off" methods. This is important for cleaning up event handlers when components unmount or to prevent memory leaks.

Removing Audio Start Listeners​

import { onAudioStart, offAudioStart } from 'audioq';

// Add listener
const cleanup = onAudioStart(0, (info) => {
console.log(`Started playing: ${info.fileName}`);
});

offAudioStart(0); // Remove all start listeners for channel 0

Removing Audio Complete Listeners​

import { onAudioComplete, offAudioComplete } from 'audioq';

// Add listener
const cleanup = onAudioComplete(0, (info) => {
console.log(`Finished playing: ${info.fileName}`);
});

offAudioComplete(0); // Remove all complete listeners for channel 0

Removing Audio Progress Listeners​

import { onAudioProgress, offAudioProgress } from 'audioq';

// Add listener
const cleanup = onAudioProgress(0, (info) => {
updateProgressBar(info.progress);
});

offAudioProgress(0); // Remove all progress listeners for channel 0

Removing Queue Change Listeners​

import { onQueueChange, offQueueChange } from 'audioq';

// Add listener
const cleanup = onQueueChange(0, (snapshot) => {
updateQueueUI(snapshot);
});

offQueueChange(0); // Remove all queue change listeners for channel 0

Next Steps​

Now that you understand the event system, explore: