selene-daemon 0.9.0-alpha.2

Official music player daemon for Selene
Documentation
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)
}