Elixir bindings for the Rockbox DSP, metadata, codecs, and
playback engine, via an erl_nif shim over the librockbox_ffi C ABI.
📖 Sound settings reference — the equalizer, tone, crossfeed, compressor and other DSP controls mirror Rockbox's own. See the official Rockbox manual — Sound Settings.
Setup
Add it to your mix.exs and fetch:
def deps do
[{:rockbox_ex_ffi, "~> 0.5"}]
endmix deps.get # pulls in rockbox_ffi_nif (the shared native package)
mix compile # no Rust toolchain needed
Requires OTP 27+ (uses the built-in :json module — no jason dependency).
How the native code is delivered
This package contains only the Elixir wrappers — no Rust source, no C shim,
no static archive. The native code lives in a separate, shared Hex package,
rockbox_ffi_nif (source in
bindings/erlang), which the Gleam binding depends on too.
You never build it: on the first load of the NIF, rockbox_ffi_nif's Erlang
loader downloads a prebuilt rockbox_ffi_nif-<target>.so matching your OS/arch
from its GitHub release into your user cache and verifies it against a shipped
sha256 manifest. (The .so statically links the Rust engine and is far too
large to bundle in a Hex tarball, so it can't ship in the package itself.)
Prebuilt targets:
| Target | Tier |
|---|---|
aarch64-apple-darwin | supported |
x86_64-apple-darwin | supported |
x86_64-linux-gnu | supported |
aarch64-linux-gnu | supported |
x86_64-unknown-freebsd | best-effort |
x86_64-unknown-netbsd | best-effort |
The *BSD artifacts are built in a VM and may lag or be absent for a given release. Any other platform (musl/Alpine, Windows, or a glibc older than the CI runner's) has no prebuilt NIF — build from source instead (see below).
Building from source
A from-source build needs the full monorepo checkout — the Cargo workspace
and include/ header are not in any Hex package. The native code builds in the
shared rockbox_ffi_nif package, and this binding picks it up via a sibling
path dependency (../erlang) automatically:
# 1. Build the shared NIF once (compiles the Rust archive + links the .so).
cargo build --release -p rockbox-ffi
cd bindings/erlang && make && cd ../elixir
# 2. The path dep uses that local build — no download, no version override.
mix deps.get
mix compile
mix test
The loader prefers a local priv/rockbox_ffi_nif.so over any cached download,
so the freshly built .so is used as-is.
Generate the API docs locally with ExDoc:
mix docs # -> doc/index.html
Published docs live at https://hexdocs.pm/rockbox_ex_ffi/.
Usage
# --- metadata ---
{:ok, meta} = Rockbox.Metadata.read("song.flac")
meta.artist # "…"
meta.duration_ms # 122324
Rockbox.Metadata.probe("track.opus") # "Opus"
# --- DSP (interleaved stereo int16 binary) ---
d = Rockbox.Dsp.new(44_100)
Rockbox.Dsp.eq_enable(d, true)
Rockbox.Dsp.set_eq_band(d, 0, 60, 0.7, 3.0)
Rockbox.Dsp.set_replaygain(d, 0, true, 0.0) # 0 = track (DSP-native)
Rockbox.Dsp.set_replaygain_gains(d, -6.02, nil, nil, nil)
out = Rockbox.Dsp.process(d, pcm_binary) # int16 LE in/out
# --- codecs (decode a file to PCM, one chunk at a time) ---
dec = Rockbox.Decoder.open("song.flac")
dec |> Rockbox.Decoder.metadata() |> Map.get(:title) # tags from the open file
case Rockbox.Decoder.next_chunk(dec) do # {samples, sample_rate} | :eof
{samples, sample_rate} -> :ok # int16 LE interleaved stereo
:eof -> :done
end
Rockbox.Decoder.finished(dec) # {true, 0} (0 = clean)
# --- playback (needs an output device) ---
p = Rockbox.Player.new(volume: 0.8, crossfade_mode: 5) # 5 = always
Rockbox.Player.set_replaygain(p, 1, 0.0, true) # 1 = track (player)
# Queue entries may be local files, http(s):// URLs to remote media,
# or live-radio / streaming URLs — mix and match freely.
Rockbox.Player.set_queue(p, ["a.flac", "https://example.com/b.mp3", "http://radio.example/stream"])
Rockbox.Player.play(p)
Rockbox.Player.status(p) # %{state: "playing", index: 0, ...}Handles (Rockbox.Dsp / Rockbox.Decoder / Rockbox.Player) are NIF
resources freed by the BEAM garbage collector — no explicit close. Only one
Rockbox.Decoder may decode at a time (the codec state is process-wide).
Two ReplayGain encodings
The DSP and player use different mode integers (a quirk of the C ABI):
Rockbox.Dsp.set_replaygain/4→0track,1album,2shuffle,3offRockbox.Player.set_replaygain/4→0off,1track,2album
Shared native package
The C shim and Erlang NIF loader are not in this package — they live in the
shared rockbox_ffi_nif package, which the Gleam binding
(bindings/gleam/) depends on as well. This package carries only the
Rockbox.* Elixir wrappers.
Releasing (maintainers)
Because the native code is shared, publish rockbox_ffi_nif first, then
this package.
- Native NIFs — bump
{vsn, ...}inbindings/erlang/src/rockbox_ffi_nif.app.src, then run thebindings-erlang-release.ymlGitHub Actions workflow (push anerlang-v<version>tag or dispatch it). It builds one.soper target on a native runner and uploads them to theerlang-v<version>release. Publish the package to Hex locally (interactive Hex auth can't run in CI):bindings/scripts/publish-erlang.sh # writes the checksum manifest, then rebar3 hex publish - This package — bump
@versioninmix.exs(Hex versions are immutable), then publish locally:
The script exportsbindings/scripts/publish-elixir.sh # sets the rockbox_ffi_nif Hex dep, mix hex.publishROCKBOX_NIF_HEX=<version>so the published tarball depends on the releasedrockbox_ffi_nifHex version rather than the local../erlangpath dep used for monorepo development.