Documentation
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};

#[derive(Debug)]
pub enum StreamError {
    NoDevice,
    Play(cpal::PlayStreamError),
    Pause(cpal::PauseStreamError),
    Build(cpal::BuildStreamError),
}

pub struct Stream {
    stream: cpal::Stream,
}

impl Drop for Stream {
    fn drop(&mut self) {
        if let Err(e) = self.stream.pause() {
            log::error!("failed to stop stream : {e}");
        }
    }
}

impl Stream {
    pub fn launch(
        config: &cpal::StreamConfig,
        callback: impl FnMut(&mut [f32], usize) + Send + 'static,
    ) -> Result<Self, StreamError> {
        let device = default_device()?;
        log::info!("selected device : {}", device.name().unwrap_or_default());

        let stream = launch(&device, config, callback).map_err(StreamError::Build)?;
        stream.play().map_err(StreamError::Play)?;
        Ok(Self { stream })
    }

    pub fn launch_with_timeout(
        config: &cpal::StreamConfig,
        timeout: std::time::Duration,
        callback: impl FnMut(&mut [f32], usize) + Send + 'static,
    ) -> Result<(), StreamError> {
        let device = default_device()?;
        log::info!("selected device : {}", device.name().unwrap_or_default());

        let stream = launch(&device, config, callback).map_err(StreamError::Build)?;

        stream.play().map_err(StreamError::Play).and_then(|_| {
            std::thread::sleep(timeout);
            stream.pause().map_err(StreamError::Pause)
        })
    }
}

fn default_device() -> Result<cpal::Device, StreamError> {
    cpal::default_host()
        .default_output_device()
        .ok_or(StreamError::NoDevice)
}

fn launch(
    device: &cpal::Device,
    config: &cpal::StreamConfig,
    mut callback: impl FnMut(&mut [f32], usize) + Send + 'static,
) -> Result<cpal::Stream, cpal::BuildStreamError> {
    let channels = config.channels as usize;
    let stream = device.build_output_stream(
        config,
        move |output: &mut [f32], _: &cpal::OutputCallbackInfo| {
            callback(output, channels);
        },
        |e| log::error!("error occurred on stream : {e}"),
        None,
    )?;
    Ok(stream)
}