diff --git a/examples/error_callback.rs b/examples/error_callback.rs new file mode 100644 index 00000000..bb55ebdf --- /dev/null +++ b/examples/error_callback.rs @@ -0,0 +1,41 @@ +use cpal::traits::HostTrait; +use rodio::source::SineWave; +use rodio::Source; +use std::error::Error; +use std::time::Duration; + +fn main() -> Result<(), Box> { + // You can use any other output device that can be queried from CPAL. + let default_device = cpal::default_host() + .default_output_device() + .ok_or("No default audio output device is found.")?; + + let (tx, rx) = std::sync::mpsc::channel(); + + let stream_handle = rodio::OutputStreamBuilder::from_device(default_device)? + .with_error_callback(move |err| { + // Filter for where err is a DeviceNotAvailable error. + if let cpal::StreamError::DeviceNotAvailable = err { + if let Err(e) = tx.send(err) { + eprintln!("Error emitting StreamError: {}", e); + } + } + }) + .open_stream_or_fallback()?; + + let mixer = stream_handle.mixer(); + + let wave = SineWave::new(740.0) + .amplify(0.1) + .take_duration(Duration::from_secs(30)); + mixer.add(wave); + + if let Ok(err) = rx.recv_timeout(Duration::from_secs(30)) { + // Here we print the error that was emitted by the error callback. + // but in a real application we may want to destroy the stream and + // try to reopen it, either with the same device or a different one. + eprintln!("Error with stream {}", err); + } + + Ok(()) +} diff --git a/src/stream.rs b/src/stream.rs index 40eb8d8c..4b78a378 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -45,15 +45,6 @@ impl Default for OutputStreamConfig { } } -/// Convenience builder for audio output stream. -/// It provides methods to configure several parameters of the audio output and opening default -/// device. See examples for use-cases. -#[derive(Default)] -pub struct OutputStreamBuilder { - device: Option, - config: OutputStreamConfig, -} - impl core::fmt::Debug for OutputStreamBuilder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let device = if let Some(device) = &self.device { @@ -69,12 +60,42 @@ impl core::fmt::Debug for OutputStreamBuilder { } } +fn default_error_callback(err: cpal::StreamError) { + #[cfg(feature = "tracing")] + tracing::error!("audio stream error: {err}"); + #[cfg(not(feature = "tracing"))] + eprintln!("audio stream error: {err}"); +} + +/// Convenience builder for audio output stream. +/// It provides methods to configure several parameters of the audio output and opening default +/// device. See examples for use-cases. +pub struct OutputStreamBuilder +where + E: FnMut(cpal::StreamError) + Send + 'static, +{ + device: Option, + config: OutputStreamConfig, + error_callback: E, +} + +impl Default for OutputStreamBuilder { + fn default() -> Self { + Self { + device: None, + config: OutputStreamConfig::default(), + error_callback: default_error_callback, + } + } +} + impl OutputStreamBuilder { /// Sets output device and its default parameters. pub fn from_device(device: cpal::Device) -> Result { let default_config = device .default_output_config() .map_err(StreamError::DefaultStreamConfigError)?; + Ok(Self::default() .with_device(device) .with_supported_config(&default_config)) @@ -88,23 +109,56 @@ impl OutputStreamBuilder { Self::from_device(default_device) } + /// Try to open a new output stream for the default output device with its default configuration. + /// Failing that attempt to open output stream with alternative configuration and/or non default + /// output devices. Returns stream for first of the tried configurations that succeeds. + /// If all attempts fail return the initial error. + pub fn open_default_stream() -> Result { + Self::from_default_device() + .and_then(|x| x.open_stream()) + .or_else(|original_err| { + let mut devices = match cpal::default_host().output_devices() { + Ok(devices) => devices, + Err(err) => { + #[cfg(feature = "tracing")] + tracing::error!("error getting list of output devices: {err}"); + #[cfg(not(feature = "tracing"))] + eprintln!("error getting list of output devices: {err}"); + return Err(original_err); + } + }; + devices + .find_map(|d| { + Self::from_device(d) + .and_then(|x| x.open_stream_or_fallback()) + .ok() + }) + .ok_or(original_err) + }) + } +} + +impl OutputStreamBuilder +where + E: FnMut(cpal::StreamError) + Send + 'static, +{ /// Sets output audio device keeping all existing stream parameters intact. /// This method is useful if you want to set other parameters yourself. /// To also set parameters that are appropriate for the device use [Self::from_device()] instead. - pub fn with_device(mut self, device: cpal::Device) -> OutputStreamBuilder { + pub fn with_device(mut self, device: cpal::Device) -> OutputStreamBuilder { self.device = Some(device); self } /// Sets number of output stream's channels. - pub fn with_channels(mut self, channel_count: ChannelCount) -> OutputStreamBuilder { + pub fn with_channels(mut self, channel_count: ChannelCount) -> OutputStreamBuilder { assert!(channel_count > 0); self.config.channel_count = channel_count; self } /// Sets output stream's sample rate. - pub fn with_sample_rate(mut self, sample_rate: SampleRate) -> OutputStreamBuilder { + pub fn with_sample_rate(mut self, sample_rate: SampleRate) -> OutputStreamBuilder { self.config.sample_rate = sample_rate; self } @@ -112,13 +166,13 @@ impl OutputStreamBuilder { /// Sets preferred output buffer size. /// Larger buffer size causes longer playback delays. Buffer sizes that are too small /// may cause higher CPU usage or playback interruptions. - pub fn with_buffer_size(mut self, buffer_size: cpal::BufferSize) -> OutputStreamBuilder { + pub fn with_buffer_size(mut self, buffer_size: cpal::BufferSize) -> OutputStreamBuilder { self.config.buffer_size = buffer_size; self } /// Select scalar type that will carry a sample. - pub fn with_sample_format(mut self, sample_format: SampleFormat) -> OutputStreamBuilder { + pub fn with_sample_format(mut self, sample_format: SampleFormat) -> OutputStreamBuilder { self.config.sample_format = sample_format; self } @@ -128,7 +182,7 @@ impl OutputStreamBuilder { pub fn with_supported_config( mut self, config: &cpal::SupportedStreamConfig, - ) -> OutputStreamBuilder { + ) -> OutputStreamBuilder { self.config = OutputStreamConfig { channel_count: config.channels() as ChannelCount, sample_rate: config.sample_rate().0 as SampleRate, @@ -140,7 +194,7 @@ impl OutputStreamBuilder { } /// Set all output stream parameters at once from CPAL stream config. - pub fn with_config(mut self, config: &cpal::StreamConfig) -> OutputStreamBuilder { + pub fn with_config(mut self, config: &cpal::StreamConfig) -> OutputStreamBuilder { self.config = OutputStreamConfig { channel_count: config.channels as ChannelCount, sample_rate: config.sample_rate.0 as SampleRate, @@ -150,23 +204,42 @@ impl OutputStreamBuilder { self } + /// Set a callback that will be called when an error occurs with the stream + pub fn with_error_callback(self, callback: F) -> OutputStreamBuilder + where + F: FnMut(cpal::StreamError) + Send + 'static, + { + OutputStreamBuilder { + device: self.device, + config: self.config, + error_callback: callback, + } + } + /// Open output stream using parameters configured so far. - pub fn open_stream(&self) -> Result { + pub fn open_stream(self) -> Result { let device = self.device.as_ref().expect("output device specified"); - OutputStream::open(device, &self.config) + + OutputStream::open(device, &self.config, self.error_callback) } /// Try opening a new output stream with the builder's current stream configuration. /// Failing that attempt to open stream with other available configurations /// supported by the device. /// If all attempts fail returns initial error. - pub fn open_stream_or_fallback(&self) -> Result { + pub fn open_stream_or_fallback(&self) -> Result + where + E: Clone, + { let device = self.device.as_ref().expect("output device specified"); - OutputStream::open(device, &self.config).or_else(|err| { + let error_callback = &self.error_callback; + + OutputStream::open(device, &self.config, error_callback.clone()).or_else(|err| { for supported_config in supported_output_configs(device)? { - if let Ok(handle) = Self::default() + if let Ok(handle) = OutputStreamBuilder::default() .with_device(device.clone()) .with_supported_config(&supported_config) + .with_error_callback(error_callback.clone()) .open_stream() { return Ok(handle); @@ -175,34 +248,6 @@ impl OutputStreamBuilder { Err(err) }) } - - /// Try to open a new output stream for the default output device with its default configuration. - /// Failing that attempt to open output stream with alternative configuration and/or non default - /// output devices. Returns stream for first of the tried configurations that succeeds. - /// If all attempts fail return the initial error. - pub fn open_default_stream() -> Result { - Self::from_default_device() - .and_then(|x| x.open_stream()) - .or_else(|original_err| { - let mut devices = match cpal::default_host().output_devices() { - Ok(devices) => devices, - Err(err) => { - #[cfg(feature = "tracing")] - tracing::error!("error getting list of output devices: {err}"); - #[cfg(not(feature = "tracing"))] - eprintln!("error getting list of output devices: {err}"); - return Err(original_err); - } - }; - devices - .find_map(|d| { - Self::from_device(d) - .and_then(|x| x.open_stream_or_fallback()) - .ok() - }) - .ok_or(original_err) - }) - } } fn clamp_supported_buffer_size( @@ -333,13 +378,17 @@ impl OutputStream { ); } - fn open( + fn open( device: &cpal::Device, config: &OutputStreamConfig, - ) -> Result { + error_callback: E, + ) -> Result + where + E: FnMut(cpal::StreamError) + Send + 'static, + { Self::validate_config(config); let (controller, source) = mixer(config.channel_count, config.sample_rate); - Self::init_stream(device, config, source).and_then(|stream| { + Self::init_stream(device, config, source, error_callback).and_then(|stream| { stream.play().map_err(StreamError::PlayStreamError)?; Ok(Self { _stream: stream, @@ -348,19 +397,18 @@ impl OutputStream { }) } - fn init_stream( + fn init_stream( device: &cpal::Device, config: &OutputStreamConfig, mut samples: MixerSource, - ) -> Result { - let error_callback = |err| { - #[cfg(feature = "tracing")] - tracing::error!("Playback error: {err}"); - #[cfg(not(feature = "tracing"))] - eprintln!("Playback error: {err}"); - }; + error_callback: E, + ) -> Result + where + E: FnMut(cpal::StreamError) + Send + 'static, + { let sample_format = config.sample_format; - let config: cpal::StreamConfig = config.into(); + let config = config.into(); + match sample_format { cpal::SampleFormat::F32 => device.build_output_stream::( &config,