use std::{
collections::VecDeque,
sync::{
Arc,
atomic::{AtomicU8, AtomicU32, Ordering},
},
thread::{self},
};
use audioadapter_buffers::direct::InterleavedSlice;
use cpal::{
Device, DeviceDescription, Stream, SupportedStreamConfig,
traits::{DeviceTrait, HostTrait, StreamTrait},
};
use lunar_lib::log::{debug, error, warn};
use rubato::{Fft, Resampler, ResamplerConstructionError};
use thiserror::Error;
use crate::{LoudnormMode, ipc_common::TrackInfo};
#[derive(Debug, Error)]
pub enum PlaybackError {
#[error("{0}")]
Cpal(#[from] cpal::Error),
#[error("Failed to find the default device")]
NoDefaultDevice,
#[error("{0}")]
Rubato(#[from] ResamplerConstructionError),
}
pub(crate) struct CpalHandle {
audio_buf: rtrb::Producer<f32>,
audio_buf_size: usize,
pub(crate) loudnorm_mode: Arc<AtomicLoudnormMode>,
track_gain: Option<f32>,
album_gain: Option<f32>,
volume: Arc<AtomicU32>,
config: SupportedStreamConfig,
resampler: Option<Fft<f32>>,
resample_buffer: VecDeque<f32>,
sample_rate: u32,
channels: u16,
_stream: Stream,
}
pub(crate) struct AtomicLoudnormMode(AtomicU8);
impl AtomicLoudnormMode {
pub fn new(val: LoudnormMode) -> Self {
Self(AtomicU8::new(val as u8))
}
pub fn load(&self, order: Ordering) -> LoudnormMode {
match self.0.load(order) {
0 => LoudnormMode::None,
1 => LoudnormMode::Track,
2 => LoudnormMode::Album,
_ => unreachable!(),
}
}
pub fn store(&self, val: LoudnormMode, order: Ordering) {
self.0.store(val as u8, order);
}
}
impl CpalHandle {
pub(crate) fn open() -> Result<Self, PlaybackError> {
let volume = Arc::new(AtomicU32::new(1f32.to_bits()));
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or(PlaybackError::NoDefaultDevice)?;
let config = device.default_output_config()?;
let audio_buffer_size = (config.sample_rate() as usize * config.channels() as usize) / 10;
let (audio_prod, audio_cons) = rtrb::RingBuffer::new(audio_buffer_size);
let stream = open_cpal_stream(audio_cons, volume.clone(), &device, &config)?;
let loudnorm_mode = Arc::new(AtomicLoudnormMode::new(LoudnormMode::Track));
let handle = CpalHandle {
audio_buf: audio_prod,
audio_buf_size: audio_buffer_size,
loudnorm_mode,
track_gain: None,
album_gain: None,
sample_rate: 0,
channels: 0,
volume,
config,
resampler: None,
resample_buffer: VecDeque::new(),
_stream: stream,
};
Ok(handle)
}
pub(crate) fn set_track_info(
&mut self,
TrackInfo {
sample_rate,
channels,
track_gain,
album_gain,
}: TrackInfo,
) -> Result<(), PlaybackError> {
let resampler = Fft::new(
sample_rate as usize,
self.config.sample_rate() as usize,
1024,
1,
channels as usize,
rubato::FixedSync::Both,
)?;
self.sample_rate = sample_rate;
self.channels = channels;
self.track_gain = track_gain;
self.album_gain = album_gain;
self.resampler = Some(resampler);
Ok(())
}
pub(crate) fn input_audio_packet(&mut self, items: &[f32]) -> Result<(), PlaybackError> {
let Some(resampler) = &mut self.resampler else {
warn!("Received audio data before getting a resampler");
return Ok(());
};
let volume = f32::from_bits(self.volume.load(Ordering::Relaxed));
let gain = match self.loudnorm_mode.load(Ordering::Relaxed) {
LoudnormMode::None => volume,
LoudnormMode::Track => self.track_gain.map_or(volume, |g| g * volume),
LoudnormMode::Album => self.album_gain.map_or(volume, |g| g * volume),
};
let channel_count = resampler.nbr_channels();
let next_frame_size = resampler.input_frames_next();
let chunk_size = next_frame_size * channel_count;
self.resample_buffer.extend(items);
while self.resample_buffer.len() >= chunk_size {
let buffer_in = InterleavedSlice::new(
&self.resample_buffer.make_contiguous()[..chunk_size],
channel_count,
next_frame_size,
)
.expect("Sampler buffer contained less capacity than expected");
let mut resampled = resampler
.process(&buffer_in, 0, None)
.expect("Resampler expects input and output to have the same number of channels")
.take_data();
self.resample_buffer.drain(..chunk_size);
resampled.iter_mut().for_each(|p| *p *= gain);
push_entire_packet(
&mut self.audio_buf,
&resampled,
self.sample_rate,
self.channels,
self.audio_buf_size,
);
}
Ok(())
}
pub fn volume(&self) -> Arc<AtomicU32> {
self.volume.clone()
}
}
pub fn push_entire_packet(
producer: &mut rtrb::Producer<f32>,
mut packet: &[f32],
sample_rate: u32,
channels: u16,
buffer_capacity: usize,
) {
let items_per_sec = f64::from(sample_rate * u32::from(channels));
while let (_, remaining) = producer.push_partial_slice(packet)
&& !remaining.is_empty()
{
packet = remaining;
thread::sleep(std::time::Duration::from_secs_f64(
packet.len().min(buffer_capacity / 2) as f64 / items_per_sec,
));
}
}
pub(crate) fn open_cpal_stream(
mut audio_buf: rtrb::Consumer<f32>,
volume: Arc<AtomicU32>,
device: &Device,
config: &SupportedStreamConfig,
) -> Result<Stream, PlaybackError> {
let data_callback = move |output: &mut [f32], _: &cpal::OutputCallbackInfo| {
let volume = f32::from_bits(volume.load(Ordering::Relaxed));
for sample in output {
*sample = audio_buf.pop().unwrap_or(0.0) * volume.powf(3.0);
}
};
let error_callback = |err| error!("Audio stream error: {err:?}");
let stream =
device.build_output_stream(config.config(), data_callback, error_callback, None)?;
debug!(
"CPAL stream opened for {}",
device
.description()
.as_ref()
.map_or("Unknown Device", DeviceDescription::name)
);
stream.play()?;
Ok(stream)
}