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

rockbox-ffi

v0.5.0

Published

TypeScript bindings (Bun + Deno + Node.js FFI) for the Rockbox DSP / metadata / playback engine

Readme

rockbox-ffi (TypeScript)

npm TypeScript Bun Deno Node.js License

TypeScript bindings for the Rockbox audio engine — metadata parsing (40+ formats), the DSP pipeline (EQ, tone, surround, compressor, ReplayGain, resampler), a streaming codec decoder, and a queue-based player with crossfade. Local files and http(s):// streams alike. One typed API, three runtimes: Bun, Deno, and Node.js.

📖 Sound settings reference — the equalizer, tone, crossfeed, compressor and other DSP controls mirror Rockbox's own. See the official Rockbox manual — Sound Settings.

import { metadata, Dsp } from "rockbox-ffi/bun";

const meta = metadata.read("song.flac");
console.log(`${meta.artist} — ${meta.title} (${meta.duration_ms} ms)`);

Install

npm  install rockbox-ffi        # Node.js  (also pulls in koffi)
bun  add     rockbox-ffi        # Bun
deno add npm:rockbox-ffi        # Deno   (or import npm:rockbox-ffi/deno directly)

Then import the entry point for your runtime — all three expose the identical API:

| Runtime | Import | Notes | | ------- | -------------------------------------- | ------------------------------------------------ | | Bun | import … from "rockbox-ffi/bun" | uses built-in bun:ffi | | Deno | import … from "npm:rockbox-ffi/deno" | run with --allow-ffi --allow-read --allow-env | | Node.js | import … from "rockbox-ffi/node" | uses koffi (a dependency) |

Prefer a single import? import { load } from "rockbox-ffi" returns a promise that resolves the correct backend at runtime (see Runtime auto-detection).

The native library

These bindings call into the Rockbox engine through a native shared library (librockbox_ffi.dylib / .so). Point the package at it with the ROCKBOX_FFI_LIB environment variable:

export ROCKBOX_FFI_LIB=/path/to/librockbox_ffi.dylib

If unset, the loader searches upward from the package for a target/release/librockbox_ffi.{dylib,so} (handy when working inside the Rockbox repo). See Building the native library.

Quick start

import {
  metadata,
  Dsp,
  Decoder,
  Player,
  sineStereo,
  DspReplayGainMode,
  ReplayGainMode,
  CrossfadeMode,
} from "rockbox-ffi/bun"; // or /node, or npm:rockbox-ffi/deno

// --- metadata --------------------------------------------------------
const meta = metadata.read("song.flac");
console.log(meta.artist, "—", meta.title, `${meta.duration_ms} ms`);
console.log(metadata.probe("track.opus")); // "Opus"  (extension guess, no I/O)

// --- DSP: process interleaved stereo Int16 ---------------------------
const dsp = new Dsp(44_100);
dsp.eqEnable(true);
dsp.setEqBand(0, /*cutoffHz*/ 60, /*q*/ 0.7, /*gainDb*/ 3.0);
dsp.setReplaygain(DspReplayGainMode.TRACK, /*noclip*/ true, /*preampDb*/ 0.0);
dsp.setReplaygainGains(/*trackGainDb*/ -6.02); // −6 dB ≈ half amplitude

const input = sineStereo(1_000, 1.0, 44_100); // 1 s of a 1 kHz test tone
const output: Int16Array = dsp.process(input);
dsp.close();

// --- codecs: decode a file to PCM, one chunk at a time ---------------
const dec = new Decoder("song.flac"); // or an http(s):// URL
console.log(dec.metadata().title); // tags from the open file
for (let c = dec.nextChunk(); c !== null; c = dec.nextChunk()) {
  // c.samples: interleaved-stereo Int16 PCM, c.sampleRate: Hz
}
console.log(dec.finished()); // { done: true, code: 0 }  (0 = clean end)
dec.close();

// --- playback (needs an audio output device) -------------------------
const player = new Player({ volume: 0.8, crossfadeMode: CrossfadeMode.ALWAYS });
player.setReplaygain(ReplayGainMode.TRACK, 0.0, true);
// Queue entries may be local files, http(s):// URLs to remote media, or
// live-radio / streaming URLs — mix and match freely.
player.setQueue([
  "a.flac",
  "https://example.com/b.mp3",
  "http://radio.example/stream",
]);
player.play();
console.log(player.status()); // { state: "playing", index: 0, ... }

API

Everything is fully typed — the tables below are a map; your editor has the details.

metadata

| Function | Returns | Description | | -------------------------- | ---------------- | -------------------------------------------------------- | | metadata.read(path) | Metadata | Parse tags, duration, ReplayGain, album-art/cue offsets | | metadata.probe(filename) | string \| null | Codec label from the extension, without opening the file |

Dsp

Interleaved-S16LE-stereo processor. Construct with a sample rate, feed it Int16Arrays, and close() (or using) when done.

const dsp = new Dsp(sampleRate: number);

| Method | Description | | ---------------------------------------------------------------- | ------------------------------------------------------------ | | process(samples: Int16Array): Int16Array | Run stereo S16 frames through the pipeline | | setInputFrequency(hz) | Change input rate (engages the resampler) | | eqEnable(on) / setEqBand(band, cutoffHz, q, gainDb) | 10-band EQ (band 0 low-shelf, 9 high-shelf) | | setEqPrecut(db) | Negative pre-gain to avoid EQ clipping | | setTone(bassDb, trebleDb) / setToneCutoffs(bHz, tHz) | Bass/treble shelves | | setSurround(delayMs, balance, fx1, fx2) | Haas surround (delayMs > 0 enables) | | setChannelConfig(mode) / setStereoWidth(pct) | Channel mode / custom width | | setCompressor(threshold, makeup, ratio, knee, rel, atk) | Dynamic-range compressor (threshold 0 = off) | | setReplaygain(mode, noclip, preampDb) | ReplayGain mode (see encodings) | | setReplaygainGains(trackDb?, albumDb?, trackPeak?, albumPeak?) | Per-track gains in dB (omit = absent) | | setReplaygainGainsRaw(tg, ag, tp, ap: bigint) | Native Q7.24 gains (the raw_* metadata fields) | | flush() / close() | Drop buffered samples / free the handle |

Decoder

Streaming codec engine: decodes one audio source to interleaved-S16LE-stereo PCM, one chunk at a time. The source may be a local file or an http(s):// URL to remote media. The codec state is process-wide, so keep only one Decoder alive at a time and close() (or using) when done.

const dec = new Decoder(pathOrUrl: string);

| Method | Description | | ---------------------------------------------- | ------------------------------------------------------------------ | | metadata(): Metadata | Tags and stream properties (same shape as metadata.read) | | nextChunk(): { samples, sampleRate } \| null | Next PCM buffer (interleaved stereo Int16); null at end of track | | seekMs(ms) | Request a seek to ms milliseconds | | elapsedMs(): number | Playback position last reported by the codec | | finished(): { done, code } | done once ended; code is 0 (clean) or negative (codec error) | | close() | Free the handle |

Player

Queue-based player backed by a live audio device and a background thread. Queue entries may be local files, http(s):// URLs to remote media, or live-radio / streaming URLs.

const player = new Player(config?: PlayerConfig);

| Method | Description | | -------------------------------------------------------------- | ---------------------------------------- | | setQueue(paths) / enqueue(path) | Replace / append (files or http(s)://) | | remove(index) / clearQueue() | Drop one track / empty the queue | | play() pause() toggle() stop() | Transport | | next() previous() skipTo(index) seekMs(ms) | Navigation | | setVolume(v) / volume() | Volume, 0.01.0 | | sampleRate() | Output rate everything resamples to | | setCrossfade(mode, foDelay?, foDur?, fiDelay?, fiDur?, mix?) | Configure crossfade | | setReplaygain(mode, preampDb, preventClipping) | ReplayGain (player encoding) | | status(): PlayerStatus | Snapshot: state, index, position, queue… | | close() | Stop playback and free the handle |

PlayerConfig (all optional): sampleRate (0 = device default), bufferSeconds, volume, replaygainMode, replaygainPreampDb, replaygainPreventClipping, crossfadeMode, fadeOutDelayMs, fadeOutDurationMs, fadeInDelayMs, fadeInDurationMs, mixMode.

Enums & helpers

DspReplayGainMode, ReplayGainMode, CrossfadeMode, MixMode, ChannelConfig, and sineStereo(freqHz, seconds, rate, amplitude?) for generating test tones. abiVersion() returns the native ABI version.

Types

Metadata, PlayerStatus, ReplayGain, AlbumArt, Cuesheet, PlayerConfig are exported for annotations. JSON field names are snake_case (e.g. duration_ms, sample_rate, replaygain.raw_track_gain).

Resource management

Dsp, Decoder, and Player hold native handles. Free them with close(), or let using do it automatically (all implement Symbol.dispose):

using dsp = new Dsp(44_100);
// dsp.close() runs at end of scope

Everything else that crosses the FFI boundary (JSON strings, sample buffers) is freed inside the wrappers — you never call a *_free yourself.

Note: the Dsp wraps a process-wide singleton, so keep only one alive at a time and use it from a single thread.

Two ReplayGain encodings

The DSP and player take different mode integers (a quirk of the underlying C ABI) — use the named enums and you won't have to remember which:

  • Dsp.setReplaygainDspReplayGainMode (TRACK=0, ALBUM=1, SHUFFLE=2, OFF=3)
  • Player.setReplaygainReplayGainMode (OFF=0, TRACK=1, ALBUM=2)

Runtime auto-detection

If you don't want to hard-code the backend, import load from the package root; it dynamically imports the right module (so the other runtimes' FFI code is never parsed):

import { load } from "rockbox-ffi";

const { metadata, Dsp, Player } = await load();

Developing

The rest is only relevant when hacking on the bindings themselves inside the Rockbox repo.

Building the native library

cargo build --release -p rockbox-ffi
#  target/release/librockbox_ffi.dylib   (macOS)
#  target/release/librockbox_ffi.so      (Linux)

The loader finds this automatically when the package sits inside the repo; elsewhere, set ROCKBOX_FFI_LIB.

Running the smoke tests

bun  run examples/smoke.bun.ts
deno run --allow-ffi --allow-read --allow-env examples/smoke.deno.ts
npm  install && npx tsx examples/smoke.node.ts
bunx tsc --noEmit                                # type check

Building & publishing

The package is authored in TypeScript and shipped as bundled ESM + .d.ts under dist/:

bun install
bun run build      # Bun.build + tsc  ->  dist/*.js + dist/*.d.ts
npm publish        # prepublishOnly runs the build automatically

build bundles the four entry points (index / bun / deno / node), keeping bun:ffi and koffi external, so consumers get the right backend via the package exports map (., ./bun, ./deno, ./node).