Skip to main content

rockbox_codecs/
lib.rs

1//! Audio decoding with [Rockbox](https://www.rockbox.org)'s codecs as a
2//! reusable library.
3//!
4//! The actual decoders are the Rockbox firmware codecs (`lib/rbcodec/codecs/`
5//! plus their vendored decoder libraries — libmad, libtremor,
6//! libffmpegFLAC, libfaad, …), compiled by `build.rs` and driven through
7//! Rockbox's `codec_api` by a small C shim modeled on the upstream
8//! `warble` test player.
9//!
10//! ```no_run
11//! let mut dec = rockbox_codecs::Decoder::open("song.flac").unwrap();
12//! println!("{} — {}", dec.metadata().artist, dec.metadata().title);
13//! while let Some(chunk) = dec.next_chunk() {
14//!     // chunk.pcm: interleaved stereo i16 at chunk.sample_rate Hz
15//! }
16//! ```
17//!
18//! All 29 codecs of Rockbox's static-link set are feature-gated (`flac`,
19//! `mpa`, `vorbis`, `alac`, `aac`, `opus`, `wma`, `ape`, …) so you only
20//! compile the decoders you need.
21//!
22//! # Concurrency
23//!
24//! Rockbox codecs share one global `codec_api` instance, so **one decode
25//! session runs at a time**: `Decoder::open` blocks until any other live
26//! `Decoder` is dropped. Within a session, decoding runs on a dedicated
27//! thread and PCM is pulled through a bounded channel.
28
29use std::ffi::{c_char, c_void, CString};
30use std::path::Path;
31use std::sync::mpsc::{Receiver, SyncSender, TrySendError};
32use std::sync::{Condvar, Mutex};
33use std::time::Duration;
34
35pub use rockbox_metadata::Metadata;
36
37extern "C" {
38    fn rbcodec_set_sink(
39        cb: Option<unsafe extern "C" fn(*mut c_void, *const i16, usize, std::ffi::c_ulong)>,
40        user: *mut c_void,
41    );
42    fn rbcodec_open(path: *const c_char) -> i32;
43    fn rbcodec_run() -> i32;
44    fn rbcodec_close();
45    fn rbcodec_request_seek(time_ms: std::ffi::c_long);
46    fn rbcodec_request_halt();
47    fn rbcodec_elapsed_ms() -> std::ffi::c_ulong;
48}
49
50/// One buffer of decoded audio: interleaved stereo `i16` frames.
51#[derive(Debug, Clone)]
52pub struct Chunk {
53    /// Interleaved stereo samples (`pcm.len() == frames * 2`).
54    pub pcm: Vec<i16>,
55    /// Sample rate the codec is currently producing, in Hz.
56    pub sample_rate: u32,
57}
58
59/// Errors returned by [`Decoder::open`].
60#[derive(Debug)]
61pub enum Error {
62    /// The file could not be opened.
63    Open(std::path::PathBuf),
64    /// Metadata parsing failed — unrecognized or corrupt file.
65    Parse(std::path::PathBuf),
66    /// The file's format was recognized but its codec isn't compiled in
67    /// (enable the matching cargo feature).
68    CodecNotAvailable(String),
69    /// The codec failed to initialize.
70    CodecInit(std::path::PathBuf),
71    /// The path contains an interior NUL byte.
72    InvalidPath,
73    /// Out of memory.
74    OutOfMemory,
75}
76
77impl std::fmt::Display for Error {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            Error::Open(p) => write!(f, "cannot open {}", p.display()),
81            Error::Parse(p) => write!(f, "unrecognized or corrupt audio file {}", p.display()),
82            Error::CodecNotAvailable(c) => write!(
83                f,
84                "no decoder for {c} compiled in (enable the matching cargo feature)"
85            ),
86            Error::CodecInit(p) => write!(f, "codec failed to initialize for {}", p.display()),
87            Error::InvalidPath => write!(f, "path contains a NUL byte"),
88            Error::OutOfMemory => write!(f, "out of memory"),
89        }
90    }
91}
92
93impl std::error::Error for Error {}
94
95/// The codec_api state is global — serialize sessions.
96struct Gate {
97    busy: Mutex<bool>,
98    cv: Condvar,
99}
100
101static GATE: Gate = Gate {
102    busy: Mutex::new(false),
103    cv: Condvar::new(),
104};
105
106impl Gate {
107    fn acquire(&self) {
108        let mut busy = self.busy.lock().unwrap_or_else(|e| e.into_inner());
109        while *busy {
110            busy = self.cv.wait(busy).unwrap_or_else(|e| e.into_inner());
111        }
112        *busy = true;
113    }
114
115    fn release(&self) {
116        *self.busy.lock().unwrap_or_else(|e| e.into_inner()) = false;
117        self.cv.notify_one();
118    }
119}
120
121enum Msg {
122    Pcm(Chunk),
123    Done(i32),
124}
125
126struct SinkCtx {
127    tx: SyncSender<Msg>,
128}
129
130/// Sink trampoline, called on the decode thread for every burst of PCM
131/// the codec produces. A blocking send provides backpressure; when the
132/// receiver is gone (Decoder dropped mid-track) we request a halt and
133/// drop samples so the codec reaches its next command poll.
134unsafe extern "C" fn sink_trampoline(
135    user: *mut c_void,
136    pcm: *const i16,
137    frames: usize,
138    frequency: std::ffi::c_ulong,
139) {
140    let ctx = &*(user as *const SinkCtx);
141    let samples = std::slice::from_raw_parts(pcm, frames * 2);
142    let chunk = Chunk {
143        pcm: samples.to_vec(),
144        sample_rate: frequency as u32,
145    };
146    if ctx.tx.send(Msg::Pcm(chunk)).is_err() {
147        rbcodec_request_halt();
148    }
149}
150
151/// A running decode session for one audio file.
152pub struct Decoder {
153    rx: Receiver<Msg>,
154    thread: Option<std::thread::JoinHandle<()>>,
155    /// Kept alive for the C sink's `user` pointer.
156    _sink: Box<SinkCtx>,
157    meta: Metadata,
158    status: Option<i32>,
159}
160
161impl Decoder {
162    /// Open `path` and start decoding. Blocks while another `Decoder`
163    /// exists anywhere in the process (the codec state is global).
164    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
165        let path = path.as_ref();
166        let c_path =
167            CString::new(path.to_string_lossy().as_bytes()).map_err(|_| Error::InvalidPath)?;
168
169        // Parse tags up-front with the sibling crate (cheap second pass).
170        let meta = rockbox_metadata::read(path).map_err(|e| match e {
171            rockbox_metadata::Error::Open(p) => Error::Open(p),
172            rockbox_metadata::Error::OutOfMemory => Error::OutOfMemory,
173            _ => Error::Parse(path.to_path_buf()),
174        })?;
175
176        GATE.acquire();
177
178        let (tx, rx) = std::sync::mpsc::sync_channel::<Msg>(8);
179        let sink = Box::new(SinkCtx { tx: tx.clone() });
180
181        let rc = unsafe {
182            rbcodec_set_sink(
183                Some(sink_trampoline),
184                &*sink as *const SinkCtx as *mut c_void,
185            );
186            rbcodec_open(c_path.as_ptr())
187        };
188        if rc != 0 {
189            GATE.release();
190            return Err(match rc {
191                -1 => Error::Open(path.to_path_buf()),
192                -2 => Error::Parse(path.to_path_buf()),
193                -3 => Error::CodecNotAvailable(meta.codec.clone()),
194                -6 => Error::OutOfMemory,
195                _ => Error::CodecInit(path.to_path_buf()),
196            });
197        }
198
199        let thread = std::thread::Builder::new()
200            .name("rbcodec".into())
201            .spawn(move || {
202                let status = unsafe { rbcodec_run() };
203                let _ = tx.send(Msg::Done(status));
204            })
205            .expect("spawn codec thread");
206
207        Ok(Decoder {
208            rx,
209            thread: Some(thread),
210            _sink: sink,
211            meta,
212            status: None,
213        })
214    }
215
216    /// Tags and stream properties of the open file.
217    pub fn metadata(&self) -> &Metadata {
218        &self.meta
219    }
220
221    /// Next buffer of decoded PCM, or `None` at end of track (or after a
222    /// codec error — check [`Decoder::status`]).
223    pub fn next_chunk(&mut self) -> Option<Chunk> {
224        if self.status.is_some() {
225            return None;
226        }
227        match self.rx.recv() {
228            Ok(Msg::Pcm(chunk)) => Some(chunk),
229            Ok(Msg::Done(status)) => {
230                self.status = Some(status);
231                None
232            }
233            Err(_) => {
234                self.status = Some(-1);
235                None
236            }
237        }
238    }
239
240    /// Request a seek to `pos` (picked up at the codec's next command
241    /// poll; PCM already queued from before the seek still drains first).
242    pub fn seek(&mut self, pos: Duration) {
243        unsafe { rbcodec_request_seek(pos.as_millis() as std::ffi::c_long) };
244    }
245
246    /// Playback position last reported by the codec.
247    pub fn elapsed(&self) -> Duration {
248        Duration::from_millis(unsafe { rbcodec_elapsed_ms() } as u64)
249    }
250
251    /// Codec exit status: `Some(0)` after a clean end of track,
252    /// `Some(negative)` after a codec error, `None` while decoding.
253    pub fn status(&self) -> Option<i32> {
254        self.status
255    }
256}
257
258impl Drop for Decoder {
259    fn drop(&mut self) {
260        unsafe { rbcodec_request_halt() };
261        // Drain so a blocked sink send can't deadlock the codec thread.
262        while let Ok(msg) = self.rx.recv_timeout(Duration::from_secs(5)) {
263            if matches!(msg, Msg::Done(_)) {
264                break;
265            }
266        }
267        if let Some(t) = self.thread.take() {
268            let _ = t.join();
269        }
270        unsafe {
271            rbcodec_close();
272            rbcodec_set_sink(None, std::ptr::null_mut());
273        }
274        GATE.release();
275    }
276}