Current section
Files
Jump to
Current section
Files
src/rockbox/decoder.gleam
//// Decode an audio file to interleaved-stereo S16LE PCM, one chunk at a time.
////
//// Wraps the Rockbox codec engine. The codec state is process-wide, so only
//// one `Decoder` may decode at a time — opening a second one blocks until the
//// first is freed. It is a NIF resource, freed by the BEAM garbage collector —
//// no explicit close.
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode
import gleam/option.{type Option, None, Some}
import rockbox/metadata.{type Metadata}
/// Opaque decoder handle (a NIF resource).
pub type Decoder
/// Open a decoder for the local audio file at `path`.
@external(erlang, "rockbox_ffi_nif", "decoder_open")
pub fn open(path: String) -> Decoder
/// Tags and stream properties (same shape as `metadata.read`).
pub fn metadata(decoder: Decoder) -> Metadata {
let assert Ok(meta) =
decode.run(json_decode(ffi_metadata_json(decoder)), metadata.decoder())
meta
}
/// Next buffer of decoded PCM as `#(samples, sample_rate)`, where `samples` is
/// a little-endian int16 buffer of interleaved stereo PCM. `None` at end of
/// track (or after a codec error — see `finished`).
pub fn next_chunk(decoder: Decoder) -> Option(#(BitArray, Int)) {
case decode.run(ffi_next_chunk(decoder), chunk_decoder()) {
Ok(chunk) -> Some(chunk)
Error(_) -> None
}
}
/// Request a seek to `ms` milliseconds.
@external(erlang, "rockbox_ffi_nif", "decoder_seek_ms")
pub fn seek_ms(decoder: Decoder, ms: Int) -> Nil
/// Playback position last reported by the codec, in milliseconds.
@external(erlang, "rockbox_ffi_nif", "decoder_elapsed_ms")
pub fn elapsed_ms(decoder: Decoder) -> Int
/// `#(done, code)`: `done` is `True` once the track has ended; `code` is the
/// exit status (`0` = clean end, negative = codec error), valid only when
/// `done`.
@external(erlang, "rockbox_ffi_nif", "decoder_finished")
pub fn finished(decoder: Decoder) -> #(Bool, Int)
// -- decoders -----------------------------------------------------------
// next_chunk returns the tuple `#(binary, sample_rate)`, or the atom `nil` at
// end of track (typed Dynamic so we classify it via decode).
fn chunk_decoder() -> decode.Decoder(#(BitArray, Int)) {
use samples <- decode.field(0, decode.bit_array)
use rate <- decode.field(1, decode.int)
decode.success(#(samples, rate))
}
// -- FFI ----------------------------------------------------------------
@external(erlang, "rockbox_ffi_nif", "decoder_metadata_json")
fn ffi_metadata_json(decoder: Decoder) -> String
@external(erlang, "rockbox_ffi_nif", "decoder_next_chunk")
fn ffi_next_chunk(decoder: Decoder) -> Dynamic
@external(erlang, "json", "decode")
fn json_decode(json: String) -> Dynamic