diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb03c86..9fa920ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `Chirp` and `Empty` now implement `Iterator::size_hint` and `ExactSizeIterator`. -- `SamplesBuffer` now implements `ExactSizeIterator`. +- All sources now implement `Iterator::size_hint()`. +- All sources now implement `ExactSizeIterator` when their inner source does. - `Zero` now implements `try_seek`, `total_duration` and `Copy`. - Added `Source::is_exhausted()` helper method to check if a source has no more samples. - Added `Red` noise generator that is more practical than `Brownian` noise. @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `64bit` feature to opt-in to 64-bit sample precision (`f64`). ### Fixed + - docs.rs will now document all features, including those that are optional. - `Chirp::next` now returns `None` when the total duration has been reached, and will work correctly for a number of samples greater than 2^24. @@ -41,15 +42,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed channel misalignment in queue with non-power-of-2 channel counts (e.g., 6 channels) by ensuring frame-aligned span lengths. - Fixed channel misalignment when sources end before their promised span length by padding with silence to complete frames. - Fixed `Empty` source to properly report exhaustion. -- Fixed `Zero::current_span_len` returning remaining samples instead of span length. +- Fixed `Source::current_span_len()` to consistently return total span length. +- Fixed `Source::size_hint()` to consistently report actual bounds based on current sources. +- Fixed `Pausable::size_hint()` to correctly account for paused samples. +- Fixed `Limit`, `TakeDuration` and `TrackPosition` to handle mid-span seeks. +- Fixed `MixerSource` to prevent overflow with very long playback. +- Fixed `PeriodicAccess` to prevent overflow with very long periods. ### Changed + +- Breaking: _Sink_ terms are replaced with _Player_ and _Stream_ terms replaced + with _Sink_. This is a simple rename, functionality is identical. + - `OutputStream` is now `MixerDeviceSink` (in anticipation of future + `QueueDeviceSink`) + - `OutputStreamBuilder` is now `DeviceSinkBuilder` + - `open_stream_or_fallback` is now `open_sink_or_fallback` + - `open_default_stream` is now `open_default_sink` + - `open_stream` is now `open_mixer` (in anticipation of future `open_queue`) + - `Sink` is now `Player` + - `SpatialSink` is now `SpatialPlayer` + - `StreamError` is now `OsSinkError` - `output_to_wav` renamed to `wav_to_file` and now takes ownership of the `Source`. - `Blue` noise generator uses uniform instead of Gaussian noise for better performance. - `Gaussian` noise generator has standard deviation of 0.6 for perceptual equivalence. - `Velvet` noise generator takes density in Hz as `usize` instead of `f32`. -- Upgrade `cpal` to v0.17. -- Clarified `Source::current_span_len()` contract documentation. +- Upgraded `cpal` to v0.17. +- Clarified `Source::current_span_len()` documentation to specify it returns total span length. +- Improved queue, mixer and sample rate conversion performance. ## Version [0.21.1] (2025-07-14) @@ -165,7 +184,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 implementation. ### Fixed -- `Sink.try_seek` now updates `controls.position` before returning. Calls to `Sink.get_pos` +- `player.try_seek` now updates `controls.position` before returning. Calls to `player.get_pos` done immediately after a seek will now return the correct value. ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2fb03fd0..34f2062b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ src/: - Follow [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) - Use `rustfmt` for formatting - Implement `Source` trait for new audio sources -- Use `Sink` for playback management +- Use `Player` for playback management ## Common Tasks diff --git a/UPGRADE.md b/UPGRADE.md index 884d2f9b..e7c91532 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -56,12 +56,12 @@ No changes are required. The following Rodio *0.20* code: ```rust let (_stream, handle) = rodio::OutputStream::try_default()?; -let sink = rodio::Sink::try_new(&handle)?; +let player = rodio::Player::try_new(&handle)?; ``` Should be written like this in Rodio *0.21*: ```rust let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; -let sink = rodio::Sink::connect_new(stream_handle.mixer()); +let player = rodio::Player::connect_new(stream_handle.mixer()); ``` The `SpatialSink` changes mirror those in `Sink` described above. diff --git a/examples/automatic_gain_control.rs b/examples/automatic_gain_control.rs index 625f982c..90ab9542 100644 --- a/examples/automatic_gain_control.rs +++ b/examples/automatic_gain_control.rs @@ -8,8 +8,8 @@ use std::thread; use std::time::Duration; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); // Decode the sound file into a source let file = File::open("assets/music.flac")?; @@ -29,7 +29,7 @@ fn main() -> Result<(), Box> { // Add the source now equipped with automatic gain control and controlled via // periodic_access to the sink for the playback. - sink.append(controlled); + player.append(controlled); // After 5 seconds of playback disable automatic gain control using the // shared AtomicBool `agc_enabled`. You could do this from another part @@ -42,6 +42,6 @@ fn main() -> Result<(), Box> { agc_enabled.store(false, Ordering::Relaxed); // Keep the program running until the playback is complete. - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/basic.rs b/examples/basic.rs index 5628bd3b..f9719017 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -6,15 +6,15 @@ use std::thread; use std::time::Duration; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; 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 + let player = rodio::play(mixer, BufReader::new(file))?; + player.set_volume(0.2); + player }; println!("Started beep1"); thread::sleep(Duration::from_millis(1500)); @@ -32,9 +32,9 @@ fn main() -> Result<(), Box> { 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 + let player = rodio::play(mixer, BufReader::new(file))?; + player.set_volume(0.2); + player }; println!("Started beep3"); thread::sleep(Duration::from_millis(1500)); diff --git a/examples/callback_on_end.rs b/examples/callback_on_end.rs index f367f7f4..edf50145 100644 --- a/examples/callback_on_end.rs +++ b/examples/callback_on_end.rs @@ -3,11 +3,11 @@ use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.wav")?; - sink.append(rodio::Decoder::try_from(file)?); + player.append(rodio::Decoder::try_from(file)?); // lets increment a number after `music.wav` has played. We are going to use atomics // however you could also use a `Mutex` or send a message through a `std::sync::mpsc`. @@ -17,7 +17,7 @@ fn main() -> Result<(), Box> { // playlist_pos into the closure. That way we can still access playlist_pos // after appending the EmptyCallback. let playlist_pos_clone = playlist_pos.clone(); - sink.append(rodio::source::EmptyCallback::new(Box::new(move || { + player.append(rodio::source::EmptyCallback::new(Box::new(move || { println!("empty callback is now running"); playlist_pos_clone.fetch_add(1, Ordering::Relaxed); }))); @@ -27,7 +27,7 @@ fn main() -> Result<(), Box> { "playlist position is: {}", playlist_pos.load(Ordering::Relaxed) ); - sink.sleep_until_end(); + player.sleep_until_end(); assert_eq!(playlist_pos.load(Ordering::Relaxed), 1); println!( "playlist position is: {}", diff --git a/examples/custom_config.rs b/examples/custom_config.rs index aa83a7ee..d7a52bd4 100644 --- a/examples/custom_config.rs +++ b/examples/custom_config.rs @@ -12,7 +12,7 @@ fn main() -> Result<(), Box> { 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)? + let stream_handle = rodio::DeviceSinkBuilder::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)) @@ -20,8 +20,8 @@ fn main() -> Result<(), Box> { .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()?; + // then use DeviceSinkBuilder::open_sink() instead. + .open_sink_or_fallback()?; let mixer = stream_handle.mixer(); let wave = SineWave::new(740.0) diff --git a/examples/distortion.rs b/examples/distortion.rs index eae17bf7..a9653a4b 100644 --- a/examples/distortion.rs +++ b/examples/distortion.rs @@ -5,7 +5,7 @@ use std::time::Duration; fn main() -> Result<(), Box> { // Open the default output stream and get the mixer - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; let mixer = stream_handle.mixer(); // Create a sine wave source and apply distortion diff --git a/examples/distortion_mp3.rs b/examples/distortion_mp3.rs index 895c7647..1dc51b3c 100644 --- a/examples/distortion_mp3.rs +++ b/examples/distortion_mp3.rs @@ -3,15 +3,15 @@ use std::error::Error; use rodio::Source; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.mp3")?; // Apply distortion effect before appending to the sink let source = rodio::Decoder::try_from(file)?.distortion(4.0, 0.3); - sink.append(source); + player.append(source); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/distortion_wav.rs b/examples/distortion_wav.rs index 0955fd51..850bad5d 100644 --- a/examples/distortion_wav.rs +++ b/examples/distortion_wav.rs @@ -3,15 +3,15 @@ use std::error::Error; use rodio::Source; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.wav")?; // Apply distortion effect before appending to the sink let source = rodio::Decoder::try_from(file)?.distortion(4.0, 0.3); - sink.append(source); + player.append(source); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/distortion_wav_alternate.rs b/examples/distortion_wav_alternate.rs index 8d3f36b4..34447d40 100644 --- a/examples/distortion_wav_alternate.rs +++ b/examples/distortion_wav_alternate.rs @@ -9,8 +9,8 @@ use std::time::Duration; use rodio::Source; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.wav")?; let source = rodio::Decoder::try_from(file)?; @@ -31,7 +31,7 @@ fn main() -> Result<(), Box> { src.set_threshold(if enable { 0.3 } else { 1.0 }); }); - sink.append(distorted); + player.append(distorted); println!("Playing music.wav with alternating distortion effect..."); // Alternate the distortion effect every second for 10 seconds @@ -43,7 +43,7 @@ fn main() -> Result<(), Box> { } // Wait for playback to finish - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/error_callback.rs b/examples/error_callback.rs index ab335583..89db8609 100644 --- a/examples/error_callback.rs +++ b/examples/error_callback.rs @@ -12,7 +12,7 @@ fn main() -> Result<(), Box> { let (tx, rx) = std::sync::mpsc::channel(); - let stream_handle = rodio::OutputStreamBuilder::from_device(default_device)? + let stream_handle = rodio::DeviceSinkBuilder::from_device(default_device)? .with_error_callback(move |err| { // Filter for where err is an actionable error. if matches!( @@ -24,7 +24,7 @@ fn main() -> Result<(), Box> { } } }) - .open_stream_or_fallback()?; + .open_sink_or_fallback()?; let mixer = stream_handle.mixer(); diff --git a/examples/limit_wav.rs b/examples/limit_wav.rs index b104d3ff..9217dfd2 100644 --- a/examples/limit_wav.rs +++ b/examples/limit_wav.rs @@ -2,18 +2,18 @@ use rodio::{source::LimitSettings, Source}; use std::error::Error; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.wav")?; let source = rodio::Decoder::try_from(file)? .amplify(3.0) .limit(LimitSettings::default()); - sink.append(source); + player.append(source); println!("Playing music.wav with limiting until finished..."); - sink.sleep_until_end(); + player.sleep_until_end(); println!("Done."); Ok(()) diff --git a/examples/low_pass.rs b/examples/low_pass.rs index ec343888..aa01835f 100644 --- a/examples/low_pass.rs +++ b/examples/low_pass.rs @@ -4,15 +4,15 @@ use std::io::BufReader; use rodio::Source; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.wav")?; let decoder = rodio::Decoder::new(BufReader::new(file))?; let source = decoder.low_pass(200); - sink.append(source); + player.append(source); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/microphone.rs b/examples/microphone.rs index c3e55918..e196208a 100644 --- a/examples/microphone.rs +++ b/examples/microphone.rs @@ -21,7 +21,7 @@ fn main() -> Result<(), Box> { let recording = input.take_duration(Duration::from_secs(5)).record(); println!("Playing the recording"); - let mut output = rodio::OutputStreamBuilder::open_default_stream()?; + let mut output = rodio::DeviceSinkBuilder::open_default_sink()?; output.mixer().add(recording); thread::sleep(Duration::from_secs(5)); diff --git a/examples/mix_multiple_sources.rs b/examples/mix_multiple_sources.rs index 4d4360d6..4341330b 100644 --- a/examples/mix_multiple_sources.rs +++ b/examples/mix_multiple_sources.rs @@ -9,10 +9,10 @@ const NOTE_DURATION: Duration = Duration::from_secs(1); const NOTE_AMPLITUDE: Float = 0.20; fn main() -> Result<(), Box> { - // Construct a dynamic controller and mixer, stream_handle, and sink. + // Construct a dynamic controller and mixer, stream_handle, and player. let (controller, mixer) = mixer::mixer(NonZero::new(2).unwrap(), NonZero::new(44_100).unwrap()); - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::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, @@ -37,10 +37,10 @@ fn main() -> Result<(), Box> { controller.add(source_a); // Append the dynamic mixer to the sink to play a C major 6th chord. - sink.append(mixer); + player.append(mixer); // Sleep the thread until sink is empty. - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/music_flac.rs b/examples/music_flac.rs index f93028be..c197cd72 100644 --- a/examples/music_flac.rs +++ b/examples/music_flac.rs @@ -1,13 +1,13 @@ use std::error::Error; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.flac")?; - sink.append(rodio::Decoder::try_from(file)?); + player.append(rodio::Decoder::try_from(file)?); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/music_m4a.rs b/examples/music_m4a.rs index b8a20921..48831372 100644 --- a/examples/music_m4a.rs +++ b/examples/music_m4a.rs @@ -1,13 +1,13 @@ use std::error::Error; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.m4a")?; - sink.append(rodio::Decoder::try_from(file)?); + player.append(rodio::Decoder::try_from(file)?); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/music_mp3.rs b/examples/music_mp3.rs index bacc7309..79d841db 100644 --- a/examples/music_mp3.rs +++ b/examples/music_mp3.rs @@ -1,13 +1,13 @@ use std::error::Error; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.mp3")?; - sink.append(rodio::Decoder::try_from(file)?); + player.append(rodio::Decoder::try_from(file)?); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/music_ogg.rs b/examples/music_ogg.rs index e9ed41d2..5c22f5af 100644 --- a/examples/music_ogg.rs +++ b/examples/music_ogg.rs @@ -1,14 +1,14 @@ use std::{error::Error, io::Cursor}; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = include_bytes!("../assets/music.ogg"); let cursor = Cursor::new(file); - sink.append(rodio::Decoder::try_from(cursor)?); + player.append(rodio::Decoder::try_from(cursor)?); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/music_wav.rs b/examples/music_wav.rs index ea50de38..87dcdc67 100644 --- a/examples/music_wav.rs +++ b/examples/music_wav.rs @@ -1,13 +1,13 @@ use std::error::Error; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.wav")?; - sink.append(rodio::Decoder::try_from(file)?); + player.append(rodio::Decoder::try_from(file)?); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/noise_generator.rs b/examples/noise_generator.rs index a3c3d444..7c8be2e4 100644 --- a/examples/noise_generator.rs +++ b/examples/noise_generator.rs @@ -7,11 +7,11 @@ use rodio::{ source::noise::{ Blue, Brownian, Pink, Velvet, Violet, WhiteGaussian, WhiteTriangular, WhiteUniform, }, - Sample, Source, + MixerDeviceSink, Sample, Source, }; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; let sample_rate = stream_handle.config().sample_rate(); play_noise( @@ -74,7 +74,7 @@ fn main() -> Result<(), Box> { } /// Helper function to play a noise type with description -fn play_noise(stream_handle: &rodio::OutputStream, source: S, name: &str, description: &str) +fn play_noise(stream_handle: &MixerDeviceSink, source: S, name: &str, description: &str) where S: Source + Send + 'static, { diff --git a/examples/reverb.rs b/examples/reverb.rs index b8c04b52..3c61776e 100644 --- a/examples/reverb.rs +++ b/examples/reverb.rs @@ -3,15 +3,15 @@ use std::error::Error; use std::time::Duration; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.ogg")?; let source = rodio::Decoder::try_from(file)?; let with_reverb = source.buffered().reverb(Duration::from_millis(40), 0.7); - sink.append(with_reverb); + player.append(with_reverb); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/seek_mp3.rs b/examples/seek_mp3.rs index c27f5346..28359600 100644 --- a/examples/seek_mp3.rs +++ b/examples/seek_mp3.rs @@ -2,22 +2,22 @@ use std::error::Error; use std::time::Duration; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/music.mp3")?; - sink.append(rodio::Decoder::try_from(file)?); + player.append(rodio::Decoder::try_from(file)?); std::thread::sleep(std::time::Duration::from_secs(2)); - sink.try_seek(Duration::from_secs(0))?; + player.try_seek(Duration::from_secs(0))?; std::thread::sleep(std::time::Duration::from_secs(2)); - sink.try_seek(Duration::from_secs(4))?; + player.try_seek(Duration::from_secs(4))?; - sink.sleep_until_end(); + player.sleep_until_end(); // This doesn't do anything since the sound has ended already. - sink.try_seek(Duration::from_secs(5))?; + player.try_seek(Duration::from_secs(5))?; println!("seek example ended"); Ok(()) diff --git a/examples/signal_generator.rs b/examples/signal_generator.rs index 2257f30d..09738af9 100644 --- a/examples/signal_generator.rs +++ b/examples/signal_generator.rs @@ -8,7 +8,7 @@ fn main() -> Result<(), Box> { use std::thread; use std::time::Duration; - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; let test_signal_duration = Duration::from_millis(1000); let interval_duration = Duration::from_millis(1500); diff --git a/examples/spatial.rs b/examples/spatial.rs index f1b97e44..ade08f67 100644 --- a/examples/spatial.rs +++ b/examples/spatial.rs @@ -18,10 +18,10 @@ fn main() -> Result<(), Box> { let total_duration = iter_duration * 2 * repeats; - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; let mut positions = ([0., 0., 0.], [-1., 0., 0.], [1., 0., 0.]); - let sink = rodio::SpatialSink::connect_new( + let player = rodio::SpatialPlayer::connect_new( stream_handle.mixer(), positions.0, positions.1, @@ -32,7 +32,7 @@ fn main() -> Result<(), Box> { let source = rodio::Decoder::try_from(file)? .repeat_infinite() .take_duration(total_duration); - sink.append(source); + player.append(source); // A sound emitter playing the music starting at the centre gradually moves to the right // until it stops and begins traveling to the left, it will eventually pass through the // listener again and go to the far left. @@ -41,20 +41,20 @@ fn main() -> Result<(), Box> { for _ in 0..num_steps { thread::sleep(refresh_duration); positions.0[0] += step_distance; - sink.set_emitter_position(positions.0); + player.set_emitter_position(positions.0); } for _ in 0..(num_steps * 2) { thread::sleep(refresh_duration); positions.0[0] -= step_distance; - sink.set_emitter_position(positions.0); + player.set_emitter_position(positions.0); } for _ in 0..num_steps { thread::sleep(refresh_duration); positions.0[0] += step_distance; - sink.set_emitter_position(positions.0); + player.set_emitter_position(positions.0); } } - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/examples/stereo.rs b/examples/stereo.rs index c86eef73..2c2c3d4a 100644 --- a/examples/stereo.rs +++ b/examples/stereo.rs @@ -4,13 +4,13 @@ use rodio::Source; use std::error::Error; fn main() -> Result<(), Box> { - let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; - let sink = rodio::Sink::connect_new(stream_handle.mixer()); + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/RL.ogg")?; - sink.append(rodio::Decoder::try_from(file)?.amplify(0.2)); + player.append(rodio::Decoder::try_from(file)?.amplify(0.2)); - sink.sleep_until_end(); + player.sleep_until_end(); Ok(()) } diff --git a/src/common.rs b/src/common.rs index f6da20eb..ca2359e2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,7 +1,7 @@ use std::fmt::{Debug, Display}; use std::num::NonZero; -/// Stream sample rate (a frame rate or samples per second per channel). +/// Sample rate (a frame rate or samples per second per channel). pub type SampleRate = NonZero; /// Number of channels in a stream. Can never be Zero diff --git a/src/conversions/sample_rate.rs b/src/conversions/sample_rate.rs index 0e58eeb4..6f55e821 100644 --- a/src/conversions/sample_rate.rs +++ b/src/conversions/sample_rate.rs @@ -1,6 +1,7 @@ use crate::common::{ChannelCount, SampleRate}; use crate::{math, Sample}; use num_rational::Ratio; +use std::collections::VecDeque; use std::mem; /// Iterator that converts from a certain sample rate to another. @@ -27,7 +28,7 @@ where /// This counter is incremented (modulo `to`) every time the iterator is called. next_output_span_pos_in_chunk: u32, /// The buffer containing the samples waiting to be output. - output_buffer: Vec, + output_buffer: VecDeque, } impl SampleRateConverter @@ -81,7 +82,10 @@ where next_output_span_pos_in_chunk: 0, current_span: first_samples, next_frame: next_samples, - output_buffer: Vec::with_capacity(num_channels.get() as usize - 1), + // Capacity: worst case is upsampling where we buffer multiple frames worth of samples. + output_buffer: VecDeque::with_capacity( + (to as f32 / from as f32).ceil() as usize * num_channels.get() as usize, + ), } } @@ -126,8 +130,8 @@ where } // Short circuit if there are some samples waiting. - if !self.output_buffer.is_empty() { - return Some(self.output_buffer.remove(0)); + if let Some(sample) = self.output_buffer.pop_front() { + return Some(sample); } // The span we are going to return from this function will be a linear interpolation @@ -172,7 +176,7 @@ where if off == 0 { result = Some(sample); } else { - self.output_buffer.push(sample); + self.output_buffer.push_back(sample); } } @@ -183,14 +187,10 @@ where result } else { // draining `self.current_span` - if !self.current_span.is_empty() { - let r = Some(self.current_span.remove(0)); - mem::swap(&mut self.output_buffer, &mut self.current_span); - self.current_span.clear(); - r - } else { - None - } + let mut current_span = self.current_span.drain(..); + let r = current_span.next()?; + self.output_buffer.extend(current_span); + Some(r) } } diff --git a/src/fixed_source.rs b/src/fixed_source.rs new file mode 100644 index 00000000..6cc4ffee --- /dev/null +++ b/src/fixed_source.rs @@ -0,0 +1,19 @@ +//! Sources of sound and various filters which never change sample rate or +//! channel count. +use std::time::Duration; + +use crate::{ChannelCount, Sample, SampleRate}; + +/// Similar to `Source`, something that can produce interleaved samples for a +/// fixed amount of channels at a fixed sample rate. Those parameters never +/// change. +pub trait FixedSource: Iterator { + /// May NEVER return something else once its returned a value + fn channels(&self) -> ChannelCount; + /// May NEVER return something else once its returned a value + fn sample_rate(&self) -> SampleRate; + /// Returns the total duration of this source, if known. + /// + /// `None` indicates at the same time "infinite" or "unknown". + fn total_duration(&self) -> Option; +} diff --git a/src/lib.rs b/src/lib.rs index cf6011bf..0e1c308d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,31 +3,31 @@ //! 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()`]. +//! - Get an OS-Sink handle to a physical device. For example, get a sink to the system's +//! default sound device with [`DeviceSinkBuilder::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. -//! - Add the source to the output stream using [`OutputStream::mixer()`](OutputStream::mixer) -//! on the output stream handle. +//! - Add the source to the OS-Sink using [`DeviceSink::mixer()`](OutputStream::mixer) +//! on the OS-Sink handle. //! //! Here is a complete example of how you would play an audio file: //! #![cfg_attr(not(feature = "playback"), doc = "```ignore")] #![cfg_attr(feature = "playback", doc = "```no_run")] //! use std::fs::File; -//! use rodio::{Decoder, OutputStream, source::Source}; +//! use rodio::{Decoder, MixerDeviceSink, 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() +//! // Get an OS-Sink handle to the default physical sound device. +//! // Note that the playback stops when the handle is dropped.//! +//! let handle = rodio::DeviceSinkBuilder::open_default_sink() //! .expect("open default audio stream"); -//! let sink = rodio::Sink::connect_new(&stream_handle.mixer()); +//! let player = rodio::Player::connect_new(&handle.mixer()); //! // Load a sound from a file, using a path relative to Cargo.toml //! let file = File::open("examples/music.ogg").unwrap(); //! // Decode that sound file into a source //! let source = Decoder::try_from(file).unwrap(); //! // Play the sound directly on the device -//! stream_handle.mixer().add(source); +//! handle.mixer().add(source); //! //! // The sound plays in a separate audio thread, //! // so we need to keep the main thread alive while it's playing. @@ -39,17 +39,17 @@ #![cfg_attr(feature = "playback", doc = "```no_run")] //! use std::fs::File; //! use std::io::BufReader; -//! use rodio::{Decoder, OutputStream, source::Source}; +//! use rodio::{Decoder, MixerDeviceSink, 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() +//! // Get an OS-Sink handle to the default physical sound device. +//! // Note that the playback stops when the sink_handle is dropped. +//! let sink_handle = rodio::DeviceSinkBuilder::open_default_sink() //! .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()); -//! // Note that the playback stops when the sink is dropped -//! let sink = rodio::play(&stream_handle.mixer(), file).unwrap(); +//! // Note that the playback stops when the player is dropped +//! let player = rodio::play(&sink_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. @@ -57,43 +57,43 @@ //! ``` //! //! -//! ## Sink +//! ## Player //! //! In order to make it easier to control the playback, the rodio library also provides a type -//! named [`Sink`] which represents an audio track. [`Sink`] plays its input sources sequentially, +//! named [`Player`] which represents an audio track. [`Player`] plays its input sources sequentially, //! one after another. To play sounds in simultaneously in parallel, use [`mixer::Mixer`] instead. //! -//! To play a song Create a [`Sink`] connect it to the output stream, -//! and [`.append()`](Sink::append) your sound to it. +//! To play a song Create a [`Player`], connect it to the OS-Sink, +//! and [`.append()`](Player::append) your sound to it. //! #![cfg_attr(not(feature = "playback"), doc = "```ignore")] #![cfg_attr(feature = "playback", doc = "```no_run")] //! use std::time::Duration; -//! use rodio::{OutputStream, Sink}; +//! use rodio::{MixerDeviceSink, Player}; //! use rodio::source::{SineWave, Source}; //! //! // _stream must live as long as the sink -//! let stream_handle = rodio::OutputStreamBuilder::open_default_stream() +//! let handle = rodio::DeviceSinkBuilder::open_default_sink() //! .expect("open default audio stream"); -//! let sink = rodio::Sink::connect_new(&stream_handle.mixer()); +//! let player = rodio::Player::connect_new(&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); -//! sink.append(source); +//! player.append(source); //! -//! // The sound plays in a separate thread. This call will block the current thread until the sink -//! // has finished playing all its queued sounds. -//! sink.sleep_until_end(); +//! // The sound plays in a separate thread. This call will block the current thread until the +//! // player has finished playing all its queued sounds. +//! player.sleep_until_end(); //! ``` //! -//! The [`append`](Sink::append) method will add the sound at the end of the -//! sink. It will be played when all the previous sounds have been played. If you want multiple -//! sounds to play simultaneously, you should create multiple [`Sink`]s. +//! The [`append`](Player::append) method will add the sound at the end of the +//! player. It will be played when all the previous sounds have been played. If you want multiple +//! sounds to play simultaneously consider building your own [`Player`] from rodio parts. //! -//! The [`Sink`] type also provides utilities such as playing/pausing or controlling the volume. +//! The [`Player`] type also provides utilities such as playing/pausing or controlling the volume. //! -//!
Note that playback through Sink will end if the associated -//! OutputStream is dropped.
+//!
Note that playback through Player will end if the associated +//! DeviceSink is dropped.
//! //! ## Filters //! @@ -177,8 +177,8 @@ pub use cpal::{ }; mod common; -mod sink; -mod spatial_sink; +mod player; +mod spatial_player; #[cfg(all(feature = "playback", feature = "experimental"))] pub mod speakers; #[cfg(feature = "playback")] @@ -190,6 +190,8 @@ mod wav_output; pub mod buffer; pub mod conversions; pub mod decoder; +#[cfg(feature = "experimental")] +pub mod fixed_source; pub mod math; #[cfg(feature = "recording")] /// Microphone input support for audio recording. @@ -201,11 +203,13 @@ pub mod static_buffer; pub use crate::common::{BitDepth, ChannelCount, Float, Sample, SampleRate}; pub use crate::decoder::Decoder; -pub use crate::sink::Sink; +#[cfg(feature = "experimental")] +pub use crate::fixed_source::FixedSource; +pub use crate::player::Player; pub use crate::source::Source; -pub use crate::spatial_sink::SpatialSink; +pub use crate::spatial_player::SpatialPlayer; #[cfg(feature = "playback")] -pub use crate::stream::{play, OutputStream, OutputStreamBuilder, PlayError, StreamError}; +pub use crate::stream::{play, DeviceSinkBuilder, DeviceSinkError, MixerDeviceSink, PlayError}; #[cfg(feature = "wav_output")] #[cfg_attr(docsrs, doc(cfg(feature = "wav_output")))] pub use crate::wav_output::wav_to_file; diff --git a/src/math.rs b/src/math.rs index fffe1de1..dbc56ec9 100644 --- a/src/math.rs +++ b/src/math.rs @@ -126,6 +126,20 @@ pub(crate) fn duration_to_float(duration: Duration) -> Float { } } +/// Convert Float to Duration with appropriate precision for the Sample type. +#[inline] +#[must_use] +pub(crate) fn duration_from_secs(secs: Float) -> Duration { + #[cfg(not(feature = "64bit"))] + { + Duration::from_secs_f32(secs) + } + #[cfg(feature = "64bit")] + { + Duration::from_secs_f64(secs) + } +} + /// Utility macro for getting a `NonZero` from a literal. Especially /// useful for passing in `ChannelCount` and `Samplerate`. /// Equivalent to: `const { core::num::NonZero::new($n).unwrap() }` diff --git a/src/mixer.rs b/src/mixer.rs index 4f3241e8..b231f99f 100644 --- a/src/mixer.rs +++ b/src/mixer.rs @@ -3,10 +3,14 @@ use crate::common::{ChannelCount, SampleRate}; use crate::source::{SeekError, Source, UniformSourceIterator}; use crate::Sample; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::Duration; +#[cfg(feature = "crossbeam-channel")] +use crossbeam_channel::{unbounded as channel, Receiver, Sender}; +#[cfg(not(feature = "crossbeam-channel"))] +use std::sync::mpsc::{channel, Receiver, Sender}; + /// Builds a new mixer. /// /// You can choose the characteristics of the output thanks to this constructor. All the sounds @@ -15,23 +19,24 @@ use std::time::Duration; /// After creating a mixer, you can add new sounds with the controller. /// /// Note that mixer without any input source behaves like an `Empty` (not: `Zero`) source, -/// and thus, just after appending to a sink, the mixer is removed from the sink. -/// As a result, input sources added to the mixer later might not be forwarded to the sink. -/// Add `Zero` source to prevent detaching the mixer from sink. +/// and thus, just after appending to a player, the mixer is removed from the player. +/// As a result, input sources added to the mixer later might not be forwarded to the player. +/// Add `Zero` source to prevent detaching the mixer from player. pub fn mixer(channels: ChannelCount, sample_rate: SampleRate) -> (Mixer, MixerSource) { + let (tx, rx) = channel(); + let input = Mixer(Arc::new(Inner { - has_pending: AtomicBool::new(false), - pending_sources: Mutex::new(Vec::new()), + pending_tx: tx, channels, sample_rate, })); let output = MixerSource { - current_sources: Vec::with_capacity(16), + current_sources: Vec::new(), input: input.clone(), - sample_count: 0, - still_pending: vec![], - still_current: vec![], + current_channel: 0, + still_pending: Vec::new(), + pending_rx: rx, }; (input, output) @@ -42,8 +47,7 @@ pub fn mixer(channels: ChannelCount, sample_rate: SampleRate) -> (Mixer, MixerSo pub struct Mixer(Arc); struct Inner { - has_pending: AtomicBool, - pending_sources: Mutex>>, + pending_tx: Sender>, channels: ChannelCount, sample_rate: SampleRate, } @@ -57,12 +61,8 @@ impl Mixer { { let uniform_source = UniformSourceIterator::new(source, self.0.channels, self.0.sample_rate); - self.0 - .pending_sources - .lock() - .unwrap() - .push(Box::new(uniform_source) as Box<_>); - self.0.has_pending.store(true, Ordering::SeqCst); // TODO: can we relax this ordering? + // Ignore send errors (channel dropped means MixerSource was dropped) + let _ = self.0.pending_tx.send(Box::new(uniform_source)); } } @@ -74,14 +74,14 @@ pub struct MixerSource { // The pending sounds. input: Mixer, - // The number of samples produced so far. - sample_count: usize, + // Current channel position within the frame. + current_channel: u16, // A temporary vec used in start_pending_sources. still_pending: Vec>, - // A temporary vec used in sum_current_sources. - still_current: Vec>, + // Receiver for pending sources from the channel. + pending_rx: Receiver>, } impl Source for MixerSource { @@ -118,14 +118,16 @@ impl Iterator for MixerSource { #[inline] fn next(&mut self) -> Option { - if self.input.0.has_pending.load(Ordering::SeqCst) { - self.start_pending_sources(); - } - - self.sample_count += 1; + self.start_pending_sources(); let sum = self.sum_current_sources(); + // Advance frame position (wraps at channel count, never overflows) + self.current_channel += 1; + if self.current_channel >= self.input.0.channels.get() { + self.current_channel = 0; + } + if self.current_sources.is_empty() { None } else { @@ -135,7 +137,33 @@ impl Iterator for MixerSource { #[inline] fn size_hint(&self) -> (usize, Option) { - (0, None) + if self.current_sources.is_empty() { + return (0, Some(0)); + } + + // The mixer continues as long as ANY source is playing, so bounds are + // determined by the longest source, not the shortest. + let mut min = 0; + let mut max: Option = Some(0); + + for source in &self.current_sources { + let (source_min, source_max) = source.size_hint(); + // Lower bound: guaranteed to produce at least until longest source's lower bound + min = min.max(source_min); + + match (max, source_max) { + (Some(current_max), Some(source_max_val)) => { + // Upper bound: might produce up to longest source's upper bound + max = Some(current_max.max(source_max_val)); + } + _ => { + // If any source is unbounded, the mixer is unbounded + max = None; + } + } + } + + (min, max) } } @@ -145,12 +173,10 @@ impl MixerSource { // in-step with the modulo of the samples produced so far. Otherwise, the // sound will play on the wrong channels, e.g. left / right will be reversed. fn start_pending_sources(&mut self) { - let mut pending = self.input.0.pending_sources.lock().unwrap(); // TODO: relax ordering? - - for source in pending.drain(..) { - let in_step = self - .sample_count - .is_multiple_of(source.channels().get() as usize); + while let Ok(source) = self.pending_rx.try_recv() { + // Only start sources at frame boundaries (when current_channel == 0) + // to ensure correct channel alignment + let in_step = self.current_channel == 0; if in_step { self.current_sources.push(source); @@ -158,24 +184,19 @@ impl MixerSource { self.still_pending.push(source); } } - std::mem::swap(&mut self.still_pending, &mut pending); - - let has_pending = !pending.is_empty(); - self.input - .0 - .has_pending - .store(has_pending, Ordering::SeqCst); // TODO: relax ordering? } fn sum_current_sources(&mut self) -> Sample { let mut sum = 0.0; - for mut source in self.current_sources.drain(..) { - if let Some(value) = source.next() { - sum += value; - self.still_current.push(source); + self.current_sources.retain_mut(|source| { + match source.next() { + Some(value) => { + sum += value; + true // Keep this source + } + None => false, // Remove exhausted source } - } - std::mem::swap(&mut self.still_current, &mut self.current_sources); + }); sum } diff --git a/src/sink.rs b/src/player.rs similarity index 87% rename from src/sink.rs rename to src/player.rs index 8c1f61d2..9be3bfdc 100644 --- a/src/sink.rs +++ b/src/player.rs @@ -15,9 +15,9 @@ use crate::{queue, source::Done, Source}; /// Handle to a device that outputs sounds. /// -/// Dropping the `Sink` stops all its sounds. You can use `detach` if you want the sounds to continue +/// Dropping the `Player` stops all its sounds. You can use `detach` if you want the sounds to continue /// playing. -pub struct Sink { +pub struct Player { queue_tx: Arc, sleep_until_end: Mutex>>, @@ -67,21 +67,21 @@ struct Controls { position: Mutex, } -impl Sink { - /// Builds a new `Sink`, beginning playback on a stream. +impl Player { + /// Builds a new `Player`, beginning playback on a stream. #[inline] - pub fn connect_new(mixer: &Mixer) -> Sink { - let (sink, source) = Sink::new(); + pub fn connect_new(mixer: &Mixer) -> Player { + let (sink, source) = Player::new(); mixer.add(source); sink } - /// Builds a new `Sink`. + /// Builds a new `Player`. #[inline] - pub fn new() -> (Sink, queue::SourcesQueueOutput) { + pub fn new() -> (Player, queue::SourcesQueueOutput) { let (queue_tx, queue_rx) = queue::queue(true); - let sink = Sink { + let sink = Player { queue_tx, sleep_until_end: Mutex::new(None), controls: Arc::new(Controls { @@ -181,7 +181,7 @@ impl Sink { /// Gets the speed of the sound. /// - /// See [`Sink::set_speed`] for details on what *speed* means. + /// See [`Player::set_speed`] for details on what *speed* means. #[inline] pub fn speed(&self) -> f32 { *self.controls.speed.lock().unwrap() @@ -205,7 +205,7 @@ impl Sink { *self.controls.speed.lock().unwrap() = value; } - /// Resumes playback of a paused sink. + /// Resumes playback of a paused player. /// /// No effect if not paused. #[inline] @@ -256,7 +256,7 @@ impl Sink { } } - /// Pauses playback of this sink. + /// Pauses playback of this player. /// /// No effect if already paused. /// @@ -267,15 +267,15 @@ impl Sink { /// Gets if a sink is paused /// - /// Sinks can be paused and resumed using `pause()` and `play()`. This returns `true` if the + /// Players can be paused and resumed using `pause()` and `play()`. This returns `true` if the /// sink is paused. pub fn is_paused(&self) -> bool { self.controls.pause.load(Ordering::SeqCst) } - /// Removes all currently loaded `Source`s from the `Sink`, and pauses it. + /// Removes all currently loaded `Source`s from the `Player`, and pauses it. /// - /// See `pause()` for information about pausing a `Sink`. + /// See `pause()` for information about pausing a `Player`. pub fn clear(&self) { let len = self.sound_count.load(Ordering::SeqCst) as u32; *self.controls.to_clear.lock().unwrap() = len; @@ -283,10 +283,10 @@ impl Sink { self.pause(); } - /// Skips to the next `Source` in the `Sink` + /// Skips to the next `Source` in the `Player` /// - /// If there are more `Source`s appended to the `Sink` at the time, - /// it will play the next one. Otherwise, the `Sink` will finish as if + /// If there are more `Source`s appended to the `Player` at the time, + /// it will play the next one. Otherwise, the `Player` will finish as if /// it had finished playing a `Source` all the way through. pub fn skip_one(&self) { let len = self.sound_count.load(Ordering::SeqCst) as u32; @@ -334,7 +334,7 @@ impl Sink { /// This takes into account any speedup or delay applied. /// /// Example: if you apply a speedup of *2* to an mp3 decoder source and - /// [`get_pos()`](Sink::get_pos) returns *5s* then the position in the mp3 + /// [`get_pos()`](Player::get_pos) returns *5s* then the position in the mp3 /// recording is *10s* from its start. #[inline] pub fn get_pos(&self) -> Duration { @@ -342,7 +342,7 @@ impl Sink { } } -impl Drop for Sink { +impl Drop for Player { #[inline] fn drop(&mut self) { self.queue_tx.set_keep_alive_if_empty(false); @@ -359,11 +359,11 @@ mod tests { use crate::buffer::SamplesBuffer; use crate::math::nz; - use crate::{Sink, Source}; + use crate::{Player, Source}; #[test] fn test_pause_and_stop() { - let (sink, mut source) = Sink::new(); + let (player, mut source) = Player::new(); assert_eq!(source.next(), Some(0.0)); // TODO (review) How did this test passed before? I might have broken something but @@ -375,49 +375,49 @@ mod tests { let v = vec![10.0, -10.0, 20.0, -20.0, 30.0, -30.0]; // Low rate to ensure immediate control. - sink.append(SamplesBuffer::new(nz!(1), nz!(1), v.clone())); + player.append(SamplesBuffer::new(nz!(1), nz!(1), v.clone())); let mut reference_src = SamplesBuffer::new(nz!(1), nz!(1), v); assert_eq!(source.next(), reference_src.next()); assert_eq!(source.next(), reference_src.next()); - sink.pause(); + player.pause(); assert_eq!(source.next(), Some(0.0)); - sink.play(); + player.play(); assert_eq!(source.next(), reference_src.next()); assert_eq!(source.next(), reference_src.next()); - sink.stop(); + player.stop(); assert_eq!(source.next(), Some(0.0)); - assert!(sink.empty()); + assert!(player.empty()); } #[test] fn test_stop_and_start() { - let (sink, mut queue_rx) = Sink::new(); + let (player, mut queue_rx) = Player::new(); let v = vec![10.0, -10.0, 20.0, -20.0, 30.0, -30.0]; - sink.append(SamplesBuffer::new(nz!(1), nz!(1), v.clone())); + player.append(SamplesBuffer::new(nz!(1), nz!(1), v.clone())); let mut src = SamplesBuffer::new(nz!(1), nz!(1), v.clone()); assert_eq!(queue_rx.next(), src.next()); assert_eq!(queue_rx.next(), src.next()); - sink.stop(); + player.stop(); - assert!(sink.controls.stopped.load(Ordering::SeqCst)); + assert!(player.controls.stopped.load(Ordering::SeqCst)); assert_eq!(queue_rx.next(), Some(0.0)); src = SamplesBuffer::new(nz!(1), nz!(1), v.clone()); - sink.append(SamplesBuffer::new(nz!(1), nz!(1), v)); + player.append(SamplesBuffer::new(nz!(1), nz!(1), v)); - assert!(!sink.controls.stopped.load(Ordering::SeqCst)); + assert!(!player.controls.stopped.load(Ordering::SeqCst)); // Flush silence let mut queue_rx = queue_rx.skip_while(|v| *v == 0.0); @@ -427,16 +427,16 @@ mod tests { #[test] fn test_volume() { - let (sink, mut queue_rx) = Sink::new(); + let (player, mut queue_rx) = Player::new(); let v = vec![10.0, -10.0, 20.0, -20.0, 30.0, -30.0]; // High rate to avoid immediate control. - sink.append(SamplesBuffer::new(nz!(2), nz!(44100), v.clone())); + player.append(SamplesBuffer::new(nz!(2), nz!(44100), v.clone())); let src = SamplesBuffer::new(nz!(2), nz!(44100), v.clone()); let mut src = src.amplify(0.5); - sink.set_volume(0.5); + player.set_volume(0.5); for _ in 0..v.len() { assert_eq!(queue_rx.next(), src.next()); diff --git a/src/queue.rs b/src/queue.rs index 3bf1695e..b0d51a7f 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -1,5 +1,6 @@ //! Queue that plays sounds one after the other. +use std::collections::VecDeque; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -26,7 +27,7 @@ use std::sync::mpsc::{channel, Receiver, Sender}; /// pub fn queue(keep_alive_if_empty: bool) -> (Arc, SourcesQueueOutput) { let input = Arc::new(SourcesQueueInput { - next_sounds: Mutex::new(Vec::new()), + next_sounds: Mutex::new(VecDeque::new()), keep_alive_if_empty: AtomicBool::new(keep_alive_if_empty), }); @@ -48,7 +49,7 @@ type SignalDone = Option>; /// The input of the queue. pub struct SourcesQueueInput { - next_sounds: Mutex>, + next_sounds: Mutex>, // See constructor. keep_alive_if_empty: AtomicBool, @@ -64,7 +65,7 @@ impl SourcesQueueInput { self.next_sounds .lock() .unwrap() - .push((Box::new(source) as Box<_>, None)); + .push_back((Box::new(source) as Box<_>, None)); } /// Adds a new source to the end of the queue. @@ -81,7 +82,7 @@ impl SourcesQueueInput { self.next_sounds .lock() .unwrap() - .push((Box::new(source) as Box<_>, Some(tx))); + .push_back((Box::new(source) as Box<_>, Some(tx))); rx } @@ -134,37 +135,16 @@ fn threshold(channels: ChannelCount) -> usize { impl Source for SourcesQueueOutput { #[inline] fn current_span_len(&self) -> Option { - // This function is non-trivial because the boundary between two sounds in the queue should - // be a span boundary as well. - // - // The current sound is free to return `None` for `current_span_len()`, in which case - // we *should* return the number of samples remaining the current sound. - // This can be estimated with `size_hint()`. - // - // If the `size_hint` is `None` as well, we are in the worst case scenario. To handle this - // situation we force a span to have a maximum number of samples indicate by this - // constant. - - // Try the current `current_span_len`. if !self.current.is_exhausted() { return self.current.current_span_len(); } else if self.input.keep_alive_if_empty.load(Ordering::Acquire) && self.input.next_sounds.lock().unwrap().is_empty() { - // The next source will be a filler silence which will have a frame-aligned length + // Return what that Zero's current_span_len() will be: Some(threshold(channels)). return Some(threshold(self.current.channels())); } - // Try the size hint. - let (lower_bound, _) = self.current.size_hint(); - // The iterator default implementation just returns 0. - // That's a problematic value, so skip it. - if lower_bound > 0 { - return Some(lower_bound); - } - - // Otherwise we use a frame-aligned threshold value. - Some(threshold(self.current.channels())) + None } #[inline] @@ -175,7 +155,7 @@ impl Source for SourcesQueueOutput { // - After append: the appended source while playing // - With keep_alive: Zero (silence) while playing self.current.channels() - } else if let Some((next, _)) = self.input.next_sounds.lock().unwrap().first() { + } else if let Some((next, _)) = self.input.next_sounds.lock().unwrap().front() { // Current source exhausted, peek at next queued source // This is critical: UniformSourceIterator queries metadata during append, // before any samples are pulled. We must report the next source's metadata. @@ -194,7 +174,7 @@ impl Source for SourcesQueueOutput { if !self.current.is_exhausted() { // Current source is active (producing samples) self.current.sample_rate() - } else if let Some((next, _)) = self.input.next_sounds.lock().unwrap().first() { + } else if let Some((next, _)) = self.input.next_sounds.lock().unwrap().front() { // Current source exhausted, peek at next queued source // This prevents wrong resampling setup in UniformSourceIterator next.sample_rate() @@ -282,7 +262,9 @@ impl SourcesQueueOutput { let (next, signal_after_end) = { let mut next = self.input.next_sounds.lock().unwrap(); - if next.is_empty() { + if let Some(next) = next.pop_front() { + next + } else { let channels = self.current.channels(); let silence = Box::new(Zero::new_samples( channels, @@ -295,8 +277,6 @@ impl SourcesQueueOutput { } else { return Err(()); } - } else { - next.remove(0) } }; diff --git a/src/source/blt.rs b/src/source/blt.rs index 5d06779b..355d38cc 100644 --- a/src/source/blt.rs +++ b/src/source/blt.rs @@ -116,10 +116,10 @@ where #[inline] fn next(&mut self) -> Option { - let last_in_span = self.input.current_span_len() == Some(1); + let current_sample_rate = self.input.sample_rate(); if self.applier.is_none() { - self.applier = Some(self.formula.to_applier(self.input.sample_rate().get())); + self.applier = Some(self.formula.to_applier(current_sample_rate.get())); } let sample = self.input.next()?; @@ -134,7 +134,14 @@ where self.y_n1 = result; self.x_n1 = sample; - if last_in_span { + // Check if sample rate changed after getting the next sample. + // Only check when span is finite and not exhausted. + let sample_rate_changed = self + .input + .current_span_len() + .is_some_and(|len| len > 0 && current_sample_rate != self.input.sample_rate()); + + if sample_rate_changed { self.applier = None; } @@ -175,7 +182,15 @@ where #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - self.input.try_seek(pos) + self.input.try_seek(pos)?; + + // Reset filter state to avoid artifacts from previous position + self.x_n1 = 0.0; + self.x_n2 = 0.0; + self.y_n1 = 0.0; + self.y_n2 = 0.0; + + Ok(()) } } diff --git a/src/source/buffered.rs b/src/source/buffered.rs index 974389dc..e45ea2ff 100644 --- a/src/source/buffered.rs +++ b/src/source/buffered.rs @@ -1,4 +1,3 @@ -use std::cmp; use std::mem; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -59,7 +58,7 @@ struct SpanData where I: Source, { - data: Vec, + data: Box<[I::Item]>, channels: ChannelCount, rate: SampleRate, next: Mutex>>, @@ -107,10 +106,12 @@ where let channels = input.channels(); let rate = input.sample_rate(); - let data: Vec = input + let max_samples = span_len.unwrap_or(32768); + let data: Box<[I::Item]> = input .by_ref() - .take(cmp::min(span_len.unwrap_or(32768), 32768)) - .collect(); + .take(max_samples) + .collect::>() + .into_boxed_slice(); if data.is_empty() { return Arc::new(Span::End); @@ -204,11 +205,12 @@ where { #[inline] fn current_span_len(&self) -> Option { - match &*self.current_span { - Span::Data(SpanData { data, .. }) => Some(data.len() - self.position_in_span), - Span::End => Some(0), + let len = match &*self.current_span { + Span::Data(SpanData { data, .. }) => data.len(), + Span::End => 0, Span::Input(_) => unreachable!(), - } + }; + Some(len) } #[inline] diff --git a/src/source/chirp.rs b/src/source/chirp.rs index ce5c0bf3..88c5bc0e 100644 --- a/src/source/chirp.rs +++ b/src/source/chirp.rs @@ -46,17 +46,6 @@ impl Chirp { elapsed_samples: 0, } } - - #[allow(dead_code)] - fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - let mut target = (pos.as_secs_f64() * self.sample_rate.get() as f64) as u64; - if target >= self.total_samples { - target = self.total_samples; - } - - self.elapsed_samples = target; - Ok(()) - } } impl Iterator for Chirp { @@ -101,4 +90,14 @@ impl Source for Chirp { let secs = self.total_samples as f64 / self.sample_rate.get() as f64; Some(Duration::from_secs_f64(secs)) } + + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + let mut target = (pos.as_secs_f64() * self.sample_rate.get() as f64) as u64; + if target >= self.total_samples { + target = self.total_samples; + } + + self.elapsed_samples = target; + Ok(()) + } } diff --git a/src/source/delay.rs b/src/source/delay.rs index 0f55aa32..5c835dc5 100644 --- a/src/source/delay.rs +++ b/src/source/delay.rs @@ -84,6 +84,8 @@ where } } +impl ExactSizeIterator for Delay where I: Iterator + Source + ExactSizeIterator {} + impl Source for Delay where I: Iterator + Source, diff --git a/src/source/dither.rs b/src/source/dither.rs index f87e472c..95cff7d4 100644 --- a/src/source/dither.rs +++ b/src/source/dither.rs @@ -22,7 +22,7 @@ //! - **Choose TPDF** for most professional audio applications (it's the default) //! - **Use target output bit depth** - Not the source bit depth! //! -//! When you later change volume (e.g., with `Sink::set_volume()`), both the signal +//! When you later change volume (e.g., with `Player::set_volume()`), both the signal //! and dither noise scale together, maintaining proper dithering behavior. use rand::{rngs::SmallRng, Rng}; @@ -161,7 +161,8 @@ pub struct Dither { input: I, noise: NoiseGenerator, current_channel: usize, - remaining_in_span: Option, + last_sample_rate: SampleRate, + last_channels: ChannelCount, lsb_amplitude: Float, } @@ -179,13 +180,13 @@ where let sample_rate = input.sample_rate(); let channels = input.channels(); - let active_span_len = input.current_span_len(); Self { input, noise: NoiseGenerator::new(algorithm, sample_rate, channels), current_channel: 0, - remaining_in_span: active_span_len, + last_sample_rate: sample_rate, + last_channels: channels, lsb_amplitude, } } @@ -213,23 +214,25 @@ where #[inline] fn next(&mut self) -> Option { - if let Some(ref mut remaining) = self.remaining_in_span { - *remaining = remaining.saturating_sub(1); - } - - // Consume next input sample *after* decrementing span position and *before* checking for - // span boundary crossing. This ensures that the source has its parameters updated - // correctly before we generate noise for the next sample. let input_sample = self.input.next()?; - let num_channels = self.input.channels(); - if self.remaining_in_span == Some(0) { - self.noise - .update_parameters(self.input.sample_rate(), num_channels); - self.current_channel = 0; - self.remaining_in_span = self.input.current_span_len(); + if self.input.current_span_len().is_some_and(|len| len > 0) { + let current_sample_rate = self.input.sample_rate(); + let current_channels = self.input.channels(); + let parameters_changed = current_sample_rate != self.last_sample_rate + || current_channels != self.last_channels; + + if parameters_changed { + self.noise + .update_parameters(current_sample_rate, current_channels); + self.current_channel = 0; + self.last_sample_rate = current_sample_rate; + self.last_channels = current_channels; + } } + let num_channels = self.input.channels(); + let noise_sample = self .noise .next(self.current_channel) diff --git a/src/source/done.rs b/src/source/done.rs index 5bc922b7..8e486cdb 100644 --- a/src/source/done.rs +++ b/src/source/done.rs @@ -66,6 +66,8 @@ where } } +impl ExactSizeIterator for Done where I: Source + ExactSizeIterator {} + impl Source for Done where I: Source, diff --git a/src/source/empty_callback.rs b/src/source/empty_callback.rs index 4ae62437..d240102b 100644 --- a/src/source/empty_callback.rs +++ b/src/source/empty_callback.rs @@ -29,12 +29,19 @@ impl Iterator for EmptyCallback { (self.callback)(); None } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (0, Some(0)) + } } +impl ExactSizeIterator for EmptyCallback {} + impl Source for EmptyCallback { #[inline] fn current_span_len(&self) -> Option { - None + Some(0) } #[inline] @@ -49,7 +56,7 @@ impl Source for EmptyCallback { #[inline] fn total_duration(&self) -> Option { - Some(Duration::new(0, 0)) + Some(Duration::ZERO) } #[inline] diff --git a/src/source/from_iter.rs b/src/source/from_iter.rs index c8c14062..e1252016 100644 --- a/src/source/from_iter.rs +++ b/src/source/from_iter.rs @@ -66,7 +66,7 @@ where if let Some(cur) = &self.current_source { (cur.size_hint().0, None) } else { - (0, None) + (0, Some(0)) } } } @@ -78,36 +78,13 @@ where { #[inline] fn current_span_len(&self) -> Option { - // This function is non-trivial because the boundary between the current source and the - // next must be a span boundary as well. - // - // The current sound is free to return `None` for `current_span_len()`, in which case - // we *should* return the number of samples remaining the current sound. - // This can be estimated with `size_hint()`. - // - // If the `size_hint` is `None` as well, we are in the worst case scenario. To handle this - // situation we force a span to have a maximum number of samples indicate by this - // constant. - const THRESHOLD: usize = 10240; - - // Try the current `current_span_len`. if let Some(src) = &self.current_source { if !src.is_exhausted() { return src.current_span_len(); } } - // Try the size hint. - if let Some(src) = &self.current_source { - if let Some(val) = src.size_hint().1 { - if val < THRESHOLD && val != 0 { - return Some(val); - } - } - } - - // Otherwise we use the constant value. - Some(THRESHOLD) + None } #[inline] diff --git a/src/source/limit.rs b/src/source/limit.rs index a16139ae..4277558a 100644 --- a/src/source/limit.rs +++ b/src/source/limit.rs @@ -67,6 +67,68 @@ use crate::{ Float, Source, }; +/// Creates a limiter that processes the input audio source. +/// +/// This function applies the specified limiting settings to control audio peaks. +/// The limiter uses feedforward processing with configurable attack/release times +/// and soft-knee characteristics for natural-sounding dynamic range control. +/// +/// # Arguments +/// +/// * `input` - Audio source to process +/// * `settings` - Limiter configuration (threshold, knee, timing) +/// +/// # Returns +/// +/// A [`Limit`] source that applies the limiting to the input audio. +/// +/// # Example +/// +/// ```rust +/// use rodio::source::{SineWave, Source, LimitSettings}; +/// +/// let source = SineWave::new(440.0).amplify(2.0); +/// let settings = LimitSettings::default().with_threshold(-6.0); +/// let limited = source.limit(settings); +/// ``` +pub(crate) fn limit(input: I, settings: LimitSettings) -> Limit { + let sample_rate = input.sample_rate(); + let attack = duration_to_coefficient(settings.attack, sample_rate); + let release = duration_to_coefficient(settings.release, sample_rate); + let channels = input.channels(); + let channels_count = channels.get() as usize; + + let base = LimitBase::new(settings.threshold, settings.knee_width, attack, release); + + let inner = match channels_count { + 1 => LimitInner::Mono(LimitMono { + input, + base, + limiter_integrator: 0.0, + limiter_peak: 0.0, + }), + 2 => LimitInner::Stereo(LimitStereo { + input, + base, + limiter_integrators: [0.0; 2], + limiter_peaks: [0.0; 2], + is_right_channel: false, + }), + n => LimitInner::MultiChannel(LimitMulti { + input, + base, + limiter_integrators: vec![0.0; n].into_boxed_slice(), + limiter_peaks: vec![0.0; n].into_boxed_slice(), + position: 0, + }), + }; + + Limit { + inner: Some(inner), + last_channels: channels, + } +} + /// Configuration settings for audio limiting. /// /// This struct defines how the limiter behaves, including when to start limiting @@ -126,8 +188,6 @@ use crate::{ /// .with_attack(Duration::from_millis(3)) // Faster attack /// .with_release(Duration::from_millis(50)); // Faster release /// ``` -#[derive(Debug, Clone)] -/// Configuration settings for audio limiting. /// /// # dB vs. dBFS Reference /// @@ -146,6 +206,7 @@ use crate::{ /// - **-6 dBFS**: Generous headroom (gentle limiting) /// - **-12 dBFS**: Conservative level (preserves significant dynamics) /// - **-20 dBFS**: Very quiet level (background/ambient sounds) +#[derive(Debug, Clone)] pub struct LimitSettings { /// Level where limiting begins (dBFS, must be negative). /// @@ -454,64 +515,6 @@ impl LimitSettings { } } -/// Creates a limiter that processes the input audio source. -/// -/// This function applies the specified limiting settings to control audio peaks. -/// The limiter uses feedforward processing with configurable attack/release times -/// and soft-knee characteristics for natural-sounding dynamic range control. -/// -/// # Arguments -/// -/// * `input` - Audio source to process -/// * `settings` - Limiter configuration (threshold, knee, timing) -/// -/// # Returns -/// -/// A [`Limit`] source that applies the limiting to the input audio. -/// -/// # Example -/// -/// ```rust -/// use rodio::source::{SineWave, Source, LimitSettings}; -/// -/// let source = SineWave::new(440.0).amplify(2.0); -/// let settings = LimitSettings::default().with_threshold(-6.0); -/// let limited = source.limit(settings); -/// ``` -pub(crate) fn limit(input: I, settings: LimitSettings) -> Limit { - let sample_rate = input.sample_rate(); - let attack = duration_to_coefficient(settings.attack, sample_rate); - let release = duration_to_coefficient(settings.release, sample_rate); - let channels = input.channels().get() as usize; - - let base = LimitBase::new(settings.threshold, settings.knee_width, attack, release); - - let inner = match channels { - 1 => LimitInner::Mono(LimitMono { - input, - base, - limiter_integrator: 0.0, - limiter_peak: 0.0, - }), - 2 => LimitInner::Stereo(LimitStereo { - input, - base, - limiter_integrators: [0.0; 2], - limiter_peaks: [0.0; 2], - position: 0, - }), - n => LimitInner::MultiChannel(LimitMulti { - input, - base, - limiter_integrators: vec![0.0; n], - limiter_peaks: vec![0.0; n], - position: 0, - }), - }; - - Limit(inner) -} - /// A source filter that applies audio limiting to prevent peaks from exceeding a threshold. /// /// This filter reduces the amplitude of audio signals that exceed the configured threshold @@ -554,23 +557,17 @@ pub(crate) fn limit(input: I, settings: LimitSettings) -> Limit { /// - **Stereo**: Two-channel optimized with interleaved processing /// - **Multi-channel**: Generic implementation for 3+ channels /// -/// # Channel Count Stability -/// -/// **Important**: The limiter is optimized for sources with fixed channel counts. -/// Most audio files (music, podcasts, etc.) maintain constant channel counts, -/// making this optimization safe and beneficial. -/// -/// If the underlying source changes channel count mid-stream (rare), the limiter -/// will continue to function but performance may be degraded. For such cases, -/// recreate the limiter when the channel count changes. -/// /// # Type Parameters /// /// * `I` - The input audio source type that implements [`Source`] #[derive(Clone, Debug)] -pub struct Limit(LimitInner) +pub struct Limit where - I: Source; + I: Source, +{ + inner: Option>, + last_channels: ChannelCount, +} impl Source for Limit where @@ -578,27 +575,27 @@ where { #[inline] fn current_span_len(&self) -> Option { - self.0.current_span_len() + self.inner.as_ref().unwrap().current_span_len() } #[inline] fn sample_rate(&self) -> SampleRate { - self.0.sample_rate() + self.inner.as_ref().unwrap().sample_rate() } #[inline] fn channels(&self) -> ChannelCount { - self.0.channels() + self.inner.as_ref().unwrap().channels() } #[inline] fn total_duration(&self) -> Option { - self.0.total_duration() + self.inner.as_ref().unwrap().total_duration() } #[inline] fn try_seek(&mut self, position: Duration) -> Result<(), SeekError> { - self.0.try_seek(position) + self.inner.as_mut().unwrap().try_seek(position) } } @@ -615,7 +612,7 @@ where /// Useful for inspecting source properties without consuming the filter. #[inline] pub fn inner(&self) -> &I { - self.0.inner() + self.inner.as_ref().unwrap().inner() } /// Returns a mutable reference to the inner audio source. @@ -625,7 +622,7 @@ where /// underlying source. #[inline] pub fn inner_mut(&mut self) -> &mut I { - self.0.inner_mut() + self.inner.as_mut().unwrap().inner_mut() } /// Consumes the limiter and returns the inner audio source. @@ -635,7 +632,7 @@ where /// Useful when limiting is no longer needed but the source should continue. #[inline] pub fn into_inner(self) -> I { - self.0.into_inner() + self.inner.unwrap().into_inner() } } @@ -648,16 +645,76 @@ where /// Provides the next limited sample. #[inline] fn next(&mut self) -> Option { - self.0.next() + let sample = self.inner.as_mut().unwrap().next()?; + + if self + .inner + .as_ref() + .unwrap() + .current_span_len() + .is_some_and(|len| len > 0) + { + let new_channels = self.inner.as_ref().unwrap().channels(); + + if new_channels != self.last_channels { + self.last_channels = new_channels; + let new_channels_count = new_channels.get() as usize; + + let parameters_changed = match self.inner.as_ref().unwrap() { + LimitInner::Mono(_) => new_channels_count != 1, + LimitInner::Stereo(_) => new_channels_count != 2, + LimitInner::MultiChannel(multi) => { + new_channels_count != multi.limiter_integrators.len() + } + }; + + if parameters_changed { + let old_inner = self.inner.take().unwrap(); + + let (input, base) = match old_inner { + LimitInner::Mono(mono) => (mono.input, mono.base), + LimitInner::Stereo(stereo) => (stereo.input, stereo.base), + LimitInner::MultiChannel(multi) => (multi.input, multi.base), + }; + + self.inner = Some(match new_channels_count { + 1 => LimitInner::Mono(LimitMono { + input, + base, + limiter_integrator: 0.0, + limiter_peak: 0.0, + }), + 2 => LimitInner::Stereo(LimitStereo { + input, + base, + limiter_integrators: [0.0; 2], + limiter_peaks: [0.0; 2], + is_right_channel: false, + }), + n => LimitInner::MultiChannel(LimitMulti { + input, + base, + limiter_integrators: vec![0.0; n].into_boxed_slice(), + limiter_peaks: vec![0.0; n].into_boxed_slice(), + position: 0, + }), + }); + } + } + } + + Some(sample) } /// Provides size hints from the inner limiter. #[inline] fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() + self.inner.as_ref().unwrap().size_hint() } } +impl ExactSizeIterator for Limit where I: Source + ExactSizeIterator {} + /// Internal limiter implementation that adapts to different channel configurations. /// /// This enum is private and automatically selects the most efficient implementation @@ -716,13 +773,8 @@ struct LimitBase { /// This variant is automatically selected by [`Limit`] for mono audio sources. /// It uses minimal state (single integrator and peak detector) for optimal /// performance with single-channel audio. -/// -/// # Internal Use -/// -/// This struct is used internally by [`LimitInner::Mono`] and is not intended -/// for direct construction. Use [`Source::limit()`] instead. #[derive(Clone, Debug)] -pub struct LimitMono { +struct LimitMono { /// Input audio source input: I, /// Common limiter parameters @@ -744,13 +796,8 @@ pub struct LimitMono { /// The fixed arrays and channel position tracking provide optimal performance /// for interleaved stereo sample processing, avoiding the dynamic allocation /// overhead of the multi-channel variant. -/// -/// # Internal Use -/// -/// This struct is used internally by [`LimitInner::Stereo`] and is not intended -/// for direct construction. Use [`Source::limit()`] instead. #[derive(Clone, Debug)] -pub struct LimitStereo { +struct LimitStereo { /// Input audio source input: I, /// Common limiter parameters @@ -759,8 +806,8 @@ pub struct LimitStereo { limiter_integrators: [Float; 2], /// Peak detection states for left and right channels limiter_peaks: [Float; 2], - /// Current channel position (0 = left, 1 = right) - position: u8, + /// Current channel: true = right, false = left + is_right_channel: bool, } /// Generic multi-channel limiter for surround sound or other configurations. @@ -775,21 +822,16 @@ pub struct LimitStereo { /// While this variant has slightly more overhead than the mono/stereo variants /// due to vector allocation and dynamic indexing, it provides the flexibility /// needed for complex audio setups while maintaining good performance. -/// -/// # Internal Use -/// -/// This struct is used internally by [`LimitInner::MultiChannel`] and is not -/// intended for direct construction. Use [`Source::limit()`] instead. #[derive(Clone, Debug)] -pub struct LimitMulti { +struct LimitMulti { /// Input audio source input: I, /// Common limiter parameters base: LimitBase, /// Peak detector integrator states (one per channel) - limiter_integrators: Vec, + limiter_integrators: Box<[Float]>, /// Peak detector states (one per channel) - limiter_peaks: Vec, + limiter_peaks: Box<[Float]>, /// Current channel position (0 to channels-1) position: usize, } @@ -908,8 +950,8 @@ where /// updates. #[inline] fn process_next(&mut self, sample: I::Item) -> I::Item { - let channel = self.position as usize; - self.position ^= 1; + let channel = self.is_right_channel as usize; + self.is_right_channel = !self.is_right_channel; let processed = self.base.process_channel( sample, @@ -1131,7 +1173,7 @@ mod tests { use std::time::Duration; fn create_test_buffer( - samples: Vec, + samples: &[Sample], channels: ChannelCount, sample_rate: SampleRate, ) -> SamplesBuffer { @@ -1141,26 +1183,35 @@ mod tests { #[test] fn test_limiter_creation() { // Test mono - let buffer = create_test_buffer(vec![0.5, 0.8, 1.0, 0.3], nz!(1), nz!(44100)); + let buffer = create_test_buffer(&[0.5, 0.8, 1.0, 0.3], nz!(1), nz!(44100)); let limiter = limit(buffer, LimitSettings::default()); assert_eq!(limiter.channels(), nz!(1)); assert_eq!(limiter.sample_rate(), nz!(44100)); - matches!(limiter.0, LimitInner::Mono(_)); + assert!(matches!( + limiter.inner.as_ref().unwrap(), + LimitInner::Mono(_) + )); // Test stereo let buffer = create_test_buffer( - vec![0.5, 0.8, 1.0, 0.3, 0.2, 0.6, 0.9, 0.4], + &[0.5, 0.8, 1.0, 0.3, 0.2, 0.6, 0.9, 0.4], nz!(2), nz!(44100), ); let limiter = limit(buffer, LimitSettings::default()); assert_eq!(limiter.channels(), nz!(2)); - matches!(limiter.0, LimitInner::Stereo(_)); + assert!(matches!( + limiter.inner.as_ref().unwrap(), + LimitInner::Stereo(_) + )); // Test multichannel - let buffer = create_test_buffer(vec![0.5; 12], nz!(3), nz!(44100)); + let buffer = create_test_buffer(&[0.5; 12], nz!(3), nz!(44100)); let limiter = limit(buffer, LimitSettings::default()); assert_eq!(limiter.channels(), nz!(3)); - matches!(limiter.0, LimitInner::MultiChannel(_)); + assert!(matches!( + limiter.inner.as_ref().unwrap(), + LimitInner::MultiChannel(_) + )); } } diff --git a/src/source/mod.rs b/src/source/mod.rs index 5cc890b1..3309ffd4 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -164,17 +164,21 @@ pub use self::noise::{Pink, WhiteUniform}; /// `sample_rate` too frequently. /// /// In order to properly handle this situation, the `current_span_len()` method should return -/// the number of samples that remain in the iterator before the samples rate and number of -/// channels can potentially change. +/// the total number of samples in the current span (i.e., before the sample rate and number of +/// channels can potentially change). /// pub trait Source: Iterator { - /// Returns the number of samples before the current span ends. + /// Returns the total length of the current span in samples. + /// + /// A span is a contiguous block of samples with unchanging channel count and sample rate. + /// This method returns the total number of samples in the current span, not the number + /// of samples remaining to be read. /// /// `None` means "infinite" or "until the sound ends". Sources that return `Some(x)` should /// return `Some(0)` if and only if when there's no more data. /// - /// After the engine has finished reading the specified number of samples, it will check - /// whether the value of `channels()` and/or `sample_rate()` have changed. + /// After the engine has finished reading the number of samples returned by this method, + /// it will check whether the value of `channels()` and/or `sample_rate()` have changed. /// /// # Frame Alignment /// @@ -397,15 +401,15 @@ pub trait Source: Iterator { /// ```rust /// // Apply Automatic Gain Control to the source (AGC is on by default) /// use rodio::source::{Source, SineWave, AutomaticGainControlSettings}; - /// use rodio::Sink; + /// use rodio::Player; /// use std::time::Duration; /// let source = SineWave::new(444.0); // An example. - /// let (sink, output) = Sink::new(); // An example. + /// let (player, output) = Player::new(); // An example. /// /// let agc_source = source.automatic_gain_control(AutomaticGainControlSettings::default()); /// /// // Add the AGC-controlled source to the sink - /// sink.append(agc_source); + /// player.append(agc_source); /// /// ``` #[inline] @@ -642,7 +646,7 @@ pub trait Source: Iterator { } /// Adds a method [`Skippable::skip`] for skipping this source. Skipping - /// makes Source::next() return None. Which in turn makes the Sink skip to + /// makes Source::next() return None. Which in turn makes the Player skip to /// the next source. fn skippable(self) -> Skippable where diff --git a/src/source/noise.rs b/src/source/noise.rs index 32cea2d9..b2712549 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -166,6 +166,11 @@ impl Iterator for WhiteUniform { fn next(&mut self) -> Option { Some(self.sampler.sample()) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl_noise_source!(WhiteUniform); @@ -221,6 +226,11 @@ impl Iterator for WhiteTriangular { fn next(&mut self) -> Option { Some(self.sampler.sample()) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl_noise_source!(WhiteTriangular); @@ -319,6 +329,11 @@ impl Iterator for Velvet { Some(output) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl_noise_source!(Velvet); @@ -393,6 +408,11 @@ impl Iterator for WhiteGaussian { fn next(&mut self) -> Option { Some(self.sampler.sample()) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl_noise_source!(WhiteGaussian); @@ -492,6 +512,11 @@ impl Iterator for Pink { // Normalize by number of generators to keep output in reasonable range Some(sum / PINK_NOISE_GENERATORS as Sample) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.white_noise.size_hint() + } } impl_noise_source!(Pink); @@ -555,6 +580,11 @@ impl Iterator for Blue { self.prev_white = white; Some(blue) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.white_noise.size_hint() + } } impl_noise_source!(Blue); @@ -618,6 +648,11 @@ impl Iterator for Violet { self.prev = blue; Some(violet) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.blue_noise.size_hint() + } } impl_noise_source!(Violet); @@ -673,6 +708,11 @@ impl> Iterator for IntegratedNoise { self.accumulator = self.accumulator * self.leak_factor + white; Some(self.accumulator * self.scale) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.white_noise.size_hint() + } } /// Brownian noise generator - true stochastic Brownian motion with Gaussian increments. @@ -724,6 +764,11 @@ impl Iterator for Brownian { fn next(&mut self) -> Option { self.inner.next() } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } } impl Source for Brownian { @@ -746,6 +791,7 @@ impl Source for Brownian { fn try_seek(&mut self, _pos: Duration) -> Result<(), crate::source::SeekError> { // Stateless noise generators can seek to any position since all positions // are equally random and don't depend on previous state + self.inner.accumulator = 0.0; Ok(()) } } @@ -801,6 +847,11 @@ impl Iterator for Red { fn next(&mut self) -> Option { self.inner.next() } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } } impl Source for Red { @@ -823,6 +874,7 @@ impl Source for Red { fn try_seek(&mut self, _pos: Duration) -> Result<(), crate::source::SeekError> { // Stateless noise generators can seek to any position since all positions // are equally random and don't depend on previous state + self.inner.accumulator = 0.0; Ok(()) } } diff --git a/src/source/pausable.rs b/src/source/pausable.rs index 5143b622..1b05ab0d 100644 --- a/src/source/pausable.rs +++ b/src/source/pausable.rs @@ -98,10 +98,14 @@ where #[inline] fn size_hint(&self) -> (usize, Option) { - self.input.size_hint() + let (lower, upper) = self.input.size_hint(); + let paused_samples = self.remaining_paused_samples as usize; + (lower + paused_samples, upper.map(|u| u + paused_samples)) } } +impl ExactSizeIterator for Pausable where I: Source + ExactSizeIterator {} + impl Source for Pausable where I: Source, diff --git a/src/source/periodic.rs b/src/source/periodic.rs index 09fc5477..b251ea31 100644 --- a/src/source/periodic.rs +++ b/src/source/periodic.rs @@ -14,7 +14,7 @@ where // TODO: handle the fact that the samples rate can change let update_frequency = (period.as_secs_f32() * (source.sample_rate().get() as f32) - * (source.channels().get() as f32)) as u32; + * (source.channels().get() as f32)) as usize; PeriodicAccess { input: source, @@ -35,10 +35,10 @@ pub struct PeriodicAccess { modifier: F, // The frequency with which local_volume should be updated by remote_volume - update_frequency: u32, + update_frequency: usize, // How many samples remain until it is time to update local_volume with remote_volume. - samples_until_update: u32, + samples_until_update: usize, } impl PeriodicAccess @@ -91,6 +91,13 @@ where } } +impl ExactSizeIterator for PeriodicAccess +where + I: Source + ExactSizeIterator, + F: FnMut(&mut I), +{ +} + impl Source for PeriodicAccess where I: Source, diff --git a/src/source/position.rs b/src/source/position.rs index 1788625b..8bd1a716 100644 --- a/src/source/position.rs +++ b/src/source/position.rs @@ -1,19 +1,24 @@ use std::time::Duration; use super::SeekError; -use crate::common::{ChannelCount, SampleRate}; -use crate::math::nz; +use crate::common::{ChannelCount, Float, SampleRate}; +use crate::math::{duration_from_secs, duration_to_float}; use crate::Source; /// Internal function that builds a `TrackPosition` object. See trait docs for /// details -pub fn track_position(source: I) -> TrackPosition { +pub fn track_position(source: I) -> TrackPosition +where + I: Source, +{ + let channels = source.channels(); + let sample_rate = source.sample_rate(); TrackPosition { input: source, samples_counted: 0, offset_duration: 0.0, - current_span_sample_rate: nz!(1), - current_span_channels: nz!(1), + current_span_sample_rate: sample_rate, + current_span_channels: channels, current_span_len: None, } } @@ -23,7 +28,7 @@ pub fn track_position(source: I) -> TrackPosition { pub struct TrackPosition { input: I, samples_counted: usize, - offset_duration: f64, + offset_duration: Float, current_span_sample_rate: SampleRate, current_span_channels: ChannelCount, current_span_len: Option, @@ -65,15 +70,16 @@ where /// track_position after speedup's and delay's. #[inline] pub fn get_pos(&self) -> Duration { - let seconds = self.samples_counted as f64 - / self.input.sample_rate().get() as f64 - / self.input.channels().get() as f64 + let seconds = self.samples_counted as Float + / self.input.sample_rate().get() as Float + / self.input.channels().get() as Float + self.offset_duration; - Duration::from_secs_f64(seconds) + duration_from_secs(seconds) } #[inline] - fn set_current_span(&mut self) { + fn reset_current_span(&mut self) { + self.samples_counted = 0; self.current_span_len = self.current_span_len(); self.current_span_sample_rate = self.sample_rate(); self.current_span_channels = self.channels(); @@ -88,28 +94,34 @@ where #[inline] fn next(&mut self) -> Option { - // This should only be executed once at the first call to next. if self.current_span_len.is_none() { - self.set_current_span(); + self.current_span_len = self.input.current_span_len(); } - let item = self.input.next(); - if item.is_some() { - self.samples_counted += 1; - - // At the end of a span add the duration of this span to - // offset_duration and start collecting samples again. - if Some(self.samples_counted) == self.current_span_len() { - self.offset_duration += self.samples_counted as f64 - / self.current_span_sample_rate.get() as f64 - / self.current_span_channels.get() as f64; - - // Reset. - self.samples_counted = 0; - self.set_current_span(); - }; - }; - item + let item = self.input.next()?; + self.samples_counted += 1; + + // Detect span boundaries by TWO mechanisms: + // 1. Reached end of span (by length) - handles same-parameter consecutive spans + // 2. Parameters changed - handles mid-stream parameter changes + let new_channels = self.input.channels(); + let new_sample_rate = self.input.sample_rate(); + + let length_boundary = self + .current_span_len + .is_some_and(|len| self.samples_counted >= len); + let parameter_boundary = new_channels != self.current_span_channels + || new_sample_rate != self.current_span_sample_rate; + + if length_boundary || parameter_boundary { + // At span boundary - accumulate duration using OLD parameters + self.offset_duration += self.samples_counted as Float + / self.current_span_sample_rate.get() as Float + / self.current_span_channels.get() as Float; + self.reset_current_span(); + } + + Some(item) } #[inline] @@ -118,6 +130,8 @@ where } } +impl ExactSizeIterator for TrackPosition where I: Source + ExactSizeIterator {} + impl Source for TrackPosition where I: Source, @@ -144,15 +158,15 @@ where #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - let result = self.input.try_seek(pos); - if result.is_ok() { - self.offset_duration = pos.as_secs_f64(); - // This assumes that the seek implementation of the codec always - // starts again at the beginning of a span. Which is the case with - // symphonia. - self.samples_counted = 0; - } - result + self.input.try_seek(pos)?; + self.offset_duration = duration_to_float(pos); + // Set current_span_len to None after seeking because we may have landed mid-span. + // We don't know how many samples remain in the current span, so we disable + // length-based boundary detection until the next parameter change, which will + // put us at the start of a fresh span where we can re-enable it. + self.reset_current_span(); + self.current_span_len = None; + Ok(()) } } diff --git a/src/source/repeat.rs b/src/source/repeat.rs index ef29968e..601b679c 100644 --- a/src/source/repeat.rs +++ b/src/source/repeat.rs @@ -46,7 +46,7 @@ where #[inline] fn size_hint(&self) -> (usize, Option) { // infinite - (0, None) + (usize::MAX, None) } } diff --git a/src/source/sawtooth.rs b/src/source/sawtooth.rs index 2bfc2309..e5bc97f1 100644 --- a/src/source/sawtooth.rs +++ b/src/source/sawtooth.rs @@ -36,6 +36,11 @@ impl Iterator for SawtoothWave { fn next(&mut self) -> Option { self.test_saw.next() } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl Source for SawtoothWave { diff --git a/src/source/signal_generator.rs b/src/source/signal_generator.rs index 34ffa443..58b29941 100644 --- a/src/source/signal_generator.rs +++ b/src/source/signal_generator.rs @@ -133,6 +133,11 @@ impl Iterator for SignalGenerator { self.phase = (self.phase + self.phase_step).rem_euclid(1.0); val } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl Source for SignalGenerator { diff --git a/src/source/sine.rs b/src/source/sine.rs index 1481d6b2..17c97008 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -36,6 +36,11 @@ impl Iterator for SineWave { fn next(&mut self) -> Option { self.test_sine.next() } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl Source for SineWave { diff --git a/src/source/skip.rs b/src/source/skip.rs index dfae5b92..8d80bf8e 100644 --- a/src/source/skip.rs +++ b/src/source/skip.rs @@ -22,7 +22,7 @@ fn do_skip_duration(input: &mut I, mut duration: Duration) where I: Source, { - while duration > Duration::new(0, 0) { + while duration > Duration::ZERO { if input.current_span_len().is_none() { // Sample rate and the amount of channels will be the same till the end. do_skip_duration_unchecked(input, duration); @@ -129,6 +129,8 @@ where } } +impl ExactSizeIterator for SkipDuration where I: Source + ExactSizeIterator {} + impl Source for SkipDuration where I: Source, @@ -150,10 +152,9 @@ where #[inline] fn total_duration(&self) -> Option { - self.input.total_duration().map(|val| { - val.checked_sub(self.skipped_duration) - .unwrap_or_else(|| Duration::from_secs(0)) - }) + self.input + .total_duration() + .map(|val| val.saturating_sub(self.skipped_duration)) } #[inline] diff --git a/src/source/skippable.rs b/src/source/skippable.rs index ba4d43d9..24fa02bb 100644 --- a/src/source/skippable.rs +++ b/src/source/skippable.rs @@ -5,7 +5,7 @@ use std::time::Duration; use super::SeekError; /// Wrap the source in a skippable. It allows ending the current source early by -/// calling [`Skippable::skip`]. If this source is in a queue such as the Sink +/// calling [`Skippable::skip`]. If this source is in a queue such as the Player /// ending the source early is equal to skipping the source. pub fn skippable(source: I) -> Skippable { Skippable { @@ -15,7 +15,7 @@ pub fn skippable(source: I) -> Skippable { } /// Wrap the source in a skippable. It allows ending the current source early by -/// calling [`Skippable::skip`]. If this source is in a queue such as the Sink +/// calling [`Skippable::skip`]. If this source is in a queue such as the Player /// ending the source early is equal to skipping the source. #[derive(Clone, Debug)] pub struct Skippable { @@ -70,6 +70,8 @@ where } } +impl ExactSizeIterator for Skippable where I: Source + ExactSizeIterator {} + impl Source for Skippable where I: Source, diff --git a/src/source/spatial.rs b/src/source/spatial.rs index 8aad0b7e..136dad9d 100644 --- a/src/source/spatial.rs +++ b/src/source/spatial.rs @@ -27,7 +27,7 @@ impl Spatial where I: Source, { - /// Builds a new `SpatialSink`, beginning playback on a stream. + /// Builds a new `SpatialPlayer`, beginning playback on a stream. pub fn new( input: I, emitter_position: [f32; 3], diff --git a/src/source/speed.rs b/src/source/speed.rs index e38c2b99..5d599a13 100644 --- a/src/source/speed.rs +++ b/src/source/speed.rs @@ -1,7 +1,7 @@ //! Playback Speed control Module. //! //! The main concept of this module is the [`Speed`] struct, which -//! encapsulates playback speed controls of the current sink. +//! encapsulates playback speed controls of the current player. //! //! In order to speed up a sink, the speed struct: //! - Increases the current sample rate by the given factor. @@ -14,18 +14,18 @@ #![cfg_attr(not(feature = "playback"), doc = "```ignore")] #![cfg_attr(feature = "playback", doc = "```no_run")] //!# use std::fs::File; -//!# use rodio::{Decoder, Sink, OutputStream, source::{Source, SineWave}}; +//!# use rodio::{Decoder, Player, source::{Source, SineWave}}; //! -//! // Get an output stream handle to the default physical sound device. -//! // 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"); +//! // Get an OS-Sink handle to the default physical sound device. +//! // Note that no sound will be played if the _handle_ is dropped. +//! let handle = rodio::DeviceSinkBuilder::open_default_sink() +//! .expect("open default audio sink"); //! // Load a sound from a file, using a path relative to `Cargo.toml` //! let file = File::open("examples/music.ogg").unwrap(); //! // Decode that sound file into a source //! let source = Decoder::try_from(file).unwrap(); //! // Play the sound directly on the device 2x faster -//! stream_handle.mixer().add(source.speed(2.0)); +//! handle.mixer().add(source.speed(2.0)); //! std::thread::sleep(std::time::Duration::from_secs(5)); //! ``` //! Here is how you would do it using the sink: @@ -35,11 +35,11 @@ //! let source = SineWave::new(440.0) //! .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); +//! let handle = rodio::DeviceSinkBuilder::open_default_sink() +//! .expect("open default audio sink"); +//! let player = rodio::Player::connect_new(&handle.mixer()); +//! player.set_speed(2.0); +//! player.append(source); //! std::thread::sleep(std::time::Duration::from_secs(5)); //! ``` //! Notice the increase in pitch as the factor increases diff --git a/src/source/square.rs b/src/source/square.rs index d4acb321..ebb16e82 100644 --- a/src/source/square.rs +++ b/src/source/square.rs @@ -36,6 +36,11 @@ impl Iterator for SquareWave { fn next(&mut self) -> Option { self.test_square.next() } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl Source for SquareWave { diff --git a/src/source/stoppable.rs b/src/source/stoppable.rs index 57cc7dae..44f21940 100644 --- a/src/source/stoppable.rs +++ b/src/source/stoppable.rs @@ -66,6 +66,8 @@ where } } +impl ExactSizeIterator for Stoppable where I: Source + ExactSizeIterator {} + impl Source for Stoppable where I: Source, diff --git a/src/source/take.rs b/src/source/take.rs index d9957e6f..bdaaa15a 100644 --- a/src/source/take.rs +++ b/src/source/take.rs @@ -10,13 +10,16 @@ pub fn take_duration(input: I, duration: Duration) -> TakeDuration where I: Source, { + let sample_rate = input.sample_rate(); + let channels = input.channels(); TakeDuration { - current_span_len: input.current_span_len(), duration_per_sample: TakeDuration::get_duration_per_sample(&input), input, remaining_duration: duration, requested_duration: duration, filter: None, + last_sample_rate: sample_rate, + last_channels: channels, } } @@ -44,10 +47,10 @@ pub struct TakeDuration { remaining_duration: Duration, requested_duration: Duration, filter: Option, - // Remaining samples in current span. - current_span_len: Option, - // Only updated when the current span len is exhausted. + // Cached duration per sample, updated when sample rate or channels change. duration_per_sample: Duration, + last_sample_rate: SampleRate, + last_channels: ChannelCount, } impl TakeDuration @@ -99,17 +102,18 @@ where type Item = ::Item; fn next(&mut self) -> Option<::Item> { - if let Some(span_len) = self.current_span_len.take() { - if span_len > 0 { - self.current_span_len = Some(span_len - 1); - } else { - self.current_span_len = self.input.current_span_len(); - // Sample rate might have changed + // Check if sample rate or channels changed (only if span is finite and not exhausted) + if self.input.current_span_len().is_some_and(|len| len > 0) { + let new_sample_rate = self.input.sample_rate(); + let new_channels = self.input.channels(); + if new_sample_rate != self.last_sample_rate || new_channels != self.last_channels { + self.last_sample_rate = new_sample_rate; + self.last_channels = new_channels; self.duration_per_sample = Self::get_duration_per_sample(&self.input); } } - if self.remaining_duration <= self.duration_per_sample { + if self.remaining_duration < self.duration_per_sample { None } else if let Some(sample) = self.input.next() { let sample = match &self.filter { @@ -125,9 +129,31 @@ where } } - // TODO: size_hint + #[inline] + fn size_hint(&self) -> (usize, Option) { + let remaining_nanos = self.remaining_duration.as_secs() * 1_000_000_000 + + self.remaining_duration.subsec_nanos() as u64; + let nanos_per_sample = self.duration_per_sample.as_secs() * 1_000_000_000 + + self.duration_per_sample.subsec_nanos() as u64; + + if nanos_per_sample == 0 || remaining_nanos == 0 { + return (0, Some(0)); + } + + let remaining_samples = (remaining_nanos / nanos_per_sample) as usize; + + let (inner_lower, inner_upper) = self.input.size_hint(); + let lower = inner_lower.min(remaining_samples); + let upper = inner_upper + .map(|u| u.min(remaining_samples)) + .or(Some(remaining_samples)); + + (lower, upper) + } } +impl ExactSizeIterator for TakeDuration where I: Source + ExactSizeIterator {} + impl Source for TakeDuration where I: Iterator + Source, @@ -138,6 +164,11 @@ where + self.remaining_duration.subsec_nanos() as u64; let nanos_per_sample = self.duration_per_sample.as_secs() * NANOS_PER_SEC + self.duration_per_sample.subsec_nanos() as u64; + + if nanos_per_sample == 0 || remaining_nanos == 0 { + return Some(0); + } + let remaining_samples = (remaining_nanos / nanos_per_sample) as usize; self.input @@ -171,6 +202,40 @@ where #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - self.input.try_seek(pos) + let result = self.input.try_seek(pos); + if result.is_ok() { + // Recalculate remaining duration after seek + self.remaining_duration = self.requested_duration.saturating_sub(pos); + // Don't update last_sample_rate or last_channels here - let next() detect the change + } + result + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::source::SineWave; + + #[test] + fn test_size_hint_with_zero_remaining() { + let source = SineWave::new(440.0).take_duration(Duration::ZERO); + assert_eq!(source.size_hint(), (0, Some(0))); + } + + #[test] + fn test_exact_duration_boundary() { + use crate::source::SineWave; + + let sample_rate = 48000; + let nanos_per_sample = (1_000_000_000 as Float / sample_rate as Float) as usize; + + let n_samples = 10; + let exact_duration = Duration::from_nanos((nanos_per_sample * n_samples) as u64); + + let source = SineWave::new(440.0).take_duration(exact_duration); + + let count = source.count(); + assert_eq!(count, n_samples); } } diff --git a/src/source/triangle.rs b/src/source/triangle.rs index 6cafc911..86a0c970 100644 --- a/src/source/triangle.rs +++ b/src/source/triangle.rs @@ -36,6 +36,11 @@ impl Iterator for TriangleWave { fn next(&mut self) -> Option { self.test_tri.next() } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl Source for TriangleWave { diff --git a/src/spatial_sink.rs b/src/spatial_player.rs similarity index 86% rename from src/spatial_sink.rs rename to src/spatial_player.rs index 8a0719c9..d1fe3cb2 100644 --- a/src/spatial_sink.rs +++ b/src/spatial_player.rs @@ -6,13 +6,13 @@ use dasp_sample::FromSample; use crate::mixer::Mixer; use crate::source::{SeekError, Spatial}; -use crate::{Float, Sink, Source}; +use crate::{Float, Player, Source}; /// A sink that allows changing the position of the source and the listeners /// ears while playing. The sources played are then transformed to give a simple /// spatial effect. See [`Spatial`] for details. -pub struct SpatialSink { - sink: Sink, +pub struct SpatialPlayer { + player: Player, positions: Arc>, } @@ -22,16 +22,16 @@ struct SoundPositions { right_ear: [f32; 3], } -impl SpatialSink { - /// Builds a new `SpatialSink`. +impl SpatialPlayer { + /// Builds a new `SpatialPlayer`. pub fn connect_new( mixer: &Mixer, emitter_position: [f32; 3], left_ear: [f32; 3], right_ear: [f32; 3], - ) -> SpatialSink { - SpatialSink { - sink: Sink::connect_new(mixer), + ) -> SpatialPlayer { + SpatialPlayer { + player: Player::connect_new(mixer), positions: Arc::new(Mutex::new(SoundPositions { emitter_position, left_ear, @@ -74,7 +74,7 @@ impl SpatialSink { let pos = positions.lock().unwrap(); i.set_positions(pos.emitter_position, pos.left_ear, pos.right_ear); }); - self.sink.append(source); + self.player.append(source); } // Gets the volume of the sound. @@ -83,7 +83,7 @@ impl SpatialSink { /// multiply each sample by this value. #[inline] pub fn volume(&self) -> Float { - self.sink.volume() + self.player.volume() } /// Changes the volume of the sound. @@ -92,7 +92,7 @@ impl SpatialSink { /// multiply each sample by this value. #[inline] pub fn set_volume(&self, value: Float) { - self.sink.set_volume(value); + self.player.set_volume(value); } /// Changes the play speed of the sound. Does not adjust the samples, only the playback speed. @@ -111,7 +111,7 @@ impl SpatialSink { /// See [`Speed`](crate::source::Speed) for details #[inline] pub fn speed(&self) -> f32 { - self.sink.speed() + self.player.speed() } /// Changes the speed of the sound. @@ -120,7 +120,7 @@ impl SpatialSink { /// change the play speed of the sound. #[inline] pub fn set_speed(&self, value: f32) { - self.sink.set_speed(value) + self.player.set_speed(value) } /// Resumes playback of a paused sound. @@ -128,62 +128,62 @@ impl SpatialSink { /// No effect if not paused. #[inline] pub fn play(&self) { - self.sink.play(); + self.player.play(); } - /// Pauses playback of this sink. + /// Pauses playback of this player. /// /// No effect if already paused. /// /// A paused sound can be resumed with `play()`. pub fn pause(&self) { - self.sink.pause(); + self.player.pause(); } /// Gets if a sound is paused /// /// Sounds can be paused and resumed using pause() and play(). This gets if a sound is paused. pub fn is_paused(&self) -> bool { - self.sink.is_paused() + self.player.is_paused() } - /// Removes all currently loaded `Source`s from the `SpatialSink` and pauses it. + /// Removes all currently loaded `Source`s from the `SpatialPlayer` and pauses it. /// - /// See `pause()` for information about pausing a `Sink`. + /// See `pause()` for information about pausing a `Player`. #[inline] pub fn clear(&self) { - self.sink.clear(); + self.player.clear(); } /// Stops the sink by emptying the queue. #[inline] pub fn stop(&self) { - self.sink.stop() + self.player.stop() } /// Destroys the sink without stopping the sounds that are still playing. #[inline] pub fn detach(self) { - self.sink.detach(); + self.player.detach(); } /// Sleeps the current thread until the sound ends. #[inline] pub fn sleep_until_end(&self) { - self.sink.sleep_until_end(); + self.player.sleep_until_end(); } /// Returns true if this sink has no more sounds to play. #[inline] pub fn empty(&self) -> bool { - self.sink.empty() + self.player.empty() } /// Returns the number of sounds currently in the queue. #[allow(clippy::len_without_is_empty)] #[inline] pub fn len(&self) -> usize { - self.sink.len() + self.player.len() } /// Attempts to seek to a given position in the current source. @@ -205,7 +205,7 @@ impl SpatialSink { /// When seeking beyond the end of a source this /// function might return an error if the duration of the source is not known. pub fn try_seek(&self, pos: Duration) -> Result<(), SeekError> { - self.sink.try_seek(pos) + self.player.try_seek(pos) } /// Returns the position of the sound that's being played. @@ -213,10 +213,10 @@ impl SpatialSink { /// This takes into account any speedup or delay applied. /// /// Example: if you apply a speedup of *2* to an mp3 decoder source and - /// [`get_pos()`](Sink::get_pos) returns *5s* then the position in the mp3 + /// [`get_pos()`](Player::get_pos) returns *5s* then the position in the mp3 /// recording is *10s* from its start. #[inline] pub fn get_pos(&self) -> Duration { - self.sink.get_pos() + self.player.get_pos() } } diff --git a/src/speakers.rs b/src/speakers.rs index 6feb70e5..9b1d8d0a 100644 --- a/src/speakers.rs +++ b/src/speakers.rs @@ -1,6 +1,6 @@ //! A speakers sink //! -//! An audio *stream* originates at a [Source] and flows to a Sink. This is a +//! An audio *stream* originates at a [Source] and flows to a player. This is a //! Sink that plays audio over the systems speakers or headphones through an //! audio output device; //! @@ -13,7 +13,7 @@ //! let speakers = SpeakersBuilder::new() //! .default_device()? //! .default_config()? -//! .open_stream()?; +//! .open_mixer()?; //! let mixer = speakers.mixer(); //! //! // Play a beep for 4 seconds @@ -46,7 +46,7 @@ //! ]) //! .prefer_buffer_sizes(512..); //! -//! let mic = builder.open_stream()?; +//! let mixer = builder.open_mixer()?; //! # Ok(()) //! # } //! ``` @@ -73,7 +73,7 @@ //! // builder remains unchanged with default configuration //! } //! -//! let mic = builder.open_stream()?; +//! let speakers = builder.open_mixer()?; //! # Ok(()) //! # } //! ``` @@ -91,22 +91,19 @@ //! } //! //! // Use a specific device (e.g., the second one) -//! let mic = SpeakersBuilder::new() +//! let speakers = SpeakersBuilder::new() //! .device(outputs[1].clone())? //! .default_config()? -//! .open_stream()?; +//! .open_mixer()?; //! # Ok(()) //! # } //! ``` use core::fmt; -use cpal::{ - traits::{DeviceTrait, HostTrait}, - Device, -}; +use cpal::traits::{DeviceTrait, HostTrait}; -use crate::{common::assert_error_traits, StreamError}; +use crate::common::assert_error_traits; mod builder; mod config; @@ -114,8 +111,6 @@ mod config; pub use builder::SpeakersBuilder; pub use config::OutputConfig; -struct Speakers; - /// Error that can occur when we can not list the output devices #[derive(Debug, thiserror::Error, Clone)] #[error("Could not list output devices")] @@ -130,7 +125,7 @@ pub struct Output { } impl Output { - /// TODO doc comment also mirror to microphone api + /// Whether this output is the default sound output for the OS pub fn is_default(&self) -> bool { self.default } @@ -176,13 +171,3 @@ pub fn available_outputs() -> Result, ListError> { }); Ok(devices.collect()) } - -impl Speakers { - fn open( - device: Device, - config: OutputConfig, - error_callback: impl FnMut(cpal::StreamError) + Send + 'static, - ) -> Result { - crate::stream::OutputStream::open(&device, &config.into_cpal_config(), error_callback) - } -} diff --git a/src/speakers/builder.rs b/src/speakers/builder.rs index 40928e48..2daee923 100644 --- a/src/speakers/builder.rs +++ b/src/speakers/builder.rs @@ -1,14 +1,13 @@ use std::{fmt::Debug, marker::PhantomData}; use cpal::{ - traits::{DeviceTrait, HostTrait}, + traits::{DeviceTrait, HostTrait, StreamTrait}, SupportedStreamConfigRange, }; use crate::{ - common::assert_error_traits, - speakers::{self, config::OutputConfig}, - ChannelCount, OutputStream, SampleRate, + common::assert_error_traits, speakers::config::OutputConfig, ChannelCount, DeviceSinkError, + FixedSource, MixerDeviceSink, SampleRate, }; /// Error configuring or opening speakers output @@ -54,7 +53,7 @@ pub struct ConfigNotSet; /// Some methods are only available when this types counterpart: `DeviceIsSet` is present. pub struct DeviceNotSet; -/// Builder for configuring and opening speakers output streams. +/// Builder for configuring and opening an OS-Sink, usually a speaker or headphone. #[must_use] pub struct SpeakersBuilder where @@ -528,7 +527,7 @@ impl SpeakersBuilder where E: FnMut(cpal::StreamError) + Send + Clone + 'static, { - /// Opens the speakers output stream. + /// Opens the OS-Sink and provide a mixer for playing sources on it. /// /// # Example /// ```no_run @@ -538,18 +537,120 @@ where /// let speakers = SpeakersBuilder::new() /// .default_device()? /// .default_config()? - /// .open_stream()?; + /// .open_mixer()?; /// let mixer = speakers.mixer(); /// mixer.add(SineWave::new(440.).take_duration(Duration::from_secs(4))); /// std::thread::sleep(Duration::from_secs(4)); /// /// # Ok::<(), Box>(()) /// ``` - pub fn open_stream(&self) -> Result { - speakers::Speakers::open( - self.device.as_ref().expect("DeviceIsSet").0.clone(), - *self.config.as_ref().expect("ConfigIsSet"), - self.error_callback.clone(), - ) + pub fn open_mixer(&self) -> Result { + let device = self.device.as_ref().expect("DeviceIsSet").0.clone(); + let config = *self.config.as_ref().expect("ConfigIsSet"); + let error_callback = self.error_callback.clone(); + crate::stream::MixerDeviceSink::open(&device, &config.into_cpal_config(), error_callback) } + + // TODO + // pub fn open_queue() -> Result { + // todo!() + // } + + /// Open the device with the current configuration and play a single + /// `FixedSource` on it. + pub fn play( + self, + mut source: impl FixedSource + Send + 'static, + ) -> Result { + use cpal::Sample as _; + + let config = self.config.expect("ConfigIsSet"); + let device = self.device.expect("DeviceIsSet").0; + + if config.channel_count != source.channels() { + return Err(PlayError::WrongChannelCount { + sink: config.channel_count, + fixed_source: source.channels(), + }); + } + if config.sample_rate != source.sample_rate() { + return Err(PlayError::WrongSampleRate { + sink: config.sample_rate, + fixed_source: source.sample_rate(), + }); + } + + let cpal_config1 = config.into_cpal_config(); + let cpal_config2 = (&cpal_config1).into(); + + macro_rules! build_output_streams { + ($($sample_format:tt, $generic:ty);+) => { + match config.sample_format { + $( + cpal::SampleFormat::$sample_format => device.build_output_stream::<$generic, _, _>( + &cpal_config2, + move |data, _| { + data.iter_mut().for_each(|d| { + *d = source + .next() + .map(cpal::Sample::from_sample) + .unwrap_or(<$generic>::EQUILIBRIUM) + }) + }, + self.error_callback, + None, + ), + )+ + _ => return Err(DeviceSinkError::UnsupportedSampleFormat.into()), + } + }; + } + + let result = build_output_streams!( + F32, f32; + F64, f64; + I8, i8; + I16, i16; + I24, cpal::I24; + I32, i32; + I64, i64; + U8, u8; + U16, u16; + U24, cpal::U24; + U32, u32; + U64, u64 + ); + + let stream = result.map_err(DeviceSinkError::BuildError)?; + stream.play().map_err(DeviceSinkError::PlayError)?; + + Ok(SinkHandle { _stream: stream }) + } +} + +// TODO cant introduce till we have introduced the other fixed source parts +// pub struct QueueSink; + +/// A sink handle. When this is dropped anything playing through this Sink will +/// stop playing. +pub struct SinkHandle { + _stream: cpal::Stream, +} + +#[derive(Debug, thiserror::Error)] +pub enum PlayError { + #[error("DeviceSink channel count ({sink}) does not match the source channel count ({fixed_source})")] + WrongChannelCount { + sink: ChannelCount, + fixed_source: ChannelCount, + }, + #[error( + "DeviceSink sample rate ({sink}) does not match the source sample rate ({fixed_source})" + )] + WrongSampleRate { + sink: SampleRate, + fixed_source: SampleRate, + }, + #[error(transparent)] + DeviceSink(#[from] crate::DeviceSinkError), } diff --git a/src/speakers/config.rs b/src/speakers/config.rs index 0b96476b..bd691643 100644 --- a/src/speakers/config.rs +++ b/src/speakers/config.rs @@ -1,6 +1,6 @@ use std::num::NonZero; -use crate::{math::nz, stream::OutputStreamConfig, ChannelCount, SampleRate}; +use crate::{math::nz, stream::DeviceSinkConfig, ChannelCount, SampleRate}; /// Describes the output stream's configuration #[derive(Copy, Clone, Debug)] @@ -44,8 +44,8 @@ impl OutputConfig { this } - pub(crate) fn into_cpal_config(self) -> crate::stream::OutputStreamConfig { - OutputStreamConfig { + pub(crate) fn into_cpal_config(self) -> crate::stream::DeviceSinkConfig { + DeviceSinkConfig { channel_count: self.channel_count, sample_rate: self.sample_rate, buffer_size: self.buffer_size, diff --git a/src/static_buffer.rs b/src/static_buffer.rs index 44ff5998..d5b77ae5 100644 --- a/src/static_buffer.rs +++ b/src/static_buffer.rs @@ -114,6 +114,8 @@ impl Iterator for StaticSamplesBuffer { } } +impl ExactSizeIterator for StaticSamplesBuffer {} + #[cfg(test)] mod tests { use crate::math::nz; diff --git a/src/stream.rs b/src/stream.rs index eaa81176..3b6a736e 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,14 +1,14 @@ //! Output audio via the OS via mixers or play directly //! //! This module provides a builder that's used to configure and open audio output. Once -//! opened sources can be mixed into the output via `OutputStream::mixer`. +//! opened sources can be mixed into the output via `DeviceSink::mixer`. //! //! There is also a convenience function `play` for using that output mixer to //! play a single sound. use crate::common::{assert_error_traits, ChannelCount, SampleRate}; use crate::math::nz; use crate::mixer::{mixer, Mixer}; -use crate::sink::Sink; +use crate::player::Player; use crate::{decoder, Source}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::{BufferSize, Sample, SampleFormat, StreamConfig, I24}; @@ -22,80 +22,80 @@ const HZ_44100: SampleRate = nz!(44_100); /// `cpal::Stream` container. Use `mixer()` method to control output. /// ///
When dropped playback will end, and the associated -/// output stream will be disposed
+/// OS-Sink will be disposed /// /// # Note /// On drop this will print a message to stderr or emit a log msg when tracing is /// enabled. Though we recommend you do not you can disable that print/log with: -/// [`OutputStream::log_on_drop(false)`](OutputStream::log_on_drop). -/// If the `OutputStream` is dropped because the program is panicking we do not print +/// [`DeviceSink::log_on_drop(false)`](DeviceSink::log_on_drop). +/// If the `DeviceSink` is dropped because the program is panicking we do not print /// or log anything. /// /// # Example /// ```no_run -/// # use rodio::OutputStreamBuilder; +/// # use rodio::DeviceSinkBuilder; /// # fn main() -> Result<(), Box> { -/// let mut stream_handle = OutputStreamBuilder::open_default_stream()?; -/// stream_handle.log_on_drop(false); // Not recommended during development -/// println!("Output config: {:?}", stream_handle.config()); -/// let mixer = stream_handle.mixer(); +/// let mut handle = DeviceSinkBuilder::open_default_sink()?; +/// handle.log_on_drop(false); // Not recommended during development +/// println!("Output config: {:?}", handle.config()); +/// let mixer = handle.mixer(); /// # Ok(()) /// # } /// ``` -pub struct OutputStream { - config: OutputStreamConfig, +pub struct MixerDeviceSink { + config: DeviceSinkConfig, mixer: Mixer, log_on_drop: bool, _stream: cpal::Stream, } -impl OutputStream { - /// Access the output stream's mixer. +impl MixerDeviceSink { + /// Access the sink's mixer. pub fn mixer(&self) -> &Mixer { &self.mixer } - /// Access the output stream's config. - pub fn config(&self) -> &OutputStreamConfig { + /// Access the sink's config. + pub fn config(&self) -> &DeviceSinkConfig { &self.config } - /// When [`OutputStream`] is dropped a message is logged to stderr or + /// When [`OS-Sink`] is dropped a message is logged to stderr or /// emitted through tracing if the tracing feature is enabled. pub fn log_on_drop(&mut self, enabled: bool) { self.log_on_drop = enabled; } } -impl Drop for OutputStream { +impl Drop for MixerDeviceSink { fn drop(&mut self) { if self.log_on_drop && !std::thread::panicking() { #[cfg(feature = "tracing")] - tracing::debug!("Dropping OutputStream, audio playing through this stream will stop"); + tracing::debug!("Dropping DeviceSink, audio playing through this sink will stop"); #[cfg(not(feature = "tracing"))] - eprintln!("Dropping OutputStream, audio playing through this stream will stop, to prevent this message from appearing use tracing or call `.log_on_drop(false)` on this OutputStream") + 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") } } } -impl fmt::Debug for OutputStream { +impl fmt::Debug for MixerDeviceSink { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("OutputStream") + f.debug_struct("MixerDeviceSink") .field("config", &self.config) .finish_non_exhaustive() } } -/// Describes the output stream's configuration +/// Describes the OS-Sink's configuration #[derive(Copy, Clone, Debug)] -pub struct OutputStreamConfig { +pub struct DeviceSinkConfig { pub(crate) channel_count: ChannelCount, pub(crate) sample_rate: SampleRate, pub(crate) buffer_size: BufferSize, pub(crate) sample_format: SampleFormat, } -impl Default for OutputStreamConfig { +impl Default for DeviceSinkConfig { fn default() -> Self { Self { channel_count: nz!(2), @@ -106,29 +106,29 @@ impl Default for OutputStreamConfig { } } -impl OutputStreamConfig { - /// Access the output stream config's channel count. +impl DeviceSinkConfig { + /// Access the OS-Sink config's channel count. pub fn channel_count(&self) -> ChannelCount { self.channel_count } - /// Access the output stream config's sample rate. + /// Access the OS-Sink config's sample rate. pub fn sample_rate(&self) -> SampleRate { self.sample_rate } - /// Access the output stream config's buffer size. + /// Access the OS-Sink config's buffer size. pub fn buffer_size(&self) -> &BufferSize { &self.buffer_size } - /// Access the output stream config's sample format. + /// Access the OS-Sink config's sample format. pub fn sample_format(&self) -> SampleFormat { self.sample_format } } -impl core::fmt::Debug for OutputStreamBuilder { +impl core::fmt::Debug for DeviceSinkBuilder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let device = if let Some(device) = &self.device { "Some(".to_owned() @@ -141,7 +141,7 @@ impl core::fmt::Debug for OutputStreamBuilder { "None".to_owned() }; - f.debug_struct("OutputStreamBuilder") + f.debug_struct("DeviceSinkBuilder") .field("device", &device) .field("config", &self.config) .finish() @@ -155,56 +155,56 @@ fn default_error_callback(err: cpal::StreamError) { eprintln!("audio stream error: {err}"); } -/// Convenience builder for audio output stream. +/// Convenience builder for audio OS-player. /// It provides methods to configure several parameters of the audio output and opening default /// device. See examples for use-cases. /// -///
When the OutputStream is dropped playback will end, and the associated -/// output stream will be disposed
-pub struct OutputStreamBuilder +///
When the DeviceSink is dropped playback will end, and the associated +/// OS-Sink will be disposed
+pub struct DeviceSinkBuilder where E: FnMut(cpal::StreamError) + Send + 'static, { device: Option, - config: OutputStreamConfig, + config: DeviceSinkConfig, error_callback: E, } -impl Default for OutputStreamBuilder { +impl Default for DeviceSinkBuilder { fn default() -> Self { Self { device: None, - config: OutputStreamConfig::default(), + config: DeviceSinkConfig::default(), error_callback: default_error_callback, } } } -impl OutputStreamBuilder { +impl DeviceSinkBuilder { /// Sets output device and its default parameters. - pub fn from_device(device: cpal::Device) -> Result { + pub fn from_device(device: cpal::Device) -> Result { let default_config = device .default_output_config() - .map_err(StreamError::DefaultStreamConfigError)?; + .map_err(DeviceSinkError::DefaultSinkConfigError)?; Ok(Self::default() .with_device(device) .with_supported_config(&default_config)) } - /// Sets default output stream parameters for default output audio device. - pub fn from_default_device() -> Result { + /// Sets default OS-Sink 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)?; + .ok_or(DeviceSinkError::NoDevice)?; 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 + /// Try to open a new OS-Sink for the default output device with its default configuration. + /// Failing that attempt to open OS-Sink 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 { + pub fn open_default_sink() -> Result { Self::from_default_device() .and_then(|x| x.open_stream()) .or_else(|original_err| { @@ -221,7 +221,7 @@ impl OutputStreamBuilder { devices .find_map(|d| { Self::from_device(d) - .and_then(|x| x.open_stream_or_fallback()) + .and_then(|x| x.open_sink_or_fallback()) .ok() }) .ok_or(original_err) @@ -229,27 +229,27 @@ impl OutputStreamBuilder { } } -impl OutputStreamBuilder +impl DeviceSinkBuilder 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) -> DeviceSinkBuilder { self.device = Some(device); self } - /// Sets number of output stream's channels. - pub fn with_channels(mut self, channel_count: ChannelCount) -> OutputStreamBuilder { + /// Sets number of OS-Sink's channels. + pub fn with_channels(mut self, channel_count: ChannelCount) -> DeviceSinkBuilder { assert!(channel_count.get() > 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 { + /// Sets OS-Sink's sample rate. + pub fn with_sample_rate(mut self, sample_rate: SampleRate) -> DeviceSinkBuilder { self.config.sample_rate = sample_rate; self } @@ -292,13 +292,13 @@ where /// - Low-latency (audio production, live monitoring): 512-1024 /// - General use (games, media playback): 1024-2048 /// - Stability-focused (background music, non-interactive): 2048-4096 - pub fn with_buffer_size(mut self, buffer_size: cpal::BufferSize) -> OutputStreamBuilder { + pub fn with_buffer_size(mut self, buffer_size: cpal::BufferSize) -> DeviceSinkBuilder { 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) -> DeviceSinkBuilder { self.config.sample_format = sample_format; self } @@ -308,8 +308,8 @@ where pub fn with_supported_config( mut self, config: &cpal::SupportedStreamConfig, - ) -> OutputStreamBuilder { - self.config = OutputStreamConfig { + ) -> DeviceSinkBuilder { + self.config = DeviceSinkConfig { channel_count: NonZero::new(config.channels()) .expect("no valid cpal config has zero channels"), sample_rate: NonZero::new(config.sample_rate()) @@ -320,9 +320,9 @@ where 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 { + /// Set all OS-Sink parameters at once from CPAL stream config. + pub fn with_config(mut self, config: &cpal::StreamConfig) -> DeviceSinkBuilder { + self.config = DeviceSinkConfig { channel_count: NonZero::new(config.channels) .expect("no valid cpal config has zero channels"), sample_rate: NonZero::new(config.sample_rate) @@ -334,38 +334,38 @@ where } /// Set a callback that will be called when an error occurs with the stream - pub fn with_error_callback(self, callback: F) -> OutputStreamBuilder + pub fn with_error_callback(self, callback: F) -> DeviceSinkBuilder where F: FnMut(cpal::StreamError) + Send + 'static, { - OutputStreamBuilder { + DeviceSinkBuilder { device: self.device, config: self.config, error_callback: callback, } } - /// Open output stream using parameters configured so far. - pub fn open_stream(self) -> Result { + /// Open OS-Sink using parameters configured so far. + pub fn open_stream(self) -> Result { let device = self.device.as_ref().expect("No output device specified"); - OutputStream::open(device, &self.config, self.error_callback) + MixerDeviceSink::open(device, &self.config, self.error_callback) } - /// Try opening a new output stream with the builder's current stream configuration. + /// Try opening a new OS-Sink with the builder's current stream configuration. /// Failing that attempt to open stream with other available configurations /// supported by the device. /// If all attempts fail returns initial error. - pub fn open_stream_or_fallback(&self) -> Result + pub fn open_sink_or_fallback(&self) -> Result where E: Clone, { let device = self.device.as_ref().expect("No output device specified"); let error_callback = &self.error_callback; - OutputStream::open(device, &self.config, error_callback.clone()).or_else(|err| { + MixerDeviceSink::open(device, &self.config, error_callback.clone()).or_else(|err| { for supported_config in supported_output_configs(device)? { - if let Ok(handle) = OutputStreamBuilder::default() + if let Ok(handle) = DeviceSinkBuilder::default() .with_device(device.clone()) .with_supported_config(&supported_config) .with_error_callback(error_callback.clone()) @@ -380,19 +380,19 @@ where } /// 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 +/// Returns a `Player` 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) + let player = Player::connect_new(mixer); + player.append(input); + Ok(player) } -impl From<&OutputStreamConfig> for StreamConfig { - fn from(config: &OutputStreamConfig) -> Self { +impl From<&DeviceSinkConfig> for StreamConfig { + fn from(config: &DeviceSinkConfig) -> Self { cpal::StreamConfig { channels: config.channel_count.get() as cpal::ChannelCount, sample_rate: config.sample_rate.get(), @@ -422,22 +422,22 @@ assert_error_traits!(PlayError); /// Errors that might occur when interfacing with audio output. #[derive(Debug, thiserror::Error)] -pub enum StreamError { - /// Could not start playing the stream, see [cpal::PlayStreamError] for +pub enum DeviceSinkError { + /// Could not start playing the sink, see [cpal::PlayStreamError] for /// details. #[error("Could not start playing the stream")] - PlayStreamError(#[source] cpal::PlayStreamError), + PlayError(#[source] cpal::PlayStreamError), /// Failed to get the stream config for the given device. See /// [cpal::DefaultStreamConfigError] for details. - #[error("Failed to get the stream config for the given device")] - DefaultStreamConfigError(#[source] cpal::DefaultStreamConfigError), - /// Error opening stream with OS. See [cpal::BuildStreamError] for details. + #[error("Failed to get the config for the given device")] + DefaultSinkConfigError(#[source] cpal::DefaultStreamConfigError), + /// Error opening sink with OS. See [cpal::BuildStreamError] for details. #[error("Error opening the stream with the OS")] - BuildStreamError(#[source] cpal::BuildStreamError), - /// Could not list supported stream configs for the device. Maybe it + BuildError(#[source] cpal::BuildStreamError), + /// Could not list supported configs for the device. Maybe it /// disconnected. For details see: [cpal::SupportedStreamConfigsError]. - #[error("Could not list supported stream configs for the device. Maybe its disconnected?")] - SupportedStreamConfigsError(#[source] cpal::SupportedStreamConfigsError), + #[error("Could not list supported configs for the device. Maybe its disconnected?")] + SupportedConfigsError(#[source] cpal::SupportedStreamConfigsError), /// Could not find any output device #[error("Could not find any output device")] NoDevice, @@ -447,8 +447,8 @@ pub enum StreamError { UnsupportedSampleFormat, } -impl OutputStream { - fn validate_config(config: &OutputStreamConfig) { +impl MixerDeviceSink { + fn validate_config(config: &DeviceSinkConfig) { if let BufferSize::Fixed(sz) = config.buffer_size { assert!(sz > 0, "fixed buffer size must be greater than zero"); } @@ -456,16 +456,16 @@ impl OutputStream { pub(crate) fn open( device: &cpal::Device, - config: &OutputStreamConfig, + config: &DeviceSinkConfig, error_callback: E, - ) -> Result + ) -> Result where E: FnMut(cpal::StreamError) + Send + 'static, { Self::validate_config(config); let (controller, source) = mixer(config.channel_count, config.sample_rate); Self::init_stream(device, config, source, error_callback).and_then(|stream| { - stream.play().map_err(StreamError::PlayStreamError)?; + stream.play().map_err(DeviceSinkError::PlayError)?; Ok(Self { _stream: stream, mixer: controller, @@ -477,10 +477,10 @@ impl OutputStream { fn init_stream( device: &cpal::Device, - config: &OutputStreamConfig, + config: &DeviceSinkConfig, mut samples: S, error_callback: E, - ) -> Result + ) -> Result where S: Source + Send + 'static, E: FnMut(cpal::StreamError) + Send + 'static, @@ -505,7 +505,7 @@ impl OutputStream { None, ), )+ - _ => return Err(StreamError::UnsupportedSampleFormat), + _ => return Err(DeviceSinkError::UnsupportedSampleFormat), } }; } @@ -525,17 +525,17 @@ impl OutputStream { U64, u64 ); - result.map_err(StreamError::BuildStreamError) + result.map_err(DeviceSinkError::BuildError) } } /// Return all formats supported by the device. pub fn supported_output_configs( device: &cpal::Device, -) -> Result, StreamError> { +) -> Result, DeviceSinkError> { let mut supported: Vec<_> = device .supported_output_configs() - .map_err(StreamError::SupportedStreamConfigsError)? + .map_err(DeviceSinkError::SupportedConfigsError)? .collect(); supported.sort_by(|a, b| b.cmp_default_heuristics(a)); diff --git a/src/wav_output.rs b/src/wav_output.rs index 981be50d..5f8f5ced 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -24,7 +24,7 @@ assert_error_traits!(ToWavError); /// Saves Source's output into a wav file. The output samples format is 32-bit /// float. This function is intended primarily for testing and diagnostics. It can be used to see -/// the output without opening output stream to a real audio device. +/// the output without opening OS-Sink to a real audio device. /// /// If the file already exists it will be overwritten. /// @@ -42,7 +42,7 @@ pub fn wav_to_file( /// Saves Source's output into a writer. The output samples format is 32-bit float. This function /// is intended primarily for testing and diagnostics. It can be used to see the output without -/// opening output stream to a real audio device. +/// opening an OS-Sink to a real audio device. /// /// # Example /// ```rust @@ -126,6 +126,11 @@ impl> Iterator for WholeFrames { self.pos += 1; Some(to_yield) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.source.size_hint() + } } #[cfg(test)]