/// Plays audio produced by a generator closure on the host's default output device. /// /// `AudioPlayer` owns the live [`cpal::Stream`] and exposes only playback /// control ([`play`](Self::play), [`pause`](Self::pause)). It has no /// knowledge of how the generator produces samples, so polyphony, pitch /// modulation, and voice mixing all belong behind the [`Generator`] closure, /// not here. pub struct AudioPlayer { stream: Stream, } use crate::generators::GeneratorFn; use cpal::{ Device, Error, ErrorKind, FromSample, I24, OutputCallbackInfo, Sample, SampleFormat, SizedSample, Stream, StreamConfig, SupportedStreamConfig, traits::{DeviceTrait, StreamTrait}, }; use std::io::ErrorKind as IoErrorKind; impl AudioPlayer { /// Builds a stream for `device`/`config` around `generator`, dispatching /// on `config`'s sample format to the matching CPAL sample type. /// /// The stream starts paused; call [`play`](Self::play) to produce sound. /// Use [`get_device_and_config`](crate::get_device_and_config) to obtain /// `device` and `config`. /// /// # Errors /// /// Returns an error if: /// - the device reports a sample format `raust` does not support, or /// - CPAL fails to build the output stream. pub fn try_new(device: &Device, config: SupportedStreamConfig, generator: GeneratorFn) -> anyhow::Result { let stream = match config.sample_format() { SampleFormat::I8 => Self::create_stream::(device, config.into(), generator), SampleFormat::I16 => Self::create_stream::(device, config.into(), generator), SampleFormat::I24 => Self::create_stream::(device, config.into(), generator), SampleFormat::I32 => Self::create_stream::(device, config.into(), generator), SampleFormat::I64 => Self::create_stream::(device, config.into(), generator), SampleFormat::U8 => Self::create_stream::(device, config.into(), generator), SampleFormat::U16 => Self::create_stream::(device, config.into(), generator), SampleFormat::U32 => Self::create_stream::(device, config.into(), generator), SampleFormat::U64 => Self::create_stream::(device, config.into(), generator), SampleFormat::F32 => Self::create_stream::(device, config.into(), generator), SampleFormat::F64 => Self::create_stream::(device, config.into(), generator), sample_format => Err(anyhow::Error::new(std::io::Error::new( IoErrorKind::InvalidData, format!("Unsupported sample format '{sample_format}'"), ))), }?; Ok(Self { stream }) } /// Builds, but does not start, an output stream of sample type `T` that /// pulls its samples from `generator`. /// /// # Errors /// /// Returns an error if CPAL fails to build the output stream for `device`. pub fn create_stream( device: &Device, config: StreamConfig, mut generator: GeneratorFn, ) -> Result where T: SizedSample + FromSample, { let channels = usize::from(config.channels); let err_fn = |err: Error| match err.kind() { ErrorKind::DeviceChanged | ErrorKind::RealtimeDenied => { eprintln!("{err}"); } _ => eprintln!("Stream error: {err}"), }; let stream = device.build_output_stream( config, move |data: &mut [T], _: &OutputCallbackInfo| { Self::write_data(data, channels, &mut generator); }, err_fn, None, )?; Ok(stream) } fn write_data(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32) where T: Sample + FromSample, { for frame in output.chunks_mut(channels) { let value: T = T::from_sample(next_sample()); for sample in frame.iter_mut() { *sample = value; } } } /// Resumes playback. /// /// The generator continues from exactly where [`pause`](Self::pause) /// left it; nothing resets. /// /// # Errors /// /// Returns an error if CPAL fails to resume the stream. pub fn play(&self) -> anyhow::Result<()> { self.stream.play()?; Ok(()) } /// Suspends playback. /// /// The backend stops calling the generator until the next /// [`play`](Self::play) call, so no samples are produced while paused. /// /// # Errors /// /// Returns an error if CPAL fails to suspend the stream. pub fn pause(&self) -> anyhow::Result<()> { self.stream.pause()?; Ok(()) } }