Building a Custom Sound Manager Class with Howler.js

In this article, you will learn how to create a custom sound manager class in JavaScript using the Howler.js library. We will cover how to initialize the class, load and organize audio tracks, implement global controls like play, pause, mute, and volume adjustments, and handle categorization for background music and sound effects to streamline audio control in your web applications or games.

Why Use a Custom Sound Manager?

While Howler.js provides a powerful API for handling web audio, managing multiple sounds individually throughout a large codebase can quickly become messy. A dedicated SoundManager class acts as a centralized controller (a singleton pattern) that keeps track of all audio instances, allows for global volume configuration, manages audio groups (like SFX and BGM), and simplifies playback with clean, reusable methods.

Setting Up the Sound Manager Class

To get started, first ensure you have installed Howler.js via npm (npm install howler) or included it via a CDN.

Below is a complete, production-ready ES6 implementation of a custom SoundManager class.

import { Howl, Howler } from 'howler';

class SoundManager {
  constructor() {
    if (SoundManager.instance) {
      return SoundManager.instance;
    }

    this.sounds = new Map();
    this.musicVolume = 1.0;
    this.sfxVolume = 1.0;
    this.isMuted = false;

    SoundManager.instance = this;
  }

  /**
   * Register and load a sound into the manager.
   * @param {string} key - Unique identifier for the sound.
   * @param {string} src - Path to the audio file.
   * @param {Object} options - Custom Howler configurations (loop, volume, html5, etc.)
   */
  load(key, src, options = {}) {
    if (this.sounds.has(key)) {
      console.warn(`Sound with key "${key}" already exists.`);
      return;
    }

    const sound = new Howl({
      src: [src],
      ...options
    });

    // Store the sound along with its category (defaulting to 'sfx')
    this.sounds.set(key, {
      howl: sound,
      category: options.category || 'sfx'
    });
  }

  /**
   * Play a registered sound by its key.
   * @param {string} key 
   */
  play(key) {
    const soundData = this.sounds.get(key);
    if (!soundData) {
      console.error(`Sound "${key}" not found.`);
      return;
    }

    // Apply specific category volume before playing
    const baseVolume = soundData.category === 'bgm' ? this.musicVolume : this.sfxVolume;
    soundData.howl.volume(baseVolume);
    soundData.howl.play();
  }

  /**
   * Pause a specific sound.
   * @param {string} key 
   */
  pause(key) {
    const soundData = this.sounds.get(key);
    if (soundData) {
      soundData.howl.pause();
    }
  }

  /**
   * Stop a specific sound.
   * @param {string} key 
   */
  stop(key) {
    const soundData = this.sounds.get(key);
    if (soundData) {
      soundData.howl.stop();
    }
  }

  /**
   * Adjust the volume for a specific category (bgm or sfx).
   * @param {string} category - 'bgm' or 'sfx'
   * @param {number} volume - Float between 0.0 and 1.0
   */
  setCategoryVolume(category, volume) {
    if (category === 'bgm') {
      this.musicVolume = volume;
    } else if (category === 'sfx') {
      this.sfxVolume = volume;
    }

    this.sounds.forEach((soundData) => {
      if (soundData.category === category) {
        soundData.howl.volume(volume);
      }
    });
  }

  /**
   * Mute or unmute all audio globally.
   * @param {boolean} muteState 
   */
  toggleMute(muteState) {
    this.isMuted = muteState !== undefined ? muteState : !this.isMuted;
    Howler.mute(this.isMuted);
  }
}

// Export a single instance of the Sound Manager
const soundManagerInstance = new SoundManager();
export default soundManagerInstance;

How to Use the Sound Manager

Once you have created and exported the SoundManager instance, you can import and utilize it across your application.

1. Load Audio Files

Define and load your assets early in your application’s lifecycle, such as in your main entry file or during a loading screen.

import soundManager from './SoundManager.js';

// Load Background Music (BGM)
soundManager.load('mainTheme', 'assets/audio/main_theme.mp3', {
  loop: true,
  html5: true, // Use HTML5 Audio for large files to stream without buffering delays
  category: 'bgm'
});

// Load Sound Effects (SFX)
soundManager.load('jump', 'assets/audio/jump.wav', { category: 'sfx' });
soundManager.load('coin', 'assets/audio/coin.wav', { category: 'sfx' });

2. Control Playback

Trigger sounds using their unique string keys anywhere in your application logic.

// Play background music
soundManager.play('mainTheme');

// Play sound effects during user interactions or game events
document.addEventListener('keydown', (event) => {
  if (event.code === 'Space') {
    soundManager.play('jump');
  }
});

3. Adjust Volumes and State globally

You can easily link these methods to a settings menu UI to give users granular control over their audio experience.

// Set background music volume to 50%
soundManager.setCategoryVolume('bgm', 0.5);

// Set sound effects volume to 80%
soundManager.setCategoryVolume('sfx', 0.8);

// Mute all game audio
soundManager.toggleMute(true);

// Unmute all game audio
soundManager.toggleMute(false);