Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add per-channel volume to CPAL sink

Use per-channel Atomics to store a linear f32 multiplier read in the
CPAL callback and resampler path. Convert Rockbox tenth-decibel volume
to linear (with mute sentinel) and clamp to unity. Expose a C ABI
pcm_cpal_set_volume(vol_l, vol_r) and wire audiohw_set_volume in the
headless audiohw-noop target to forward calls. Also add the header
declaration and make battery stubs conditional on
CONFIG_BATTERY_MEASURE.

+114 -28
+35 -4
crates/cpal-sink/src/lib.rs
··· 26 26 27 27 use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; 28 28 use std::collections::VecDeque; 29 + use std::sync::atomic::{AtomicU32, Ordering}; 29 30 use std::sync::{Condvar, Mutex, OnceLock}; 31 + 32 + // Per-channel linear volume multiplier stored as f32 bits in an atomic so the 33 + // cpal callback can read without a mutex. Initialised to 1.0 (0 dB). 34 + static VOLUME_L: AtomicU32 = AtomicU32::new(0x3F80_0000); // 1.0f32 35 + static VOLUME_R: AtomicU32 = AtomicU32::new(0x3F80_0000); // 1.0f32 36 + 37 + fn tenth_db_to_linear(tenth_db: i32) -> f32 { 38 + if tenth_db == i32::MIN { 39 + return 0.0; // PCM_MUTE_LEVEL 40 + } 41 + // tenth_db is in 1/10 dB units; amplitude = 10^(tenth_db / 200) 42 + let linear = 10.0_f32.powf(tenth_db as f32 / 200.0); 43 + linear.min(1.0) // clamp: we don't boost above unity 44 + } 30 45 31 46 const RING_CAPACITY: usize = 512 * 1024; 32 47 ··· 123 138 let frames = output.len() / 2; // output.len() is always even (stereo) 124 139 let mut wrote = 0usize; 125 140 141 + let vol_l = f32::from_bits(VOLUME_L.load(Ordering::Relaxed)); 142 + let vol_r = f32::from_bits(VOLUME_R.load(Ordering::Relaxed)); 143 + 126 144 // Ensure we have a "current" frame loaded. 127 145 if !rs.cur_valid { 128 146 if let Some((l, r)) = pop_frame(ring) { ··· 156 174 } 157 175 } 158 176 159 - // Linear interpolate between prev and cur using fractional phase. 177 + // Linear interpolate between prev and cur using fractional phase, then scale. 160 178 let t = rs.phase as f32; 161 - output[i * 2] = rs.prev_l + t * (rs.cur_l - rs.prev_l); 162 - output[i * 2 + 1] = rs.prev_r + t * (rs.cur_r - rs.prev_r); 179 + output[i * 2] = (rs.prev_l + t * (rs.cur_l - rs.prev_l)) * vol_l; 180 + output[i * 2 + 1] = (rs.prev_r + t * (rs.cur_r - rs.prev_r)) * vol_r; 163 181 wrote += 1; 164 182 } 165 183 ··· 264 282 device.build_output_stream( 265 283 &config, 266 284 move |output: &mut [i16], _| { 285 + let vol_l = f32::from_bits(VOLUME_L.load(Ordering::Relaxed)); 286 + let vol_r = f32::from_bits(VOLUME_R.load(Ordering::Relaxed)); 267 287 let (lock, cvar) = ring_ref; 268 288 let mut r = lock.lock().unwrap(); 269 289 let need_bytes = output.len() * 2; ··· 276 296 .enumerate() 277 297 { 278 298 if chunk.len() == 2 { 279 - output[i] = i16::from_le_bytes([chunk[0], chunk[1]]); 299 + let sample = i16::from_le_bytes([chunk[0], chunk[1]]) as f32; 300 + let vol = if i % 2 == 0 { vol_l } else { vol_r }; 301 + output[i] = (sample * vol).clamp(-32768.0, 32767.0) as i16; 280 302 } 281 303 } 282 304 let filled = have / 2; ··· 377 399 return; 378 400 } 379 401 r.buf.extend(data.iter().copied()); 402 + } 403 + 404 + /// Set per-channel volume. `vol_l` and `vol_r` are in tenth-decibel units 405 + /// (the Rockbox "centibel" convention: 0 = 0 dB, -740 = -74 dB, INT_MIN = mute). 406 + /// Called from audiohw_set_volume in audiohw-noop.c. 407 + #[no_mangle] 408 + pub extern "C" fn pcm_cpal_set_volume(vol_l: i32, vol_r: i32) { 409 + VOLUME_L.store(tenth_db_to_linear(vol_l).to_bits(), Ordering::Relaxed); 410 + VOLUME_R.store(tenth_db_to_linear(vol_r).to_bits(), Ordering::Relaxed); 380 411 } 381 412 382 413 #[no_mangle]
+66 -23
firmware/target/hosted/headless/audiohw-noop.c
··· 1 1 /* SPDX-License-Identifier: GPL-2.0-or-later 2 2 * 3 - * No-op audio hardware driver for the headless host build. 4 - * Volume is handled by the OS audio mixer via cpal; the firmware does 5 - * not drive a codec chip directly on this target. 3 + * No-op audio hardware driver for the headless host build, modelled on 4 + * firmware/drivers/audio/sdl.c. Volume is handled by the OS audio mixer 5 + * via cpal; the firmware does not drive a codec chip directly on this target. 6 + * 7 + * Each function is compiled only when the corresponding AUDIOHW_HAVE_* cap 8 + * is defined, so the set built is always consistent with the config and lld 9 + * never sees a duplicate symbol. 6 10 */ 7 11 8 - void audiohw_init(void) { } 9 - void audiohw_preinit(void) { } 10 - void audiohw_postinit(void) { } 11 - void audiohw_close(void) { } 12 + #include "config.h" 13 + 14 + /* audiohw_set_volume is always required (called from sound.c:set_prescaled_volume). 15 + * Delegates to the Rust cpal sink so the OS audio level tracks Rockbox's volume. 16 + * Values are in tenth-decibel units (0 = 0 dB, -740 = -74 dB, INT_MIN = mute). */ 17 + extern void pcm_cpal_set_volume(int vol_l, int vol_r); 18 + 19 + #if defined(AUDIOHW_HAVE_MONO_VOLUME) 20 + void audiohw_set_volume(int volume) 21 + { 22 + pcm_cpal_set_volume(volume, volume); 23 + } 24 + #else 25 + void audiohw_set_volume(int vol_l, int vol_r) 26 + { 27 + pcm_cpal_set_volume(vol_l, vol_r); 28 + } 29 + #endif 30 + 31 + #if defined(AUDIOHW_HAVE_PRESCALER) 32 + void audiohw_set_prescaler(int value) { (void)value; } 33 + #endif 34 + #if defined(AUDIOHW_HAVE_BALANCE) 35 + void audiohw_set_balance(int value) { (void)value; } 36 + #endif 37 + /* bass/treble: skip when HAVE_SW_TONE_CONTROLS — audiohw-swcodec.c owns them */ 38 + #ifndef HAVE_SW_TONE_CONTROLS 39 + #if defined(AUDIOHW_HAVE_BASS) 40 + void audiohw_set_bass(int value) { (void)value; } 41 + #endif 42 + #if defined(AUDIOHW_HAVE_TREBLE) 43 + void audiohw_set_treble(int value) { (void)value; } 44 + #endif 45 + #endif /* HAVE_SW_TONE_CONTROLS */ 46 + #if defined(AUDIOHW_HAVE_BASS_CUTOFF) 47 + void audiohw_set_bass_cutoff(int value) { (void)value; } 48 + #endif 49 + #if defined(AUDIOHW_HAVE_TREBLE_CUTOFF) 50 + void audiohw_set_treble_cutoff(int value) { (void)value; } 51 + #endif 52 + #if defined(AUDIOHW_HAVE_EQ) 53 + void audiohw_set_eq_band_gain(unsigned band, int value) { (void)band; (void)value; } 54 + #endif 55 + #if defined(AUDIOHW_HAVE_EQ_FREQUENCY) 56 + void audiohw_set_eq_band_frequency(unsigned band, int value) { (void)band; (void)value; } 57 + #endif 58 + #if defined(AUDIOHW_HAVE_EQ_WIDTH) 59 + void audiohw_set_eq_band_width(unsigned band, int value) { (void)band; (void)value; } 60 + #endif 61 + #if defined(AUDIOHW_HAVE_DEPTH_3D) 62 + void audiohw_set_depth_3d(int value) { (void)value; } 63 + #endif 64 + #if defined(AUDIOHW_HAVE_LINEOUT) 65 + void audiohw_set_lineout_volume(int vol_l, int vol_r) { (void)vol_l; (void)vol_r; } 66 + #endif 67 + #if defined(AUDIOHW_HAVE_FILTER_ROLL_OFF) 68 + void audiohw_set_filter_roll_off(int value) { (void)value; } 69 + #endif 12 70 13 - void audiohw_set_volume(int vol_l, int vol_r) { (void)vol_l; (void)vol_r; } 14 - void audiohw_set_lineout_volume(int vol_l, int vol_r) { (void)vol_l; (void)vol_r; } 15 - void audiohw_set_prescaler(int val) { (void)val; } 16 - void audiohw_set_balance(int val) { (void)val; } 17 - void audiohw_set_treble(int val) { (void)val; } 18 - void audiohw_set_bass(int val) { (void)val; } 19 - void audiohw_set_bass_cutoff(int val) { (void)val; } 20 - void audiohw_set_treble_cutoff(int val) { (void)val; } 21 - void audiohw_set_eq_band_gain(unsigned band, int val) { (void)band; (void)val; } 22 - void audiohw_set_eq_band_frequency(unsigned band, int val) { (void)band; (void)val; } 23 - void audiohw_set_eq_band_width(unsigned band, int val) { (void)band; (void)val; } 24 - void audiohw_set_filter_roll_off(int value) { (void)value; } 25 - void audiohw_set_depth_3d(int val) { (void)val; } 26 - void audiohw_set_loudness(int val) { (void)val; } 27 - void audiohw_mute(int mute) { (void)mute; } 28 - void audiohw_set_frequency(int fsel) { (void)fsel; } 71 + void audiohw_close(void) {}
+12 -1
firmware/target/hosted/headless/cpuinfo-noop.c
··· 28 28 29 29 void cpufreq_set_governor(const char *g, int cpu) { (void)g; (void)cpu; } 30 30 31 - /* Battery stubs — real desktop battery info not needed by the engine. */ 31 + /* Battery stubs — real desktop battery info not needed by the engine. 32 + * powermgmt.c provides stubs for measures it does not track (returning -1). 33 + * We only need to supply the stub when powermgmt.c expects a HW implementation, 34 + * i.e. when CONFIG_BATTERY_MEASURE says the capability IS present. */ 35 + #include "config.h" 36 + 37 + #if (CONFIG_BATTERY_MEASURE & PERCENTAGE_MEASURE) 32 38 int _battery_level(void) { return 100; } 39 + #endif 40 + #if (CONFIG_BATTERY_MEASURE & VOLTAGE_MEASURE) 33 41 unsigned _battery_voltage(void) { return 0; } 42 + #endif 43 + #if (CONFIG_BATTERY_MEASURE & TIME_MEASURE) 34 44 int _battery_time(void) { return 0; } 45 + #endif
+1
firmware/target/hosted/headless/pcm-cpal.c
··· 34 34 extern void pcm_cpal_init(void); 35 35 extern void pcm_cpal_postinit(void); 36 36 extern void pcm_cpal_set_sample_rate(uint32_t rate_hz); 37 + extern void pcm_cpal_set_volume(int vol_l, int vol_r); 37 38 extern void pcm_cpal_start(void); 38 39 extern void pcm_cpal_push(const void *data, size_t size); 39 40 extern void pcm_cpal_stop(void);