๐ŸŽต sound-manager-ts | SoundManager for Web Audio API

npm version License: MIT Bundle Size npm downloads Donate โค API Reference

A powerful and lightweight (14KB gzipped) sound management system I crafted to make Web Audio API accessible and enjoyable. Perfect for web applications, games, and interactive experiences that demand precise audio control without the complexity. No more wrestling with time calculations or audio states - everything is handled for you. Simply listen to sound events or use getSoundState(โ€˜soundIdโ€™) to access comprehensive audio data, ready to integrate with your UI.

Live Demos & Playgrounds

Visit the main demo page

Codepen.io (Demo / Playground) JavScript

Codepen.io (Demo / Playground) TypeScript

Why Choose This Package?

๐Ÿš€ Modern & Efficient

๐ŸŽฎ Perfect for Games & Apps

๐Ÿ› ๏ธ Developer Friendly

Features

Note

Browser Support

Supports all modern browsers including Chrome, Firefox, Safari, and Edge (98.5% global coverage).

Transform your web audio experience with just a few lines of code!

Documentation

About me

Chris Schardijn (Front-end Developer)

My journey in web development spans back to the Flash era, where among various projects, I developed a sound manager using ActionScript 3.0. As technology evolved, so did I, embracing new challenges and opportunities to grow. This Sound Manager TypeScript project represents not just a modern reimagining of a concept I once built in Flash, but also my challange for continuous learning and adaptation in the ever-changing landscape of web development.

I built this library in my spare time. What started as a personal study project has grown into a robust solution that Iโ€™m excited to share with the developer community.

Feel free to use this library in your projects, and I hope it inspires you to pursue your own passion projects, regardless of how technology changes. Sometimes the best learning comes from rebuilding something you once loved in a completely new way.

๐Ÿš€ Quick Start

npm install sound-manager-ts
import { SoundManager } from "sound-manager-ts";

const soundManager = new SoundManager();

soundManager.addEventListener(SoundEventsEnum.LOADED, (event: SoundEvent) => {
  console.log('Sound loaded', event);
});

await soundManager.loadSounds([{ id: "music", url: "/sounds/music.mp3" }]);
soundManager.play("music");

Installation / imlement in your project

Implement in your project

1. Using the Sound Manager as TypeScript Module

For TypeScript projects, it is recommended to install the package and import it directly. This method provides better type safety and allows you to take full advantage of TypeScript features.

Install the package

npm install sound-manager-ts

After the installation a folder

In your TypeScript file, you can import and use the Sound Manager like this:

import { SoundManager, SoundManagerConfig, SoundEventsEnum } from "sound-manager-ts";

// Optional configuration
const config: SoundManagerConfig = {
  autoMuteOnHidden: true, // Mute when tab is hidden
  autoResumeOnFocus: true, // Resume on tab focus
  defaultVolume: 0.8, // Default volume (0-1)
};

// Initialize sound manager with config
const soundManager = new SoundManager(config);

// Listen to load event
soundManager.addEventListener(SoundEventsEnum.LOADED, (event: SoundEvent) => {
  console.log('Sound loaded', event);
});


// Define sounds to preload
const soundsToLoad = [
  { id: "background-music", url: "/assets/sounds/background.mp3" },
  { id: "click-effect", url: "/assets/sounds/click.wav" },
];

// Preload sounds
soundManager
  .loadSounds(soundsToLoad)
  .then(() => {
    console.log("All sounds loaded successfully");
  })
  .catch((error) => {
    console.error("Error loading sounds:", error);
  });

// Play a sound
soundManager.play("background-music", {
  volume: 0.7,
  fadeInDuration: 2,
});

2. Using Sound Manager as a Library File / CDN Installation

If you prefer to include Sound Manager directly as a library file in your project, you can use the UMD (Universal Module Definition) version. This approach allows you to integrate the sound manager without package managers or build tools - simply include the JavaScript file in your HTML.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Sound Manager Implementation</title>
  <style>
    body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
    .sound-controls { margin: 20px 0; padding: 15px; background: #f5f5f5; border-radius: 5px; }
    button { padding: 8px 12px; margin-right: 10px; cursor: pointer; }
  </style>
</head>
<body>
  <h1>Sound Manager Implementation</h1>
  
  <div class="sound-controls">
    <button id="playBtn">Play Background Music</button>
    <button id="stopBtn">Stop Music</button>
    <button id="clickBtn">Play Click Sound</button>
  </div>

  <!-- 
    ====================================================================
    CDN INSTALLATION OPTIONS
    ====================================================================
  -->
  
  <!-- Option 1: UMD Version (Works everywhere) -->
  <script src="https://unpkg.com/sound-manager-ts@5.7.1/dist/sound-manager-ts.umd.js"></script>
  
  <!-- 
    Alternative UMD options:
    - Download and use local file: <script src="/path/to/sound-manager-ts.umd.js"></script>
    - Specific version: <script src="https://unpkg.com/sound-manager-ts@5.5.8/dist/sound-manager-ts.umd.js"></script>
  -->
  
  <!-- Option 2: ESM Version (Modern browsers/bundlers) -->
  <!--
  <script type="module">
    import { SoundManager } from 'https://unpkg.com/sound-manager-ts@5.7.1/dist/sound-manager-ts.es.js';
    // Your ESM code here
  </script>
  -->

  <script>
    // ====================================================================
    // INITIALIZATION
    // ====================================================================
    const soundManager = new SoundManagerTS.SoundManager({
      debug: true, // Enable console logs for debugging
      autoMuteOnHidden: true, // Mute when tab is hidden
      autoResumeOnFocus: true, // Resume when tab regains focus
      defaultVolume: 0.7, // Default volume (0-1)
      spatialAudio: false, // Enable 3D audio if needed
      fadeInDuration: 1, // Default fade-in duration (seconds)
      fadeOutDuration: 1  // Default fade-out duration (seconds)
    });

    
    // ====================================================================
    // Add eventlistener LOADED
    // ====================================================================
    soundManager.addEventListener(SoundEventsEnum.LOADED, (event: SoundEvent) => {
      console.log('Sound loaded', event);
    });

    // ====================================================================
    // SOUND DEFINITIONS
    // ====================================================================
    const sounds = {
      background: {
        id: "background-music",
        url: "https://example.com/sounds/background.mp3",
        options: { loop: true, volume: 0.6 }
      },
      click: {
        id: "click-effect", 
        url: "https://example.com/sounds/click.wav",
        options: { volume: 0.8 }
      }
    };

    // ====================================================================
    // SOUND LOADING (Using async/await)
    // ====================================================================
    async function initializeSounds() {
      try {
        // Load all sounds
        await soundManager.loadSounds([
          { id: sounds.background.id, url: sounds.background.url },
          { id: sounds.click.id, url: sounds.click.url }
        ]);
        
        console.log("All sounds loaded successfully");
        
        // Set up event listeners after sounds are loaded
        setupControls();
        
      } catch (error) {
        console.error("Error loading sounds:", error);
        alert("Failed to load sounds. Please check console for details.");
      }
    }

    // ====================================================================
    // CONTROL FUNCTIONS
    // ====================================================================
    function setupControls() {
      document.getElementById('playBtn').addEventListener('click', () => {
        soundManager.play(sounds.background.id, {
          ...sounds.background.options,
          fadeInDuration: 2 // Override default fade-in
        });
      });

      document.getElementById('stopBtn').addEventListener('click', () => {
        soundManager.stop(sounds.background.id);
      });

      document.getElementById('clickBtn').addEventListener('click', () => {
        soundManager.play(sounds.click.id, sounds.click.options);
      });
    }

    // ====================================================================
    // ERROR HANDLING & EVENTS
    // ====================================================================
    soundManager.addEventListener(SoundManagerTS.SoundEventsEnum.ERROR, (event) => {
      console.error("Sound Manager Error:", event.error);
    });

    soundManager.addEventListener(SoundManagerTS.SoundEventsEnum.ENDED, (event) => {
      console.log(`Sound ${event.soundId} finished playing`);
    });

    soundManager.addEventListener(SoundManagerTS.SoundEventsEnum.PROGRESS, (event) => {
      console.log(`Sound Progress:  ${event.progress} %`);
      console.log(`Sound Progress Info: ${event.progressInfo}`);
    });

    // Initialize the sound manager when page loads
    window.addEventListener('DOMContentLoaded', initializeSounds);
  </script>
</body>
</html>

Usage

import { SoundManager, SoundManagerConfig, SoundEventsEnum } from 'sound-manager-ts';

// Optional configuration
export interface SoundManagerConfig {
  autoUnlock?: boolean; // Unlock audio for mobile browser that have restrictions
  autoMuteOnHidden?: boolean; // Automatically mute when page or tab of your browser is not active
  autoResumeOnFocus?: boolean; // Automatically resume when page or tab of your browser gets focus
  createNewInstance?: boolean; // Create a new instance of the sound when playing it. 
  // By default this is false. This is useful when you want to play the same sound multiple times simultaneously. 

  // ------- Loading Configuration: -------------------------------------------------------------
  // Loading Behaviour
  webAudioPreferred?: boolean; // Whether to prefer Web Audio API (default: true)
  html5AudioFallback?: boolean; // Whether to use HTML5 Audio as fallback (default: true)
  maxParallelLoads?: number; // Maximum parallel sound loads (default: 6)
  retryDelay?: number; // Delay between retry attempts in seconds (default: 0.5 seconds)

  // Network Handling
  fetchRetries?: number; // Number of retries for failed fetches (default: 2)
  fetchTimeout?: number; // Timeout for fetch requests in seconds
  corsProxy?: string; // URL of CORS proxy service, the ones I tested that work great are: 
  // corsProxy: "https://cors-anywhere.herokuapp.com/", or corsProxy: "https://corsproxy.io/?",  or your own proxy
  fetchStrategy?: 'direct-first' | 'proxy-first' | 'direct-only';

  // Security & Limits
  maxAudioSize?: number; // in bytes, currently the max is set to 50MB  (50 * 1024 * 1024)
  audioCache?: boolean; // Cache the audio file when loading.
  crossOrigin?: "anonymous" | "use-credentials" | null;
  credentialStrategy?: 'auto' | 'omit' | 'include';
  
  // -----End Loading Configuration-------------------------------------------------------------

  debug?: boolean; // Enable debug logging
  defaultDuration?: number; // Default duration for new sounds, default is undefined (full length of the sound)
  defaultPan?: number; // The default pan value = 0, in the center. Posiible values are (-1 to 1)
  defaultPanSpatialPosition?: { x: number; y: number; z: number };
  defaultPanType?: SoundPanType; // Default pan type
  defaultPlaybackRate?: number // The default playbackRate is 1
  defaultStartTime?: number; // Default start time for new sounds
  defaultVolume?: number; // Default volume for new sounds (0-1)
  fadeInDuration?: number; // Default fade-in duration in seconds
  fadeOutDuration?: number; // Default fade-out duration in seconds
  loopSounds?: boolean // Loop all sounds by default
  maxLoops?: number // if loopSounds is true and maxLoops is set, the sound will loop maxLoops times  (-1 is for infinite)
  pannerNodeConfig?: SoundPannerConfig; // Panner settings for 3D sound
  spatialAudio?: boolean; // Enable spatial audio features
  trackProgress?: boolean; // Track progress of the sound playback. 
  // This will keep track of the process and will dispatch the 'progress' event. This is useful when you want to show the progress of the sound playback.
}

// Initialize sound manager with config
const soundManager = new SoundManager(config);

// Listen to Sound Loaded event
soundManager.addEventListener(SoundEventsEnum.LOADED, (event: SoundEvent) => {
  console.log('Sound loaded', event);
});

// Define sounds to preload
const soundsToLoad = [
    { id: 'background-music', url: '/assets/sounds/background.mp3' },
    { id: 'click-effect', url: '/assets/sounds/click.wav' }
];

// Preload sounds (recommended)
try {
    await soundManager.loadSounds(soundsToLoad);
    console.log('All sounds loaded successfully');
} catch (error) {
    console.error('Error loading sounds:', error);
}

// Cancel a load when a component unmounts (e.g. React useEffect cleanup)
const controller = new AbortController();

try {
    await soundManager.loadSound('background-music', '/assets/sounds/background.mp3', controller.signal);
} catch (error) {
    if (error.name !== 'AbortError') {
        console.error('Error loading sound:', error);
    }
}

// In cleanup (e.g. useEffect return function):
controller.abort();

// Add event listeners
soundManager.addEventListener(SoundEventsEnum.STARTED, (event) => {
    console.log(`Sound ${event.soundId} started playing at ${event.timestamp}`);
});

soundManager.addEventListener(SoundEventsEnum.ENDED, (event) => {
    console.log(`Sound ${event.soundId} finished playing`);
});

// Play a sound with options
soundManager.play('background-music', {
    volume: 0.7,
    loop: true,
    fadeInDuration: 2,
    fadeOutDuration: 2,
    playbackRate: 0.5,
    pan: -0.5,
    startTime: 0
});

// Control individual sounds
soundManager.pause('background-music');
soundManager.resume('background-music');
soundManager.stop('background-music');
soundManager.seek('background-music', 12); // Seek to 12 seconds

// Volume control
soundManager.setSoundVolume('background-music', 0.5);
soundManager.setGlobalVolume(0.8);

// Pan control
soundManager.setPan('background-music', -0.5); // Pan left
soundManager.setGlobalPan(0.3); // Slight right pan for all sounds

// Fade effects
soundManager.fadeIn('background-music', 2); // Fade in over 2 seconds
soundManager.fadeOut('background-music', 1); // Fade out over 1 second
soundManager.fadeGlobalIn(1.5); // Fade in all sounds
soundManager.fadeGlobalOut(1.5); // Fade out all sounds

// Playback rate
soundManager.setPlaybackRate('background-music', 1.5);

// Full example using Sprites
const soundsToLoad = [
	{ id: "game-sound", url: gameSounds },        
];

await this.soundManager.loadSounds(soundsToLoad);

let mySprite: any = {
	intro: [0, 2], // 0,2 means start from 0 seconds until 2 seconds.
	levelup: [2.4, 4], // start from 2.4 seconds till 4 seconds.
	jump: [4, 5],
	fail: [5, 7]
};

this.soundManager.setSoundSprite("game-sound", mySprite);

this.soundManager.playSprite("game-sound", "intro", { fadeInDuration: 1, pan: 0.8, playbackRate: 1.5});
this.soundManager.playSprite("game-sound", "jump", { loop: true});
this.soundManager.playSprite("game-sound", "levelup", { fadeOutDuration: 1, pan: -0.8});

setTimeout( ()=> {
    this.soundManager.playSprite(this.id, "fail", { pan: 0.8});
}, 500);


// Sound Group example.
// 
// In this example, when pressing the letter c, a piano note is triggerd. These piano notes are
// played in the sound group 'pian-group' where volume, paning and more can be managed.

// First, we create a Sound Group called piano-group.  
// This group will manage up to 12 sound instances and set default options like volume and panning.

this.soundManager.createSoundGroup('piano-group', {
  maxInstances: 12, // Limit the group to 12 simultaneous sounds
  playOptions: {
    volume: 0.8, // Default volume for sounds in this group
    pan: 0, // Default panning (center)
  },
});

// Next, we set up an event listener to play a new sound instance whenever a key is pressed. 
// In this case, pressing the C key will play the piano-note sound.

document.addEventListener('keydown', (e) => {
  if (e.key === 'c') {
    // Play the "piano-note" sound with custom options
    const sound = this.soundManager.play('piano-note', {
      // groupId: 'piano-group', // Optionally, you can add the sound to a group here
      trackProgress: true, // Enable progress tracking for this instance
      loop: true, // Loop the sound
      volume: 1, // Set volume (overrides group default)
      playbackRate: 1, // Playback speed (1 = normal speed)
      pan: Math.random() * 2 - 1, // Random panning between left (-1) and right (1)
      createNewInstance: true, // Create a new instance of the sound
    });
  }
});

// To track the progress of each sound instance, we add an event listener for the PROGRESS event. 
// This allows you to monitor how far along each sound is in its playback.
this.soundManager.addEventListener(
  SoundEventsEnum.PROGRESS,
  (event) => {
      console.log(`Progress for instance ${event.instanceId}: ${event.progress}`);
  },
  { originalId: "piano-note" } // Optional: Filter by originalId
);


// 3D Spatial Audio
// Set on a specific sound the 3d / spatial audio positioni
soundManager.setSpatialPosition(5, 0, -2, 'background-music');

// Set the master spatial position (x, y, z)
soundManager.setMasterSpatialPosition(10, 0, -3);

// Mute controls
soundManager.muteAllSounds();
soundManager.unmuteAllSounds();
soundManager.mute('background-music');
soundManager.unmute('background-music');
soundManager.toggleMute('background-music');
soundManager.toggleGlobalMute();

// Spatial audio (if enabled in config)
soundManager.setSpatialPosition(1, 0, -1, 'background-music');
soundManager.resetSpatialPosition('background-music');
soundManager.removeSpatialEffect('background-music');
soundManager.isSpatialAudioActive('background-music');
soundManager.updatePannerConfigById('background-music',
    <SoundPannerConfig>{
        panningModel: PanningModel.HRTF,
        distanceModel: DistanceModel.Inverse,
        refDistance: 1,
        maxDistance: 10000,
        rolloffFactor: 0.2,
        coneInnerAngle: 360,
        coneOuterAngle: 360,
        coneOuterGain: 0,
    }
);

// State checks
const isPlaying = soundManager.isPlaying('background-music');
const isPaused = soundManager.isPaused('background-music');
const isStopped = soundManager.isStopped('background-music');
const state = soundManager.getSoundState('background-music');

// Reset all sound settings to default values
soundManager.reset();

// Or use the SoundResetOptions
soundManager.reset({
  keepVolumes: true; // Keep current volume settings
  keepPanning: false; // Keep current panning settings
  keepSpatial: false; // Keep spatial audio settings
  unloadSounds: false; // Unload all sounds
})

// Cleanup
soundManager.destroy();

The SoundManager API

Public methods on the SoundManager

export interface SoundManagerInterface {
  // Playback control
  play(id: string, options?: PlayOptions, skipDispatchEvent?: boolean): void;
  playSprite(id: string, spriteKey: string, options: PlayOptions, skipDispatchEvent?: boolean): void
  pause(id: string, skipDispatchEvent?: boolean): void;
  resume(id: string, skipDispatchEvent?: boolean): void;
  stop(id: string, skipDispatchEvent?: boolean): void;
  seek(id: string, time: number, skipDispatchEvent?: boolean): void;

  // Volume control
  getVolume(id: string): number;
  setSoundVolume(id: string, volume: number, skipDispatchEvent?: boolean): void;
  getSoundVolume(id: string): number;
  setGlobalVolume(volume: number): void;
  getGlobalVolume(): number;

  // Loop control
  setLoop(id: string, loop: boolean): void
  getLoop(id: string): boolean

  // Mute control
  muteAllSounds(): void;
  unmuteAllSounds(): void;
  mute(id: string): void;
  unmute(id: string): void;
  toggleGlobalMute(): void;
  toggleMute(id: string): void;

  // Sound loading and management
  loadSounds(soundsToLoad: { id: string; url: string }[], signal?: AbortSignal): Promise<void>;
  loadSound(id: string, url: string, signal?: AbortSignal): Promise<void>;
  updateSoundUrl(id: string, newUrl: string): Promise<void>;
  unloadSound(id: string): void
  removeSound(id: string): void
  isSoundLoaded(id: string): boolean;
  hasSound(id: string): boolean;

  // State checks
  isPlaying(id: string): boolean;
  isPaused(id: string): boolean;
  isStopped(id: string): boolean;
  getSoundState(id: string): SoundStateInfo;
  getSoundCount(): number;
  isReady(): boolean;

  // Progress tracking
  getCurrentTime(id: string): number;
  getDuration(id: string): number;
  getProgress(id: string): number; // Returns the progress as a ratio (0-1)
  getProgressPercentage(id: string): number;
  startProgressTracking(id: string): void;
  stopProgressTracking(id: string): void;
  setProgressUpdateInterval(interval: number): void;

  // Batch operations
  stopAllSounds(): void;
  pauseAllSounds(): void;
  resumeAllSounds(): void;
  reset(options?: SoundResetOptions): void;
  resetSound(id: string, options?: SoundResetOptions): void;

  // Fading
  fadeIn(id: string, duration: number, startVolume?: number, endVolume?: number, skipDispatchEvent?: boolean): void;
  fadeOut(id: string, duration?: number, startVolume?: number, endVolume?: number, stopAfterFade?: boolean, skipDispatchEvent?: boolean): void;
  fadeGlobalIn(duration?: number, startVolume?: number, endVolume?: number): void;
  fadeGlobalOut(duration?: number, startVolume?: number, endVolume?: number): void;

  // Spatial audio
  isSpatialAudioEnabled(): boolean;
  isSpatialAudioSupported(): boolean;
  setSpatialPosition(x: number, y: number, z: number, soundId?: string | null, soundPannerConfig?: SoundPannerConfig, skipEvent?: boolean): void;
  getSpatialPosition(soundId: string): { x: number; y: number; z: number } | null;
  setMasterSpatialPosition(x: number, y: number, z: number, config?: SoundPannerConfig, skipEvent?: boolean): void;
  getMasterSpatialPosition(): { x: number; y: number; z: number } | null;
  resetMasterSpatialPosition(): void;
  resetSpatialPosition(id: string): void;
  removeSpatialEffect(id: string): void;
  isSpatialAudioActive(id: string): boolean;
  updatePannerConfigById(soundId: string, newConfig: Partial<SoundPannerConfig>): void;

  // Pan control
  setPan(id: string, pan: number): void;
  removePan(id: string): void;
  setGlobalPan(value: number): void;
  getGlobalPan(): number;
  resetPan(id?: string): void;
  resetGlobalPan(): void;
  cleanupGlobalPan(): void;
  isStereoPanActive(id: string): boolean;

  // Sprite logic
  setSoundSprite(id: string, sprite: { [key: string]: [number, number] }): void;
  getSpriteConfig(id: string): { [key: string]: [number, number] } | undefined;
  removeSpriteSound(spriteKey: string): void;
  removeSpriteConfig(id: string): void;

  // Sound group management
  createSoundGroup(groupName: string, options: { maxInstances?: number; playOptions?: PlayOptions }): void;
  addToSoundGroup(groupName: string, soundId: string): void;
  removeFromSoundGroup(groupName: string, soundId: string): void;
  getGroup(groupName: string): SoundGroup | undefined;
  removeSoundGroup(groupName: string): void;

  // Context management
  suspendContext(): Promise<void>;
  resumeContext(): Promise<void>;
  getContext(): AudioContext;
  getMasterOutput(): AudioNode; // Returns the master output node for external connections (e.g. AnalyserNode)

  // Utilities
  setDebugMode(debug: boolean): void;
  getConfig(): Readonly<SoundManagerConfig>;
  getSound(id: string): Sound | undefined;
  getBuffer(id: string): AudioBuffer | undefined;
  getSource(id: string): AudioBufferSourceNode | undefined;
  getGainNode(id: string): GainNode | undefined;
  getSoundIds(): string[];
  updateSoundOptions(soundId: string, options: Partial<PlayOptions>): void;
  setPlaybackRate(id: string, rate: number): void;
  getPlaybackRate(id: string): number;
  getLastError(): Error | null;
  roundValue(value: number, decimals: number): number; // Default precision is this.DEFAULT_PRECISION
  destroy(): void;

  // Listeners / Event handling
  addEventListener(type: SoundEventsEnum, callback: (event: SoundEvent) => void): void;
  removeEventListener(type: SoundEventsEnum, callback: (event: SoundEvent) => void): void;
  removeEventListenersForInstance(instanceId: string): void;
  dispatchEvent(event: SoundEvent): void;
  hasEventListener(type: SoundEventsEnum): boolean;

}

PlayOptions

Options for playing a sound

export interface PlayOptions {
  createNewInstance?: boolean; // Create a new instance of the sound when playing it. 
  //  By default this is false. This is useful when you want to play the same sound multiple times simultaneously.
  duration?:number; // in seconds
  fadeInDuration?: number; // in seconds
  fadeInStartVolume?: number; // 0 to 1
  fadeOutDuration?: number; // in seconds, when you play a sound it will immidiately start fading out
  fadeOutEndVolume?: number; // 0 to 1
  fadeOutBeforeEndDuration?: number; // in seconds, fade out before the sound ends
  groupId?: string; // Group ID for the sounds that will be in this group. 
  isSeeking?: boolean; // used internally for the seek method
  loop?: boolean; // default: false
  maxLoops?: number; // -1 for infinte, number > 0 for specific number of loops
  pan?: number; // -1 (left) to 1 (right)
  panSpatialPosition?: { x: number; y: number; z: number }; //  If you want to use 3D panning you must also set panType to SoundPanType.Spatial
  panType?: SoundPanType; // 'stereo' or 'spatial' (default is 'stereo') 
  pauseAtDurationReached?: boolean; // This will only work if you set the duration and if that duration 
  // is reached it will pause. Note: Loop must be false.
  playbackRate?: number; // 0.5 to 4 (normal speed is 1) 
  startTime?: number; // in seconds
  trackProgress?: boolean; // Track progress of the sound playback. 
  // This will keep track of the process and will dispatch the 'progress' event. 
  // This is useful when you want to show the progress of the sound playback.
  volume?: number; // 0 to 1
}

SoundEvent

Event object dispatched by the sound manager:

export interface SoundEvent {
  currentTime?: number;
  duration?:number;
  error?: Error;
  instanceId?: string; // Add this for instance tracking
  isMaster?: boolean;
  isMuted?: boolean;
  options?: PlayOptions;
  originalId?: string; // Add this to track the original sound ID
  pan?: number;
  pannerConfig?: SoundPannerConfig;
  playbackRate?: number;
  position?: { x: number; y: number; z: number };
  previousPan?: number;
  progress?: number; // ratio from 0 to 1
  progressInfo?: SoundProgressStateInfo;
  resetOptions?: SoundResetOptions;
  bufferSize?: number;
  channels?: number;
  fileSize?: number;
  sampleRate?: number;
  sound?: Sound;
  soundId?: string;
  state?: SoundStateInfo;
  timestamp?: number;
  type: SoundEventsEnum;
  volume?: number;
}

SoundEventsEnum

Available event types:

export enum SoundEventsEnum {
  ENDED = 'ended',
  ERROR = 'error',
  FADE_IN_COMPLETED = 'fade_in_completed',
  FADE_MASTER_IN_COMPLETED = 'fade_master_in_completed',
  FADE_MASTER_OUT_COMPLETED = 'fade_master_out_completed',
  FADE_OUT_COMPLETED = 'fade_out_completed',
  GLOBAL_SPATIAL_POSITION_CHANGED = 'global_spatial_position_changed',
  LOADED = 'loaded',
  LOOP_COMPLETED = 'loop_completed',
  MASTER_PAN_CHANGED = 'master_pan_changed',
  MASTER_VOLUME_CHANGED = 'master_volume_changed',
  MUTE_GLOBAL = 'mute_global',
  MUTED = 'muted',
  OPTIONS_UPDATED = 'options_updated',
  PAN_CHANGED = 'pan_changed',
  PAN_RESET = 'pan_reset',
  PAUSED = 'paused',
  PLAYBACK_RATE_CHANGED = 'playback_rate_changed',
  PROGRESS = 'progress',
  RESET = 'reset',
  RESUMED = 'resumed',
  SEEKED = 'seeked',
  SPATIAL_POSITION_CHANGED = 'spatial_position_changed',
  SPATIAL_POSITION_RESET = 'spatial_position_reset',
  SPRITE_SET = 'sprite_set',
  STARTED = 'started',
  STOPPED = 'stopped',
  UNLOADED = 'unloaded',
  UNMUTE_GLOBAL = 'unmute_global',
  UNMUTED = 'unmuted',
  UPDATED_URL = 'updated_url',
  VOLUME_CHANGED = 'volume_changed',
}

SoundGroup

export interface SoundGroup {
  id: string; // internal usage (groupName)
  sounds: Set<string>; // Stores sound IDs belonging to this group
  maxInstances?: number; // Maximum number of concurrent instances allowed in the group
  playOptions?: PlayOptions; // Add playOptions to the group
}

SoundManagerConfig

Configuration options:

export interface SoundManagerConfig {
  autoUnlock?: boolean; // Unlock audio for mobile browser that have restrictions
  autoMuteOnHidden?: boolean; // Automatically mute when page or tab of your browser is not active
  autoResumeOnFocus?: boolean; // Automatically resume when page or tab of your browser gets focus
  createNewInstance?: boolean; // Create a new instance of the sound when playing it. 
  // By default this is false. This is useful when you want to play the same sound multiple times simultaneously. 

  // ------- Loading Configuration: -------------------------------------------------------------
  // Loading Behaviour
  webAudioPreferred?: boolean; // Whether to prefer Web Audio API (default: true)
  html5AudioFallback?: boolean; // Whether to use HTML5 Audio as fallback (default: true)
  maxParallelLoads?: number; // Maximum parallel sound loads (default: 6)
  retryDelay?: number; // Delay between retry attempts in seconds (default: 0.5 seconds)

  // Network Handling
  fetchRetries?: number; // Number of retries for failed fetches (default: 2)
  fetchTimeout?: number; // Timeout for fetch requests in seconds
  corsProxy?: string; // URL of CORS proxy service, the ones I tested that work great are: 
  // corsProxy: "https://cors-anywhere.herokuapp.com/", or corsProxy: "https://corsproxy.io/?",  or your own proxy
  fetchStrategy?: 'direct-first' | 'proxy-first' | 'direct-only';

  // Security & Limits
  maxAudioSize?: number; // in bytes, currently the max is set to 50MB  (50 * 1024 * 1024)
  audioCache?: boolean; // Cache the audio file when loading.
  crossOrigin?: "anonymous" | "use-credentials" | null;
  credentialStrategy?: 'auto' | 'omit' | 'include';
  
  // -----End Loading Configuration-------------------------------------------------------------

  debug?: boolean; // Enable debug logging
  defaultDuration?: number; // Default duration for new sounds, default is undefined (full length of the sound)
  defaultPan?: number; // The default pan value = 0, in the center. Posiible values are (-1 to 1)
  defaultPanSpatialPosition?: { x: number; y: number; z: number };
  defaultPanType?: SoundPanType; // Default pan type
  defaultPlaybackRate?: number // The default playbackRate is 1
  defaultStartTime?: number; // Default start time for new sounds
  defaultVolume?: number; // Default volume for new sounds (0-1)
  fadeInDuration?: number; // Default fade-in duration in seconds
  fadeOutDuration?: number; // Default fade-out duration in seconds
  loopSounds?: boolean // Loop all sounds by default
  maxLoops?: number // if loopSounds is true and maxLoops is set, the sound will loop maxLoops times  (-1 is for infinite)
  pannerNodeConfig?: SoundPannerConfig; // Panner settings for 3D sound
  spatialAudio?: boolean; // Enable spatial audio features
  trackProgress?: boolean; // Track progress of the sound playback. 
  // This will keep track of the process and will dispatch the 'progress' event. This is useful when you want to show the progress of the sound playback.
}

Sound State Information

Information about a soundโ€™s current state:

export interface SoundStateInfo {
  progress: number; // ratio from 0 to 1
  startTime: number; // in seconds
  currentTime: number; // in seconds
  elapsedTime: number; // in seconds
  adjustedElapsedTime: number; // Elapsed time adjusted for playback rate
  duration: number; // in seconds
  rawDuration: number | null; // in seconds
  playbackRate: number | null;
  state: SoundState;
  volume: number; // value from 0 to 1
  pan: number; // value form 0 to 1
  panSpatialPosition: { x: number; y: number; z: number };
}

SoundState

Possible states of a sound:

export enum SoundState {
  Playing = "playing",
  Paused = "paused",
  Stopped = "stopped",
}

The Sound Object

export interface Sound {
  buffer: AudioBuffer;
  source: AudioBufferSourceNode | null;
  positionTracker?: ConstantSourceNode;
  currentLoopCount?: number;
  gainNode: GainNode;
  groupId?: string;
  id: string;
  isFadingIn?: boolean;
  isFadingOut?: boolean;
  originalVolume?: number;
  pannerNode?: PannerNode | null; // for 3D panning
  pan?: number; // Normal panning value -1 to 1
  panSpatialPosition? : { x: number; y: number; z: number };
  panType?: SoundPanType; 
  pausedAt?: number;
  playOptions?: PlayOptions;
  previousVolume?: number;
  sprite?: { [key: string]: [number, number] }; // Sprite support
  startTime?: number; // in seconds
  state?: SoundState;
  stereoPanner?: StereoPannerNode | null; // just plain left to right panning
  volume?: number; // values from 0 to 1
  duration?: number; // in seconds
  currentTime?:number; // in seconds
  instanceId?:string;
  instanceCount?:number;
  baseId?: string; // Base sound ID (e.g., "game-sound_jump")
}

SoundProgressStateInfo

Sound progress information, is connected to the sound event ->progressInfo

export interface SoundProgressStateInfo {
  soundId: string;
  currentTime: number;
  duration: number;
  rawDuration: number;
  progress: number; // 0-1
}

SoundPanType

export enum SoundPanType {
    Stereo = 'stereo',
    Spatial = 'spatial'
}

Spatial Audio

export enum PanningModel {
  HRTF = "HRTF",
  EqualPower = "equalpower",
}

export enum DistanceModel {
  Linear = "linear",
  Inverse = "inverse",
  Exponential = "exponential",
}

export interface SoundPannerConfig {
  /**
   * Determines which spatialisation algorithm to use to position the audio in 3D space.
   * - 'HRTF': More accurate, head-related transfer function (default)
   * - 'equalpower': Basic equal-power panning
   */
  panningModel?: PanningModel;

  /**
   * Determines how the volume of the audio source decreases as it moves away from the listener.
   * - 'linear': Volume reduces linearly with distance
   * - 'inverse': Volume reduces inversely with distance (realistic, default)
   * - 'exponential': Volume reduces exponentially with distance
   */
  distanceModel?: DistanceModel;

  /**
   * The reference distance for reducing volume as the audio source moves further from the listener.
   * Default is 1 meter.
   * @min 0
   */
  refDistance?: number;

  /**
   * The maximum distance between the audio source and the listener, after which the volume will not be reduced any further.
   * Default is 10000 meters.
   * @min refDistance
   */
  maxDistance?: number;

  /**
   * Describes how quickly the volume reduces as the source moves away from the listener.
   * - For 'linear': Valid range [0, 1], default 1
   * - For 'inverse': Valid range [0, โˆž], default 1
   * - For 'exponential': Valid range [0, โˆž], default 1
   */
  rolloffFactor?: number;

  /**
   * The angle, in degrees, of a cone inside which there will be no volume reduction.
   * Default is 360 (no cone).
   * @range [0, 360]
   */
  coneInnerAngle?: number;

  /**
   * The angle, in degrees, of a cone outside which the volume will be reduced by a constant value.
   * Default is 360 (no cone).
   * @range [0, 360]
   */
  coneOuterAngle?: number;

  /**
   * The amount of volume reduction outside the outer cone.
   * Default is 0.
   * @range [0, 1]
   */
  coneOuterGain?: number;
}

Reset options

export interface SoundResetOptions {
  keepVolumes?: boolean; // Keep current volume settings
  keepPanning?: boolean; // Keep current panning settings
  keepSpatial?: boolean; // Keep spatial audio settings
  keepPlaybackRate?: boolean // Keep playback rate
  unloadSounds?: boolean; // Unload all sounds
}

Demo

A comprehensive demo showcasing all features is available online:

The demo includes 5 interactive pages:

๐Ÿ› Bug Report

Found a bug or unexpected behavior? Please use the Bug Report page to submit an issue. Your feedback helps improve the library for everyone.

Licence

This project is developed by Chris Schardijn. It is free to use in your project.

๐Ÿ“‹ Version History

5.7.1

5.7.0

5.6.0

const controller = new AbortController();
soundManager.loadSound('bg', '/audio/bg.mp3', controller.signal)
  .catch(err => { if (err.name !== 'AbortError') throw err; });

// Cancel on cleanup:
controller.abort();

5.5.9

5.5.8

5.5.7

5.5.1 ~ 5.5.6

5.5.0 - Enhanced Audio Loading & Mobile Support

๐Ÿš€ New Features

โš™๏ธ Configuration Updates

interface SoundManagerConfig {
  // Loading Behavior
  webAudioPreferred?: boolean;       // Default: true
  html5AudioFallback?: boolean;      // Default: true
  maxParallelLoads?: number;         // Default: 10
  retryDelay?: number;               // Seconds, default: 0.5
  
  // Network Handling
  fetchRetries?: number;             // Default: 2
  fetchTimeout?: number;             // Seconds, default: 10
  corsProxy?: string;                // e.g. "https://corsproxy.io/?"
  fetchStrategy?: 'direct-first' | 'proxy-first' | 'direct-only';
  
  // Security & Limits
  maxAudioSize?: number;             // Bytes, default: 50MB
  audioCache?: boolean;              // Default: false
  crossOrigin?: "anonymous" | "use-credentials" | null;
  
  // Mobile
  autoUnlock?: boolean;              // Default: true
}

5.1.0 ~ 5.4.0

5.0.0 (Major & critical udpate )

Old Method New Method
soundManager.setSoundPosition(id) soundManager.setSpatialPosition(id)
soundManager.resetSoundPosition(id) soundManager.resetSpatialPosition(id)
playOptions (interface) PlayOptions (interface)
preloadSounds loadSounds

- Added more PlayOptions
  * fadeIn -> renamded to fadeInDuration
  * fadeOut -> renamed to fadeOutDuration
  * panSpatialPosition?: { x: number; y: number; z: number }
  * PanType?: SoundPanType (stereo or spatial)
  * trackProgress?: boolean (wheter to track playback progress)
  * createNewInstance (if false, it will use the previously instance of the sound)
  * playbackRate
  * isSeeking
  * duration (seconds)
  * pauseAtDurationReached (by default it will trigger the stop method when the duration is reached)
  
- Added more information to the getSoundState(id) `SoundStateInfo`
  * elapsedTime
  * panSpatialPosition
  * rawDuration

- Rebuild demo page
  * seperate component for the spatial grid
  * added dark theme
  * seperate component master constrols
  * seperate component sound controls
  * added playbackRate UI 
  
- Bug fixes
  * startTime in PlayOptions was not working correctly.
  * fix issues with the PlayOptions 
  * fix issues with spatial audio
  * fix reset method
  * fix playBackRate issues
  * fadeIn / fadeOut
  * fix issues with new instances
  * fix issues with Sprites
  * fixed typescript support .d.ts files


### 4.0.0 (Major update)

- Added PROGRESS event listener for sound playback monitoring
- Rebuilt sound sprite system with improved logic
- Fixed sound configuration issues (volume, fadeIn/fadeOut)
- Simplified build process and removed unnecessary Vite plugins
- Rebuild demo structure and split the code in components.
- Added new sound management methods:

```typescript
  getCurrentTime(id: string): number;
  getProgress(id: string): number; // Returns the progress as a ratio (0-1)
  getProgressPercentage(id: string): number;
  setDebugMode(debug: boolean): void;

3.2.0

๐ŸŽ‰ Added features

3.1.0

3.0.0

๐ŸšจBreaking changes and new Features

Improvements

Old Method New Method
soundManager.playSound(id) soundManager.play(id)
soundManager.stopSound(id) soundManager.stop(id)
soundManager.pauseSound(id) soundManager.pause(id)
soundManager.resumeSound(id) soundManager.resume(id)
soundManager.seekTo(id, time) soundManager.seek(id, time)
soundManager.setVolumeById(id, volume) soundManager.setSoundVolume(id, volume)
soundManager.getVolumeById(id) soundManager.getSoundVolume(id)
soundManager.setGlobalVolume(volume) soundManager.setGlobalVolume(volume)
soundManager.getGlobalVolume() soundManager.getGlobalVolume()
soundManager.muteAllSounds() soundManager.muteAll()
soundManager.unmuteAllSounds() soundManager.unmuteAll()
soundManager.muteSoundById(id) soundManager.mute(id)
soundManager.unmuteSoundById(id) soundManager.unmute(id)
soundManager.toggleMute() soundManager.toggleGlobalMute()
soundManager.fadeMasterIn(...) soundManager.fadeGlobalIn(...)
soundManager.fadeMasterOut(...) soundManager.fadeGlobalOut(...)
soundManager.setMasterPan(value) soundManager.setGlobalPan(value)
soundManager.getMasterPan() soundManager.getGlobalPan()
soundManager.resetMasterPan() soundManager.resetGlobalPan()
soundManager.cleanupMasterPan() soundManager.cleanupGlobalPan()

Added features

2.3.0

2.2.0

2.1.3 ~ 2.1.9 (Current)

2.1.2

๐Ÿ› Bug Fixes

๐ŸŽต Audio Improvements

2.1.1

๐Ÿ“š Documentation

โšก Dependencies

2.1.0

โœจ New Features

๐Ÿ”ง Improvements

2.0.0 (Major Release)

๐ŸŽ‰ Major Features

๐Ÿ”จ Technical Improvements

๐ŸŽฎ Demo

1.3.0

๐Ÿ› Bug Fixes

โœจ Enhancements

1.2.0

๐Ÿ”ง Build System

1.1.0

๐Ÿš€ Major Improvements

๐Ÿ”’ Security & Stability

๐Ÿ“ Development

1.0.4

๐ŸŽ‰ Initial Release

๐Ÿš€ Upcoming Features