awedio 0.8.0

A low-overhead and adaptable audio playback library
Documentation
//! [`CpalBackend`] outputs audio using the [cpal](https://www.docs.rs/cpal)
//! crate.

use crate::{
    manager::{BackendSource, Manager, Renderer},
    Sound,
};
use cpal::{
    traits::{DeviceTrait, HostTrait, StreamTrait},
    Error as CpalError, ErrorKind, FromSample, SizedSample,
};
use std::error::Error;

pub use cpal::BufferSize as CpalBufferSize;

/// A backend that uses [cpal](https://www.docs.rs/cpal) to output to devices.
///
/// This backend does not currently update the output device if the default
/// output device of the host changes.
pub struct CpalBackend {
    channel_count: u16,
    sample_rate: u32,
    sample_format: cpal::SampleFormat,
    buffer_size: CpalBufferSize,
    device: cpal::Device,
    stream: Option<cpal::Stream>,
}

impl CpalBackend {
    /// Create a new CpalBackend with defaults for all fields.
    ///
    /// Returns None if a default device or config could not be obtained.
    pub fn with_defaults() -> Option<CpalBackend> {
        let host = cpal::default_host();

        let device = host.default_output_device()?;

        let default_config = device.default_output_config().ok()?;
        let sample_rate = default_config.sample_rate();
        let channel_count = default_config.channels();
        let sample_format = default_config.sample_format();

        Some(CpalBackend {
            channel_count,
            sample_rate,
            buffer_size: CpalBufferSize::Default,
            device,
            stream: None,
            sample_format,
        })
    }

    /// Create a new backend.
    ///
    /// Returns None if an output device is not found
    pub fn with_default_host_and_device(
        channel_count: u16,
        sample_rate: u32,
        buffer_size: CpalBufferSize,
    ) -> Option<CpalBackend> {
        let host = cpal::default_host();

        let device = host.default_output_device()?;
        let sample_format = device.default_output_config().ok()?.sample_format();

        Some(CpalBackend {
            channel_count,
            sample_rate,
            buffer_size,
            device,
            stream: None,
            sample_format,
        })
    }

    /// Create a new CpalBackend specifying all fields.
    pub fn new(
        channel_count: u16,
        sample_rate: u32,
        buffer_size: CpalBufferSize,
        device: cpal::Device,
        sample_format: cpal::SampleFormat,
    ) -> CpalBackend {
        CpalBackend {
            channel_count,
            sample_rate,
            buffer_size,
            device,
            stream: None,
            sample_format,
        }
    }
}

impl CpalBackend {
    /// Start a cpal output stream and connect it to the returned Manager.
    ///
    /// Only a single stream is supported at a time per CpalBackend object.
    ///
    /// Cpal stream errors will be reported by calling `error_callback`.
    pub fn start<E>(&mut self, error_callback: E) -> Result<Manager, CpalBackendError>
    where
        E: FnMut(CpalError) + Send + 'static,
    {
        let (manager, mut renderer) = Manager::new();
        renderer.set_output_channel_count_and_sample_rate(self.channel_count, self.sample_rate);
        let Ok(crate::NextSample::MetadataChanged) = renderer.next_sample() else {
            panic!("expected MetadataChanged event")
        };

        let config = cpal::StreamConfig {
            channels: self.channel_count,
            sample_rate: self.sample_rate,
            buffer_size: self.buffer_size,
        };

        let timeout = None;
        let stream = match self.sample_format {
            cpal::SampleFormat::I16 => self
                .device
                .build_output_stream(
                    config,
                    make_data_callback::<i16>(renderer, self.channel_count),
                    error_callback,
                    timeout,
                )
                .map_err(CpalBackendError::BuildStream)?,
            cpal::SampleFormat::F32 => self
                .device
                .build_output_stream(
                    config,
                    make_data_callback::<f32>(renderer, self.channel_count),
                    error_callback,
                    timeout,
                )
                .map_err(CpalBackendError::BuildStream)?,
            sample_format => {
                return Err(CpalBackendError::BuildStream(CpalError::with_message(
                    ErrorKind::UnsupportedConfig,
                    format!(
                        "unsupported output stream sample format: {:?}",
                        sample_format
                    ),
                )))
            }
        };

        stream.play().map_err(CpalBackendError::PlayStream)?;
        self.stream = Some(stream);
        Ok(manager)
    }
}

/// Converts Awedio's internal i16 samples to the format required by the audio
/// device (type T).
fn make_data_callback<T>(
    mut renderer: Renderer,
    channel_count: u16,
) -> impl FnMut(&mut [T], &cpal::OutputCallbackInfo)
where
    T: SizedSample + FromSample<i16>,
{
    move |buffer: &mut [T], _info: &cpal::OutputCallbackInfo| {
        assert!(buffer.len().is_multiple_of(channel_count as usize));

        renderer.on_start_of_batch();

        buffer.fill_with(|| {
            let sample = renderer
                .next_sample()
                .expect("renderer should never return an Error");
            match sample {
                crate::NextSample::Sample(s) => T::from_sample(s),
                crate::NextSample::MetadataChanged => {
                    unreachable!("we never change metadata mid-batch")
                }
                crate::NextSample::Paused => T::from_sample(0), // TODO: implement pausing
                crate::NextSample::Finished => T::from_sample(0), // TODO: implement finishing
            }
        });
    }
}

/// An error from the [`CpalBackend`]
#[derive(Debug)]
pub enum CpalBackendError {
    /// No output device or configuration found.
    NoDevice,
    /// An error while building the output stream
    BuildStream(CpalError),
    /// An error while starting to play the stream.
    PlayStream(CpalError),
}

impl std::fmt::Display for CpalBackendError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CpalBackendError::NoDevice => {
                write!(f, "unable to find suitable device or config")
            }
            CpalBackendError::BuildStream(_) => {
                write!(f, "unable to build stream")
            }
            CpalBackendError::PlayStream(_) => {
                write!(f, "unable to play stream")
            }
        }
    }
}

impl Error for CpalBackendError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            CpalBackendError::NoDevice => None,
            CpalBackendError::BuildStream(e) => Some(e),
            CpalBackendError::PlayStream(e) => Some(e),
        }
    }
}