How to Manage Howler.js in an Angular Service

This article explains how to efficiently integrate and manage howler.js audio instances within an Angular application using a centralized Angular service. You will learn how to install the library, set up a service to control audio playback, manage multiple audio instances using a Map, and clean up resources to prevent memory leaks.

1. Install Howler.js and Type Definitions

First, install the howler library and its TypeScript definitions to ensure type safety in your Angular project:

npm install howler
npm install --save-dev @types/howler

2. Create the Audio Service

Generate a singleton service using the Angular CLI:

ng generate service audio

Managing audio through a service keeps your components clean, ensures a single source of truth for audio playback states, and makes audio controls globally accessible.

3. Implement Audio Instance Management

To manage multiple audio files, use a Map within your service. This allows you to reference, play, pause, and destroy specific audio instances using unique string keys.

Here is the complete implementation of the AudioService:

import { Injectable, OnDestroy } from '@angular/core';
import { Howl } from 'howler';

@Injectable({
  providedIn: 'root'
})
export class AudioService implements OnDestroy {
  private sounds = new Map<string, Howl>();

  /**
   * Load and play a sound. If it already exists, it plays the existing instance.
   * @param key Unique identifier for the audio instance
   * @param src URL or path to the audio file
   */
  play(key: string, src: string): void {
    let sound = this.sounds.get(key);

    if (!sound) {
      sound = new Howl({
        src: [src],
        html5: true, // Use HTML5 Audio to handle large files/streaming
        onend: () => {
          console.log(`Finished playing: ${key}`);
        }
      });
      this.sounds.set(key, sound);
    }

    if (!sound.playing()) {
      sound.play();
    }
  }

  /**
   * Pause a specific sound by key.
   */
  pause(key: string): void {
    const sound = this.sounds.get(key);
    if (sound && sound.playing()) {
      sound.pause();
    }
  }

  /**
   * Stop and reset playback of a specific sound.
   */
  stop(key: string): void {
    const sound = this.sounds.get(key);
    if (sound) {
      sound.stop();
    }
  }

  /**
   * Unload a specific sound from memory.
   */
  unload(key: string): void {
    const sound = this.sounds.get(key);
    if (sound) {
      sound.unload();
      this.sounds.delete(key);
    }
  }

  /**
   * Unload all sounds to prevent memory leaks when the service is destroyed.
   */
  ngOnDestroy(): void {
    this.sounds.forEach((sound) => sound.unload());
    this.sounds.clear();
  }
}

4. Inject and Use the Service in a Component

To control audio from your components, inject the AudioService and call its methods inside your template handlers:

import { Component } from '@angular/core';
import { AudioService } from './audio.service';

@Component({
  selector: 'app-audio-player',
  template: `
    <button (click)="playTrack()">Play Track</button>
    <button (click)="pauseTrack()">Pause Track</button>
    <button (click)="stopTrack()">Stop Track</button>
  `
})
export class AudioPlayerComponent {
  private trackKey = 'background-music';
  private trackUrl = 'assets/audio/background.mp3';

  constructor(private audioService: AudioService) {}

  playTrack(): void {
    this.audioService.play(this.trackKey, this.trackUrl);
  }

  pauseTrack(): void {
    this.audioService.pause(this.trackKey);
  }

  stopTrack(): void {
    this.audioService.stop(this.trackKey);
  }
}

Best Practices for Howler.js in Angular