npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

lyra-audio

v0.1.26

Published

Flexible audio player library with Web Audio API and HTML5 support

Readme

lyra-audio

A flexible, lightweight audio player library for the browser. Supports both Web Audio API and HTML5 Audio playback strategies, with optional HLS streaming via hls.js.

Features

  • 🎵 Dual playback strategies — HTML5 Audio for streaming, Web Audio API for precise control
  • 📡 HLS streaming — optional support via hls.js (peer dependency)
  • 🎛️ Built-in 10-band EQ — with enable/disable toggle
  • 📊 Audio analysis — frequency and time-domain data via AnalyserNode
  • 🔊 Volume, mute, playback rate, loop — full playback control
  • 🎚️ Fade in / out — smooth volume transitions via Web Audio API
  • 📦 Multiple source types — URL, Blob, File, ArrayBuffer, Uint8Array, HLS
  • 🛡️ Cancellation support — safe loading cancellation with CancellationToken
  • 🔄 State machine — predictable player lifecycle with validated transitions
  • 📝 TypeScript first — full type definitions with branded types for safety

Table of Contents


Installation

npm install lyra-audio
# or
pnpm add lyra-audio

For HLS support, also install hls.js:

pnpm add hls.js

Quick Start

import { Player } from "lyra-audio";

const player = Player.auto();

await player.load("https://example.com/song.mp3");
await player.play();

player.on("timeupdate", ({ currentTime, duration, progress }) => {
  console.log(
    `${currentTime}s / ${duration}s (${(progress * 100).toFixed(1)}%)`,
  );
});

player.on("ended", () => console.log("Track finished"));

await player.dispose();

Factory Methods

Three factory methods cover the most common use cases:

import { Player } from "lyra-audio";

// Auto-detect the best strategy based on source type
const player = Player.auto();

// Optimized for music — uses "playback" latency hint for better audio quality
const musicPlayer = Player.forMusic();

// Optimized for live streaming — HTML5 only, metadata preload
const streamPlayer = Player.forStreaming();

// Full control via constructor
const customPlayer = new Player({
  mode: "webaudio", // "html5" | "webaudio" | "auto"
  volume: 0.5,
  autoplay: true,
  loop: true,
  latencyHint: "playback",
  muted: false,
  playbackRate: 1,
  preload: "auto", // "none" | "metadata" | "auto"
});

Choosing a strategy

| Strategy | Best for | Notes | | ---------- | ---------------------------------- | -------------------------------------------------- | | html5 | Streaming, HLS, large files | Lower memory usage | | webaudio | EQ, visualization, precise control | Decodes full file before playback | | auto | General use | Picks html5 for URLs/HLS, webaudio for buffers |


Loading Sources

load() accepts several source types. Calling load() again automatically cancels any in-progress loading.

// Plain URL
await player.load("https://example.com/track.mp3");

// File from <input type="file">
const file = inputElement.files[0];
await player.load(file);

// Blob
await player.load(someBlob);

// ArrayBuffer or Uint8Array — decoded via Web Audio API
await player.load({ data: arrayBuffer });
await player.load({ data: uint8Array });

// URL with custom headers (e.g. authenticated endpoints)
await player.load({
  url: "https://api.example.com/audio/123",
  headers: { Authorization: "Bearer token" },
});

// HLS stream — requires hls.js peer dependency
await player.load({
  url: "https://example.com/stream/playlist.m3u8",
  type: "hls",
});

HLS Streaming

Pass the Hls constructor when creating the player. lyra-audio treats it as an optional peer dependency and will not import it automatically.

import Hls from "hls.js";
import { Player } from "lyra-audio";

const player = new Player({
  Hls,
  hlsConfig: {
    maxBufferLength: 30,
    maxMaxBufferLength: 60,
  },
});

await player.load("https://example.com/live/playlist.m3u8");

// Receive available quality levels once the manifest is parsed
player.on("qualitiesavailable", (levels) => {
  console.log("Qualities:", levels);
  // [{ index: 0, bitrate: 500000, label: "500kbps" }, ...]
});

// Switch quality manually (-1 = auto)
const levels = player.getQualityLevels();
player.setQuality(levels[0].index);

// Track active quality
player.on("qualitychange", (level) => {
  console.log("Switched to:", level.label);
});

Playback Control

await player.play();
player.pause();
player.stop(); // pause + seek to 0
await player.togglePlay();

// Seeking
player.seek(30); // seek to 30 seconds
player.seekPercent(0.5); // seek to 50% of duration

// Playback rate (0.0625–16, clamped automatically)
player.setPlaybackRate(1.5);

player.setLoop(true);

// State and position
console.log(player.state); // current state string
console.log(player.currentTime); // TimeSeconds
console.log(player.duration); // TimeSeconds
console.log(player.isPlaying); // boolean
console.log(player.isReady); // true when ready/playing/paused/buffering
console.log(player.mode); // "html5" | "webaudio"

Volume & Mute

player.setVolume(0.8); // 0.0–1.0, clamped automatically
player.setMuted(true);
player.toggleMute();

console.log(player.volume); // Volume (0–1)
console.log(player.muted); // boolean

player.on("volumechange", ({ volume, muted }) => {
  updateUI(volume, muted);
});

Fade Effects

Fade methods are available after load() completes. If called before load() or after dispose(), they return immediately without doing anything.

// Fade to a specific volume over N seconds
await player.fadeTo(0.3, 2);

// Convenience methods
await player.fadeIn(1.5); // fade from 0 to current volume
await player.fadeOut(1.0); // fade to silence

// Fade out then pause/stop (restores volume after)
await player.fadeOutAndPause(1.0);
await player.fadeOutAndStop(1.0);

// Cancel an in-progress fade immediately
player.cancelFade();

console.log(player.isFading); // boolean

Equalizer

The EQ is a 10-band parametric equalizer built on BiquadFilterNode. It is available via player.graph after load() completes.

await player.load("https://example.com/song.mp3");

const graph = player.graph;
if (!graph) return; // null before load() or after dispose()

// Band indices: 0=32Hz, 1=64Hz, 2=125Hz, 3=250Hz, 4=500Hz,
//               5=1kHz,  6=2kHz,  7=4kHz,  8=8kHz,  9=16kHz

// Set individual band gain in dB
graph.setEQBand(0, 6); // boost 32Hz by 6dB
graph.setEQBand(9, -3); // cut 16kHz by 3dB

// Set all 10 bands at once
graph.setEQBands([6, 4, 2, 0, 0, 0, 0, 2, 4, 6]);

// Read current gain of a band
const bassGain = graph.getEQBand(0);

// Toggle EQ processing (bands retain their values when disabled)
graph.setEQEnabled(false);
graph.setEQEnabled(true);
console.log(graph.eqEnabled); // boolean

// Reset all bands to 0dB
graph.resetEQ();

// Inspect current band config
console.log(graph.bands);
// [{ frequency: 32, gain: 0, Q: 1, type: "lowshelf" }, ...]

If you are certain load() has already been called, you can use graphOrThrow to skip the null check:

// Throws a descriptive error if graph is not ready instead of silent null
player.graphOrThrow.setEQBand(0, 6);

Audio Visualization

getFrequencyData() and getTimeDomainData() return a reference to the same internal Uint8Array on every call. Copy it with .slice() if you need to hold the data across frames.

const graph = player.graph;
if (!graph) return;

function draw() {
  if (!player.graph) return;

  // Frequency spectrum — 0–255 per bin
  const freqData = graph.getFrequencyData();

  // Waveform — 0–255, 128 = silence
  const timeData = graph.getTimeDomainData();

  // Snapshot if you need to store it:
  const snapshot = freqData.slice();

  renderVisualizer(freqData, timeData);
  requestAnimationFrame(draw);
}

draw();

// Configure the AnalyserNode directly
graph.analyzer.fftSize = 4096;
graph.analyzer.smoothingTimeConstant = 0.85;

You can also pass analyser options when constructing AudioGraph directly:

import { AudioGraph } from "lyra-audio";

const graph = new AudioGraph(audioContext, {
  analyser: {
    fftSize: 4096,
    smoothingTimeConstant: 0.85,
    minDecibels: -90,
    maxDecibels: -10,
  },
});

Cancellation

Calling load() again automatically cancels any in-progress load. For manual control use CancellationToken:

import { CancellationToken, CancellationError } from "lyra-audio";

let token = new CancellationToken();

async function loadTrack(url: string) {
  token.cancel();
  token = new CancellationToken();

  try {
    await token.wrap(player.load(url));
  } catch (err) {
    if (err instanceof CancellationError) {
      console.log("Load cancelled");
    }
  }
}
const token = new CancellationToken();

token.isCancelled; // false
token.cancel();
token.isCancelled; // true
token.throwIfCancelled(); // throws CancellationError

// Wrap any Promise to make it cancellation-aware
await token.wrap(somePromise);

// Cancel old token and get a fresh one
// Always capture the return value — the old token is cancelled and unusable
token = CancellationToken.replace(token);

Events

player.on() returns an unsubscribe function.

const unsubscribe = player.on("canplay", () => {
  console.log("Ready!");
  unsubscribe();
});

// Wait for a single event (Promise-based)
const { duration } = await player.waitFor("loadedmetadata", {
  timeout: 5000,
  signal: abortController.signal,
});

Event reference

// ── Lifecycle ──────────────────────────────────────────────────────────────
player.on("loadstart", () => {});
player.on("loadedmetadata", ({ duration }) => {}); // fired before canplay
player.on("canplay", () => {});
player.on("canplaythrough", () => {}); // enough data to play to end

// ── Playback ───────────────────────────────────────────────────────────────
player.on("play", () => {}); // play() was called
player.on("playing", () => {}); // audio is actually producing output
player.on("pause", () => {});
player.on("ended", () => {});
player.on("stop", () => {});

// ── Time ───────────────────────────────────────────────────────────────────
player.on("timeupdate", ({ currentTime, duration, progress }) => {
  // progress: 0–1
});
player.on("durationchange", (duration) => {});
player.on("seeking", (time) => {});
player.on("seeked", (time) => {});

// ── Buffering ─────────────────────────────────────────────────────────────
player.on("waiting", () => {}); // buffering started, playback stalled
player.on("buffered", () => {}); // buffering ended, playback resumed

// ── State ─────────────────────────────────────────────────────────────────
player.on("statechange", ({ from, to }) => {});

// ── Volume ────────────────────────────────────────────────────────────────
player.on("volumechange", ({ volume, muted }) => {});
player.on("ratechange", (rate) => {});

// ── Quality (HLS only) ────────────────────────────────────────────────────
player.on("qualitiesavailable", (levels) => {}); // QualityLevel[]
player.on("qualitychange", (level) => {}); // QualityLevel

// ── Errors ────────────────────────────────────────────────────────────────
player.on("error", ({ code, message, cause }) => {});

// ── Cleanup ───────────────────────────────────────────────────────────────
player.on("dispose", () => {});

Error Handling

All errors include a PlayerErrorCode:

import { PlayerErrorCode, PlayerError } from "lyra-audio";

player.on("error", ({ code, message, cause }) => {
  switch (code) {
    case PlayerErrorCode.LOAD_ABORTED:
      // Cancelled — usually safe to ignore
      break;
    case PlayerErrorCode.LOAD_NETWORK:
      showToast("Network error. Check your connection.");
      break;
    case PlayerErrorCode.LOAD_DECODE:
      showToast("Could not decode audio file.");
      break;
    case PlayerErrorCode.LOAD_NOT_SUPPORTED:
      showToast("Audio format not supported.");
      break;
    case PlayerErrorCode.PLAYBACK_NOT_ALLOWED:
      // Browser autoplay policy — a user gesture is required
      showPlayButton();
      break;
    case PlayerErrorCode.PLAYBACK_FAILED:
      showToast("Playback failed.");
      break;
    case PlayerErrorCode.HLS_FATAL:
    case PlayerErrorCode.HLS_NETWORK:
    case PlayerErrorCode.HLS_MEDIA:
      showToast("Streaming error.");
      break;
  }
});

// load() and play() also throw — wrap in try/catch if needed
try {
  await player.load(url);
  await player.play();
} catch (err) {
  if (err instanceof PlayerError) {
    console.error(err.code, err.message, err.cause);
  }
}

Player State

The player follows a strict state machine. Invalid transitions are ignored with a console warning.

idle ──► loading ──► ready ──► playing ──► paused
  ▲         │           │         │           │
  └─────────┴───────────┴────► error ◄────────┘
                                  │
                               disposed

| State | Meaning | | ----------- | ----------------------------------------------------- | | idle | Initial state, nothing loaded | | loading | load() in progress | | ready | Loaded and ready to play (also after stop()) | | playing | Audio is playing | | paused | Paused mid-playback | | buffering | Playing but stalled waiting for data | | error | An error occurred — recover by calling load() again | | disposed | dispose() was called — player is unusable |

console.log(player.state);

player.on("statechange", ({ from, to }) => {
  console.log(`${from} → ${to}`);
});

TypeScript: Branded Types

lyra-audio uses branded primitive types to prevent accidentally passing a raw number where a typed value is expected:

import {
  createVolume,
  createTimeSeconds,
  createPlaybackRate,
  type Volume,
  type TimeSeconds,
  type PlaybackRate,
} from "lyra-audio";

// Constructors validate and clamp values
const vol = createVolume(1.5); // clamped to 1.0
const time = createTimeSeconds(-5); // clamped to 0
const rate = createPlaybackRate(2); // clamped to 0.0625–16

// Plain number is not assignable to a branded type
const v: Volume = 0.5; // TS error
const v: Volume = createVolume(0.5); // ✅

// Event payloads already use branded types
player.on("timeupdate", ({ currentTime, duration }) => {
  const mid = createTimeSeconds(duration / 2);
  player.seek(mid);
});

API Reference

Player

| Method / Property | Type | Description | | ------------------------------- | -------------------- | ------------------------------------------ | | Player.auto(options?) | static | Factory: auto strategy | | Player.forMusic(options?) | static | Factory: latencyHint "playback" | | Player.forStreaming(options?) | static | Factory: HTML5, metadata preload | | load(source) | Promise<void> | Load a source. Cancels any previous load. | | play() | Promise<void> | Start or resume playback | | pause() | void | Pause playback | | stop() | void | Pause and seek to 0 | | togglePlay() | Promise<void> | Toggle play/pause | | seek(seconds) | void | Seek to absolute position | | seekPercent(0–1) | void | Seek to relative position | | setVolume(0–1) | void | Set volume (clamped automatically) | | setMuted(bool) | void | Set mute state | | toggleMute() | void | Toggle mute | | setPlaybackRate(rate) | void | Set speed (clamped to 0.0625–16) | | setLoop(bool) | void | Enable/disable loop | | fadeTo(vol, sec) | Promise<void> | Fade to volume over duration | | fadeIn(sec) | Promise<void> | Fade from 0 to current volume | | fadeOut(sec) | Promise<void> | Fade to silence | | fadeOutAndPause(sec) | Promise<void> | Fade out then pause | | fadeOutAndStop(sec) | Promise<void> | Fade out then stop | | cancelFade() | void | Cancel in-progress fade | | getQualityLevels() | QualityLevel[] | HLS quality levels | | setQuality(index) | void | Select HLS quality level | | getCurrentQuality() | number | Current HLS quality index (-1 = auto) | | dispose() | Promise<void> | Release all resources | | state | PlayerState | Current state | | currentTime | TimeSeconds | Current playback position | | duration | TimeSeconds | Total duration | | volume | Volume | Current volume (0–1) | | muted | boolean | Mute state | | playbackRate | PlaybackRate | Current playback rate | | loop | boolean | Loop state | | isPlaying | boolean | True if currently playing | | isReady | boolean | True if ready/playing/paused/buffering | | isFading | boolean | True if a fade is in progress | | mode | PlaybackMode | Active strategy: "html5" | "webaudio" | | graph | AudioGraph \| null | Audio graph — available after load() | | graphOrThrow | AudioGraph | Same, but throws if not ready | | audioContext | AudioContext | Underlying AudioContext (lazy-created) |

AudioGraph

| Method / Property | Type | Description | | ------------------------- | --------------- | -------------------------------------------------- | | setEQBand(index, dB) | void | Set single band gain in dB | | setEQBands(gains[]) | void | Set all 10 bands at once | | getEQBand(index) | number | Get current gain for a band | | resetEQ() | void | Reset all bands to 0dB | | setEQEnabled(bool) | void | Toggle EQ processing | | setVolume(0–1) | void | Set output volume | | fadeTo(vol, sec, from?) | Promise<void> | Fade output volume | | cancelFade() | void | Cancel in-progress fade | | getFrequencyData() | Uint8Array | Frequency spectrum (live buffer — copy if storing) | | getTimeDomainData() | Uint8Array | Waveform data (live buffer — copy if storing) | | eqEnabled | boolean | Whether EQ is active | | bands | EQBand[] | Current band configuration | | isFading | boolean | Whether a fade is running | | input | AudioNode | Graph input node | | output | AudioNode | Graph output node | | analyzer | AnalyserNode | Direct access for custom configuration |

CancellationToken

| Method / Property | Type | Description | | -------------------------------- | ------------- | ------------------------------------------------------ | | cancel() | void | Cancel the token | | isCancelled | boolean | Whether the token is cancelled | | throwIfCancelled() | void | Throws CancellationError if cancelled | | wrap(promise) | Promise<T> | Rejects with CancellationError if token is cancelled | | signal | AbortSignal | Underlying AbortSignal | | CancellationToken.replace(old) | static | Cancels old token, returns a new one |