From 742090d57a4c83b23bd990ba0dcf939d09dbeb94 Mon Sep 17 00:00:00 2001 From: Carmine Paolino Date: Thu, 15 Oct 2026 14:13:48 +0200 Subject: [PATCH 1/3] Let a microphone's error callback be set MicrophoneBuilder carries an error callback but has no way to set one, so every microphone prints each stream error to stderr. An application that wants to log them its own way, rate-limit them, or close a stream that keeps failing cannot. Add with_error_callback, as DeviceSinkBuilder has. --- CHANGELOG.md | 1 + src/microphone/builder.rs | 43 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0f149dec..951481604 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `DEFAULT_SAMPLE_RATE` set to match `cpal::SAMPLE_RATE_48K`. - Added `Source::resample` and `ResampleConfig` for high-quality sample rate conversion. - Presets for `AGC`. +- Added `MicrophoneBuilder::with_error_callback` to replace the default error callback, which prints every stream error to stderr. ### Changed diff --git a/src/microphone/builder.rs b/src/microphone/builder.rs index a698424bd..ce3da4082 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -123,6 +123,29 @@ impl MicrophoneBuilder where E: FnMut(cpal::Error) + Send + Clone + 'static, { + /// Sets a callback that is called when an error occurs with the stream, + /// in place of the default one, which prints every error to stderr. + /// + /// # Example + /// ```no_run + /// # use rodio::microphone::MicrophoneBuilder; + /// let builder = MicrophoneBuilder::new() + /// .with_error_callback(|err| eprintln!("microphone: {err}")); + /// ``` + pub fn with_error_callback(self, error_callback: F) -> MicrophoneBuilder + where + F: FnMut(cpal::Error) + Send + Clone + 'static, + { + MicrophoneBuilder { + device: self.device, + config: self.config, + error_callback, + + device_set: PhantomData, + config_set: PhantomData, + } + } + /// Sets the input device to use. /// /// # Example @@ -555,3 +578,23 @@ where ) } } + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::Arc; + + use super::MicrophoneBuilder; + + #[test] + fn with_error_callback_replaces_the_default() { + let calls = Arc::new(AtomicUsize::new(0)); + let counted = Arc::clone(&calls); + let builder = MicrophoneBuilder::new().with_error_callback(move |_| { + counted.fetch_add(1, Ordering::Relaxed); + }); + let mut callback = builder.error_callback.clone(); + callback(cpal::Error::new(cpal::ErrorKind::Xrun)); + assert_eq!(calls.load(Ordering::Relaxed), 1); + } +} From 9fd1679e396432f78c585ac3b2670793c6c375a3 Mon Sep 17 00:00:00 2001 From: Carmine Paolino Date: Fri, 6 Nov 2026 00:27:58 +0100 Subject: [PATCH 2/3] Show what a microphone's error callback is for The example only repeated the default, printing to stderr. Send the errors that need action through a bounded channel instead, which is what an application reopening the stream would do. Remove the test: it moved a callback into the builder and out again, with no cpal failure behind it. --- src/microphone/builder.rs | 49 +++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/microphone/builder.rs b/src/microphone/builder.rs index ce3da4082..6e6d65d88 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -124,13 +124,36 @@ where E: FnMut(cpal::Error) + Send + Clone + 'static, { /// Sets a callback that is called when an error occurs with the stream, - /// in place of the default one, which prints every error to stderr. + /// in place of the default one, which logs every error. /// /// # Example + /// Tell the application when the stream needs reopening. A failing stream can + /// report the same error many times a second, so this keeps only the errors + /// that need action, in a bounded channel that never blocks the audio thread. /// ```no_run /// # use rodio::microphone::MicrophoneBuilder; - /// let builder = MicrophoneBuilder::new() - /// .with_error_callback(|err| eprintln!("microphone: {err}")); + /// use std::sync::mpsc; + /// + /// let (failure_tx, failure_rx) = mpsc::sync_channel(1); + /// let microphone = MicrophoneBuilder::new() + /// .with_error_callback(move |err| { + /// if matches!( + /// err.kind(), + /// cpal::ErrorKind::DeviceNotAvailable | cpal::ErrorKind::StreamInvalidated + /// ) { + /// let _channel_full_or_closed = failure_tx.try_send(err); + /// } + /// }) + /// .default_device()? + /// .default_config()? + /// .open_stream()?; + /// + /// // Somewhere the application checks regularly, such as its main loop. + /// if let Ok(err) = failure_rx.try_recv() { + /// eprintln!("microphone stream failed: {err}"); + /// // Drop `microphone` and open it again, on another device if this one is gone. + /// } + /// # Ok::<(), Box>(()) /// ``` pub fn with_error_callback(self, error_callback: F) -> MicrophoneBuilder where @@ -578,23 +601,3 @@ where ) } } - -#[cfg(test)] -mod tests { - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::sync::Arc; - - use super::MicrophoneBuilder; - - #[test] - fn with_error_callback_replaces_the_default() { - let calls = Arc::new(AtomicUsize::new(0)); - let counted = Arc::clone(&calls); - let builder = MicrophoneBuilder::new().with_error_callback(move |_| { - counted.fetch_add(1, Ordering::Relaxed); - }); - let mut callback = builder.error_callback.clone(); - callback(cpal::Error::new(cpal::ErrorKind::Xrun)); - assert_eq!(calls.load(Ordering::Relaxed), 1); - } -} From 9d4727ab86d2fa5612ac383b9494cf61c394cd84 Mon Sep 17 00:00:00 2001 From: Carmine Paolino Date: Fri, 6 Nov 2026 00:27:58 +0100 Subject: [PATCH 3/3] Let a speaker's error callback be set SpeakersBuilder has the same gap as MicrophoneBuilder: it carries an error callback but has no way to set one. Add with_error_callback in the same form. --- CHANGELOG.md | 2 +- src/speakers/builder.rs | 46 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 951481604..87f34c2c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `DEFAULT_SAMPLE_RATE` set to match `cpal::SAMPLE_RATE_48K`. - Added `Source::resample` and `ResampleConfig` for high-quality sample rate conversion. - Presets for `AGC`. -- Added `MicrophoneBuilder::with_error_callback` to replace the default error callback, which prints every stream error to stderr. +- Added `with_error_callback` to `MicrophoneBuilder` and `SpeakersBuilder` to replace the default error callback, which logs every stream error. ### Changed diff --git a/src/speakers/builder.rs b/src/speakers/builder.rs index 0ad891656..3061c7e27 100644 --- a/src/speakers/builder.rs +++ b/src/speakers/builder.rs @@ -125,6 +125,52 @@ impl SpeakersBuilder where E: FnMut(cpal::Error) + Send + Clone + 'static, { + /// Sets a callback that is called when an error occurs with the stream, + /// in place of the default one, which logs every error. + /// + /// # Example + /// Tell the application when the stream needs reopening. A failing stream can + /// report the same error many times a second, so this keeps only the errors + /// that need action, in a bounded channel that never blocks the audio thread. + /// ```no_run + /// # use rodio::speakers::SpeakersBuilder; + /// use std::sync::mpsc; + /// + /// let (failure_tx, failure_rx) = mpsc::sync_channel(1); + /// let speakers = SpeakersBuilder::new() + /// .with_error_callback(move |err| { + /// if matches!( + /// err.kind(), + /// cpal::ErrorKind::DeviceNotAvailable | cpal::ErrorKind::StreamInvalidated + /// ) { + /// let _channel_full_or_closed = failure_tx.try_send(err); + /// } + /// }) + /// .default_device()? + /// .default_config()? + /// .open_mixer()?; + /// + /// // Somewhere the application checks regularly, such as its main loop. + /// if let Ok(err) = failure_rx.try_recv() { + /// eprintln!("speakers stream failed: {err}"); + /// // Drop `speakers` and open it again, on another device if this one is gone. + /// } + /// # Ok::<(), Box>(()) + /// ``` + pub fn with_error_callback(self, error_callback: F) -> SpeakersBuilder + where + F: FnMut(cpal::Error) + Send + Clone + 'static, + { + SpeakersBuilder { + device: self.device, + config: self.config, + error_callback, + + device_set: PhantomData, + config_set: PhantomData, + } + } + /// Sets the output device to use. /// /// # Example