selene-daemon 0.5.4

Official music player daemon for Selene
Documentation
use std::{
    collections::VecDeque,
    sync::{
        Arc,
        mpsc::{Receiver, SendError, Sender, channel},
    },
};

use cpal::{
    Device, DeviceDescription, Stream, SupportedStreamConfig,
    traits::{DeviceTrait, HostTrait, StreamTrait},
};
use lunar_lib::debug;
use ringbuf::{
    CachingCons, CachingProd, HeapRb, SharedRb,
    storage::Heap,
    traits::{Consumer, Producer, Split},
};

use crate::{config::daemon_config, player::PlayerError};

pub struct DeviceConfig {
    pub device: Device,
    pub config: SupportedStreamConfig,
}

impl DeviceConfig {
    pub fn default_config() -> Result<Self, CpalError> {
        let host = cpal::default_host();
        let device = host
            .default_output_device()
            .ok_or(CpalError::NoDefaultDevice)?;
        let config = device.default_output_config()?;

        Ok(Self { device, config })
    }
}

pub struct CpalHandle {
    pub(crate) _stream: Stream,
    pub tx: Sender<()>,
    pub audio_buf: CachingProd<Arc<SharedRb<Heap<f32>>>>,
    pub(crate) pending_packet: VecDeque<f32>,
}

impl Drop for CpalHandle {
    fn drop(&mut self) {
        debug!("Cpal handle was dropped");
    }
}

impl CpalHandle {
    pub fn open(device_config: &DeviceConfig) -> Result<Self, PlayerError> {
        let (tx, rx) = channel();

        let rb = HeapRb::new(daemon_config().playback.audio_buffer_size * 1024);
        let (prod, cons) = rb.split();

        let stream = open_cpal_stream(cons, rx, device_config)?;

        Ok(Self {
            _stream: stream,
            tx,
            audio_buf: prod,
            pending_packet: VecDeque::new(),
        })
    }

    pub fn clear_buf(&self) -> Result<(), SendError<()>> {
        self.tx.send(())
    }

    /// Attempts to consume the rest of the pending packet.
    ///
    /// Returns [`None`] if there is no packet
    /// Returns [`true`] if there is a packet and it finished consuming
    /// Returns [`false`] if there is a packet and it has not finished consuming
    pub fn consume_packet(&mut self, volume: f32) -> Option<bool> {
        if !self.pending_packet.is_empty() {
            let pushed = self
                .audio_buf
                .push_iter(self.pending_packet.iter().map(|a| a * (volume.powf(3.0))));

            self.pending_packet.drain(..pushed);
            return Some(self.pending_packet.is_empty());
        }
        None
    }
}

#[derive(Debug, thiserror::Error)]
pub enum CpalError {
    #[error("{0}")]
    PlayStreamError(#[from] cpal::PlayStreamError),

    #[error("{0}")]
    BuildStreamError(#[from] cpal::BuildStreamError),

    #[error("{0}")]
    DefaultStreamConfigError(#[from] cpal::DefaultStreamConfigError),

    #[error("{0}")]
    DeviceIdError(#[from] cpal::DeviceIdError),

    #[error("Failed to find the default device")]
    NoDefaultDevice,
}

fn open_cpal_stream(
    mut audio_buf: CachingCons<Arc<SharedRb<Heap<f32>>>>,
    rx: Receiver<()>,
    device_config: &DeviceConfig,
) -> Result<Stream, CpalError> {
    let data_callback = move |output: &mut [f32], _: &cpal::OutputCallbackInfo| {
        if let Ok(()) = rx.try_recv() {
            audio_buf.clear();
        }

        for sample in output.iter_mut() {
            *sample = audio_buf.try_pop().unwrap_or(0.0);
        }
    };

    let error_callback = |err| eprintln!("Audio stream error: {err:?}");

    let stream = device_config.device.build_output_stream(
        &device_config.config.clone().into(),
        data_callback,
        error_callback,
        None,
    )?;

    debug!(
        "CPAL stream opened for {}",
        device_config
            .device
            .description()
            .as_ref()
            .map_or("Unknown Device", DeviceDescription::name)
    );
    stream.play()?;

    Ok(stream)
}