use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering};
use std::sync::{Mutex, OnceLock};
use rockbox_dsp::{eq_band_setting, Dsp, EQ_NUM_BANDS};
use crate::settings::{EqBand, Settings, EQ_BANDS};
pub struct Equalizer {
enabled: AtomicBool,
version: AtomicU64,
bands: Mutex<Vec<EqBand>>,
bass: AtomicI32,
treble: AtomicI32,
bass_cutoff: AtomicI32,
treble_cutoff: AtomicI32,
}
static GLOBAL: OnceLock<Equalizer> = OnceLock::new();
impl Equalizer {
pub fn global() -> &'static Equalizer {
GLOBAL.get_or_init(|| {
let settings = Settings::load();
Equalizer {
enabled: AtomicBool::new(settings.eq_enabled),
version: AtomicU64::new(0),
bands: Mutex::new(settings.eq_band_settings),
bass: AtomicI32::new(settings.bass),
treble: AtomicI32::new(settings.treble),
bass_cutoff: AtomicI32::new(settings.bass_cutoff),
treble_cutoff: AtomicI32::new(settings.treble_cutoff),
}
})
}
pub fn is_enabled(&self) -> bool {
self.enabled.load(Ordering::Relaxed)
}
pub fn is_active(&self) -> bool {
self.is_enabled() || self.bass() != 0 || self.treble() != 0
}
pub fn bass(&self) -> i32 {
self.bass.load(Ordering::Relaxed)
}
pub fn treble(&self) -> i32 {
self.treble.load(Ordering::Relaxed)
}
pub fn adjust_bass(&self, delta_db: i32) {
let new = (self.bass() + delta_db).clamp(-24, 24);
self.bass.store(new, Ordering::Relaxed);
self.version.fetch_add(1, Ordering::Relaxed);
}
pub fn adjust_treble(&self, delta_db: i32) {
let new = (self.treble() + delta_db).clamp(-24, 24);
self.treble.store(new, Ordering::Relaxed);
self.version.fetch_add(1, Ordering::Relaxed);
}
pub fn version(&self) -> u64 {
self.version.load(Ordering::Relaxed)
}
pub fn bands(&self) -> Vec<EqBand> {
self.bands.lock().unwrap().clone()
}
pub fn set_enabled(&self, enabled: bool) {
self.enabled.store(enabled, Ordering::Relaxed);
self.version.fetch_add(1, Ordering::Relaxed);
}
pub fn set_bands(&self, new_bands: Vec<EqBand>) {
*self.bands.lock().unwrap() = new_bands;
self.version.fetch_add(1, Ordering::Relaxed);
}
pub fn adjust_band_gain(&self, band: usize, delta_tenths_db: i32) -> i32 {
let mut bands = self.bands.lock().unwrap();
let gain = if let Some(b) = bands.get_mut(band) {
b.gain = (b.gain + delta_tenths_db).clamp(-240, 240);
b.gain
} else {
0
};
drop(bands);
self.version.fetch_add(1, Ordering::Relaxed);
gain
}
pub fn reset_gains(&self) {
let mut bands = self.bands.lock().unwrap();
for b in bands.iter_mut() {
b.gain = 0;
}
drop(bands);
self.bass.store(0, Ordering::Relaxed);
self.treble.store(0, Ordering::Relaxed);
self.version.fetch_add(1, Ordering::Relaxed);
}
pub fn save(&self) {
let settings = Settings {
eq_enabled: self.is_enabled(),
eq_band_settings: self.bands(),
bass: self.bass(),
treble: self.treble(),
bass_cutoff: self.bass_cutoff.load(Ordering::Relaxed),
treble_cutoff: self.treble_cutoff.load(Ordering::Relaxed),
};
if let Err(err) = settings.save() {
eprintln!("warning: failed to save settings: {}", err);
}
}
}
struct SendDsp(Dsp);
unsafe impl Send for SendDsp {}
static DSP_CALL: Mutex<()> = Mutex::new(());
pub struct EqProcessor {
dsp: Option<SendDsp>,
dsp_rate: u32,
applied_version: u64,
out: Vec<i16>,
}
impl EqProcessor {
pub fn new() -> Self {
Self {
dsp: None,
dsp_rate: 0,
applied_version: u64::MAX,
out: Vec::new(),
}
}
pub fn process(&mut self, buffer: &mut Vec<i16>, channels: u16, sample_rate: u32) -> u16 {
let eq = Equalizer::global();
if !eq.is_active() || buffer.is_empty() || channels == 0 || channels > 2 {
return channels;
}
let _guard = DSP_CALL.lock().unwrap();
let version = eq.version();
if self.dsp.is_none() || self.dsp_rate != sample_rate {
let mut dsp = Dsp::new(sample_rate);
apply_settings(&mut dsp, eq);
self.dsp = Some(SendDsp(dsp));
self.dsp_rate = sample_rate;
self.applied_version = version;
} else if self.applied_version != version {
let dsp = &mut self.dsp.as_mut().unwrap().0;
apply_settings(dsp, eq);
self.applied_version = version;
}
if channels == 1 {
let mono = std::mem::take(buffer);
buffer.reserve(mono.len() * 2);
for s in mono {
buffer.push(s);
buffer.push(s);
}
}
let dsp = &mut self.dsp.as_mut().unwrap().0;
self.out.clear();
dsp.process(buffer, &mut self.out);
std::mem::swap(buffer, &mut self.out);
2
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::settings::default_eq_band_settings;
fn sine_stereo(freq_hz: f64, rate: u32, frames: usize) -> Vec<i16> {
let mut pcm = Vec::with_capacity(frames * 2);
for n in 0..frames {
let t = n as f64 / rate as f64;
let s = (0.25 * (2.0 * std::f64::consts::PI * freq_hz * t).sin() * 32767.0) as i16;
pcm.push(s);
pcm.push(s);
}
pcm
}
fn rms(pcm: &[i16]) -> f64 {
let sum: f64 = pcm.iter().map(|&s| (s as f64) * (s as f64)).sum();
(sum / pcm.len() as f64).sqrt()
}
#[test]
fn processor_follows_global_state() {
let eq = Equalizer::global();
eq.set_bands(default_eq_band_settings());
eq.reset_gains();
let mut processor = EqProcessor::new();
eq.set_enabled(false);
let original = sine_stereo(1000.0, 44100, 44100);
let mut buf = original.clone();
assert_eq!(processor.process(&mut buf, 2, 44100), 2);
assert_eq!(buf, original);
eq.set_enabled(true);
assert_eq!(eq.bands()[5].cutoff, 1000);
eq.adjust_band_gain(5, -120);
let mut cut = original.clone();
assert_eq!(processor.process(&mut cut, 2, 44100), 2);
let ratio = rms(&original) / rms(&cut);
assert!(
ratio > 2.0 && ratio < 8.0,
"expected ~4x attenuation at 1 kHz, got {ratio:.2}x"
);
let mono: Vec<i16> = original.iter().step_by(2).cloned().collect();
let mut mono_buf = mono.clone();
assert_eq!(processor.process(&mut mono_buf, 1, 44100), 2);
assert_eq!(mono_buf.len(), mono.len() * 2);
eq.reset_gains();
eq.set_enabled(false);
eq.adjust_bass(-12);
assert!(eq.is_active());
let low = sine_stereo(100.0, 44100, 44100);
let mut low_cut = low.clone();
assert_eq!(processor.process(&mut low_cut, 2, 44100), 2);
let ratio = rms(&low) / rms(&low_cut);
assert!(
ratio > 2.0 && ratio < 8.0,
"expected ~4x bass attenuation at 100 Hz, got {ratio:.2}x"
);
eq.reset_gains();
eq.set_enabled(false);
}
}
fn apply_settings(dsp: &mut Dsp, eq: &Equalizer) {
for (i, band) in eq
.bands()
.iter()
.take(EQ_NUM_BANDS.min(EQ_BANDS))
.enumerate()
{
dsp.set_eq_band_raw(
i,
eq_band_setting {
cutoff: band.cutoff,
q: band.q,
gain: band.gain,
},
);
}
dsp.eq_enable(eq.is_enabled());
dsp.set_tone_cutoffs(
eq.bass_cutoff.load(Ordering::Relaxed),
eq.treble_cutoff.load(Ordering::Relaxed),
);
dsp.set_tone(eq.bass(), eq.treble());
}