use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use alsa::pcm::{Access, Format, HwParams, State, PCM};
use alsa::{Direction, ValueOr};
use log::{info, warn};
use openairplay2::AudioSink;
fn start_samples(rate: u32, channels: u8) -> usize {
(rate as usize / 2) * channels as usize }
pub fn volume_to_gain(db: f32) -> f32 {
if db <= -144.0 {
return 0.0;
}
10f32.powf(db.min(0.0) / 20.0)
}
fn apply_gain(samples: &mut [i16], gain: f32) {
if gain >= 0.999 {
return;
}
if gain <= 0.0 {
samples.fill(0);
return;
}
for s in samples.iter_mut() {
*s = (f32::from(*s) * gain) as i16;
}
}
#[derive(Clone)]
pub struct SharedGain(Arc<AtomicU32>);
impl SharedGain {
pub fn new() -> SharedGain {
SharedGain(Arc::new(AtomicU32::new(1.0f32.to_bits())))
}
pub fn set(&self, gain: f32) {
self.0
.store(gain.clamp(0.0, 1.0).to_bits(), Ordering::Relaxed);
}
pub fn get(&self) -> f32 {
f32::from_bits(self.0.load(Ordering::Relaxed))
}
}
impl Default for SharedGain {
fn default() -> SharedGain {
SharedGain::new()
}
}
pub struct NullSink;
impl AudioSink for NullSink {
fn write(&mut self, _pcm: &[i16]) {}
fn flush(&mut self) {}
}
pub struct AlsaSink {
output: Option<AlsaOutput>,
gain: SharedGain,
scratch: Vec<i16>,
prebuffer: Vec<i16>,
threshold: usize,
started: bool,
}
impl AlsaSink {
pub fn open(device: &str, rate: u32, channels: u8, gain: SharedGain) -> AlsaSink {
let output = match AlsaOutput::open(device, rate, channels) {
Ok(out) => {
info!("player: ALSA \"{device}\" {rate} Hz {channels}ch");
Some(out)
}
Err(e) => {
warn!("player: cannot open ALSA ({e}); decode-only");
None
}
};
AlsaSink {
output,
gain,
scratch: Vec::new(),
prebuffer: Vec::new(),
threshold: start_samples(rate, channels),
started: false,
}
}
}
impl AudioSink for AlsaSink {
fn write(&mut self, pcm: &[i16]) {
let Some(out) = self.output.as_mut() else {
return; };
self.scratch.clear();
self.scratch.extend_from_slice(pcm);
apply_gain(&mut self.scratch, self.gain.get());
if self.started {
out.write(&self.scratch);
} else {
self.prebuffer.extend_from_slice(&self.scratch);
if self.prebuffer.len() >= self.threshold {
self.started = true;
out.write(&self.prebuffer);
self.prebuffer.clear();
}
}
}
fn flush(&mut self) {
self.prebuffer.clear();
self.started = false;
if let Some(out) = self.output.as_mut() {
out.reset(); }
}
}
struct AlsaOutput {
pcm: PCM,
channels: usize,
}
impl AlsaOutput {
fn open(device: &str, rate: u32, channels: u8) -> Result<AlsaOutput, alsa::Error> {
let pcm = PCM::new(device, Direction::Playback, false)?;
{
let hwp = HwParams::any(&pcm)?;
hwp.set_channels(u32::from(channels))?;
hwp.set_rate(rate, ValueOr::Nearest)?;
hwp.set_format(Format::s16())?;
hwp.set_access(Access::RWInterleaved)?;
let _ = hwp.set_buffer_time_near(500_000, ValueOr::Nearest);
pcm.hw_params(&hwp)?;
}
pcm.prepare()?;
Ok(AlsaOutput {
pcm,
channels: channels as usize,
})
}
fn write(&mut self, samples: &[i16]) {
let Ok(io) = self.pcm.io_i16() else {
warn!("player: ALSA io handle lost");
return;
};
let mut frames = samples;
while !frames.is_empty() {
match io.writei(frames) {
Ok(0) => break,
Ok(written) => frames = &frames[written * self.channels..],
Err(e) => {
if self.pcm.try_recover(e, true).is_err() {
warn!("player: unrecoverable ALSA write error");
return;
}
}
}
}
if self.pcm.state() != State::Running {
let _ = self.pcm.start();
}
}
fn reset(&mut self) {
let _ = self.pcm.drop();
let _ = self.pcm.prepare();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prebuffer_threshold_for_44100_stereo() {
assert_eq!(start_samples(44100, 2), 44100); }
#[test]
fn apply_gain_scales_samples() {
let mut s = [100i16, -100, 20000, -20000];
apply_gain(&mut s, 0.5);
assert_eq!(s, [50, -50, 10000, -10000]);
let mut s = [1i16, -32768, 32767];
apply_gain(&mut s, 0.0);
assert_eq!(s, [0, 0, 0]);
let mut s = [1i16, -32768, 32767];
apply_gain(&mut s, 1.0);
assert_eq!(s, [1, -32768, 32767]);
}
#[test]
fn volume_db_to_gain() {
assert!((volume_to_gain(0.0) - 1.0).abs() < 1e-6); assert!((volume_to_gain(-6.0206) - 0.5).abs() < 1e-3); assert!((volume_to_gain(-20.0) - 0.1).abs() < 1e-4); assert_eq!(volume_to_gain(-144.0), 0.0); assert_eq!(volume_to_gain(-200.0), 0.0); assert!((volume_to_gain(6.0) - 1.0).abs() < 1e-6);
let gain = SharedGain::new();
assert_eq!(gain.get(), 1.0);
gain.set(volume_to_gain(-20.0));
assert!((gain.get() - 0.1).abs() < 1e-4);
gain.set(2.0);
assert_eq!(gain.get(), 1.0);
}
}