Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add with_error_callback to OutputStreamBuilder #708

Merged
merged 7 commits into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions examples/error_callback.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Error>> {
// 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(())
}
170 changes: 109 additions & 61 deletions src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<cpal::Device>,
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 {
Expand All @@ -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<E = fn(cpal::StreamError)>
where
E: FnMut(cpal::StreamError) + Send + 'static,
{
device: Option<cpal::Device>,
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<OutputStreamBuilder, StreamError> {
let default_config = device
.default_output_config()
.map_err(StreamError::DefaultStreamConfigError)?;

Ok(Self::default()
.with_device(device)
.with_supported_config(&default_config))
Expand All @@ -88,37 +109,70 @@ 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<OutputStream, StreamError> {
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<E> OutputStreamBuilder<E>
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<E> {
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<E> {
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<E> {
self.config.sample_rate = sample_rate;
self
}

/// 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<E> {
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<E> {
self.config.sample_format = sample_format;
self
}
Expand All @@ -128,7 +182,7 @@ impl OutputStreamBuilder {
pub fn with_supported_config(
mut self,
config: &cpal::SupportedStreamConfig,
) -> OutputStreamBuilder {
) -> OutputStreamBuilder<E> {
self.config = OutputStreamConfig {
channel_count: config.channels() as ChannelCount,
sample_rate: config.sample_rate().0 as SampleRate,
Expand All @@ -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<E> {
self.config = OutputStreamConfig {
channel_count: config.channels as ChannelCount,
sample_rate: config.sample_rate.0 as SampleRate,
Expand All @@ -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<F>(self, callback: F) -> OutputStreamBuilder<F>
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<OutputStream, StreamError> {
pub fn open_stream(self) -> Result<OutputStream, StreamError> {
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<OutputStream, StreamError> {
pub fn open_stream_or_fallback(&self) -> Result<OutputStream, StreamError>
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);
Expand All @@ -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<OutputStream, StreamError> {
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(
Expand Down Expand Up @@ -333,13 +378,17 @@ impl OutputStream {
);
}

fn open(
fn open<E>(
device: &cpal::Device,
config: &OutputStreamConfig,
) -> Result<OutputStream, StreamError> {
error_callback: E,
) -> Result<OutputStream, StreamError>
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,
Expand All @@ -348,19 +397,18 @@ impl OutputStream {
})
}

fn init_stream(
fn init_stream<E>(
device: &cpal::Device,
config: &OutputStreamConfig,
mut samples: MixerSource,
) -> Result<cpal::Stream, StreamError> {
let error_callback = |err| {
#[cfg(feature = "tracing")]
tracing::error!("Playback error: {err}");
#[cfg(not(feature = "tracing"))]
eprintln!("Playback error: {err}");
};
error_callback: E,
) -> Result<cpal::Stream, StreamError>
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::<f32, _, _>(
&config,
Expand Down