Skip to main content

rockbox_dsp/
lib.rs

1//! FFI bindings to the Rockbox DSP pipeline (`lib/rbcodec/dsp/`), compiled
2//! standalone by `build.rs` — no firmware, no kernel, no SDL.
3//!
4//! Pipeline stages (fixed order, each individually enable-able):
5//! pre-gain (replaygain / EQ precut) → timestretch → resampler → crossfeed → 10-band EQ →
6//! tone controls → bass enhancement (PBE) → auditory fatigue reduction →
7//! Haas surround → channel modes → compressor.
8//!
9//! The raw API mirrors `dsp_core.h`; [`Dsp`] is a minimal safe wrapper for
10//! the common case (interleaved 16-bit stereo in → interleaved 16-bit
11//! stereo out), suitable for feeding a cpal output callback.
12//!
13//! Unit conventions (see `dsp_filter.c`): EQ/tone gains and Q are in
14//! **tenths** (`gain = -80` → −8.0 dB, `q = 7` → Q 0.7 — pass `q * 10`).
15
16#![allow(non_camel_case_types)]
17
18use std::ffi::{c_int, c_long, c_uint, c_void};
19use std::sync::Once;
20
21/* enum dsp_ids */
22pub const CODEC_IDX_AUDIO: c_uint = 0;
23pub const CODEC_IDX_VOICE: c_uint = 1;
24
25/* enum dsp_settings */
26pub const DSP_RESET: c_uint = 0;
27pub const DSP_SET_FREQUENCY: c_uint = 1;
28pub const DSP_SET_SAMPLE_DEPTH: c_uint = 2;
29pub const DSP_SET_STEREO_MODE: c_uint = 3;
30pub const DSP_FLUSH: c_uint = 4;
31pub const DSP_SET_PITCH: c_uint = 5;
32pub const DSP_SET_OUT_FREQUENCY: c_uint = 6;
33pub const DSP_GET_OUT_FREQUENCY: c_uint = 7;
34
35/* enum dsp_stereo_modes */
36pub const STEREO_INTERLEAVED: isize = 0;
37pub const STEREO_NONINTERLEAVED: isize = 1;
38pub const STEREO_MONO: isize = 2;
39
40pub const EQ_NUM_BANDS: usize = 10;
41
42/* enum replaygain_types (dsp_misc.h) */
43pub const REPLAYGAIN_TRACK: c_int = 0;
44pub const REPLAYGAIN_ALBUM: c_int = 1;
45pub const REPLAYGAIN_SHUFFLE: c_int = 2; /* track gain if shuffling, else album */
46pub const REPLAYGAIN_OFF: c_int = 3;
47
48/// `struct sample_format` (dsp_core.h)
49#[repr(C)]
50#[derive(Clone, Copy, Default)]
51pub struct sample_format {
52    pub version: u8,
53    pub num_channels: u8,
54    pub frac_bits: u8,
55    pub output_scale: u8,
56    pub frequency: i32,
57    pub codec_frequency: i32,
58}
59
60#[repr(C)]
61#[derive(Clone, Copy)]
62pub union dsp_buffer_ptrs {
63    pub pin: [*const c_void; 2],
64    pub p32: [*mut i32; 2],
65    pub p16out: *mut i16,
66}
67
68#[repr(C)]
69#[derive(Clone, Copy)]
70pub union dsp_buffer_count {
71    pub proc_mask: u32,
72    pub bufcount: c_int,
73}
74
75/// `struct dsp_buffer` (dsp_core.h)
76#[repr(C)]
77pub struct dsp_buffer {
78    pub remcount: i32,
79    pub ptrs: dsp_buffer_ptrs,
80    pub count: dsp_buffer_count,
81    pub format: sample_format,
82}
83
84/// `struct eq_band_setting` (eq.h) — `q` and `gain` in tenths, `cutoff` in Hz
85#[repr(C)]
86#[derive(Clone, Copy)]
87pub struct eq_band_setting {
88    pub cutoff: c_int,
89    pub q: c_int,
90    pub gain: c_int,
91}
92
93/// `struct compressor_settings` (compressor.h) — threshold in dB (0 = off),
94/// makeup_gain 0 = off / 1 = auto, ratio index (0 = 2:1 … 4 = limit),
95/// knee index (0 = hard … 2 = soft), attack/release in ms
96#[repr(C)]
97#[derive(Clone, Copy, Default)]
98pub struct compressor_settings {
99    pub threshold: c_int,
100    pub makeup_gain: c_int,
101    pub ratio: c_int,
102    pub knee: c_int,
103    pub release_time: c_int,
104    pub attack_time: c_int,
105}
106
107/// `struct replaygain_settings` (dsp_misc.h) — `type_` is one of the
108/// `REPLAYGAIN_*` constants, `preamp` in dB × 10, `noclip` scales down
109/// to prevent clipping (works even without gain tags if a peak is known)
110#[repr(C)]
111#[derive(Clone, Copy)]
112pub struct replaygain_settings {
113    pub noclip: bool,
114    pub type_: c_int,
115    pub preamp: c_int,
116}
117
118/* enum AUDIOHW_CHANNEL_CONFIG (shim/sound.h) — channel_config values */
119pub const SOUND_CHAN_STEREO: c_int = 0;
120pub const SOUND_CHAN_MONO: c_int = 1;
121pub const SOUND_CHAN_CUSTOM: c_int = 2;
122pub const SOUND_CHAN_MONO_LEFT: c_int = 3;
123pub const SOUND_CHAN_MONO_RIGHT: c_int = 4;
124pub const SOUND_CHAN_KARAOKE: c_int = 5;
125pub const SOUND_CHAN_SWAP: c_int = 6;
126
127/// Opaque `struct dsp_config`
128#[repr(C)]
129pub struct dsp_config {
130    _private: [u8; 0],
131}
132
133extern "C" {
134    pub fn dsp_init();
135    pub fn dsp_get_config(dsp_id: c_uint) -> *mut dsp_config;
136    pub fn dsp_get_id(dsp: *const dsp_config) -> c_uint;
137    pub fn dsp_configure(dsp: *mut dsp_config, setting: c_uint, value: isize) -> isize;
138    pub fn dsp_process(
139        dsp: *mut dsp_config,
140        src: *mut dsp_buffer,
141        dst: *mut dsp_buffer,
142        thread_yield: bool,
143    );
144
145    /* eq.c */
146    pub fn dsp_eq_enable(enable: bool);
147    pub fn dsp_set_eq_precut(precut: c_int);
148    pub fn dsp_set_eq_coefs(band: c_int, setting: *const eq_band_setting);
149
150    /* dsp_sample_output.c */
151    pub fn dsp_dither_enable(enable: bool);
152
153    /* tone_controls.c — gains in dB×10; prescale must be set LAST: it
154     * recomputes the filter coefficients and enables/disables the stage */
155    pub fn tone_set_bass(bass: c_int);
156    pub fn tone_set_treble(treble: c_int);
157    pub fn tone_set_bass_cutoff(hz: c_int);
158    pub fn tone_set_treble_cutoff(hz: c_int);
159    pub fn tone_set_prescale(prescale: c_int);
160
161    /* surround.c — Haas surround. enable takes the delay in ms (>0 = on) */
162    pub fn dsp_surround_enable(delay_ms: c_int);
163    pub fn dsp_surround_set_balance(balance: c_int);
164    pub fn dsp_surround_set_cutoff(frq_l: c_int, frq_h: c_int);
165    pub fn dsp_surround_side_only(side_only: bool);
166    pub fn dsp_surround_mix(mix: c_int);
167
168    /* channel_mode.c — SOUND_CHAN_* config + custom stereo width in % */
169    pub fn channel_mode_set_config(value: c_int);
170    pub fn channel_mode_custom_set_width(value: c_int);
171
172    /* compressor.c */
173    pub fn dsp_set_compressor(settings: *const compressor_settings);
174
175    /* dsp_misc.c */
176    pub fn dsp_replaygain_set_settings(settings: *const replaygain_settings);
177    pub fn dsp_set_pitch(pitch: i32);
178    pub fn dsp_get_pitch() -> i32;
179    pub fn dsp_set_all_output_frequency(samplerate: c_uint);
180    pub fn dsp_get_output_frequency(dsp: *mut dsp_config) -> c_uint;
181
182    /* shim/rbdsp_shim.c — sends REPLAYGAIN_SET_GAINS to the audio DSP.
183     * Gains/peaks are Q7.24 linear factors (get_replaygain_int output);
184     * 0 = not tagged. */
185    pub fn rbdsp_replaygain_set_gains(
186        track_gain: c_long,
187        album_gain: c_long,
188        track_peak: c_long,
189        album_peak: c_long,
190    );
191    /* dB × 100 → Q7.24 linear scale factor */
192    pub fn get_replaygain_int(int_gain: c_long) -> c_long;
193}
194
195static DSP_INIT: Once = Once::new();
196
197/// Safe wrapper around the audio DSP instance for interleaved 16-bit stereo.
198///
199/// The underlying `dsp_config` is a static singleton (`CODEC_IDX_AUDIO`), so
200/// only one `Dsp` may exist at a time and it is not `Send`/`Sync`.
201pub struct Dsp {
202    cfg: *mut dsp_config,
203}
204
205impl Dsp {
206    /// Set up the audio DSP for interleaved S16LE stereo at `sample_rate`,
207    /// output at the same rate (no resampling).
208    pub fn new(sample_rate: u32) -> Self {
209        DSP_INIT.call_once(|| unsafe { dsp_init() });
210        let cfg = unsafe { dsp_get_config(CODEC_IDX_AUDIO) };
211        assert!(!cfg.is_null());
212        unsafe {
213            dsp_configure(cfg, DSP_RESET, 0);
214            dsp_configure(cfg, DSP_SET_OUT_FREQUENCY, sample_rate as isize);
215            dsp_configure(cfg, DSP_SET_FREQUENCY, sample_rate as isize);
216            dsp_configure(cfg, DSP_SET_SAMPLE_DEPTH, 16);
217            dsp_configure(cfg, DSP_SET_STEREO_MODE, STEREO_INTERLEAVED);
218        }
219        Dsp { cfg }
220    }
221
222    pub fn raw(&mut self) -> *mut dsp_config {
223        self.cfg
224    }
225
226    /// Change the input sample rate (e.g. on track change); the resampler
227    /// stage engages automatically when it differs from the output rate.
228    pub fn set_input_frequency(&mut self, hz: u32) {
229        unsafe { dsp_configure(self.cfg, DSP_SET_FREQUENCY, hz as isize) };
230    }
231
232    /// Drop any samples buffered inside the pipeline (call on seek/stop).
233    pub fn flush(&mut self) {
234        unsafe { dsp_configure(self.cfg, DSP_FLUSH, 0) };
235    }
236
237    pub fn eq_enable(&mut self, enable: bool) {
238        unsafe { dsp_eq_enable(enable) };
239    }
240
241    /// Bass/treble tone controls — shelving filters at fixed cutoffs
242    /// (200 Hz bass, 3.5 kHz treble). Gains in plain dB, matching
243    /// rockboxd's `bass` / `treble` settings.toml keys. 0/0 disables
244    /// the stage. Mirrors firmware sound.c: values ×10, prescale by
245    /// the larger boost to avoid clipping.
246    pub fn set_tone(&mut self, bass_db: i32, treble_db: i32) {
247        let bass = bass_db * 10;
248        let treble = treble_db * 10;
249        unsafe {
250            tone_set_bass(bass);
251            tone_set_treble(treble);
252            tone_set_prescale(bass.max(treble).max(0));
253        }
254    }
255
256    /// Override the tone-control shelf cutoffs in Hz (0 = defaults:
257    /// 200 Hz bass, 3500 Hz treble). Call before [`Dsp::set_tone`] — the
258    /// coefficients are recomputed by its prescale step.
259    pub fn set_tone_cutoffs(&mut self, bass_hz: i32, treble_hz: i32) {
260        unsafe {
261            tone_set_bass_cutoff(bass_hz);
262            tone_set_treble_cutoff(treble_hz);
263        }
264    }
265
266    /// Haas surround. `delay_ms` > 0 enables the stage (rockboxd's
267    /// `surround_enabled` key IS the delay in ms, 0 = off); `balance` in
268    /// percent, `fx1`/`fx2` are the band-split cutoffs in Hz. Argument
269    /// order mirrors apps/settings.c: balance, cutoffs, then enable.
270    pub fn set_surround(&mut self, delay_ms: i32, balance: i32, fx1: i32, fx2: i32) {
271        unsafe {
272            dsp_surround_set_balance(balance);
273            if fx1 > 0 && fx2 > 0 {
274                dsp_surround_set_cutoff(fx1, fx2);
275            }
276            dsp_surround_enable(delay_ms);
277        }
278    }
279
280    /// Channel configuration (`SOUND_CHAN_*`: stereo / mono / custom /
281    /// karaoke / swap …).
282    pub fn set_channel_config(&mut self, mode: i32) {
283        unsafe { channel_mode_set_config(mode) };
284    }
285
286    /// Stereo width in percent — only audible when the channel config is
287    /// `SOUND_CHAN_CUSTOM` (100 = unchanged, 0 = mono, >100 = wider).
288    pub fn set_stereo_width(&mut self, percent: i32) {
289        unsafe { channel_mode_custom_set_width(percent) };
290    }
291
292    /// Dynamic-range compressor; `threshold` of 0 disables the stage.
293    pub fn set_compressor(&mut self, settings: &compressor_settings) {
294        unsafe { dsp_set_compressor(settings) };
295    }
296
297    /// Replaygain mode (`REPLAYGAIN_TRACK` / `_ALBUM` / `_SHUFFLE` /
298    /// `_OFF`), clipping prevention, and preamp in plain dB. This is the
299    /// per-player setting; the per-track values come from
300    /// [`Dsp::set_replaygain_gains`] — both are needed before the
301    /// pre-gain stage engages.
302    pub fn set_replaygain(&mut self, mode: i32, noclip: bool, preamp_db: f32) {
303        let settings = replaygain_settings {
304            noclip,
305            type_: mode,
306            preamp: (preamp_db * 10.0).round() as c_int,
307        };
308        unsafe { dsp_replaygain_set_settings(&settings) };
309    }
310
311    /// Per-track replaygain values as tagged in the file's metadata:
312    /// gains in plain dB, peaks as linear peak amplitude (1.0 = full
313    /// scale), `None` = tag absent. Call on every track change — the
314    /// `DSP_RESET` issued by [`Dsp::new`] zeroes the gains but keeps the
315    /// mode from [`Dsp::set_replaygain`].
316    pub fn set_replaygain_gains(
317        &mut self,
318        track_gain_db: Option<f32>,
319        album_gain_db: Option<f32>,
320        track_peak: Option<f32>,
321        album_peak: Option<f32>,
322    ) {
323        let gain = |db: Option<f32>| {
324            db.map_or(0, |db| unsafe {
325                get_replaygain_int((db * 100.0).round() as c_long)
326            })
327        };
328        let peak = |p: Option<f32>| p.map_or(0, |p| (p * (1 << 24) as f32).round() as c_long);
329        unsafe {
330            rbdsp_replaygain_set_gains(
331                gain(track_gain_db),
332                gain(album_gain_db),
333                peak(track_peak),
334                peak(album_peak),
335            )
336        };
337    }
338
339    /// Native-unit variant of [`Dsp::set_replaygain_gains`]: Q7.24 linear
340    /// factors exactly as Rockbox's metadata parser stores them in
341    /// `mp3entry` (`get_replaygain_int` output), 0 = not tagged.
342    pub fn set_replaygain_gains_raw(
343        &mut self,
344        track_gain: c_long,
345        album_gain: c_long,
346        track_peak: c_long,
347        album_peak: c_long,
348    ) {
349        unsafe { rbdsp_replaygain_set_gains(track_gain, album_gain, track_peak, album_peak) };
350    }
351
352    /// Configure one EQ band. Band 0 is a low shelf, band `EQ_NUM_BANDS-1`
353    /// a high shelf, the rest are peaking filters. `q` and `gain_db` are in
354    /// plain units here; the ×10 fixed-point convention is applied inside.
355    pub fn set_eq_band(&mut self, band: usize, cutoff_hz: i32, q: f32, gain_db: f32) {
356        assert!(band < EQ_NUM_BANDS);
357        let setting = eq_band_setting {
358            cutoff: cutoff_hz,
359            q: (q * 10.0).round() as c_int,
360            gain: (gain_db * 10.0).round() as c_int,
361        };
362        unsafe { dsp_set_eq_coefs(band as c_int, &setting) };
363    }
364
365    /// Configure one EQ band with native Rockbox units — `q` and `gain` in
366    /// tenths (`q = 70` → Q 7.0, `gain = -125` → −12.5 dB), exactly the
367    /// format of rockboxd's `[[eq_band_settings]]` in settings.toml.
368    pub fn set_eq_band_raw(&mut self, band: usize, setting: eq_band_setting) {
369        assert!(band < EQ_NUM_BANDS);
370        unsafe { dsp_set_eq_coefs(band as c_int, &setting) };
371    }
372
373    /// EQ precut in dB (applied as negative pre-gain to avoid clipping).
374    pub fn set_eq_precut(&mut self, db: f32) {
375        unsafe { dsp_set_eq_precut((db * 10.0).round() as c_int) };
376    }
377
378    /// Run interleaved stereo S16 frames through the pipeline, appending
379    /// processed interleaved stereo S16 frames to `out`. Returns the number
380    /// of frames produced (not necessarily equal to the input count — the
381    /// resampler/timestretch stages may buffer internally).
382    pub fn process(&mut self, input: &[i16], out: &mut Vec<i16>) -> usize {
383        assert!(input.len() % 2 == 0, "input must be interleaved stereo");
384        let mut produced = 0usize;
385        let mut chunk = [0i16; 8192]; // 4096 frames per dsp_process call
386
387        let mut src = dsp_buffer {
388            remcount: (input.len() / 2) as i32,
389            ptrs: dsp_buffer_ptrs {
390                pin: [input.as_ptr() as *const c_void; 2],
391            },
392            count: dsp_buffer_count { proc_mask: 0 },
393            format: sample_format::default(), // tagged by dsp_process
394        };
395
396        loop {
397            let mut dst = dsp_buffer {
398                remcount: 0,
399                ptrs: dsp_buffer_ptrs {
400                    p16out: chunk.as_mut_ptr(),
401                },
402                count: dsp_buffer_count {
403                    bufcount: (chunk.len() / 2) as c_int,
404                },
405                format: sample_format::default(),
406            };
407            unsafe { dsp_process(self.cfg, &mut src, &mut dst, false) };
408            let frames = dst.remcount as usize;
409            if frames == 0 && src.remcount <= 0 {
410                break;
411            }
412            out.extend_from_slice(&chunk[..frames * 2]);
413            produced += frames;
414            if src.remcount <= 0 && frames < chunk.len() / 2 {
415                break; // input drained and pipeline made no full chunk
416            }
417        }
418        produced
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    /* The underlying dsp_config is a static singleton, so all assertions
427     * against it live in one test. */
428    #[test]
429    fn replaygain_track_gain_scales_amplitude() {
430        let mut dsp = Dsp::new(44100);
431        dsp.set_replaygain(REPLAYGAIN_TRACK, false, 0.0);
432        dsp.set_replaygain_gains(Some(-6.0206), None, None, None); /* ×0.5 */
433
434        let input: Vec<i16> = (0..44100)
435            .flat_map(|i| {
436                let s = ((i as f32 * 2.0 * std::f32::consts::PI * 1000.0 / 44100.0).sin() * 16000.0)
437                    as i16;
438                [s, s]
439            })
440            .collect();
441        let mut out = Vec::new();
442        dsp.process(&input, &mut out);
443
444        let peak = out.iter().map(|s| (*s as i32).abs()).max().unwrap();
445        assert!(
446            (7600..=8400).contains(&peak),
447            "expected ~8000 after -6.02 dB track gain, got peak {peak}"
448        );
449
450        /* REPLAYGAIN_OFF restores unity */
451        dsp.set_replaygain(REPLAYGAIN_OFF, false, 0.0);
452        dsp.flush();
453        out.clear();
454        dsp.process(&input, &mut out);
455        let peak = out.iter().map(|s| (*s as i32).abs()).max().unwrap();
456        assert!(
457            (15200..=16000).contains(&peak),
458            "expected ~16000 with replaygain off, got peak {peak}"
459        );
460    }
461}