Skip to main content

rodio/
stream.rs

1//! Output audio via the OS via mixers or play directly
2//!
3//! This module provides a builder that's used to configure and open audio output. Once
4//! opened sources can be mixed into the output via `DeviceSink::mixer`.
5//!
6//! There is also a convenience function `play` for using that output mixer to
7//! play a single sound.
8//!
9//! # Buffer size
10//! Rodio configures a default buffer size of 100ms latency regardless of the
11//! system default. This is to get a good _"out of the box experience"_ on all
12//! systems as we found out that the system default is sometimes set completely
13//! wrong. That would lead to audio playback breaking apparently randomly on
14//! some systems.
15//!
16//! You can manually specify the buffer size if you want lower latency. For more
17//! info see [buffer_size](DeviceSinkBuilder::with_buffer_size).
18//!
19//! If you find a good way to reliably get a good buffer size on all platforms
20//! please contribute your solution to us!
21use crate::common::{assert_error_traits, ChannelCount, SampleRate};
22use crate::math::{nearest_multiple_of_two, nz};
23use crate::mixer::{mixer, Mixer};
24use crate::player::Player;
25use crate::{decoder, Source};
26use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
27use cpal::{BufferSize, Sample, SampleFormat, StreamConfig, I24};
28use std::fmt;
29use std::io::{Read, Seek};
30use std::marker::Sync;
31use std::num::NonZero;
32
33const HZ_44100: SampleRate = nz!(44_100);
34
35/// `cpal::Stream` container. Use `mixer()` method to control output.
36///
37/// <div class="warning">When dropped playback will end, and the associated
38/// OS-Sink will be disposed</div>
39///
40/// # Note
41/// On drop this will print a message to stderr or emit a log msg when tracing is
42/// enabled. Though we recommend you do not you can disable that print/log with:
43/// [`MixerDeviceSink::log_on_drop(false)`](MixerDeviceSink::log_on_drop).
44/// If the `DeviceSink` is dropped because the program is panicking we do not print
45/// or log anything.
46///
47/// # Example
48/// ```no_run
49/// # use rodio::DeviceSinkBuilder;
50/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
51/// let mut handle = DeviceSinkBuilder::open_default_sink()?;
52/// handle.log_on_drop(false); // Not recommended during development
53/// println!("Output config: {:?}", handle.config());
54/// let mixer = handle.mixer();
55/// # Ok(())
56/// # }
57/// ```
58pub struct MixerDeviceSink {
59    config: DeviceSinkConfig,
60    mixer: Mixer,
61    log_on_drop: bool,
62    _stream: cpal::Stream,
63}
64
65impl MixerDeviceSink {
66    /// Access the sink's mixer.
67    pub fn mixer(&self) -> &Mixer {
68        &self.mixer
69    }
70
71    /// Access the sink's config.
72    pub fn config(&self) -> &DeviceSinkConfig {
73        &self.config
74    }
75
76    /// When [`MixerDeviceSink`] is dropped a message is logged to stderr or
77    /// emitted through tracing if the tracing feature is enabled.
78    pub fn log_on_drop(&mut self, enabled: bool) {
79        self.log_on_drop = enabled;
80    }
81}
82
83impl Drop for MixerDeviceSink {
84    fn drop(&mut self) {
85        if self.log_on_drop && !std::thread::panicking() {
86            #[cfg(feature = "tracing")]
87            tracing::debug!("Dropping DeviceSink, audio playing through this sink will stop");
88            #[cfg(not(feature = "tracing"))]
89            eprintln!("Dropping DeviceSink, audio playing through this sink will stop, to prevent this message from appearing use tracing or call `.log_on_drop(false)` on this DeviceSink")
90        }
91    }
92}
93
94impl fmt::Debug for MixerDeviceSink {
95    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
96        f.debug_struct("MixerDeviceSink")
97            .field("config", &self.config)
98            .finish_non_exhaustive()
99    }
100}
101
102/// Describes the OS-Sink's configuration
103#[derive(Copy, Clone, Debug)]
104pub struct DeviceSinkConfig {
105    pub(crate) channel_count: ChannelCount,
106    pub(crate) sample_rate: SampleRate,
107    pub(crate) buffer_size: BufferSize,
108    pub(crate) sample_format: SampleFormat,
109}
110
111impl Default for DeviceSinkConfig {
112    fn default() -> Self {
113        Self {
114            channel_count: nz!(2),
115            sample_rate: HZ_44100,
116            buffer_size: BufferSize::Default,
117            sample_format: SampleFormat::F32,
118        }
119    }
120}
121
122impl DeviceSinkConfig {
123    /// Access the OS-Sink config's channel count.
124    pub fn channel_count(&self) -> ChannelCount {
125        self.channel_count
126    }
127
128    /// Access the OS-Sink config's sample rate.
129    pub fn sample_rate(&self) -> SampleRate {
130        self.sample_rate
131    }
132
133    /// Access the OS-Sink config's buffer size.
134    pub fn buffer_size(&self) -> &BufferSize {
135        &self.buffer_size
136    }
137
138    /// Access the OS-Sink config's sample format.
139    pub fn sample_format(&self) -> SampleFormat {
140        self.sample_format
141    }
142}
143
144impl core::fmt::Debug for DeviceSinkBuilder {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        let device = if let Some(device) = &self.device {
147            "Some(".to_owned()
148                + &device
149                    .description()
150                    .ok()
151                    .map_or("UnNamed".to_string(), |d| d.name().to_string())
152                + ")"
153        } else {
154            "None".to_owned()
155        };
156
157        f.debug_struct("DeviceSinkBuilder")
158            .field("device", &device)
159            .field("config", &self.config)
160            .finish()
161    }
162}
163
164fn default_error_callback(err: cpal::StreamError) {
165    #[cfg(feature = "tracing")]
166    tracing::error!("audio stream error: {err}");
167    #[cfg(not(feature = "tracing"))]
168    eprintln!("audio stream error: {err}");
169}
170
171/// Convenience builder for audio OS-player.
172/// It provides methods to configure several parameters of the audio output and opening default
173/// device. See examples for use-cases.
174///
175/// <div class="warning">When the DeviceSink is dropped playback will end, and the associated
176/// OS-Sink will be disposed</div>
177pub struct DeviceSinkBuilder<E = fn(cpal::StreamError)>
178where
179    E: FnMut(cpal::StreamError) + Send + 'static,
180{
181    device: Option<cpal::Device>,
182    config: DeviceSinkConfig,
183    error_callback: E,
184}
185
186impl Default for DeviceSinkBuilder {
187    fn default() -> Self {
188        Self {
189            device: None,
190            config: DeviceSinkConfig::default(),
191            error_callback: default_error_callback,
192        }
193    }
194}
195
196impl DeviceSinkBuilder {
197    /// Sets output device and its default parameters.
198    pub fn from_device(device: cpal::Device) -> Result<DeviceSinkBuilder, DeviceSinkError> {
199        let default_config = device
200            .default_output_config()
201            .map_err(DeviceSinkError::DefaultSinkConfigError)?;
202
203        let mut device = Self::default()
204            .with_device(device)
205            .with_supported_config(&default_config);
206
207        // aim for 50ms of audio
208        let sample_rate = device.config.sample_rate().get();
209        let safe_buffer_size = nearest_multiple_of_two(sample_rate / (1000 / 50));
210
211        // This is suboptimal, the builder might still change the sample rate or
212        // channel count which would throw the buffer size off. We have fixed
213        // that in the new speakers API, which will eventually replace this.
214        device.config.buffer_size = match device.config.buffer_size {
215            BufferSize::Default => BufferSize::Fixed(safe_buffer_size),
216            fixed @ BufferSize::Fixed(_) => fixed,
217        };
218        Ok(device)
219    }
220
221    /// Sets default OS-Sink parameters for default output audio device.
222    pub fn from_default_device() -> Result<DeviceSinkBuilder, DeviceSinkError> {
223        let default_device = cpal::default_host()
224            .default_output_device()
225            .ok_or(DeviceSinkError::NoDevice)?;
226        Self::from_device(default_device)
227    }
228
229    /// Try to open a new OS-Sink for the default output device with its default configuration.
230    /// Failing that attempt to open OS-Sink with alternative configuration and/or non default
231    /// output devices. Returns stream for first of the tried configurations that succeeds.
232    /// If all attempts fail return the initial error.
233    pub fn open_default_sink() -> Result<MixerDeviceSink, DeviceSinkError> {
234        Self::from_default_device()
235            .and_then(|x| x.open_stream())
236            .or_else(|original_err| {
237                let devices = match cpal::default_host().output_devices() {
238                    Ok(devices) => devices,
239                    Err(err) => {
240                        #[cfg(feature = "tracing")]
241                        tracing::error!("error getting list of output devices: {err}");
242                        #[cfg(not(feature = "tracing"))]
243                        eprintln!("error getting list of output devices: {err}");
244                        return Err(original_err);
245                    }
246                };
247                devices
248                    .filter(|dev| {
249                        dev.description()
250                            .map(|desc| desc.driver().is_some_and(|driver| driver != "null"))
251                            .unwrap_or(false)
252                    })
253                    .find_map(|d| {
254                        Self::from_device(d)
255                            .and_then(|x| x.open_sink_or_fallback())
256                            .ok()
257                    })
258                    .ok_or(original_err)
259            })
260    }
261}
262
263impl<E> DeviceSinkBuilder<E>
264where
265    E: FnMut(cpal::StreamError) + Send + 'static,
266{
267    /// Sets output audio device keeping all existing stream parameters intact.
268    /// This method is useful if you want to set other parameters yourself.
269    /// To also set parameters that are appropriate for the device use [Self::from_device()] instead.
270    pub fn with_device(mut self, device: cpal::Device) -> DeviceSinkBuilder<E> {
271        self.device = Some(device);
272        self
273    }
274
275    /// Sets number of OS-Sink's channels.
276    pub fn with_channels(mut self, channel_count: ChannelCount) -> DeviceSinkBuilder<E> {
277        assert!(channel_count.get() > 0);
278        self.config.channel_count = channel_count;
279        self
280    }
281
282    /// Sets OS-Sink's sample rate.
283    pub fn with_sample_rate(mut self, sample_rate: SampleRate) -> DeviceSinkBuilder<E> {
284        self.config.sample_rate = sample_rate;
285        self
286    }
287
288    /// Sets preferred output buffer size.
289    ///
290    /// To play sound without any glitches the audio card may never receive a
291    /// sample to late. Some samples might take longer to generate then
292    /// others. For example because:
293    ///  - The OS preempts the thread creating the samples. This happens more
294    ///    often if the computer is under high load.
295    ///  - The decoder needs to read more data from disk.
296    ///  - Rodio code takes longer to run for some samples then others
297    ///  - The OS can only send audio samples in groups to the DAC.
298    ///
299    /// The OS solves this by buffering samples. The larger that buffer the
300    /// smaller the impact of variable sample generation time. On the other
301    /// hand Rodio controls audio by changing the value of samples. We can not
302    /// change a sample already in the OS buffer. That means there is a
303    /// minimum delay (latency) of `<buffer size>/<sample_rate*channel_count>`
304    /// seconds before a change made through rodio takes effect.
305    ///
306    /// # Large vs Small buffer
307    /// - A larger buffer size results in high latency. Changes made trough
308    ///   Rodio (volume/skip/effects etc) takes longer before they can be heard.
309    /// - A small buffer might cause:
310    ///   - Higher CPU usage
311    ///   - Playback interruptions such as buffer underruns.
312    ///   - Rodio to log errors like: `alsa::poll() returned POLLERR`
313    ///
314    /// # Recommendation
315    /// If low latency is important to you consider offering the user a method
316    /// to find the minimum buffer size that works well on their system under
317    /// expected conditions. A good example of this approach can be seen in
318    /// [mumble](https://www.mumble.info/documentation/user/audio-settings/)
319    /// (specifically the *Output Delay* & *Jitter buffer*.
320    ///
321    /// These are some typical values that are a good starting point. They may also
322    /// break audio completely, it depends on the system.
323    /// - Low-latency (audio production, live monitoring): 512-1024
324    /// - General use (games, media playback): 1024-2048
325    /// - Stability-focused (background music, non-interactive): 2048-4096
326    pub fn with_buffer_size(mut self, buffer_size: cpal::BufferSize) -> DeviceSinkBuilder<E> {
327        self.config.buffer_size = buffer_size;
328        self
329    }
330
331    /// Select scalar type that will carry a sample.
332    pub fn with_sample_format(mut self, sample_format: SampleFormat) -> DeviceSinkBuilder<E> {
333        self.config.sample_format = sample_format;
334        self
335    }
336
337    /// Set available parameters from a CPAL supported config. You can get a list of
338    /// such configurations for an output device using [crate::stream::supported_output_configs()]
339    pub fn with_supported_config(
340        mut self,
341        config: &cpal::SupportedStreamConfig,
342    ) -> DeviceSinkBuilder<E> {
343        self.config = DeviceSinkConfig {
344            channel_count: NonZero::new(config.channels())
345                .expect("no valid cpal config has zero channels"),
346            sample_rate: NonZero::new(config.sample_rate())
347                .expect("no valid cpal config has zero sample rate"),
348            sample_format: config.sample_format(),
349            ..Default::default()
350        };
351        self
352    }
353
354    /// Set all OS-Sink parameters at once from CPAL stream config.
355    pub fn with_config(mut self, config: &cpal::StreamConfig) -> DeviceSinkBuilder<E> {
356        self.config = DeviceSinkConfig {
357            channel_count: NonZero::new(config.channels)
358                .expect("no valid cpal config has zero channels"),
359            sample_rate: NonZero::new(config.sample_rate)
360                .expect("no valid cpal config has zero sample rate"),
361            buffer_size: config.buffer_size,
362            ..self.config
363        };
364        self
365    }
366
367    /// Set a callback that will be called when an error occurs with the stream
368    pub fn with_error_callback<F>(self, callback: F) -> DeviceSinkBuilder<F>
369    where
370        F: FnMut(cpal::StreamError) + Send + 'static,
371    {
372        DeviceSinkBuilder {
373            device: self.device,
374            config: self.config,
375            error_callback: callback,
376        }
377    }
378
379    /// Open OS-Sink using parameters configured so far.
380    pub fn open_stream(self) -> Result<MixerDeviceSink, DeviceSinkError> {
381        let device = self.device.as_ref().expect("No output device specified");
382
383        MixerDeviceSink::open(device, &self.config, self.error_callback)
384    }
385
386    /// Try opening a new OS-Sink with the builder's current stream configuration.
387    /// Failing that attempt to open stream with other available configurations
388    /// supported by the device.
389    /// If all attempts fail returns initial error.
390    pub fn open_sink_or_fallback(&self) -> Result<MixerDeviceSink, DeviceSinkError>
391    where
392        E: Clone,
393    {
394        let device = self.device.as_ref().expect("No output device specified");
395        let error_callback = &self.error_callback;
396
397        MixerDeviceSink::open(device, &self.config, error_callback.clone()).or_else(|err| {
398            for supported_config in supported_output_configs(device)? {
399                if let Ok(handle) = DeviceSinkBuilder::default()
400                    .with_device(device.clone())
401                    .with_supported_config(&supported_config)
402                    .with_error_callback(error_callback.clone())
403                    .open_stream()
404                {
405                    return Ok(handle);
406                }
407            }
408            Err(err)
409        })
410    }
411}
412
413/// A convenience function. Plays a sound once.
414/// Returns a `Player` that can be used to control the sound.
415pub fn play<R>(mixer: &Mixer, input: R) -> Result<Player, PlayError>
416where
417    R: Read + Seek + Send + Sync + 'static,
418{
419    let input = decoder::Decoder::new(input)?;
420    let player = Player::connect_new(mixer);
421    player.append(input);
422    Ok(player)
423}
424
425impl From<&DeviceSinkConfig> for StreamConfig {
426    fn from(config: &DeviceSinkConfig) -> Self {
427        cpal::StreamConfig {
428            channels: config.channel_count.get() as cpal::ChannelCount,
429            sample_rate: config.sample_rate.get(),
430            buffer_size: config.buffer_size,
431        }
432    }
433}
434
435/// An error occurred while attempting to play a sound.
436#[derive(Debug, thiserror::Error, Clone)]
437pub enum PlayError
438where
439    Self: Send + Sync + 'static,
440{
441    /// Attempting to decode the audio failed.
442    #[error("Failed to decode audio")]
443    DecoderError(
444        #[from]
445        #[source]
446        decoder::DecoderError,
447    ),
448    /// The output device was lost.
449    #[error("No output device")]
450    NoDevice,
451}
452assert_error_traits!(PlayError);
453
454/// Errors that might occur when interfacing with audio output.
455#[derive(Debug, thiserror::Error)]
456pub enum DeviceSinkError {
457    /// Could not start playing the sink, see [cpal::PlayStreamError] for
458    /// details.
459    #[error("Could not start playing the stream")]
460    PlayError(#[source] cpal::PlayStreamError),
461    /// Failed to get the stream config for the given device. See
462    /// [cpal::DefaultStreamConfigError] for details.
463    #[error("Failed to get the config for the given device")]
464    DefaultSinkConfigError(#[source] cpal::DefaultStreamConfigError),
465    /// Error opening sink with OS. See [cpal::BuildStreamError] for details.
466    #[error("Error opening the stream with the OS")]
467    BuildError(#[source] cpal::BuildStreamError),
468    /// Could not list supported configs for the device. Maybe it
469    /// disconnected. For details see: [cpal::SupportedStreamConfigsError].
470    #[error("Could not list supported configs for the device. Maybe its disconnected?")]
471    SupportedConfigsError(#[source] cpal::SupportedStreamConfigsError),
472    /// Could not find any output device
473    #[error("Could not find any output device")]
474    NoDevice,
475    /// New cpal sample format that rodio does not yet support please open
476    /// an issue if you run into this.
477    #[error("New cpal sample format that rodio does not yet support please open an issue if you run into this.")]
478    UnsupportedSampleFormat,
479}
480
481impl MixerDeviceSink {
482    fn validate_config(config: &DeviceSinkConfig) {
483        if let BufferSize::Fixed(sz) = config.buffer_size {
484            assert!(sz > 0, "fixed buffer size must be greater than zero");
485        }
486    }
487
488    pub(crate) fn open<E>(
489        device: &cpal::Device,
490        config: &DeviceSinkConfig,
491        error_callback: E,
492    ) -> Result<MixerDeviceSink, DeviceSinkError>
493    where
494        E: FnMut(cpal::StreamError) + Send + 'static,
495    {
496        Self::validate_config(config);
497        let (controller, source) = mixer(config.channel_count, config.sample_rate);
498        Self::init_stream(device, config, source, error_callback).and_then(|stream| {
499            stream.play().map_err(DeviceSinkError::PlayError)?;
500            Ok(Self {
501                _stream: stream,
502                mixer: controller,
503                config: *config,
504                log_on_drop: true,
505            })
506        })
507    }
508
509    fn init_stream<S, E>(
510        device: &cpal::Device,
511        config: &DeviceSinkConfig,
512        mut samples: S,
513        error_callback: E,
514    ) -> Result<cpal::Stream, DeviceSinkError>
515    where
516        S: Source + Send + 'static,
517        E: FnMut(cpal::StreamError) + Send + 'static,
518    {
519        let cpal_config = config.into();
520
521        macro_rules! build_output_streams {
522            ($($sample_format:tt, $generic:ty);+) => {
523                match config.sample_format {
524                    $(
525                        cpal::SampleFormat::$sample_format => device.build_output_stream::<$generic, _, _>(
526                            &cpal_config,
527                            move |data, _| {
528                                data.iter_mut().for_each(|d| {
529                                    *d = samples
530                                        .next()
531                                        .map(Sample::from_sample)
532                                        .unwrap_or(<$generic>::EQUILIBRIUM)
533                                })
534                            },
535                            error_callback,
536                            None,
537                        ),
538                    )+
539                    _ => return Err(DeviceSinkError::UnsupportedSampleFormat),
540                }
541            };
542        }
543
544        let result = build_output_streams!(
545            F32, f32;
546            F64, f64;
547            I8, i8;
548            I16, i16;
549            I24, I24;
550            I32, i32;
551            I64, i64;
552            U8, u8;
553            U16, u16;
554            U24, cpal::U24;
555            U32, u32;
556            U64, u64
557        );
558
559        result.map_err(DeviceSinkError::BuildError)
560    }
561}
562
563/// Return all formats supported by the device.
564pub fn supported_output_configs(
565    device: &cpal::Device,
566) -> Result<impl Iterator<Item = cpal::SupportedStreamConfig>, DeviceSinkError> {
567    let mut supported: Vec<_> = device
568        .supported_output_configs()
569        .map_err(DeviceSinkError::SupportedConfigsError)?
570        .collect();
571    supported.sort_by(|a, b| b.cmp_default_heuristics(a));
572
573    Ok(supported.into_iter().flat_map(|sf| {
574        let max_rate = sf.max_sample_rate();
575        let min_rate = sf.min_sample_rate();
576        let mut formats = vec![sf.with_max_sample_rate()];
577        let preferred_rate = HZ_44100.get();
578        if preferred_rate < max_rate && preferred_rate > min_rate {
579            formats.push(sf.with_sample_rate(preferred_rate))
580        }
581        formats.push(sf.with_sample_rate(min_rate));
582        formats
583    }))
584}