rockbox_ffi_nif
View SourceShared Erlang NIF over the librockbox_ffi C ABI β
the Rockbox DSP / metadata / codecs / playback engine exposed to the BEAM.
This is the common native layer for the language bindings that run on the
BEAM. The Elixir (rockbox_ex_ffi) and Gleam
(rockbox_ffi) packages both depend on this one instead of each vendoring
their own copy of the C shim and Erlang loader; the ergonomic wrappers live in
those packages.
π Sound settings reference β the equalizer, tone, crossfeed, compressor and other DSP controls mirror Rockbox's own. See the official Rockbox manual β Sound Settings.
What's here
| Path | Role |
|---|---|
c_src/rockbox_ffi_nif.c | erl_nif shim β a 1:1 surface of include/rockbox_ffi.h |
src/rockbox_ffi_nif.erl | NIF loader + stub module; downloads the .so on first load |
priv/rockbox_ffi_nif.manifest | per-triple sha256 checksums for the first-load download |
Makefile | local-dev build of priv/rockbox_ffi_nif.so |
Usage
Every function is a raw NIF: strings/paths are UTF-8 binaries, *_json
functions return raw JSON binaries (decode with OTP 27's json module), and
handles (RbDecoder, RbPlayer, RbDsp) are opaque reference()s freed by
the GC β there is no explicit close. For application code prefer the ergonomic
Elixir or
Gleam
wrappers; the examples below use the raw surface directly.
Read metadata
%% Guess the codec from the extension (no I/O)β¦
<<"FLAC">> = rockbox_ffi_nif:meta_probe(<<"song.flac">>),
%% β¦or parse the full tag/stream properties.
Json = rockbox_ffi_nif:meta_read_json(<<"/music/song.flac">>),
#{<<"title">> := Title,
<<"artist">> := Artist,
<<"duration_ms">> := DurMs,
<<"sample_rate">> := Rate} = json:decode(Json).Decode a file to PCM
decoder_next_chunk/1 hands back interleaved-stereo little-endian int16
PCM until it returns nil at end of track.
D = rockbox_ffi_nif:decoder_open(<<"/music/song.mp3">>),
%% Metadata is available from the open decoder too.
Meta = json:decode(rockbox_ffi_nif:decoder_metadata_json(D)),
%% Pull every chunk into one PCM binary.
Drain = fun Drain(Acc) ->
case rockbox_ffi_nif:decoder_next_chunk(D) of
nil -> lists:reverse(Acc);
{Pcm, _Rate} -> Drain([Pcm | Acc])
end
end,
Pcm = iolist_to_binary(Drain([])),
%% Confirm the track ended cleanly (0 = clean, negative = codec error).
{true, 0} = rockbox_ffi_nif:decoder_finished(D).Seek before decoding more:
rockbox_ffi_nif:decoder_seek_ms(D, 30000), %% jump to 30s
Ms = rockbox_ffi_nif:decoder_elapsed_ms(D).Run PCM through the DSP pipeline
Dsp = rockbox_ffi_nif:dsp_new(44100),
%% Turn on the equalizer and lift a 1 kHz band by 6 dB.
rockbox_ffi_nif:dsp_eq_enable(Dsp, true),
rockbox_ffi_nif:dsp_set_eq_band(Dsp, 4, 1000, 1.0, 6.0),
Out = rockbox_ffi_nif:dsp_process(Dsp, Pcm).Play a queue
A player owns a live audio device and a background engine thread; dropping the last reference to the handle stops playback.
P = rockbox_ffi_nif:player_new(),
%% Queue local files and/or http(s)/stream URLs.
Queue = json:encode([<<"/music/a.mp3">>, <<"/music/b.flac">>]),
rockbox_ffi_nif:player_set_queue_json(P, Queue),
rockbox_ffi_nif:player_enqueue(P, <<"http://host/stream.mp3">>),
rockbox_ffi_nif:player_set_volume(P, 0.8),
rockbox_ffi_nif:player_play(P),
%% Transport controls.
rockbox_ffi_nif:player_next(P),
rockbox_ffi_nif:player_seek_ms(P, 15000),
%% Queue editing: drop the entry at index 0, or wipe the queue entirely.
rockbox_ffi_nif:player_remove(P, 0),
rockbox_ffi_nif:player_clear_queue(P),
%% Poll playback state.
Status = json:decode(rockbox_ffi_nif:player_status_json(P)),
rockbox_ffi_nif:player_pause(P).The player exposes the same DSP chain as the standalone pipeline β
player_set_eq_band/5, player_set_tone/5, player_set_crossfade/7,
player_set_replaygain/4, and more (see the
module docs).
NIF delivery
The compiled NIF is a multi-megabyte shared object (it statically links the
Rust librockbox_ffi.a), too large to bundle in a Hex tarball. So the Hex
package ships only the checksum manifest, and rockbox_ffi_nif.erl's -on_load
hook downloads the matching rockbox_ffi_nif-<triple>.so from the GitHub
release named in the manifest into the user cache on first use, verifying it
against the manifest sha256. This is why both the Elixir and Gleam bindings get
their native code the same way.
Local development
Inside a full monorepo checkout (needs the Cargo workspace + include/ header):
# 1. Build the Rust static archive.
cargo build --release -p rockbox-ffi
# 2. Build the NIF into priv/rockbox_ffi_nif.so.
cd bindings/erlang && make
# 3. Compile the Erlang app.
rebar3 compile
The Elixir/Gleam bindings pick up this locally-built .so via a path dependency
on ../erlang β the loader prefers a local priv/*.so over any cached download.
Supported prebuilt targets
aarch64-apple-darwin, x86_64-apple-darwin, aarch64-linux-gnu,
x86_64-linux-gnu, x86_64-unknown-freebsd, x86_64-unknown-netbsd.
Requires OTP 27+ (the bindings decode JSON with the built-in json module).
Other platforms build from source (steps above).