rockbox-clj-ffi — Clojure #
Clojure bindings for the Rockbox DSP, metadata, codecs, and
playback engine, over the shared
rockbox-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.
No JNI, no native glue to compile: the binding calls the Java Foreign Function
& Memory API (JEP 454, stable since JDK 22) through interop to locate
librockbox_ffi at runtime and bind every function to a MethodHandle
downcall. Keep src/rockbox/ffi.clj in sync with
include/rockbox_ffi.h.
Toolchain #
Requires a JDK with a stable FFM API (JDK 22+) and the Clojure CLI. Both are
pinned in mise.toml (Temurin 25 + Clojure 1.12):
mise install # provisions Temurin 25 + the Clojure CLI
Build the shared library once from the repo root (the loader also honours
ROCKBOX_FFI_LIB, else walks up to target/release):
cargo build --release -p rockbox-ffi
Run #
mise exec -- clojure -M:smoke # end-to-end smoke test
mise exec -- clojure -M:play /path/to/audio # play through the output device
Usage #
(require '[rockbox.ffi.metadata :as metadata]
'[rockbox.ffi.dsp :as dsp]
'[rockbox.ffi.decoder :as decoder]
'[rockbox.ffi.player :as player])
;; metadata -> map with keyword keys
(metadata/read "/music/song.flac") ; => {:title "…" :codec "FLAC" …}
(metadata/probe "song.flac") ; => "FLAC"
;; DSP (interleaved stereo int16 short-array)
(dsp/with-dsp [d 44100]
(dsp/eq-enable d true)
(dsp/set-eq-band d 0 100 0.7 3.0)
(dsp/process d samples)) ; => short-array
;; Codecs — decode a file to PCM, one chunk at a time
(decoder/with-decoder [d "/music/song.flac"]
(:title (decoder/metadata d)) ; tags from the open file
(loop []
(when-let [[samples rate] (decoder/next-chunk d)]
;; samples: interleaved-stereo signed 16-bit PCM at `rate` Hz
(recur)))
(decoder/finished d)) ; => [true 0] (0 = clean end)
;; Player (queue + transport)
(player/with-player [p {:volume 0.8}]
;; Queue entries may be local files, http(s):// URLs to remote media,
;; or live-radio streams, or HLS / MPEG-DASH manifests (.m3u8/.mpd) — mix and match freely.
(player/set-queue p ["/music/a.flac"
"https://example.com/b.mp3"
"http://radio.example/stream"
"https://cdn.example.com/live/main.m3u8"])
(player/play p)
(:state (player/status p)))
- Rich values (metadata, player status) come back as maps with keyword keys,
parsed from the ABI's JSON with
clojure.data.json. - Native memory is freed automatically:
with-dsp/with-decoder/with-playerfree their handle, and everychar*/int16*the ABI returns is freed inside the binding. The codec engine is process-wide, so only one decoder may run at a time — opening a second blocks until the first is freed. - Enum arguments accept either a keyword or the raw int (see
src/rockbox/enums.clj). Two ReplayGain encodings:dsp-replaygain-mode(:track0,:album1,:shuffle2,:off3) forrockbox.dsp,replaygain-mode(:off0,:track1,:album2) forrockbox.player.
Bundled native libraries #
The published jar bundles the prebuilt librockbox_ffi for every OS/arch under
native/<target>/ — rockbox.ffi/extract-bundled picks the one matching the
running JVM, extracts it to a temp file, and loads it. So a consumer just adds
the dependency; no Rust toolchain, no separate .dylib/.so. ROCKBOX_FFI_LIB
still overrides, and a repo checkout falls back to target/release.
Publishing (Clojars, io.github.tsirysndr) #
Coordinates: io.github.tsirysndr/rockbox-clj-ffi. The build mirrors the sibling
org.clojars.tsiry/rockbox-clj SDK — a plain <scm> url plus a version-derived
<tag> (clojure-ffi-v<version>), which builds cleanly on cljdoc.org. One-time
setup: register the group on clojars.org under Verified
Groups → GitHub (or use org.clojars.tsirysndr, auto-granted), and create a
deploy token.
# 1. stage the prebuilt libs for every platform into the jar resources
bindings/scripts/fetch-libs.sh --all
# 2. one-shot release: stamp cljdoc config, commit + tag clojure-ffi-v0.1.1,
# deploy to Clojars, push the tag, and request a cljdoc build
VERSION=0.1.1 CLOJARS_USERNAME=tsirysndr CLOJARS_PASSWORD=<deploy-token> \
mise exec -- clojure -T:build release
release requires a clean working tree. Lower-level tasks are available too:
clojure -T:build jar (build only), deploy (Clojars only — assumes you have
already stamped + tagged), stamp-cljdoc, and request-cljdoc. The
<scm><tag> must be a real git tag so cljdoc can check out the exact
sources — release creates and pushes it. Consume it with:
io.github.tsirysndr/rockbox-clj-ffi {:mvn/version "0.1.1"}
Coexisting with the rockbox-clj SDK #
This repo also publishes a separate gRPC SDK, org.clojars.tsiry/rockbox-clj
(tagged clojure-v*). Nothing here collides with it:
- Clojars coordinate — different group and artifact
(
io.github.tsirysndr/rockbox-clj-ffivsorg.clojars.tsiry/rockbox-clj), so the two are independent projects on Clojars. - Clojure namespaces — this binding lives under
rockbox.ffi.*; the SDK owns the barerockbox.*(rockbox.core,rockbox.playback, …). A project can depend on both with no namespace clash. - cljdoc README — cljdoc renders a single repo-root
doc/cljdoc.ednper checked-out tag (it has no per-subdirectory config). Since each artifact builds from its own release tag,releasestamps that repo-root file to point at this binding's README before taggingclojure-ffi-v*; the SDK'sreleasere-stampssdk/clojure/README.mdforclojure-v*. Each cljdoc build reads the config at its own tag, so neither shows the other's README. - Release tags — the SDK uses
clojure-v*; this binding usesclojure-ffi-v*(matches neither the SDK's nor the sharedbindings-v*tag-triggered workflows, so it triggers none of them). Its prebuilt native libs still ship from thebindings-v*GitHub release viafetch-libs.sh.