diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1e1101..e5cbfb99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Output audio stream buffer size can now be adjusted. + +### Changed +- Breaking: `OutputStreamBuilder` should now be used to initialize audio output stream. +- Breaking: `OutputStreamHandle` removed, use `OutputStream` and `OutputStream::mixer()` instead. +- Breaking: `DynamicMixerController` renamed to `Mixer`, `DynamicMixer` renamed to `MixerSource`. +- Breaking: `Sink::try_new` renamed to `connect_new` and does not return error anymore. + `Sink::new_idle` was renamed to `new`. + # Version 0.20.1 (2024-11-08) ### Fixed diff --git a/examples/automatic_gain_control.rs b/examples/automatic_gain_control.rs index 17603ac7..aae0ba62 100644 --- a/examples/automatic_gain_control.rs +++ b/examples/automatic_gain_control.rs @@ -1,5 +1,6 @@ use rodio::source::Source; use rodio::Decoder; +use std::error::Error; use std::fs::File; use std::io::BufReader; use std::sync::atomic::{AtomicBool, Ordering}; @@ -7,13 +8,13 @@ use std::sync::Arc; use std::thread; use std::time::Duration; -fn main() { - let (_stream, handle) = rodio::OutputStream::try_default().unwrap(); - let sink = rodio::Sink::try_new(&handle).unwrap(); +fn main() -> Result<(), Box> { + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let sink = rodio::Sink::connect_new(&stream_handle.mixer()); // Decode the sound file into a source - let file = BufReader::new(File::open("assets/music.flac").unwrap()); - let source = Decoder::new(file).unwrap(); + let file = BufReader::new(File::open("assets/music.flac")?); + let source = Decoder::new(file)?; // Apply automatic gain control to the source let agc_source = source.automatic_gain_control(1.0, 4.0, 0.005, 5.0); @@ -24,14 +25,15 @@ fn main() { let agc_enabled = Arc::new(AtomicBool::new(true)); let agc_enabled_clone = agc_enabled.clone(); let controlled = agc_source.periodic_access(Duration::from_millis(5), move |agc_source| { + #[cfg(not(feature = "experimental"))] agc_source.set_enabled(agc_enabled_clone.load(Ordering::Relaxed)); }); // Add the source now equipped with automatic gain control and controlled via - // periodic_access to the sink for playback + // periodic_access to the sink for the playback. sink.append(controlled); - // after 5 seconds of playback disable automatic gain control using the + // After 5 seconds of playback disable automatic gain control using the // shared AtomicBool `agc_enabled`. You could do this from another part // of the program since `agc_enabled` is of type Arc which // is freely clone-able and move-able. @@ -41,6 +43,7 @@ fn main() { thread::sleep(Duration::from_secs(5)); agc_enabled.store(false, Ordering::Relaxed); - // Keep the program running until playback is complete + // Keep the program running until the playback is complete. sink.sleep_until_end(); + Ok(()) } diff --git a/examples/basic.rs b/examples/basic.rs index d57d3792..fa3ef3ea 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,30 +1,46 @@ +use rodio::source::SineWave; +use rodio::Source; +use std::error::Error; use std::io::BufReader; use std::thread; use std::time::Duration; - -fn main() { - let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap(); - - let file = std::fs::File::open("assets/beep.wav").unwrap(); - let beep1 = stream_handle.play_once(BufReader::new(file)).unwrap(); - beep1.set_volume(0.2); +#[cfg(feature = "tracing")] +use tracing; + +fn main() -> Result<(), Box> { + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let mixer = stream_handle.mixer(); + + let beep1 = { + // Play a WAV file. + let file = std::fs::File::open("assets/beep.wav")?; + let sink = rodio::play(&mixer, BufReader::new(file))?; + sink.set_volume(0.2); + sink + }; println!("Started beep1"); - thread::sleep(Duration::from_millis(1500)); - let file = std::fs::File::open("assets/beep2.wav").unwrap(); - let beep2 = stream_handle.play_once(BufReader::new(file)).unwrap(); - beep2.set_volume(0.3); - beep2.detach(); + { + // Generate sine wave. + let wave = SineWave::new(740.0) + .amplify(0.2) + .take_duration(Duration::from_secs(3)); + mixer.add(wave); + } println!("Started beep2"); - thread::sleep(Duration::from_millis(1500)); - let file = std::fs::File::open("assets/beep3.ogg").unwrap(); - let beep3 = stream_handle.play_once(file).unwrap(); - beep3.set_volume(0.2); - println!("Started beep3"); + let beep3 = { + // Play an OGG file. + let file = std::fs::File::open("assets/beep3.ogg")?; + let sink = rodio::play(&mixer, BufReader::new(file))?; + sink.set_volume(0.2); + sink + }; + println!("Started beep3"); thread::sleep(Duration::from_millis(1500)); + drop(beep1); println!("Stopped beep1"); @@ -33,4 +49,6 @@ fn main() { println!("Stopped beep3"); thread::sleep(Duration::from_millis(1500)); + + Ok(()) } diff --git a/examples/custom_config.rs b/examples/custom_config.rs new file mode 100644 index 00000000..40b6ca47 --- /dev/null +++ b/examples/custom_config.rs @@ -0,0 +1,35 @@ +use cpal::traits::HostTrait; +use cpal::{BufferSize, SampleFormat, SampleRate}; +use rodio::source::SineWave; +use rodio::Source; +use std::error::Error; +use std::thread; +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 stream_handle = rodio::OutputStreamBuilder::from_device(default_device)? + // No need to set all parameters explicitly here, + // the defaults were set from the device's description. + .with_buffer_size(BufferSize::Fixed(256)) + .with_sample_rate(SampleRate(48_000)) + .with_sample_format(SampleFormat::F32) + // Note that the function below still tries alternative configs if the specified one fails. + // If you need to only use the exact specified configuration, + // then use OutputStreamBuilder::open_stream() instead. + .open_stream_or_fallback()?; + let mixer = stream_handle.mixer(); + + let wave = SineWave::new(740.0) + .amplify(0.1) + .take_duration(Duration::from_secs(1)); + mixer.add(wave); + + println!("Beep..."); + thread::sleep(Duration::from_millis(1500)); + + Ok(()) +} diff --git a/examples/mix_multiple_sources.rs b/examples/mix_multiple_sources.rs index 3363d5f5..55c3fb09 100644 --- a/examples/mix_multiple_sources.rs +++ b/examples/mix_multiple_sources.rs @@ -1,12 +1,13 @@ +use rodio::mixer; use rodio::source::{SineWave, Source}; -use rodio::{dynamic_mixer, OutputStream, Sink}; +use std::error::Error; use std::time::Duration; -fn main() { +fn main() -> Result<(), Box> { // Construct a dynamic controller and mixer, stream_handle, and sink. - let (controller, mixer) = dynamic_mixer::mixer::(2, 44_100); - let (_stream, stream_handle) = OutputStream::try_default().unwrap(); - let sink = Sink::try_new(&stream_handle).unwrap(); + let (controller, mixer) = mixer::mixer::(2, 44_100); + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let sink = rodio::Sink::connect_new(&stream_handle.mixer()); // Create four unique sources. The frequencies used here correspond // notes in the key of C and in octave 4: C4, or middle C on a piano, @@ -35,4 +36,6 @@ fn main() { // Sleep the thread until sink is empty. sink.sleep_until_end(); + + Ok(()) } diff --git a/examples/music_flac.rs b/examples/music_flac.rs index 580d19bb..13013bab 100644 --- a/examples/music_flac.rs +++ b/examples/music_flac.rs @@ -1,11 +1,14 @@ +use std::error::Error; use std::io::BufReader; -fn main() { - let (_stream, handle) = rodio::OutputStream::try_default().unwrap(); - let sink = rodio::Sink::try_new(&handle).unwrap(); +fn main() -> Result<(), Box> { + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let sink = rodio::Sink::connect_new(&stream_handle.mixer()); - let file = std::fs::File::open("assets/music.flac").unwrap(); - sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap()); + let file = std::fs::File::open("assets/music.flac")?; + sink.append(rodio::Decoder::new(BufReader::new(file))?); sink.sleep_until_end(); + + Ok(()) } diff --git a/examples/music_m4a.rs b/examples/music_m4a.rs index 2c50f336..15b58432 100644 --- a/examples/music_m4a.rs +++ b/examples/music_m4a.rs @@ -1,11 +1,14 @@ +use std::error::Error; use std::io::BufReader; -fn main() { - let (_stream, handle) = rodio::OutputStream::try_default().unwrap(); - let sink = rodio::Sink::try_new(&handle).unwrap(); +fn main() -> Result<(), Box> { + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let sink = rodio::Sink::connect_new(&stream_handle.mixer()); - let file = std::fs::File::open("assets/music.m4a").unwrap(); - sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap()); + let file = std::fs::File::open("assets/music.m4a")?; + sink.append(rodio::Decoder::new(BufReader::new(file))?); sink.sleep_until_end(); + + Ok(()) } diff --git a/examples/music_mp3.rs b/examples/music_mp3.rs index f6ac371e..cb94159f 100644 --- a/examples/music_mp3.rs +++ b/examples/music_mp3.rs @@ -1,11 +1,14 @@ +use std::error::Error; use std::io::BufReader; -fn main() { - let (_stream, handle) = rodio::OutputStream::try_default().unwrap(); - let sink = rodio::Sink::try_new(&handle).unwrap(); +fn main() -> Result<(), Box> { + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let sink = rodio::Sink::connect_new(&stream_handle.mixer()); - let file = std::fs::File::open("assets/music.mp3").unwrap(); - sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap()); + let file = std::fs::File::open("assets/music.mp3")?; + sink.append(rodio::Decoder::new(BufReader::new(file))?); sink.sleep_until_end(); + + Ok(()) } diff --git a/examples/music_ogg.rs b/examples/music_ogg.rs index 63887152..74c36229 100644 --- a/examples/music_ogg.rs +++ b/examples/music_ogg.rs @@ -1,11 +1,14 @@ +use std::error::Error; use std::io::BufReader; -fn main() { - let (_stream, handle) = rodio::OutputStream::try_default().unwrap(); - let sink = rodio::Sink::try_new(&handle).unwrap(); +fn main() -> Result<(), Box> { + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let sink = rodio::Sink::connect_new(&stream_handle.mixer()); - let file = std::fs::File::open("assets/music.ogg").unwrap(); - sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap()); + let file = std::fs::File::open("assets/music.ogg")?; + sink.append(rodio::Decoder::new(BufReader::new(file))?); sink.sleep_until_end(); + + Ok(()) } diff --git a/examples/music_wav.rs b/examples/music_wav.rs index f44631c1..a29d929b 100644 --- a/examples/music_wav.rs +++ b/examples/music_wav.rs @@ -1,11 +1,14 @@ +use std::error::Error; use std::io::BufReader; -fn main() { - let (_stream, handle) = rodio::OutputStream::try_default().unwrap(); - let sink = rodio::Sink::try_new(&handle).unwrap(); +fn main() -> Result<(), Box> { + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let sink = rodio::Sink::connect_new(&stream_handle.mixer()); - let file = std::fs::File::open("assets/music.wav").unwrap(); - sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap()); + let file = std::fs::File::open("assets/music.wav")?; + sink.append(rodio::Decoder::new(BufReader::new(file))?); sink.sleep_until_end(); + + Ok(()) } diff --git a/examples/noise_generator.rs b/examples/noise_generator.rs index a72bcb7c..577f9d01 100644 --- a/examples/noise_generator.rs +++ b/examples/noise_generator.rs @@ -1,37 +1,37 @@ //! Noise generator example. Use the "noise" feature to enable the noise generator sources. +use std::error::Error; + #[cfg(feature = "noise")] -fn main() { +fn main() -> Result<(), Box> { use rodio::source::{pink, white, Source}; use std::thread; use std::time::Duration; - let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap(); + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let noise_duration = Duration::from_millis(1000); let interval_duration = Duration::from_millis(1500); - stream_handle - .play_raw( - white(cpal::SampleRate(48000)) - .amplify(0.1) - .take_duration(noise_duration), - ) - .unwrap(); + stream_handle.mixer().add( + white(cpal::SampleRate(48000)) + .amplify(0.1) + .take_duration(noise_duration), + ); println!("Playing white noise"); thread::sleep(interval_duration); - stream_handle - .play_raw( - pink(cpal::SampleRate(48000)) - .amplify(0.1) - .take_duration(noise_duration), - ) - .unwrap(); + stream_handle.mixer().add( + pink(cpal::SampleRate(48000)) + .amplify(0.1) + .take_duration(noise_duration), + ); println!("Playing pink noise"); thread::sleep(interval_duration); + + Ok(()) } #[cfg(not(feature = "noise"))] diff --git a/examples/reverb.rs b/examples/reverb.rs index 883aa554..dc74e71d 100644 --- a/examples/reverb.rs +++ b/examples/reverb.rs @@ -1,15 +1,18 @@ use rodio::Source; +use std::error::Error; use std::io::BufReader; use std::time::Duration; -fn main() { - let (_stream, handle) = rodio::OutputStream::try_default().unwrap(); - let sink = rodio::Sink::try_new(&handle).unwrap(); +fn main() -> Result<(), Box> { + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let sink = rodio::Sink::connect_new(&stream_handle.mixer()); - let file = std::fs::File::open("assets/music.ogg").unwrap(); - let source = rodio::Decoder::new(BufReader::new(file)).unwrap(); + let file = std::fs::File::open("assets/music.ogg")?; + let source = rodio::Decoder::new(BufReader::new(file))?; let with_reverb = source.buffered().reverb(Duration::from_millis(40), 0.7); sink.append(with_reverb); sink.sleep_until_end(); + + Ok(()) } diff --git a/examples/seek_mp3.rs b/examples/seek_mp3.rs index 5042003c..a78238bc 100644 --- a/examples/seek_mp3.rs +++ b/examples/seek_mp3.rs @@ -1,22 +1,25 @@ +use std::error::Error; use std::io::BufReader; use std::time::Duration; -fn main() { - let (_stream, handle) = rodio::OutputStream::try_default().unwrap(); - let sink = rodio::Sink::try_new(&handle).unwrap(); +fn main() -> Result<(), Box> { + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let sink = rodio::Sink::connect_new(&stream_handle.mixer()); - let file = std::fs::File::open("assets/music.mp3").unwrap(); - sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap()); + let file = std::fs::File::open("assets/music.mp3")?; + sink.append(rodio::Decoder::new(BufReader::new(file))?); std::thread::sleep(std::time::Duration::from_secs(2)); - sink.try_seek(Duration::from_secs(0)).unwrap(); + sink.try_seek(Duration::from_secs(0))?; std::thread::sleep(std::time::Duration::from_secs(2)); - sink.try_seek(Duration::from_secs(4)).unwrap(); + sink.try_seek(Duration::from_secs(4))?; sink.sleep_until_end(); - // wont do anything since the sound has ended already - sink.try_seek(Duration::from_secs(5)).unwrap(); + // This doesn't do anything since the sound has ended already. + sink.try_seek(Duration::from_secs(5))?; println!("seek example ended"); + + Ok(()) } diff --git a/examples/signal_generator.rs b/examples/signal_generator.rs index 08fd4769..e746692d 100644 --- a/examples/signal_generator.rs +++ b/examples/signal_generator.rs @@ -1,83 +1,71 @@ //! Test signal generator example. -fn main() { +use std::error::Error; + +fn main() -> Result<(), Box> { use rodio::source::{chirp, Function, SignalGenerator, Source}; use std::thread; use std::time::Duration; - let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap(); + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let test_signal_duration = Duration::from_millis(1000); let interval_duration = Duration::from_millis(1500); + let sample_rate = cpal::SampleRate(48000); println!("Playing 1000 Hz tone"); - stream_handle - .play_raw( - SignalGenerator::new(cpal::SampleRate(48000), 1000.0, Function::Sine) - .amplify(0.1) - .take_duration(test_signal_duration), - ) - .unwrap(); + stream_handle.mixer().add( + SignalGenerator::new(sample_rate, 1000.0, Function::Sine) + .amplify(0.1) + .take_duration(test_signal_duration), + ); thread::sleep(interval_duration); println!("Playing 10,000 Hz tone"); - stream_handle - .play_raw( - SignalGenerator::new(cpal::SampleRate(48000), 10000.0, Function::Sine) - .amplify(0.1) - .take_duration(test_signal_duration), - ) - .unwrap(); + stream_handle.mixer().add( + SignalGenerator::new(sample_rate, 10000.0, Function::Sine) + .amplify(0.1) + .take_duration(test_signal_duration), + ); thread::sleep(interval_duration); println!("Playing 440 Hz Triangle Wave"); - stream_handle - .play_raw( - SignalGenerator::new(cpal::SampleRate(48000), 440.0, Function::Triangle) - .amplify(0.1) - .take_duration(test_signal_duration), - ) - .unwrap(); + stream_handle.mixer().add( + SignalGenerator::new(sample_rate, 440.0, Function::Triangle) + .amplify(0.1) + .take_duration(test_signal_duration), + ); thread::sleep(interval_duration); println!("Playing 440 Hz Sawtooth Wave"); - stream_handle - .play_raw( - SignalGenerator::new(cpal::SampleRate(48000), 440.0, Function::Sawtooth) - .amplify(0.1) - .take_duration(test_signal_duration), - ) - .unwrap(); + stream_handle.mixer().add( + SignalGenerator::new(sample_rate, 440.0, Function::Sawtooth) + .amplify(0.1) + .take_duration(test_signal_duration), + ); thread::sleep(interval_duration); println!("Playing 440 Hz Square Wave"); - stream_handle - .play_raw( - SignalGenerator::new(cpal::SampleRate(48000), 440.0, Function::Square) - .amplify(0.1) - .take_duration(test_signal_duration), - ) - .unwrap(); + stream_handle.mixer().add( + SignalGenerator::new(sample_rate, 440.0, Function::Square) + .amplify(0.1) + .take_duration(test_signal_duration), + ); thread::sleep(interval_duration); println!("Playing 20-10000 Hz Sweep"); - stream_handle - .play_raw( - chirp( - cpal::SampleRate(48000), - 20.0, - 10000.0, - Duration::from_secs(1), - ) + stream_handle.mixer().add( + chirp(sample_rate, 20.0, 10000.0, Duration::from_secs(1)) .amplify(0.1) .take_duration(test_signal_duration), - ) - .unwrap(); + ); thread::sleep(interval_duration); + + Ok(()) } diff --git a/examples/spatial.rs b/examples/spatial.rs index b2e9513e..31505ecc 100644 --- a/examples/spatial.rs +++ b/examples/spatial.rs @@ -1,10 +1,11 @@ +use std::error::Error; use std::io::BufReader; use std::thread; use std::time::Duration; use rodio::Source; -fn main() { +fn main() -> Result<(), Box> { let iter_duration = Duration::from_secs(5); let iter_distance = 5.; @@ -18,13 +19,18 @@ fn main() { let total_duration = iter_duration * 2 * repeats; - let (_stream, handle) = rodio::OutputStream::try_default().unwrap(); - let mut positions = ([0., 0., 0.], [-1., 0., 0.], [1., 0., 0.]); - let sink = rodio::SpatialSink::try_new(&handle, positions.0, positions.1, positions.2).unwrap(); + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let file = std::fs::File::open("assets/music.ogg").unwrap(); - let source = rodio::Decoder::new(BufReader::new(file)) - .unwrap() + let mut positions = ([0., 0., 0.], [-1., 0., 0.], [1., 0., 0.]); + let sink = rodio::SpatialSink::connect_new( + &stream_handle.mixer(), + positions.0, + positions.1, + positions.2, + ); + + let file = std::fs::File::open("assets/music.ogg")?; + let source = rodio::Decoder::new(BufReader::new(file))? .repeat_infinite() .take_duration(total_duration); sink.append(source); @@ -50,4 +56,6 @@ fn main() { } } sink.sleep_until_end(); + + Ok(()) } diff --git a/examples/stereo.rs b/examples/stereo.rs index fb1ada56..5b5ec500 100644 --- a/examples/stereo.rs +++ b/examples/stereo.rs @@ -1,12 +1,17 @@ //! Plays a tone alternating between right and left ears, with right being first. + +use rodio::Source; +use std::error::Error; use std::io::BufReader; -fn main() { - let (_stream, handle) = rodio::OutputStream::try_default().unwrap(); - let sink = rodio::Sink::try_new(&handle).unwrap(); +fn main() -> Result<(), Box> { + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let sink = rodio::Sink::connect_new(&stream_handle.mixer()); - let file = std::fs::File::open("assets/RL.ogg").unwrap(); - sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap()); + let file = std::fs::File::open("assets/RL.ogg")?; + sink.append(rodio::Decoder::new(BufReader::new(file))?.amplify(0.2)); sink.sleep_until_end(); + + Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 6f67f23c..ea0a3a35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,17 +3,17 @@ //! The main concept of this library is the [`Source`] trait, which //! represents a sound (streaming or not). In order to play a sound, there are three steps: //! +//! - Get an output stream handle to a physical device. For example, get a stream to the system's +//! default sound device with [`OutputStreamBuilder::open_default_stream()`]. //! - Create an object that represents the streaming sound. It can be a sine wave, a buffer, a //! [`decoder`], etc. or even your own type that implements the [`Source`] trait. -//! - Get an output stream handle to a physical device. For example, get a stream to the system's -//! default sound device with [`OutputStream::try_default()`] -//! - Call [`.play_raw(source)`](OutputStreamHandle::play_raw) on the output stream handle. +//! - Add the source to the output stream using [`OutputStream::mixer()`](OutputStream::mixer) +//! on the output stream handle. //! -//! The [`play_raw`](OutputStreamHandle::play_raw) function expects the source to produce [`f32`]s, -//! which may not be the case. If you get a compilation error, try calling -//! [`.convert_samples()`](Source::convert_samples) on the source to fix it. +//! The output stream expects the sources to produce [`f32`]s. In case the output sample format +//! is different use [`.convert_samples()`](Source::convert_samples) to adapt them. //! -//! For example, here is how you would play an audio file: +//! Here is a complete example of how you would play an audio file: //! //! ```no_run //! use std::fs::File; @@ -21,29 +21,51 @@ //! use rodio::{Decoder, OutputStream, source::Source}; //! //! // Get an output stream handle to the default physical sound device. -//! // Note that no sound will be played if _stream is dropped -//! let (_stream, stream_handle) = OutputStream::try_default().unwrap(); +//! // Note that the playback stops when the stream_handle is dropped. +//! let stream_handle = rodio::OutputStreamBuilder::open_default_stream() +//! .expect("open default audio stream"); +//! let sink = rodio::Sink::connect_new(&stream_handle.mixer()); //! // Load a sound from a file, using a path relative to Cargo.toml //! let file = BufReader::new(File::open("examples/music.ogg").unwrap()); //! // Decode that sound file into a source //! let source = Decoder::new(file).unwrap(); //! // Play the sound directly on the device -//! stream_handle.play_raw(source.convert_samples()); +//! stream_handle.mixer().add(source.convert_samples()); //! //! // The sound plays in a separate audio thread, //! // so we need to keep the main thread alive while it's playing. //! std::thread::sleep(std::time::Duration::from_secs(5)); //! ``` //! +//! [rodio::play()] helps to simplify the above +//! ```no_run +//! use std::fs::File; +//! use std::io::BufReader; +//! use rodio::{Decoder, OutputStream, source::Source}; +//! +//! // Get an output stream handle to the default physical sound device. +//! // Note that the playback stops when the stream_handle is dropped. +//! let stream_handle = rodio::OutputStreamBuilder::open_default_stream() +//! .expect("open default audio stream"); +//! +//! // Load a sound from a file, using a path relative to Cargo.toml +//! let file = BufReader::new(File::open("examples/music.ogg").unwrap()); +//! rodio::play(&stream_handle.mixer(), file).unwrap(); +//! +//! // The sound plays in a separate audio thread, +//! // so we need to keep the main thread alive while it's playing. +//! std::thread::sleep(std::time::Duration::from_secs(5)); +//! ``` +//! +//! //! ## Sink //! //! In order to make it easier to control the playback, the rodio library also provides a type -//! named [`Sink`] which represents an audio track. -//! -//! Instead of playing the sound with [`play_raw`](OutputStreamHandle::play_raw), you can add it to -//! a [`Sink`] instead. +//! named [`Sink`] which represents an audio track. [`Sink`] plays its input sources sequentially, +//! one after another. To play sounds in simultaneously in parallel, use [`mixer::Mixer`] instead. //! -//! - Get a [`Sink`] to the output stream, and [`.append()`](Sink::append) your sound to it. +//! To play a soung Create a [`Sink`] connect it to the output stream, +//! and [`.append()`](Sink::append) your sound to it. //! //! ```no_run //! use std::fs::File; @@ -53,8 +75,9 @@ //! use rodio::source::{SineWave, Source}; //! //! // _stream must live as long as the sink -//! let (_stream, stream_handle) = OutputStream::try_default().unwrap(); -//! let sink = Sink::try_new(&stream_handle).unwrap(); +//! let stream_handle = rodio::OutputStreamBuilder::open_default_stream() +//! .expect("open default audio stream"); +//! let sink = rodio::Sink::connect_new(&stream_handle.mixer()); //! //! // Add a dummy source of the sake of the example. //! let source = SineWave::new(440.0).take_duration(Duration::from_secs_f32(0.25)).amplify(0.20); @@ -139,7 +162,7 @@ mod stream; pub mod buffer; pub mod decoder; -pub mod dynamic_mixer; +pub mod mixer; pub mod queue; pub mod source; pub mod static_buffer; @@ -149,4 +172,4 @@ pub use crate::decoder::Decoder; pub use crate::sink::Sink; pub use crate::source::Source; pub use crate::spatial_sink::SpatialSink; -pub use crate::stream::{OutputStream, OutputStreamHandle, PlayError, StreamError}; +pub use crate::stream::{play, OutputStream, OutputStreamBuilder, PlayError, StreamError}; diff --git a/src/dynamic_mixer.rs b/src/mixer.rs similarity index 91% rename from src/dynamic_mixer.rs rename to src/mixer.rs index c2abd1c7..92b3e4eb 100644 --- a/src/dynamic_mixer.rs +++ b/src/mixer.rs @@ -13,21 +13,18 @@ use crate::Sample; /// added to the mixer will be converted to these values. /// /// After creating a mixer, you can add new sounds with the controller. -pub fn mixer( - channels: u16, - sample_rate: u32, -) -> (Arc>, DynamicMixer) +pub fn mixer(channels: u16, sample_rate: u32) -> (Arc>, MixerSource) where S: Sample + Send + 'static, { - let input = Arc::new(DynamicMixerController { + let input = Arc::new(Mixer { has_pending: AtomicBool::new(false), pending_sources: Mutex::new(Vec::new()), channels, sample_rate, }); - let output = DynamicMixer { + let output = MixerSource { current_sources: Vec::with_capacity(16), input: input.clone(), sample_count: 0, @@ -39,14 +36,14 @@ where } /// The input of the mixer. -pub struct DynamicMixerController { +pub struct Mixer { has_pending: AtomicBool, pending_sources: Mutex + Send>>>, channels: u16, sample_rate: u32, } -impl DynamicMixerController +impl Mixer where S: Sample + Send + 'static, { @@ -66,12 +63,12 @@ where } /// The output of the mixer. Implements `Source`. -pub struct DynamicMixer { +pub struct MixerSource { // The current iterator that produces samples. current_sources: Vec + Send>>, // The pending sounds. - input: Arc>, + input: Arc>, // The number of samples produced so far. sample_count: usize, @@ -83,7 +80,7 @@ pub struct DynamicMixer { still_current: Vec + Send>>, } -impl Source for DynamicMixer +impl Source for MixerSource where S: Sample + Send + 'static, { @@ -144,7 +141,7 @@ where } } -impl Iterator for DynamicMixer +impl Iterator for MixerSource where S: Sample + Send + 'static, { @@ -173,7 +170,7 @@ where } } -impl DynamicMixer +impl MixerSource where S: Sample + Send + 'static, { @@ -217,12 +214,12 @@ where #[cfg(test)] mod tests { use crate::buffer::SamplesBuffer; - use crate::dynamic_mixer; + use crate::mixer; use crate::source::Source; #[test] fn basic() { - let (tx, mut rx) = dynamic_mixer::mixer(1, 48000); + let (tx, mut rx) = mixer::mixer(1, 48000); tx.add(SamplesBuffer::new(1, 48000, vec![10i16, -10, 10, -10])); tx.add(SamplesBuffer::new(1, 48000, vec![5i16, 5, 5, 5])); @@ -238,7 +235,7 @@ mod tests { #[test] fn channels_conv() { - let (tx, mut rx) = dynamic_mixer::mixer(2, 48000); + let (tx, mut rx) = mixer::mixer(2, 48000); tx.add(SamplesBuffer::new(1, 48000, vec![10i16, -10, 10, -10])); tx.add(SamplesBuffer::new(1, 48000, vec![5i16, 5, 5, 5])); @@ -258,7 +255,7 @@ mod tests { #[test] fn rate_conv() { - let (tx, mut rx) = dynamic_mixer::mixer(1, 96000); + let (tx, mut rx) = mixer::mixer(1, 96000); tx.add(SamplesBuffer::new(1, 48000, vec![10i16, -10, 10, -10])); tx.add(SamplesBuffer::new(1, 48000, vec![5i16, 5, 5, 5])); @@ -277,7 +274,7 @@ mod tests { #[test] fn start_afterwards() { - let (tx, mut rx) = dynamic_mixer::mixer(1, 48000); + let (tx, mut rx) = mixer::mixer(1, 48000); tx.add(SamplesBuffer::new(1, 48000, vec![10i16, -10, 10, -10])); diff --git a/src/sink.rs b/src/sink.rs index 69da7a99..8dd5175c 100644 --- a/src/sink.rs +++ b/src/sink.rs @@ -2,19 +2,19 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; +use cpal::FromSample; #[cfg(feature = "crossbeam-channel")] use crossbeam_channel::{Receiver, Sender}; #[cfg(not(feature = "crossbeam-channel"))] use std::sync::mpsc::{Receiver, Sender}; +use crate::mixer::Mixer; use crate::source::SeekError; -use crate::stream::{OutputStreamHandle, PlayError}; use crate::{queue, source::Done, Sample, Source}; -use cpal::FromSample; /// Handle to a device that outputs sounds. /// -/// Dropping the `Sink` stops all sounds. You can use `detach` if you want the sounds to continue +/// Dropping the `Sink` stops all its sounds. You can use `detach` if you want the sounds to continue /// playing. pub struct Sink { queue_tx: Arc>, @@ -70,15 +70,15 @@ struct Controls { impl Sink { /// Builds a new `Sink`, beginning playback on a stream. #[inline] - pub fn try_new(stream: &OutputStreamHandle) -> Result { - let (sink, queue_rx) = Sink::new_idle(); - stream.play_raw(queue_rx)?; - Ok(sink) + pub fn connect_new(mixer: &Mixer) -> Sink { + let (sink, source) = Sink::new(); + mixer.add(source); + sink } /// Builds a new `Sink`. #[inline] - pub fn new_idle() -> (Sink, queue::SourcesQueueOutput) { + pub fn new() -> (Sink, queue::SourcesQueueOutput) { let (queue_tx, queue_rx) = queue::queue(true); let sink = Sink { @@ -107,7 +107,7 @@ impl Sink { f32: FromSample, S::Item: Sample + Send, { - // Wait for queue to flush then resume stopped playback + // Wait for the queue to flush then resume stopped playback if self.controls.stopped.load(Ordering::SeqCst) { if self.sound_count.load(Ordering::SeqCst) > 0 { self.sleep_until_end(); @@ -366,13 +366,14 @@ impl Drop for Sink { #[cfg(test)] mod tests { + use std::sync::atomic::Ordering; + use crate::buffer::SamplesBuffer; use crate::{Sink, Source}; - use std::sync::atomic::Ordering; #[test] fn test_pause_and_stop() { - let (sink, mut queue_rx) = Sink::new_idle(); + let (sink, mut queue_rx) = Sink::new(); // assert_eq!(queue_rx.next(), Some(0.0)); @@ -403,7 +404,7 @@ mod tests { #[test] fn test_stop_and_start() { - let (sink, mut queue_rx) = Sink::new_idle(); + let (sink, mut queue_rx) = Sink::new(); let v = vec![10i16, -10, 20, -20, 30, -30]; @@ -431,7 +432,7 @@ mod tests { #[test] fn test_volume() { - let (sink, mut queue_rx) = Sink::new_idle(); + let (sink, mut queue_rx) = Sink::new(); let v = vec![10i16, -10, 20, -20, 30, -30]; diff --git a/src/source/mod.rs b/src/source/mod.rs index 7f3a4ee1..ceac73bd 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -93,7 +93,7 @@ pub use self::noise::{pink, white, PinkNoise, WhiteNoise}; /// amplitude every 20µs). By doing so we obtain a list of numerical values, each value being /// called a *sample*. /// -/// Therefore a sound can be represented in memory by a frequency and a list of samples. The +/// Therefore, a sound can be represented in memory by a frequency and a list of samples. The /// frequency is expressed in hertz and corresponds to the number of samples that have been /// read per second. For example if we read one sample every 20µs, the frequency would be /// 50000 Hz. In reality, common values for the frequency are 44100, 48000 and 96000. @@ -114,7 +114,7 @@ pub use self::noise::{pink, white, PinkNoise, WhiteNoise}; /// channel, then the second sample of the second channel, and so on. The same applies if you have /// more than two channels. The rodio library only supports this schema. /// -/// Therefore in order to represent a sound in memory in fact we need three characteristics: the +/// Therefore, in order to represent a sound in memory in fact we need three characteristics: the /// frequency, the number of channels, and the list of samples. /// /// ## The `Source` trait @@ -303,19 +303,16 @@ where /// /// ```rust /// // Apply Automatic Gain Control to the source (AGC is on by default) - /// let agc_source = source.automatic_gain_control(1.0, 4.0, 0.005, 5.0); - /// - /// // Get a handle to control the AGC's enabled state (optional) - /// let agc_control = agc_source.get_agc_control(); + /// use rodio::source::{Source, SineWave}; + /// use rodio::Sink; + /// let source = SineWave::new(444.0); // An example. + /// let (sink, output) = Sink::new(); // An example. /// - /// // You can toggle AGC on/off at any time (optional) - /// agc_control.store(false, std::sync::atomic::Ordering::Relaxed); + /// let agc_source = source.automatic_gain_control(1.0, 4.0, 0.005, 5.0); /// /// // Add the AGC-controlled source to the sink /// sink.append(agc_source); /// - /// // Note: Using agc_control is optional. If you don't need to toggle AGC, - /// // you can simply use the agc_source directly without getting agc_control. /// ``` #[inline] fn automatic_gain_control( @@ -503,7 +500,7 @@ where /// /// This can get confusing when using [`get_pos()`](TrackPosition::get_pos) /// together with [`Source::try_seek()`] as the latter does take all - /// speedup's and delay's into account. Its recommended therefore to apply + /// speedup's and delay's into account. It's recommended therefore to apply /// track_position after speedup's and delay's. fn track_position(self) -> TrackPosition where @@ -559,7 +556,7 @@ where /// Attempts to seek to a given position in the current source. /// - /// As long as the duration of the source is known seek is guaranteed to saturate + /// As long as the duration of the source is known, seek is guaranteed to saturate /// at the end of the source. For example given a source that reports a total duration /// of 42 seconds calling `try_seek()` with 60 seconds as argument will seek to /// 42 seconds. @@ -582,8 +579,8 @@ where } // We might add decoders requiring new error types, without non_exhaustive -// this would break users builds -/// Occurs when try_seek fails because the underlying decoder has an error or +// this would break users' builds. +/// Occurs when `try_seek` fails because the underlying decoder has an error or /// does not support seeking. #[non_exhaustive] #[derive(Debug)] @@ -599,8 +596,8 @@ pub enum SeekError { #[cfg(feature = "wav")] /// The hound (wav) decoder ran into an issue HoundDecoder(std::io::Error), - // Prefer adding an enum variant to using this. Its meant for end users their - // own try_seek implementations + // Prefer adding an enum variant to using this. It's meant for end users their + // own `try_seek` implementations. /// Any other error probably in a custom Source Other(Box), } diff --git a/src/source/speed.rs b/src/source/speed.rs index 5287883f..8e278e9b 100644 --- a/src/source/speed.rs +++ b/src/source/speed.rs @@ -17,24 +17,26 @@ //!# use rodio::{Decoder, Sink, OutputStream, source::{Source, SineWave}}; //! //! // Get an output stream handle to the default physical sound device. -//! // Note that no sound will be played if _stream is dropped -//! let (_stream, stream_handle) = OutputStream::try_default().unwrap(); -//! // Load a sound from a file, using a path relative to Cargo.toml +//! // Note that no sound will be played if the _stream is dropped. +//! let stream_handle = rodio::OutputStreamBuilder::open_default_stream() +//! .expect("open default audio stream"); +//! // Load a sound from a file, using a path relative to `Cargo.toml` //! let file = BufReader::new(File::open("examples/music.ogg").unwrap()); //! // Decode that sound file into a source //! let source = Decoder::new(file).unwrap(); //! // Play the sound directly on the device 2x faster -//! stream_handle.play_raw(source.convert_samples().speed(2.0)); - +//! stream_handle.mixer().add(source.convert_samples().speed(2.0)); //! std::thread::sleep(std::time::Duration::from_secs(5)); //! ``` //! here is how you would do it using the sink -//! ``` +//!```no_run +//! use rodio::source::{Source, SineWave}; //! let source = SineWave::new(440.0) -//! .take_duration(Duration::from_secs_f32(20.25)) -//! .amplify(0.20); -//! -//! let sink = Sink::try_new(&stream_handle)?; +//! .take_duration(std::time::Duration::from_secs_f32(20.25)) +//! .amplify(0.20); +//! let stream_handle = rodio::OutputStreamBuilder::open_default_stream() +//! .expect("open default audio stream"); +//! let sink = rodio::Sink::connect_new(&stream_handle.mixer()); //! sink.set_speed(2.0); //! sink.append(source); //! std::thread::sleep(std::time::Duration::from_secs(5)); diff --git a/src/spatial_sink.rs b/src/spatial_sink.rs index ef3aca2a..6a4a0c95 100644 --- a/src/spatial_sink.rs +++ b/src/spatial_sink.rs @@ -4,8 +4,8 @@ use std::time::Duration; use cpal::FromSample; +use crate::mixer::Mixer; use crate::source::{SeekError, Spatial}; -use crate::stream::{OutputStreamHandle, PlayError}; use crate::{Sample, Sink, Source}; /// A sink that allows changing the position of the source and the listeners @@ -24,20 +24,20 @@ struct SoundPositions { impl SpatialSink { /// Builds a new `SpatialSink`. - pub fn try_new( - stream: &OutputStreamHandle, + pub fn connect_new( + mixer: &Mixer, emitter_position: [f32; 3], left_ear: [f32; 3], right_ear: [f32; 3], - ) -> Result { - Ok(SpatialSink { - sink: Sink::try_new(stream)?, + ) -> SpatialSink { + SpatialSink { + sink: Sink::connect_new(mixer), positions: Arc::new(Mutex::new(SoundPositions { emitter_position, left_ear, right_ear, })), - }) + } } /// Sets the position of the sound emitter in 3 dimensional space. diff --git a/src/stream.rs b/src/stream.rs index dd491244..8b31882c 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,101 +1,229 @@ use std::io::{Read, Seek}; -use std::sync::{Arc, Weak}; +use std::marker::Sync; +use std::sync::Arc; use std::{error, fmt}; use crate::decoder; -use crate::dynamic_mixer::{self, DynamicMixerController}; +use crate::mixer::{mixer, Mixer, MixerSource}; use crate::sink::Sink; -use crate::source::Source; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::{Sample, SupportedStreamConfig}; +use cpal::{ + BufferSize, ChannelCount, FrameCount, Sample, SampleFormat, SampleRate, StreamConfig, + SupportedBufferSize, +}; -/// `cpal::Stream` container. Also see the more useful `OutputStreamHandle`. -/// -/// If this is dropped playback will end & attached `OutputStreamHandle`s will no longer work. +const HZ_44100: cpal::SampleRate = cpal::SampleRate(44_100); + +/// `cpal::Stream` container. +/// Use `mixer()` method to control output. +/// If this is dropped, playback will end, and the associated output stream will be disposed. pub struct OutputStream { - mixer: Arc>, + mixer: Arc>, _stream: cpal::Stream, } -/// More flexible handle to a `OutputStream` that provides playback. -#[derive(Clone)] -pub struct OutputStreamHandle { - mixer: Weak>, +impl OutputStream { + /// Access the output stream's mixer. + pub fn mixer(&self) -> Arc> { + self.mixer.clone() + } } -impl OutputStream { - /// Returns a new stream & handle using the given output device and the default output - /// configuration. - pub fn try_from_device( - device: &cpal::Device, - ) -> Result<(Self, OutputStreamHandle), StreamError> { +#[derive(Copy, Clone, Debug)] +struct OutputStreamConfig { + pub channel_count: ChannelCount, + pub sample_rate: SampleRate, + pub buffer_size: BufferSize, + pub sample_format: SampleFormat, +} + +/// 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 Default for OutputStreamConfig { + fn default() -> Self { + Self { + channel_count: 2, + sample_rate: HZ_44100, + buffer_size: BufferSize::Default, + sample_format: SampleFormat::I8, + } + } +} + +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)?; - OutputStream::try_from_device_config(device, default_config) + Ok(Self::default() + .with_device(device) + .with_supported_config(&default_config)) } - /// Returns a new stream & handle using the given device and stream config. - /// - /// If the supplied `SupportedStreamConfig` is invalid for the device this function will - /// fail to create an output stream and instead return a `StreamError` - pub fn try_from_device_config( - device: &cpal::Device, - config: SupportedStreamConfig, - ) -> Result<(Self, OutputStreamHandle), StreamError> { - let (mixer, _stream) = device.try_new_output_stream_config(config)?; - _stream.play().map_err(StreamError::PlayStreamError)?; - let out = Self { mixer, _stream }; - let handle = OutputStreamHandle { - mixer: Arc::downgrade(&out.mixer), - }; - Ok((out, handle)) - } - - /// Return a new stream & handle using the default output device. - /// - /// On failure will fallback to trying any non-default output devices. - pub fn try_default() -> Result<(Self, OutputStreamHandle), StreamError> { + /// Sets default output stream parameters for default output audio device. + pub fn from_default_device() -> Result { let default_device = cpal::default_host() .default_output_device() .ok_or(StreamError::NoDevice)?; + Self::from_device(default_device) + } - let default_stream = Self::try_from_device(&default_device); + /// 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 { + self.device = Some(device); + self + } - default_stream.or_else(|original_err| { - // default device didn't work, try other ones - let mut devices = match cpal::default_host().output_devices() { - Ok(d) => d, - Err(_) => return Err(original_err), - }; + /// Sets number of output stream's channels. + pub fn with_channels(mut self, channel_count: cpal::ChannelCount) -> OutputStreamBuilder { + assert!(channel_count > 0); + self.config.channel_count = channel_count; + self + } - devices - .find_map(|d| Self::try_from_device(&d).ok()) - .ok_or(original_err) + /// Sets output stream's sample rate. + pub fn with_sample_rate(mut self, sample_rate: cpal::SampleRate) -> OutputStreamBuilder { + 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 { + 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 { + self.config.sample_format = sample_format; + self + } + + /// Set available parameters from a CPAL supported config. You can ge list of + /// such configurations for an output device using [crate::stream::supported_output_configs()] + pub fn with_supported_config( + mut self, + config: &cpal::SupportedStreamConfig, + ) -> OutputStreamBuilder { + self.config = OutputStreamConfig { + channel_count: config.channels(), + sample_rate: config.sample_rate(), + // In case of supported range limit buffer size to avoid unexpectedly long playback delays. + buffer_size: clamp_supported_buffer_size(config.buffer_size(), 1024), + sample_format: config.sample_format(), + }; + self + } + + /// Set all output stream parameters at once from CPAL stream config. + pub fn with_config(mut self, config: &cpal::StreamConfig) -> OutputStreamBuilder { + self.config = OutputStreamConfig { + channel_count: config.channels, + sample_rate: config.sample_rate, + buffer_size: config.buffer_size, + ..self.config + }; + self + } + + /// Open output stream using parameters configured so far. + pub fn open_stream(&self) -> Result { + let device = self.device.as_ref().expect("output device specified"); + OutputStream::open(device, &self.config) + } + + /// 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 { + let device = self.device.as_ref().expect("output device specified"); + OutputStream::open(device, &self.config).or_else(|err| { + for supported_config in supported_output_configs(device)? { + if let Ok(handle) = Self::default() + .with_device(device.clone()) + .with_supported_config(&supported_config) + .open_stream() + { + return Ok(handle); + } + } + 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) + }) + } } -impl OutputStreamHandle { - /// Plays a source with a device until it ends. - pub fn play_raw(&self, source: S) -> Result<(), PlayError> - where - S: Source + Send + 'static, - { - let mixer = self.mixer.upgrade().ok_or(PlayError::NoDevice)?; - mixer.add(source); - Ok(()) +fn clamp_supported_buffer_size( + buffer_size: &SupportedBufferSize, + preferred_size: FrameCount, +) -> BufferSize { + match buffer_size { + SupportedBufferSize::Range { min, max } => { + BufferSize::Fixed(preferred_size.clamp(*min, *max)) + } + SupportedBufferSize::Unknown => BufferSize::Default, } +} + +/// A convenience function. Plays a sound once. +/// Returns a `Sink` that can be used to control the sound. +pub fn play(mixer: &Mixer, input: R) -> Result +where + R: Read + Seek + Send + Sync + 'static, +{ + let input = decoder::Decoder::new(input)?; + let sink = Sink::connect_new(mixer); + sink.append(input); + Ok(sink) +} - /// Plays a sound once. Returns a `Sink` that can be used to control the sound. - pub fn play_once(&self, input: R) -> Result - where - R: Read + Seek + Send + Sync + 'static, - { - let input = decoder::Decoder::new(input)?; - let sink = Sink::try_new(self)?; - sink.append(input); - Ok(sink) +impl From<&OutputStreamConfig> for StreamConfig { + fn from(config: &OutputStreamConfig) -> Self { + cpal::StreamConfig { + channels: config.channel_count, + sample_rate: config.sample_rate, + buffer_size: config.buffer_size, + } } } @@ -138,10 +266,10 @@ pub enum StreamError { /// Could not start playing the stream, see [cpal::PlayStreamError] for /// details. PlayStreamError(cpal::PlayStreamError), - /// Failed to get the stream config for device the given device. See - /// [cpal::DefaultStreamConfigError] for details + /// Failed to get the stream config for the given device. See + /// [cpal::DefaultStreamConfigError] for details. DefaultStreamConfigError(cpal::DefaultStreamConfigError), - /// Error opening stream with OS. See [cpal::BuildStreamError] for details + /// Error opening stream with OS. See [cpal::BuildStreamError] for details. BuildStreamError(cpal::BuildStreamError), /// Could not list supported stream configs for device. Maybe it /// disconnected, for details see: [cpal::SupportedStreamConfigsError]. @@ -174,94 +302,96 @@ impl error::Error for StreamError { } } -/// Extensions to `cpal::Device` -pub(crate) trait CpalDeviceExt { - fn new_output_stream_with_format( - &self, - format: cpal::SupportedStreamConfig, - ) -> Result<(Arc>, cpal::Stream), cpal::BuildStreamError>; - - fn try_new_output_stream_config( - &self, - config: cpal::SupportedStreamConfig, - ) -> Result<(Arc>, cpal::Stream), StreamError>; -} - -impl CpalDeviceExt for cpal::Device { - fn new_output_stream_with_format( - &self, - format: cpal::SupportedStreamConfig, - ) -> Result<(Arc>, cpal::Stream), cpal::BuildStreamError> { - let (mixer_tx, mut mixer_rx) = - dynamic_mixer::mixer::(format.channels(), format.sample_rate().0); +impl OutputStream { + fn open( + device: &cpal::Device, + config: &OutputStreamConfig, + ) -> Result { + let (controller, source) = mixer(config.channel_count, config.sample_rate.0); + Self::init_stream(device, config, source) + .map_err(StreamError::BuildStreamError) + .and_then(|stream| { + stream.play().map_err(StreamError::PlayStreamError)?; + Ok(Self { + _stream: stream, + mixer: controller, + }) + }) + } + fn init_stream( + device: &cpal::Device, + config: &OutputStreamConfig, + mut samples: MixerSource, + ) -> Result { let error_callback = |err| { #[cfg(feature = "tracing")] - tracing::error!("an error occurred on output stream: {err}"); + tracing::error!("error initializing output stream: {err}"); #[cfg(not(feature = "tracing"))] - eprintln!("an error occurred on output stream: {err}"); + eprintln!("error initializing output stream: {err}"); }; - - match format.sample_format() { - cpal::SampleFormat::F32 => self.build_output_stream::( - &format.config(), + let sample_format = config.sample_format; + let config = config.into(); + match sample_format { + cpal::SampleFormat::F32 => device.build_output_stream::( + &config, move |data, _| { data.iter_mut() - .for_each(|d| *d = mixer_rx.next().unwrap_or(0f32)) + .for_each(|d| *d = samples.next().unwrap_or(0f32)) }, error_callback, None, ), - cpal::SampleFormat::F64 => self.build_output_stream::( - &format.config(), + cpal::SampleFormat::F64 => device.build_output_stream::( + &config, move |data, _| { data.iter_mut() - .for_each(|d| *d = mixer_rx.next().map(Sample::from_sample).unwrap_or(0f64)) + .for_each(|d| *d = samples.next().map(Sample::from_sample).unwrap_or(0f64)) }, error_callback, None, ), - cpal::SampleFormat::I8 => self.build_output_stream::( - &format.config(), + cpal::SampleFormat::I8 => device.build_output_stream::( + &config, move |data, _| { data.iter_mut() - .for_each(|d| *d = mixer_rx.next().map(Sample::from_sample).unwrap_or(0i8)) + .for_each(|d| *d = samples.next().map(Sample::from_sample).unwrap_or(0i8)) }, error_callback, None, ), - cpal::SampleFormat::I16 => self.build_output_stream::( - &format.config(), + cpal::SampleFormat::I16 => device.build_output_stream::( + &config, move |data, _| { data.iter_mut() - .for_each(|d| *d = mixer_rx.next().map(Sample::from_sample).unwrap_or(0i16)) + .for_each(|d| *d = samples.next().map(Sample::from_sample).unwrap_or(0i16)) }, error_callback, None, ), - cpal::SampleFormat::I32 => self.build_output_stream::( - &format.config(), + cpal::SampleFormat::I32 => device.build_output_stream::( + &config, move |data, _| { data.iter_mut() - .for_each(|d| *d = mixer_rx.next().map(Sample::from_sample).unwrap_or(0i32)) + .for_each(|d| *d = samples.next().map(Sample::from_sample).unwrap_or(0i32)) }, error_callback, None, ), - cpal::SampleFormat::I64 => self.build_output_stream::( - &format.config(), + cpal::SampleFormat::I64 => device.build_output_stream::( + &config, move |data, _| { data.iter_mut() - .for_each(|d| *d = mixer_rx.next().map(Sample::from_sample).unwrap_or(0i64)) + .for_each(|d| *d = samples.next().map(Sample::from_sample).unwrap_or(0i64)) }, error_callback, None, ), - cpal::SampleFormat::U8 => self.build_output_stream::( - &format.config(), + cpal::SampleFormat::U8 => device.build_output_stream::( + &config, move |data, _| { data.iter_mut().for_each(|d| { - *d = mixer_rx + *d = samples .next() .map(Sample::from_sample) .unwrap_or(u8::MAX / 2) @@ -270,11 +400,11 @@ impl CpalDeviceExt for cpal::Device { error_callback, None, ), - cpal::SampleFormat::U16 => self.build_output_stream::( - &format.config(), + cpal::SampleFormat::U16 => device.build_output_stream::( + &config, move |data, _| { data.iter_mut().for_each(|d| { - *d = mixer_rx + *d = samples .next() .map(Sample::from_sample) .unwrap_or(u16::MAX / 2) @@ -283,11 +413,11 @@ impl CpalDeviceExt for cpal::Device { error_callback, None, ), - cpal::SampleFormat::U32 => self.build_output_stream::( - &format.config(), + cpal::SampleFormat::U32 => device.build_output_stream::( + &config, move |data, _| { data.iter_mut().for_each(|d| { - *d = mixer_rx + *d = samples .next() .map(Sample::from_sample) .unwrap_or(u32::MAX / 2) @@ -296,11 +426,11 @@ impl CpalDeviceExt for cpal::Device { error_callback, None, ), - cpal::SampleFormat::U64 => self.build_output_stream::( - &format.config(), + cpal::SampleFormat::U64 => device.build_output_stream::( + &config, move |data, _| { data.iter_mut().for_each(|d| { - *d = mixer_rx + *d = samples .next() .map(Sample::from_sample) .unwrap_or(u64::MAX / 2) @@ -309,31 +439,15 @@ impl CpalDeviceExt for cpal::Device { error_callback, None, ), - _ => return Err(cpal::BuildStreamError::StreamConfigNotSupported), + _ => Err(cpal::BuildStreamError::StreamConfigNotSupported), } - .map(|stream| (mixer_tx, stream)) - } - - fn try_new_output_stream_config( - &self, - config: SupportedStreamConfig, - ) -> Result<(Arc>, cpal::Stream), StreamError> { - self.new_output_stream_with_format(config).or_else(|err| { - // look through all supported formats to see if another works - supported_output_formats(self)? - .find_map(|format| self.new_output_stream_with_format(format).ok()) - // return original error if nothing works - .ok_or(StreamError::BuildStreamError(err)) - }) } } -/// All the supported output formats with sample rates -fn supported_output_formats( +/// Return all formats supported by the device. +fn supported_output_configs( device: &cpal::Device, ) -> Result, StreamError> { - const HZ_44100: cpal::SampleRate = cpal::SampleRate(44_100); - let mut supported: Vec<_> = device .supported_output_configs() .map_err(StreamError::SupportedStreamConfigsError)?