From a365e10a5c12cd4d76e502086b7b23bb1c44a2bd Mon Sep 17 00:00:00 2001 From: Yara Date: Tue, 21 Jul 2026 17:58:11 +0200 Subject: [PATCH 01/11] Warn for in progress rewrite in Readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index ccc96feaf..51320308c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ [![Crates.io Downloads](https://img.shields.io/crates/d/rodio.svg)](https://crates.io/crates/rodio) [![Build Status](https://github.com/RustAudio/rodio/workflows/CI/badge.svg)](https://github.com/RustAudio/rodio/actions) +> [!WARNING] +> We are currently rewriting Rodio's core audio engine. The current state should +> be working but under documented. This is expected to take at least a month. +> For the latest unreleased work pre engine rewrite see [this checkout](https://github.com/RustAudio/rodio/tree/c5f1e94290922fe4ee963ce6f85754ea6ba36243) + Rust playback library. Playback is handled by [cpal](https://github.com/RustAudio/cpal). Format decoding is handled by [Symphonia](https://github.com/pdeljanov/Symphonia) by default, or by optional format-specific decoders: From 3f086ff2215add27e3280e27626de211ee6f9e41 Mon Sep 17 00:00:00 2001 From: Yara Date: Tue, 21 Jul 2026 17:58:11 +0200 Subject: [PATCH 02/11] land const source --- README.md | 7 +- src/const_source.rs | 132 +++++++++++++++++++++++++++++++++++++ src/const_source/buffer.rs | 80 ++++++++++++++++++++++ src/const_source/chain.rs | 52 +++++++++++++++ src/docs/total_duration.md | 6 ++ src/lib.rs | 5 ++ src/source/mod.rs | 4 +- 7 files changed, 281 insertions(+), 5 deletions(-) create mode 100644 src/const_source.rs create mode 100644 src/const_source/buffer.rs create mode 100644 src/const_source/chain.rs create mode 100644 src/docs/total_duration.md diff --git a/README.md b/README.md index 51320308c..8586483ff 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,11 @@ > [!WARNING] > We are currently rewriting Rodio's core audio engine. The current state should -> be working but under documented. This is expected to take at least a month. -> For the latest unreleased work pre engine rewrite see [this checkout](https://github.com/RustAudio/rodio/tree/c5f1e94290922fe4ee963ce6f85754ea6ba36243) +> be working but under documented. This is expected to take at least a month. +> For the latest unreleased work pre engine rewrite see [this +> checkout](https://github.com/RustAudio/rodio/tree/c5f1e94290922fe4ee963ce6f85754ea6ba36243). +> To follow our progress see the [tracking +> issue](https://github.com/RustAudio/rodio/issues/901) Rust playback library. diff --git a/src/const_source.rs b/src/const_source.rs new file mode 100644 index 000000000..8aecbb106 --- /dev/null +++ b/src/const_source.rs @@ -0,0 +1,132 @@ +//! A sound source that has it's sample rate and number of channels fixed at compile time. +//! Some practical examples: +//! - An effect performed by a neural network trained on a specific channel +//! count and sample rate +//! - A custom audio source streaming in at a pre-determined fixed sample rate. +//! We use this in our VoIP example. +//! +//! A const pipeline _may_ also be optimized more by the compiler as many +//! branches are known at compile time, like when converting from one channel +//! count to another. +use std::marker::PhantomData; +use std::num::NonZeroU16; +use std::num::NonZeroU32; +use std::time::Duration; + +use crate::ChannelCount; +use crate::Sample; +use crate::SampleRate; +use crate::Source as DynamicSource; // will be renamed to this upstream + +mod chain; +mod buffer; + +pub use chain::SourceChain; +pub use buffer::SamplesBuffer; + +/// A source which sample rate and channel count are fixed at compile time. +pub trait ConstSource: Iterator { + /// Returns the sample rate which is the first generic (SR) of a ConstSource + fn sample_rate(&self) -> SampleRate { + const { NonZeroU32::new(SR).expect("SampleRate must be > 0") } + } + /// Returns the channel count which is the second generic (CH) of a ConstSource + fn channels(&self) -> ChannelCount { + const { NonZeroU16::new(CH).expect("Channel count must be > 0") } + } + + #[doc = include_str!("docs/total_duration.md")] + fn total_duration(&self) -> Option; + + /// Use this const source as if it's a dynamic source. You generally do not + /// want to do this since there are less effects for dynamic sources and + /// those that are available can not be implemented as efficient. This is + /// therefore purely provided for backwards compatibility. + fn into_dynamic_source(self) -> IntoDynamicSource + where + Self: Sized, + { + IntoDynamicSource { inner: self } + } + + /// Add another source to play directly after this one. + /// + /// # Example + /// + /// ```rust + /// # use rodio::const_source::ConstSource; + /// # use rodio::const_source::SamplesBuffer; + /// let preamble = SamplesBuffer::<44100, 1>::new([1.0, 1.0]); + /// let signal = SamplesBuffer::<44100, 1>::new([2.0, 2.0]); + /// + /// let mixed = preamble.chain_source(signal); + /// assert_eq!(mixed.collect::>(), vec![1.0,1.0,2.0,2.0]) + /// ``` + fn chain_source(self, next: S) -> SourceChain + where + Self: Sized, + S: ConstSource, + { + SourceChain::new(self, next) + } + + // placeholder until effects land (need this for some examples) + #[allow(missing_docs)] + fn take_duration(self, _duration: Duration) -> Placeholder + where + Self: Sized, + { + todo!() + } +} + +// placeholder until effects land (need this for some examples) +#[allow(missing_docs)] +pub struct Placeholder +where + S: ConstSource, +{ + inner: PhantomData, +} + +/// A `DynamicSource` converted from a `ConstSource`. Useful for passing to old +/// APIs that do not accept a `ConstSource` nor `FixedSource`. +#[derive(Clone)] +pub struct IntoDynamicSource +where + S: ConstSource, +{ + inner: S, +} + +impl Iterator for IntoDynamicSource +where + S: ConstSource, +{ + type Item = Sample; + + fn next(&mut self) -> Option { + self.inner.next() + } +} + +impl DynamicSource for IntoDynamicSource +where + S: ConstSource, +{ + fn current_span_len(&self) -> Option { + None + } + + fn channels(&self) -> ChannelCount { + const { NonZeroU16::new(CH).expect("channel count must be larger then zero") } + } + + fn sample_rate(&self) -> SampleRate { + const { NonZeroU32::new(SR).expect("sample rate must be larger then zero") } + } + + fn total_duration(&self) -> Option { + self.inner.total_duration() + } +} diff --git a/src/const_source/buffer.rs b/src/const_source/buffer.rs new file mode 100644 index 000000000..67f5b87e3 --- /dev/null +++ b/src/const_source/buffer.rs @@ -0,0 +1,80 @@ +use std::sync::Arc; +use std::time::Duration; + +use crate::Sample; +use crate::ConstSource; + +/// A buffer of samples treated as a source. +#[derive(Debug, Clone)] +pub struct SamplesBuffer { + data: Arc<[Sample]>, + pos: usize, +} + +impl SamplesBuffer { + /// Builds a new `SamplesBuffer`. + /// + /// Note any call to total_duration will panic if the buffer is larger then + /// 16 billion elements. + pub fn new(data: D) -> SamplesBuffer + where + D: Into>, + { + const { assert!(SR > 0) }; + const { assert!(CH > 0) }; + + SamplesBuffer { + data: data.into().into(), + pos: 0, + } + } +} + +impl ConstSource for SamplesBuffer { + /// # Panics + /// If the length of the buffer is larger than approximately 16 billion elements. + /// This is because the calculation of the duration would overflow. + #[inline] + fn total_duration(&self) -> Option { + let duration_ns = 1_000_000_000u64 + .checked_mul(self.data.len() as u64) + .unwrap() + / SR as u64 + / CH as u64; + Some(Duration::new( + duration_ns / 1_000_000_000, + (duration_ns % 1_000_000_000) as u32, + )) + } + // /// This jumps in memory till the sample for `pos`. + // #[inline] + // fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + // // This is fast because all the samples are in memory already + // // and due to the constant sample_rate we can jump to the right + // // sample directly. + // let curr_channel = self.pos % self.channels() as usize; + // let new_pos = pos.as_secs_f32() * self.sample_rate() as f32 * self.channels() as f32; + // // saturate pos at the end of the source + // let new_pos = new_pos as usize; + // let new_pos = new_pos.min(self.data.len()); + // // make sure the next sample is for the right channel + // let new_pos = new_pos.next_multiple_of(self.channels() as usize); + // let new_pos = new_pos - curr_channel; + // self.pos = new_pos; + // Ok(()) + // } +} + +impl Iterator for SamplesBuffer { + type Item = Sample; + #[inline] + fn next(&mut self) -> Option { + let sample = self.data.get(self.pos)?; + self.pos += 1; + Some(*sample) + } + #[inline] + fn size_hint(&self) -> (usize, Option) { + (self.data.len(), Some(self.data.len())) + } +} diff --git a/src/const_source/chain.rs b/src/const_source/chain.rs new file mode 100644 index 000000000..801047cb7 --- /dev/null +++ b/src/const_source/chain.rs @@ -0,0 +1,52 @@ +use crate::ConstSource; +use crate::Sample; + +/// Two chained sources, the one played after the other. +#[derive(Clone)] +pub struct SourceChain { + inner: S1, + next: S2, + playing_inner: bool, +} + +impl, S2: ConstSource> + SourceChain +{ + pub(crate) fn new(s1: S1, s2: S2) -> Self { + SourceChain { + inner: s1, + next: s2, + playing_inner: true, + } + } +} + +impl, S2: ConstSource> + ConstSource for SourceChain +{ + fn total_duration(&self) -> Option { + self.inner + .total_duration() + .and_then(|d| self.next.total_duration().map(|d2| d2 + d)) + } +} + +impl, S2: ConstSource> Iterator + for SourceChain +{ + type Item = Sample; + + fn next(&mut self) -> Option { + if self.playing_inner { + match self.inner.next() { + Some(sample) => Some(sample), + None => { + self.playing_inner = false; + self.next.next() + } + } + } else { + self.next.next() + } + } +} diff --git a/src/docs/total_duration.md b/src/docs/total_duration.md new file mode 100644 index 000000000..e05826cd6 --- /dev/null +++ b/src/docs/total_duration.md @@ -0,0 +1,6 @@ +Returns the total duration of this source, if known. + +`None` indicates at the same time "infinite" or "unknown". + +Note this value is free to change. For example if you removed an item from a +queue the total_duration lowers. diff --git a/src/lib.rs b/src/lib.rs index dbd6174c3..9e4eb2441 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -228,6 +228,8 @@ pub mod conversions; pub mod decoder; #[cfg(feature = "experimental")] pub mod fixed_source; +pub mod const_source; + pub mod math; #[cfg(feature = "recording")] /// Microphone input support for audio recording. @@ -241,6 +243,7 @@ pub use crate::common::{BitDepth, ChannelCount, Float, Sample, SampleRate, DEFAU pub use crate::decoder::Decoder; #[cfg(feature = "experimental")] pub use crate::fixed_source::FixedSource; +pub use crate::const_source::ConstSource; pub use crate::player::Player; pub use crate::source::Source; pub use crate::spatial_player::SpatialPlayer; @@ -250,3 +253,5 @@ pub use crate::stream::{play, DeviceSinkBuilder, DeviceSinkError, MixerDeviceSin pub use crate::wav_output::wav_to_file; #[cfg(feature = "wav_output")] pub use crate::wav_output::wav_to_writer; + +pub use Source as DynamicSource; diff --git a/src/source/mod.rs b/src/source/mod.rs index b8ea8553a..222578905 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -217,9 +217,7 @@ pub trait Source: Iterator { /// Returns the rate at which the source should be played. In number of samples per second. fn sample_rate(&self) -> SampleRate; - /// Returns the total duration of this source, if known. - /// - /// `None` indicates at the same time "infinite" or "unknown". + #[doc = include_str!("../docs/total_duration.md")] fn total_duration(&self) -> Option; /// Stores the source in a buffer in addition to returning it. This iterator can be cloned. From 8ed9f6e646e271db14bf0e78e6b3811d7280ba87 Mon Sep 17 00:00:00 2001 From: Yara Date: Tue, 21 Jul 2026 17:58:11 +0200 Subject: [PATCH 03/11] Add silence generator (only for const source) --- src/generators.rs | 10 ++++++++ src/generators/silence.rs | 54 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 3 files changed, 65 insertions(+) create mode 100644 src/generators.rs create mode 100644 src/generators/silence.rs diff --git a/src/generators.rs b/src/generators.rs new file mode 100644 index 000000000..bb69047f1 --- /dev/null +++ b/src/generators.rs @@ -0,0 +1,10 @@ +//! Sources that produce sound without consuming anything + +// Note we make everything be it effects or generators available under / + +mod silence; + +/// Generators with both the sample rate and channel count known at compile time. +pub mod const_source { + pub use super::silence::const_source::Silence; +} diff --git a/src/generators/silence.rs b/src/generators/silence.rs new file mode 100644 index 000000000..943717fa5 --- /dev/null +++ b/src/generators/silence.rs @@ -0,0 +1,54 @@ +pub mod const_source { + use crate::ConstSource; + + /// A source producing an infinite amount of Silence. Like all generators you + /// probably want to limit the duration of this source. + /// + /// # Example + /// Padding a [`TakeDuration`](crate::effects::const_source::TakeDuration) to + /// guarantee an exact playtime: + /// + /// ```rust,no_run + /// # use std::time::Duration; + /// # use rodio::nz; + /// # use rodio::generators::const_source::Silence; + /// # use rodio::ConstSource; + /// # let unknown_length = Silence::new(); + /// let silence: Silence<44100> = Silence::new(); + /// let two_seconds = unknown_length + /// .chain_source(silence) + /// .take_duration(Duration::from_secs(2)); + /// ``` + pub struct Silence; + + impl Silence { + /// Create an infinite silence + pub fn new() -> Self { + Self + } + } + + impl Default for Silence { + fn default() -> Self { + Self + } + } + + impl ConstSource for Silence { + fn total_duration(&self) -> Option { + None + } + } + + impl Iterator for Silence { + type Item = crate::Sample; + + fn next(&mut self) -> Option { + Some(0.0) + } + + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 9e4eb2441..69a57c5fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -230,6 +230,7 @@ pub mod decoder; pub mod fixed_source; pub mod const_source; +pub mod generators; pub mod math; #[cfg(feature = "recording")] /// Microphone input support for audio recording. From f4e9464bbd88977e612882134f8b1a1262946ad4 Mon Sep 17 00:00:00 2001 From: Yara Date: Wed, 22 Jul 2026 17:51:42 +0200 Subject: [PATCH 04/11] add seeking --- src/const_source.rs | 17 ++++++--- src/const_source/buffer.rs | 38 +++++++++++---------- src/const_source/chain.rs | 70 +++++++++++++++++++++++++++++++++----- src/docs/try_seek.md | 16 +++++++++ src/generators/silence.rs | 12 +++++-- src/lib.rs | 4 +-- src/source/mod.rs | 17 +-------- 7 files changed, 123 insertions(+), 51 deletions(-) create mode 100644 src/docs/try_seek.md diff --git a/src/const_source.rs b/src/const_source.rs index 8aecbb106..daa37d0d1 100644 --- a/src/const_source.rs +++ b/src/const_source.rs @@ -1,4 +1,4 @@ -//! A sound source that has it's sample rate and number of channels fixed at compile time. +//! A sound source that has it's sample rate and number of channels fixed at compile time. //! Some practical examples: //! - An effect performed by a neural network trained on a specific channel //! count and sample rate @@ -13,16 +13,17 @@ use std::num::NonZeroU16; use std::num::NonZeroU32; use std::time::Duration; +use crate::source::SeekError; use crate::ChannelCount; use crate::Sample; use crate::SampleRate; -use crate::Source as DynamicSource; // will be renamed to this upstream +use crate::Source as DynamicSource; // Source will (probably) be renamed to this later -mod chain; mod buffer; +mod chain; -pub use chain::SourceChain; pub use buffer::SamplesBuffer; +pub use chain::SourceChain; /// A source which sample rate and channel count are fixed at compile time. pub trait ConstSource: Iterator { @@ -38,6 +39,14 @@ pub trait ConstSource: Iterator { #[doc = include_str!("docs/total_duration.md")] fn total_duration(&self) -> Option; + #[allow(unused_variables)] + #[doc = include_str!("docs/try_seek.md")] + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + Err(SeekError::NotSupported { + underlying_source: std::any::type_name::(), + }) + } + /// Use this const source as if it's a dynamic source. You generally do not /// want to do this since there are less effects for dynamic sources and /// those that are available can not be implemented as efficient. This is diff --git a/src/const_source/buffer.rs b/src/const_source/buffer.rs index 67f5b87e3..835a63d78 100644 --- a/src/const_source/buffer.rs +++ b/src/const_source/buffer.rs @@ -1,8 +1,9 @@ use std::sync::Arc; use std::time::Duration; -use crate::Sample; +use crate::source::SeekError; use crate::ConstSource; +use crate::Sample; /// A buffer of samples treated as a source. #[derive(Debug, Clone)] @@ -46,23 +47,24 @@ impl ConstSource for SamplesBuffer (duration_ns % 1_000_000_000) as u32, )) } - // /// This jumps in memory till the sample for `pos`. - // #[inline] - // fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - // // This is fast because all the samples are in memory already - // // and due to the constant sample_rate we can jump to the right - // // sample directly. - // let curr_channel = self.pos % self.channels() as usize; - // let new_pos = pos.as_secs_f32() * self.sample_rate() as f32 * self.channels() as f32; - // // saturate pos at the end of the source - // let new_pos = new_pos as usize; - // let new_pos = new_pos.min(self.data.len()); - // // make sure the next sample is for the right channel - // let new_pos = new_pos.next_multiple_of(self.channels() as usize); - // let new_pos = new_pos - curr_channel; - // self.pos = new_pos; - // Ok(()) - // } + /// This jumps in memory till the sample for `pos`. + #[inline] + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + // This is fast because all the samples are in memory already + // and due to the constant sample_rate we can jump to the right + // sample directly. + let curr_channel = self.pos % self.channels().get() as usize; + let new_pos = + pos.as_secs_f32() * self.sample_rate().get() as f32 * self.channels().get() as f32; + // saturate pos at the end of the source + let new_pos = new_pos as usize; + let new_pos = new_pos.min(self.data.len()); + // make sure the next sample is for the right channel + let new_pos = new_pos.next_multiple_of(self.channels().get() as usize); + let new_pos = new_pos - curr_channel; + self.pos = new_pos; + Ok(()) + } } impl Iterator for SamplesBuffer { diff --git a/src/const_source/chain.rs b/src/const_source/chain.rs index 801047cb7..1e1148b2b 100644 --- a/src/const_source/chain.rs +++ b/src/const_source/chain.rs @@ -1,11 +1,15 @@ +use std::any::type_name_of_val; +use std::sync::Arc; + +use crate::source::SeekError; use crate::ConstSource; use crate::Sample; /// Two chained sources, the one played after the other. #[derive(Clone)] pub struct SourceChain { - inner: S1, - next: S2, + first: S1, + second: S2, playing_inner: bool, } @@ -14,20 +18,68 @@ impl, S2: ConstSource Self { SourceChain { - inner: s1, - next: s2, + first: s1, + second: s2, playing_inner: true, } } + + fn try_seek_inner(&mut self, pos: std::time::Duration) -> Result<(), ChainSeekError> { + let Some(first) = self.first.total_duration() else { + return Err(ChainSeekError::NoTotalDurationForFirst { + ty: type_name_of_val(&self.first), + }); + }; + + if pos < first { + self.first + .try_seek(pos) + .map_err(|error| ChainSeekError::FailedToSeekInFirst { + ty: type_name_of_val(&self.first), + error, + }) + } else { + self.second + .try_seek(pos) + .map_err(|error| ChainSeekError::FailedToSeekInSecond { + ty: type_name_of_val(&self.second), + error, + }) + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ChainSeekError { + #[error("Could not get duration of first source ({ty}")] + NoTotalDurationForFirst { ty: &'static str }, + #[error("Could not seek in first source")] + FailedToSeekInFirst { + ty: &'static str, + #[source] + error: SeekError, + }, + #[error("Could not seek in second source")] + FailedToSeekInSecond { + ty: &'static str, + #[source] + error: SeekError, + }, } impl, S2: ConstSource> ConstSource for SourceChain { fn total_duration(&self) -> Option { - self.inner + self.first .total_duration() - .and_then(|d| self.next.total_duration().map(|d2| d2 + d)) + .and_then(|d| self.second.total_duration().map(|d2| d2 + d)) + } + + fn try_seek(&mut self, pos: std::time::Duration) -> Result<(), SeekError> { + self.try_seek_inner(pos) + .map_err(Arc::new) + .map_err(|e| SeekError::Other(e)) } } @@ -38,15 +90,15 @@ impl, S2: ConstSource Option { if self.playing_inner { - match self.inner.next() { + match self.first.next() { Some(sample) => Some(sample), None => { self.playing_inner = false; - self.next.next() + self.second.next() } } } else { - self.next.next() + self.second.next() } } } diff --git a/src/docs/try_seek.md b/src/docs/try_seek.md new file mode 100644 index 000000000..cc614292b --- /dev/null +++ b/src/docs/try_seek.md @@ -0,0 +1,16 @@ +Attempts to seek to a given position in the current source. + +As long as the duration of the source is known, seek is guaranteed to saturate +at the end of the source. For example given a source that reports a total duration +of 42 seconds calling `try_seek()` with 60 seconds as argument will seek to +42 seconds. + +# Errors +This function will return [`SeekError::NotSupported`] if one of the underlying +sources does not support seeking. + +It will return an error if an implementation ran +into one during the seek. + +Seeking beyond the end of a source might return an error if the total duration of +the source is not known. diff --git a/src/generators/silence.rs b/src/generators/silence.rs index 943717fa5..fa0ed7d8e 100644 --- a/src/generators/silence.rs +++ b/src/generators/silence.rs @@ -1,11 +1,14 @@ pub mod const_source { + use std::time::Duration; + + use crate::source::SeekError; use crate::ConstSource; /// A source producing an infinite amount of Silence. Like all generators you /// probably want to limit the duration of this source. /// /// # Example - /// Padding a [`TakeDuration`](crate::effects::const_source::TakeDuration) to + /// Padding a [`TakeDuration`](crate::const_source::Placeholder) to /// guarantee an exact playtime: /// /// ```rust,no_run @@ -27,7 +30,7 @@ pub mod const_source { Self } } - + impl Default for Silence { fn default() -> Self { Self @@ -38,6 +41,11 @@ pub mod const_source { fn total_duration(&self) -> Option { None } + + /// This does nothing since all silence is equal :3 + fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { + Ok(()) + } } impl Iterator for Silence { diff --git a/src/lib.rs b/src/lib.rs index 69a57c5fc..b1553d60d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -224,11 +224,11 @@ pub mod stream; mod wav_output; pub mod buffer; +pub mod const_source; pub mod conversions; pub mod decoder; #[cfg(feature = "experimental")] pub mod fixed_source; -pub mod const_source; pub mod generators; pub mod math; @@ -241,10 +241,10 @@ pub mod source; pub mod static_buffer; pub use crate::common::{BitDepth, ChannelCount, Float, Sample, SampleRate, DEFAULT_SAMPLE_RATE}; +pub use crate::const_source::ConstSource; pub use crate::decoder::Decoder; #[cfg(feature = "experimental")] pub use crate::fixed_source::FixedSource; -pub use crate::const_source::ConstSource; pub use crate::player::Player; pub use crate::source::Source; pub use crate::spatial_player::SpatialPlayer; diff --git a/src/source/mod.rs b/src/source/mod.rs index 222578905..237735bee 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -695,23 +695,8 @@ pub trait Source: Iterator { SampleRateConverter::new(self, target_rate, config) } - /// Attempts to seek to a given position in the current source. - /// - /// As long as the duration of the source is known, seek is guaranteed to saturate - /// at the end of the source. For example given a source that reports a total duration - /// of 42 seconds calling `try_seek()` with 60 seconds as argument will seek to - /// 42 seconds. - /// - /// # Errors - /// This function will return [`SeekError::NotSupported`] if one of the underlying - /// sources does not support seeking. - /// - /// It will return an error if an implementation ran - /// into one during the seek. - /// - /// Seeking beyond the end of a source might return an error if the total duration of - /// the source is not known. #[allow(unused_variables)] + #[doc = include_str!("../docs/try_seek.md")] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { Err(SeekError::NotSupported { underlying_source: std::any::type_name::(), From a5e02a91a27ae196327bd7476ec841e4dc249dc7 Mon Sep 17 00:00:00 2001 From: Yara Date: Wed, 22 Jul 2026 17:51:42 +0200 Subject: [PATCH 05/11] Extract code shared between sources to common/source --- src/buffer.rs | 66 ++--------------------- src/common.rs | 2 + src/common/source.rs | 20 +++++++ src/common/source/buffer.rs | 66 +++++++++++++++++++++++ src/common/source/chain.rs | 94 +++++++++++++++++++++++++++++++++ src/const_source.rs | 8 +++ src/const_source/buffer.rs | 46 +--------------- src/const_source/chain.rs | 75 ++------------------------ src/docs/collect_into_buffer.md | 5 ++ 9 files changed, 204 insertions(+), 178 deletions(-) create mode 100644 src/common/source.rs create mode 100644 src/common/source/buffer.rs create mode 100644 src/common/source/chain.rs create mode 100644 src/docs/collect_into_buffer.md diff --git a/src/buffer.rs b/src/buffer.rs index 187a6b79b..f9c03a412 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -12,9 +12,8 @@ //! use crate::common::{ChannelCount, SampleRate}; -use crate::math::{duration_to_float, NANOS_PER_SEC}; use crate::source::{SeekError, UniformSourceIterator}; -use crate::{Float, Sample, Source}; +use crate::{Sample, Source}; use std::sync::Arc; use std::time::Duration; @@ -25,37 +24,19 @@ pub struct SamplesBuffer { pos: usize, channels: ChannelCount, sample_rate: SampleRate, - duration: Duration, } impl SamplesBuffer { /// Builds a new `SamplesBuffer`. - /// - /// # Panics - /// - /// - Panics if the samples rate is zero. - /// - Panics if the length of the buffer is larger than approximately 16 billion elements. - /// This is because the calculation of the duration would overflow. - /// pub fn new(channels: ChannelCount, sample_rate: SampleRate, data: D) -> Self where D: Into>, { - let data: Arc<[Sample]> = data.into().into(); - let duration_ns = NANOS_PER_SEC.checked_mul(data.len() as u64).unwrap() - / sample_rate.get() as u64 - / channels.get() as u64; - let duration = Duration::new( - duration_ns / NANOS_PER_SEC, - (duration_ns % NANOS_PER_SEC) as u32, - ); - Self { - data, + data: data.into().into(), pos: 0, channels, sample_rate, - duration, } } @@ -91,50 +72,11 @@ impl Source for SamplesBuffer { self.sample_rate } - #[inline] - fn total_duration(&self) -> Option { - Some(self.duration) - } - - /// This jumps in memory till the sample for `pos`. - #[inline] - fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - // This is fast because all the samples are in memory already - // and due to the constant sample_rate we can jump to the right - // sample directly. - - let curr_channel = self.pos % self.channels().get() as usize; - let new_pos = duration_to_float(pos) - * self.sample_rate().get() as Float - * self.channels().get() as Float; - // saturate pos at the end of the source - let new_pos = new_pos as usize; - let new_pos = new_pos.min(self.data.len()); - - // make sure the next sample is for the right channel - let new_pos = new_pos.next_multiple_of(self.channels().get() as usize); - let new_pos = new_pos - curr_channel; - - self.pos = new_pos; - Ok(()) - } + crate::common::source::buffer::source_impl! {} } impl Iterator for SamplesBuffer { - type Item = Sample; - - #[inline] - fn next(&mut self) -> Option { - let sample = self.data.get(self.pos)?; - self.pos += 1; - Some(*sample) - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - let remaining = self.data.len() - self.pos; - (remaining, Some(remaining)) - } + crate::common::source::buffer::iter_impl! {} } impl ExactSizeIterator for SamplesBuffer {} diff --git a/src/common.rs b/src/common.rs index d1ed01366..edc44b7f8 100644 --- a/src/common.rs +++ b/src/common.rs @@ -3,6 +3,8 @@ use std::num::NonZero; use crate::math::nz; +pub(crate) mod source; + /// Sample rate (a frame rate or samples per second per channel). pub type SampleRate = NonZero; diff --git a/src/common/source.rs b/src/common/source.rs new file mode 100644 index 000000000..6798c3555 --- /dev/null +++ b/src/common/source.rs @@ -0,0 +1,20 @@ +//! Code shared between source types. +//! +//! Since we have three source types there is a lot of code duplication. We +//! combat that by placing whatever can be shared here. +//! +//! This can be: +//! - shared types like error enums +//! - shared free functions +//! - shared members / impl blocks through macro_rules +//! +//! # Note +//! Effects are defined through a macro and do not need this kind of +//! deduplication +//! +//! This modules structure mirrors that of what it deduplicates. For example +//! the code shared between [fixed_source::chain] and [const_source::chain] is in +//! common/source/chain.rs + +pub(crate) mod buffer; +pub(crate) mod chain; diff --git a/src/common/source/buffer.rs b/src/common/source/buffer.rs new file mode 100644 index 000000000..49a625b81 --- /dev/null +++ b/src/common/source/buffer.rs @@ -0,0 +1,66 @@ +macro_rules! source_impl { + () => { + /// # Panics + /// If the length of the buffer is larger than approximately 16 billion elements. + /// This is because the calculation of the duration would overflow. + #[inline] + fn total_duration(&self) -> Option { + use crate::math::NANOS_PER_SEC; + + let duration_ns = NANOS_PER_SEC + .checked_mul(self.data.len() as u64) + .expect("slices longer then 16 billion elements are not supported") + / self.sample_rate().get() as u64 + / self.channels().get() as u64; + let duration = Duration::new( + duration_ns / NANOS_PER_SEC, + (duration_ns % NANOS_PER_SEC) as u32, + ); + + Some(duration) + } + + /// This jumps in memory to the sample corresponding to `pos`. + #[inline] + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + // This is fast because all the samples are in memory already + // and due to the constant sample_rate we can jump to the right + // sample directly. + + let curr_channel = self.pos % self.channels().get() as usize; + let new_pos = crate::math::duration_to_float(pos) + * self.sample_rate().get() as crate::Float + * self.channels().get() as crate::Float; + // saturate pos at the end of the source + let new_pos = new_pos as usize; + let new_pos = new_pos.min(self.data.len()); + + // make sure the next sample is for the right channel + let new_pos = new_pos.next_multiple_of(self.channels().get() as usize); + let new_pos = new_pos - curr_channel; + + self.pos = new_pos; + Ok(()) + } + }; +} + +macro_rules! iter_impl { + () => { + type Item = Sample; + #[inline] + fn next(&mut self) -> Option { + let sample = self.data.get(self.pos)?; + self.pos += 1; + Some(*sample) + } + #[inline] + fn size_hint(&self) -> (usize, Option) { + let remaining = self.data.len() - self.pos; + (remaining, Some(remaining)) + } + }; +} + +pub(crate) use iter_impl; +pub(crate) use source_impl; diff --git a/src/common/source/chain.rs b/src/common/source/chain.rs new file mode 100644 index 000000000..65f370b3d --- /dev/null +++ b/src/common/source/chain.rs @@ -0,0 +1,94 @@ +use crate::source::SeekError; + +#[derive(Debug, thiserror::Error)] +pub enum ChainSeekError { + #[error("Could not get duration of first source ({ty}")] + NoTotalDurationForFirst { ty: &'static str }, + #[error("Could not seek in first source")] + FailedToSeekInFirst { + ty: &'static str, + #[source] + error: SeekError, + }, + #[error("Could not seek in second source")] + FailedToSeekInSecond { + ty: &'static str, + #[source] + error: SeekError, + }, +} + +macro_rules! source_impl { + () => { + fn channels(&self) -> crate::ChannelCount { + self.first.channels() + } + + fn sample_rate(&self) -> crate::SampleRate { + self.first.sample_rate() + } + + fn total_duration(&self) -> Option { + self.first + .total_duration() + .and_then(|d| self.second.total_duration().map(|d2| d2 + d)) + } + + fn try_seek(&mut self, pos: std::time::Duration) -> Result<(), crate::source::SeekError> { + use crate::source::SeekError; + use std::any::type_name_of_val; + use std::sync::Arc; + + let Some(first) = self.first.total_duration() else { + return Err(ChainSeekError::NoTotalDurationForFirst { + ty: type_name_of_val(&self.first), + }) + .map_err(Arc::new) + .map_err(|e| SeekError::Other(e)); + }; + + if pos < first { + self.first + .try_seek(pos) + .map_err(|error| ChainSeekError::FailedToSeekInFirst { + ty: type_name_of_val(&self.first), + error, + }) + .map_err(Arc::new) + .map_err(|e| SeekError::Other(e)) + } else { + self.second + .try_seek(pos) + .map_err(|error| ChainSeekError::FailedToSeekInSecond { + ty: type_name_of_val(&self.second), + error, + }) + .map_err(Arc::new) + .map_err(|e| SeekError::Other(e)) + } + } + }; +} + +macro_rules! iter_impl { + () => { + type Item = Sample; + + fn next(&mut self) -> Option { + if self.playing_inner { + match self.first.next() { + Some(sample) => Some(sample), + None => { + self.playing_inner = false; + self.second.next() + } + } + } else { + self.second.next() + } + } + }; +} + +pub(crate) use iter_impl; +pub(crate) use source_impl; diff --git a/src/const_source.rs b/src/const_source.rs index daa37d0d1..969786d3d 100644 --- a/src/const_source.rs +++ b/src/const_source.rs @@ -58,6 +58,14 @@ pub trait ConstSource: Iterator { IntoDynamicSource { inner: self } } + #[doc = include_str!("docs/collect_into_buffer.md")] + fn collect_into_buffer(self) -> SamplesBuffer + where + Self: Sized, + { + SamplesBuffer::new(self.collect::>()) + } + /// Add another source to play directly after this one. /// /// # Example diff --git a/src/const_source/buffer.rs b/src/const_source/buffer.rs index 835a63d78..55aaaa6b3 100644 --- a/src/const_source/buffer.rs +++ b/src/const_source/buffer.rs @@ -32,51 +32,9 @@ impl SamplesBuffer { } impl ConstSource for SamplesBuffer { - /// # Panics - /// If the length of the buffer is larger than approximately 16 billion elements. - /// This is because the calculation of the duration would overflow. - #[inline] - fn total_duration(&self) -> Option { - let duration_ns = 1_000_000_000u64 - .checked_mul(self.data.len() as u64) - .unwrap() - / SR as u64 - / CH as u64; - Some(Duration::new( - duration_ns / 1_000_000_000, - (duration_ns % 1_000_000_000) as u32, - )) - } - /// This jumps in memory till the sample for `pos`. - #[inline] - fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - // This is fast because all the samples are in memory already - // and due to the constant sample_rate we can jump to the right - // sample directly. - let curr_channel = self.pos % self.channels().get() as usize; - let new_pos = - pos.as_secs_f32() * self.sample_rate().get() as f32 * self.channels().get() as f32; - // saturate pos at the end of the source - let new_pos = new_pos as usize; - let new_pos = new_pos.min(self.data.len()); - // make sure the next sample is for the right channel - let new_pos = new_pos.next_multiple_of(self.channels().get() as usize); - let new_pos = new_pos - curr_channel; - self.pos = new_pos; - Ok(()) - } + crate::common::source::buffer::source_impl! {} } impl Iterator for SamplesBuffer { - type Item = Sample; - #[inline] - fn next(&mut self) -> Option { - let sample = self.data.get(self.pos)?; - self.pos += 1; - Some(*sample) - } - #[inline] - fn size_hint(&self) -> (usize, Option) { - (self.data.len(), Some(self.data.len())) - } + crate::common::source::buffer::iter_impl! {} } diff --git a/src/const_source/chain.rs b/src/const_source/chain.rs index 1e1148b2b..eb26c64b4 100644 --- a/src/const_source/chain.rs +++ b/src/const_source/chain.rs @@ -1,7 +1,3 @@ -use std::any::type_name_of_val; -use std::sync::Arc; - -use crate::source::SeekError; use crate::ConstSource; use crate::Sample; @@ -23,82 +19,17 @@ impl, S2: ConstSource Result<(), ChainSeekError> { - let Some(first) = self.first.total_duration() else { - return Err(ChainSeekError::NoTotalDurationForFirst { - ty: type_name_of_val(&self.first), - }); - }; - - if pos < first { - self.first - .try_seek(pos) - .map_err(|error| ChainSeekError::FailedToSeekInFirst { - ty: type_name_of_val(&self.first), - error, - }) - } else { - self.second - .try_seek(pos) - .map_err(|error| ChainSeekError::FailedToSeekInSecond { - ty: type_name_of_val(&self.second), - error, - }) - } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum ChainSeekError { - #[error("Could not get duration of first source ({ty}")] - NoTotalDurationForFirst { ty: &'static str }, - #[error("Could not seek in first source")] - FailedToSeekInFirst { - ty: &'static str, - #[source] - error: SeekError, - }, - #[error("Could not seek in second source")] - FailedToSeekInSecond { - ty: &'static str, - #[source] - error: SeekError, - }, } +pub use crate::common::source::chain::ChainSeekError; impl, S2: ConstSource> ConstSource for SourceChain { - fn total_duration(&self) -> Option { - self.first - .total_duration() - .and_then(|d| self.second.total_duration().map(|d2| d2 + d)) - } - - fn try_seek(&mut self, pos: std::time::Duration) -> Result<(), SeekError> { - self.try_seek_inner(pos) - .map_err(Arc::new) - .map_err(|e| SeekError::Other(e)) - } + crate::common::source::chain::source_impl! {} } impl, S2: ConstSource> Iterator for SourceChain { - type Item = Sample; - - fn next(&mut self) -> Option { - if self.playing_inner { - match self.first.next() { - Some(sample) => Some(sample), - None => { - self.playing_inner = false; - self.second.next() - } - } - } else { - self.second.next() - } - } + crate::common::source::chain::iter_impl! {} } diff --git a/src/docs/collect_into_buffer.md b/src/docs/collect_into_buffer.md new file mode 100644 index 000000000..d97b115bc --- /dev/null +++ b/src/docs/collect_into_buffer.md @@ -0,0 +1,5 @@ +Builds a new `SamplesBuffer`. + +# Panics +- Panics if the length of the buffer is larger than approximately 16 billion elements. + This is because the calculation of the duration would overflow. From 49a3fa0b71d31823405ef12ec5327b4aa33fccfb Mon Sep 17 00:00:00 2001 From: Yara Date: Sun, 23 Aug 2026 15:58:34 +0200 Subject: [PATCH 06/11] review feedback --- src/common/source/buffer.rs | 4 +-- src/common/source/chain.rs | 53 +++++++++++++++++++++++++++++++------ src/const_source/buffer.rs | 3 --- src/const_source/chain.rs | 10 +++++-- src/generators/silence.rs | 5 +++- 5 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/common/source/buffer.rs b/src/common/source/buffer.rs index 49a625b81..ab5dda5c6 100644 --- a/src/common/source/buffer.rs +++ b/src/common/source/buffer.rs @@ -37,7 +37,7 @@ macro_rules! source_impl { // make sure the next sample is for the right channel let new_pos = new_pos.next_multiple_of(self.channels().get() as usize); - let new_pos = new_pos - curr_channel; + let new_pos = new_pos + curr_channel; self.pos = new_pos; Ok(()) @@ -56,7 +56,7 @@ macro_rules! iter_impl { } #[inline] fn size_hint(&self) -> (usize, Option) { - let remaining = self.data.len() - self.pos; + let remaining = self.data.len().saturating_sub(self.pos); (remaining, Some(remaining)) } }; diff --git a/src/common/source/chain.rs b/src/common/source/chain.rs index 65f370b3d..33853c5db 100644 --- a/src/common/source/chain.rs +++ b/src/common/source/chain.rs @@ -2,15 +2,21 @@ use crate::source::SeekError; #[derive(Debug, thiserror::Error)] pub enum ChainSeekError { - #[error("Could not get duration of first source ({ty}")] + #[error("Could not get duration of first source ({ty})")] NoTotalDurationForFirst { ty: &'static str }, - #[error("Could not seek in first source")] + #[error("Could not seek in first source ({ty})")] FailedToSeekInFirst { ty: &'static str, #[source] error: SeekError, }, - #[error("Could not seek in second source")] + #[error("Could not reset first source ({ty}) to start")] + FailedToResetFirst { + ty: &'static str, + #[source] + error: SeekError, + }, + #[error("Could not seek in second source ({ty})")] FailedToSeekInSecond { ty: &'static str, #[source] @@ -48,6 +54,23 @@ macro_rules! source_impl { }; if pos < first { + // Reset first source to prevent a jump to the current position + // after the first source completes again. + if !self.playing_first { + // FIXME(yara): implement Seekable trait for all sources and extract + // this to a function. (all sources are required to impl Seekable). + // Might wanna do a similar thing for other shared functionality + // like total duration + self.second + .try_seek(std::time::Duration::ZERO) + .map_err(|error| ChainSeekError::FailedToResetFirst { + ty: type_name_of_val(&self.first), + error, + }) + .map_err(Arc::new) + .map_err(|e| SeekError::Other(e))?; + } + self.first .try_seek(pos) .map_err(|error| ChainSeekError::FailedToSeekInFirst { @@ -55,16 +78,20 @@ macro_rules! source_impl { error, }) .map_err(Arc::new) - .map_err(|e| SeekError::Other(e)) + .map_err(|e| SeekError::Other(e))?; + self.playing_first = true; + Ok(()) } else { self.second - .try_seek(pos) + .try_seek(pos - first) .map_err(|error| ChainSeekError::FailedToSeekInSecond { ty: type_name_of_val(&self.second), error, }) .map_err(Arc::new) - .map_err(|e| SeekError::Other(e)) + .map_err(|e| SeekError::Other(e))?; + self.playing_first = false; + Ok(()) } } }; @@ -75,11 +102,11 @@ macro_rules! iter_impl { type Item = Sample; fn next(&mut self) -> Option { - if self.playing_inner { + if self.playing_first { match self.first.next() { Some(sample) => Some(sample), None => { - self.playing_inner = false; + self.playing_first = false; self.second.next() } } @@ -87,6 +114,16 @@ macro_rules! iter_impl { self.second.next() } } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let (lower_bound_a, upper_bound_a) = self.first.size_hint(); + let (lower_bound_b, upper_bound_b) = self.second.size_hint(); + + let lower_bound = lower_bound_a + lower_bound_b; + let upper_bound = upper_bound_a.zip(upper_bound_b).map(|(a, b)| a + b); + (lower_bound, upper_bound) + } }; } diff --git a/src/const_source/buffer.rs b/src/const_source/buffer.rs index 55aaaa6b3..946bea361 100644 --- a/src/const_source/buffer.rs +++ b/src/const_source/buffer.rs @@ -14,9 +14,6 @@ pub struct SamplesBuffer { impl SamplesBuffer { /// Builds a new `SamplesBuffer`. - /// - /// Note any call to total_duration will panic if the buffer is larger then - /// 16 billion elements. pub fn new(data: D) -> SamplesBuffer where D: Into>, diff --git a/src/const_source/chain.rs b/src/const_source/chain.rs index eb26c64b4..c77cbe8ef 100644 --- a/src/const_source/chain.rs +++ b/src/const_source/chain.rs @@ -6,7 +6,7 @@ use crate::Sample; pub struct SourceChain { first: S1, second: S2, - playing_inner: bool, + playing_first: bool, } impl, S2: ConstSource> @@ -16,11 +16,12 @@ impl, S2: ConstSource, S2: ConstSource> ConstSource for SourceChain @@ -33,3 +34,8 @@ impl, S2: ConstSource, S2: ConstSource> + ExactSizeIterator for SourceChain +{ +} diff --git a/src/generators/silence.rs b/src/generators/silence.rs index fa0ed7d8e..a223f444b 100644 --- a/src/generators/silence.rs +++ b/src/generators/silence.rs @@ -3,6 +3,9 @@ pub mod const_source { use crate::source::SeekError; use crate::ConstSource; + use crate::Sample; + + use dasp_sample::Sample as _; /// A source producing an infinite amount of Silence. Like all generators you /// probably want to limit the duration of this source. @@ -52,7 +55,7 @@ pub mod const_source { type Item = crate::Sample; fn next(&mut self) -> Option { - Some(0.0) + Some(Sample::EQUILIBRIUM) } fn size_hint(&self) -> (usize, Option) { From 3708b06a735b6a1295b84a8b4748023821cd2183 Mon Sep 17 00:00:00 2001 From: Yara Date: Tue, 21 Jul 2026 17:58:11 +0200 Subject: [PATCH 07/11] expand fixed source to match const --- src/fixed_source.rs | 107 +++++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 4 +- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/src/fixed_source.rs b/src/fixed_source.rs index 6cc4ffee8..8ed636094 100644 --- a/src/fixed_source.rs +++ b/src/fixed_source.rs @@ -2,7 +2,7 @@ //! channel count. use std::time::Duration; -use crate::{ChannelCount, Sample, SampleRate}; +use crate::{ChannelCount, ConstSource, 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 @@ -16,4 +16,109 @@ pub trait FixedSource: Iterator { /// /// `None` indicates at the same time "infinite" or "unknown". fn total_duration(&self) -> Option; + + /// Tries to convert from a fixed source to a const one assuming + /// the parameters already match. If they do not this returns an error. + /// + /// If the parameters do not match you can resample using: + /// [`with_sample_rate`](Self::with_sample_rate) and + /// [`with_channel_count`](Self::with_channel_count). + fn try_into_const_source( + self, + ) -> Result, ParameterMismatch> + where + Self: Sized, + { + if self.channels().get() != CH || self.sample_rate().get() != SR { + Err(ParameterMismatch { + sample_rate: self.sample_rate(), + channel_count: self.channels(), + }) + } else { + Ok(IntoConstSource(self)) + } + } + + /// Use this fixed source as if it's a dynamic source. You generally do not + /// want to do this since there are less effects for dynamic sources and + /// those that are available can not be implemented as efficient. This is + /// therefore purely provided for backwards compatibility. + fn into_dynamic_source(self) -> IntoDynamicSource + where + Self: Sized, + { + IntoDynamicSource(self) + } +} + +/// A [`ConstSource`] adapted from a [`FixedSource`]. +pub struct IntoConstSource(S); + +impl ConstSource + for IntoConstSource +{ + fn total_duration(&self) -> Option { + self.0.total_duration() + } +} + +impl Iterator for IntoConstSource { + type Item = Sample; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +/// Error that occurs when a [`FixedSource`] can not be converted into a +/// [`ConstSource`] with a certain sample rate and channel count. +#[derive(Debug)] +pub struct ParameterMismatch { + sample_rate: SampleRate, + channel_count: ChannelCount, +} + +impl std::error::Error for ParameterMismatch {} + +impl std::fmt::Display for ParameterMismatch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.sample_rate.get() == SR && self.channel_count.get() == CH { + unreachable!("ParameterMismatch error can only occur when params mismatch"); + } else if self.sample_rate.get() == SR && self.channel_count.get() != CH { + f.write_fmt(format_args!("Fixed source's channel count: {}, does not match target const source's channel count: {}", self.channel_count.get(), CH)) + } else if self.sample_rate.get() != SR && self.channel_count.get() != CH { + f.write_fmt(format_args!("Fixed source's sample rate and channel count ({}, {}) do not match target const source's sample rate and channel count ({} {})", self.sample_rate.get(), self.channel_count.get(), SR, CH)) + } else { + f.write_fmt(format_args!("Fixed source's sample rate : {}, does not match target const source's sample rate: {}", self.sample_rate.get(), SR)) + } + } +} + +/// A [`DynamicSource`] adapted from a [`FixedSource`]. +pub struct IntoDynamicSource(S); + +impl crate::DynamicSource for IntoDynamicSource { + fn current_span_len(&self) -> Option { + None + } + + fn channels(&self) -> ChannelCount { + self.0.channels() + } + + fn sample_rate(&self) -> SampleRate { + self.0.sample_rate() + } + + fn total_duration(&self) -> Option { + self.0.total_duration() + } +} + +impl Iterator for IntoDynamicSource { + type Item = crate::Sample; + + fn next(&mut self) -> Option { + self.0.next() + } } diff --git a/src/lib.rs b/src/lib.rs index b1553d60d..229f0e6af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -227,10 +227,11 @@ pub mod buffer; pub mod const_source; pub mod conversions; pub mod decoder; -#[cfg(feature = "experimental")] + pub mod fixed_source; pub mod generators; + pub mod math; #[cfg(feature = "recording")] /// Microphone input support for audio recording. @@ -243,7 +244,6 @@ pub mod static_buffer; pub use crate::common::{BitDepth, ChannelCount, Float, Sample, SampleRate, DEFAULT_SAMPLE_RATE}; pub use crate::const_source::ConstSource; pub use crate::decoder::Decoder; -#[cfg(feature = "experimental")] pub use crate::fixed_source::FixedSource; pub use crate::player::Player; pub use crate::source::Source; From ca4f014409b517ef654ecd349cdbfdde7a2e50cb Mon Sep 17 00:00:00 2001 From: Yara Date: Wed, 22 Jul 2026 17:51:42 +0200 Subject: [PATCH 08/11] Add fixed source 'adapter' to ConstSource --- src/const_source.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/const_source.rs b/src/const_source.rs index 969786d3d..455385e19 100644 --- a/src/const_source.rs +++ b/src/const_source.rs @@ -15,6 +15,7 @@ use std::time::Duration; use crate::source::SeekError; use crate::ChannelCount; +use crate::FixedSource; use crate::Sample; use crate::SampleRate; use crate::Source as DynamicSource; // Source will (probably) be renamed to this later @@ -58,6 +59,33 @@ pub trait ConstSource: Iterator { IntoDynamicSource { inner: self } } + /// Use this const source as if it's a fixed source which is generally + /// easier to work with since it drops the generics. The same effects are + /// available for both. + /// + /// # Example + /// + /// ```rust + /// # struct CustomEffect(S); + /// # use rodio::{FixedSource, ConstSource}; + /// # use rodio::generators::const_source; + /// + /// // Note custom effect can only wrap a FixedSource + /// fn apply_custom_effect(source: S) -> CustomEffect { + /// CustomEffect(source) + /// } + /// + /// let source = const_source::Silence::<44100>::new(); + /// let source = source.into_fixed_source(); + /// apply_custom_effect(source); + /// ``` + fn into_fixed_source(self) -> IntoFixedSource + where + Self: Sized, + { + IntoFixedSource { inner: self } + } + #[doc = include_str!("docs/collect_into_buffer.md")] fn collect_into_buffer(self) -> SamplesBuffer where @@ -147,3 +175,41 @@ where self.inner.total_duration() } } + +/// A `FixedSource` converted from a `ConstSource`. Useful for passing to APIs +/// that do not accept a `ConstSource`. +#[derive(Clone)] +pub struct IntoFixedSource +where + S: ConstSource, +{ + inner: S, +} + +impl Iterator for IntoFixedSource +where + S: ConstSource, +{ + type Item = Sample; + + fn next(&mut self) -> Option { + self.inner.next() + } +} + +impl FixedSource for IntoFixedSource +where + S: ConstSource, +{ + fn channels(&self) -> ChannelCount { + const { NonZeroU16::new(CH).expect("channel count must be larger then zero") } + } + + fn sample_rate(&self) -> SampleRate { + const { NonZeroU32::new(SR).expect("sample rate must be larger then zero") } + } + + fn total_duration(&self) -> Option { + self.inner.total_duration() + } +} From b457874c00957961203c0dd4c7f32f019937f090 Mon Sep 17 00:00:00 2001 From: Yara Date: Tue, 21 Jul 2026 17:58:11 +0200 Subject: [PATCH 09/11] add fixedsource to silence generator Add fixed source to silence generator --- src/const_source.rs | 3 +- src/fixed_source.rs | 61 ++++++++++++++++++++++++-- src/fixed_source/buffer.rs | 89 ++++++++++++++++++++++++++++++++++++++ src/fixed_source/chain.rs | 86 ++++++++++++++++++++++++++++++++++++ src/generators.rs | 5 +++ src/generators/silence.rs | 64 +++++++++++++++++++++++++++ 6 files changed, 303 insertions(+), 5 deletions(-) create mode 100644 src/fixed_source/buffer.rs create mode 100644 src/fixed_source/chain.rs diff --git a/src/const_source.rs b/src/const_source.rs index 455385e19..0c215512c 100644 --- a/src/const_source.rs +++ b/src/const_source.rs @@ -8,7 +8,6 @@ //! A const pipeline _may_ also be optimized more by the compiler as many //! branches are known at compile time, like when converting from one channel //! count to another. -use std::marker::PhantomData; use std::num::NonZeroU16; use std::num::NonZeroU32; use std::time::Duration; @@ -131,7 +130,7 @@ pub struct Placeholder where S: ConstSource, { - inner: PhantomData, + inner: std::marker::PhantomData, } /// A `DynamicSource` converted from a `ConstSource`. Useful for passing to old diff --git a/src/fixed_source.rs b/src/fixed_source.rs index 8ed636094..ecb9d2a9f 100644 --- a/src/fixed_source.rs +++ b/src/fixed_source.rs @@ -4,6 +4,12 @@ use std::time::Duration; use crate::{ChannelCount, ConstSource, Sample, SampleRate}; +mod buffer; +mod chain; + +pub use buffer::SamplesBuffer; +pub use chain::SourceChain; + /// Similar to `Source`, something that can produce interleaved samples for a /// fixed amount of channels at a fixed sample rate. Those parameters never /// change. @@ -21,8 +27,8 @@ pub trait FixedSource: Iterator { /// the parameters already match. If they do not this returns an error. /// /// If the parameters do not match you can resample using: - /// [`with_sample_rate`](Self::with_sample_rate) and - /// [`with_channel_count`](Self::with_channel_count). + /// [`with_sample_rate`](Self::placeholder) and + /// [`with_channel_count`](Self::placeholder). fn try_into_const_source( self, ) -> Result, ParameterMismatch> @@ -49,6 +55,55 @@ pub trait FixedSource: Iterator { { IntoDynamicSource(self) } + + /// Add another source to play directly after this one. + /// + /// # Example + /// + /// ```rust + /// # use rodio::nz; + /// # use rodio::FixedSource; + /// # use rodio::fixed_source::SamplesBuffer; + /// # fn main() -> Result<(), Box> { + /// let preamble = SamplesBuffer::new(nz!(1), nz!(1), [1.0, 1.0]); + /// let signal = SamplesBuffer::new(nz!(1), nz!(1), [2.0, 2.0]); + /// + /// let mixed = preamble.try_chain_source(signal)?; + /// assert_eq!(mixed.collect::>(), vec![1.0,1.0,2.0,2.0]); + /// # Ok(()) + /// # } + /// ``` + fn try_chain_source( + self, + next: S, + ) -> Result, chain::ParamsMismatch> + where + Self: Sized, + { + SourceChain::new(self, next) + } + + // placeholder until effects land (need this for some examples) + #[allow(missing_docs)] + fn take_duration(self, _duration: Duration) -> Placeholder + where + Self: Sized, + { + todo!() + } + + /// here to make docs links work without the linked item being in + /// remove before next release + fn placeholder(&self) {} +} + +// placeholder until effects land (need this for some examples) +#[allow(missing_docs)] +pub struct Placeholder +where + S: FixedSource, +{ + inner: std::marker::PhantomData, } /// A [`ConstSource`] adapted from a [`FixedSource`]. @@ -94,7 +149,7 @@ impl std::fmt::Display for ParameterMismatch(S); impl crate::DynamicSource for IntoDynamicSource { diff --git a/src/fixed_source/buffer.rs b/src/fixed_source/buffer.rs new file mode 100644 index 000000000..dcd5bfaa2 --- /dev/null +++ b/src/fixed_source/buffer.rs @@ -0,0 +1,89 @@ +use std::sync::Arc; +use std::time::Duration; + +use crate::FixedSource; +use crate::{ChannelCount, Sample, SampleRate}; + +/// A buffer of samples treated as a source. +#[derive(Debug, Clone)] +pub struct SamplesBuffer { + data: Arc<[Sample]>, + pos: usize, + channels: ChannelCount, + sample_rate: SampleRate, +} + +impl SamplesBuffer { + /// Builds a new `SamplesBuffer`. + pub fn new(channels: ChannelCount, sample_rate: SampleRate, data: D) -> SamplesBuffer + where + D: Into>, + { + let data: Arc<[f32]> = data.into().into(); + SamplesBuffer { + data, + pos: 0, + channels, + sample_rate, + } + } +} + +impl FixedSource for SamplesBuffer { + #[inline] + fn channels(&self) -> ChannelCount { + self.channels + } + #[inline] + fn sample_rate(&self) -> SampleRate { + self.sample_rate + } + /// # Panics + /// If the length of the buffer is larger than approximately 16 billion elements. + /// This is because the calculation of the duration would overflow. + #[inline] + fn total_duration(&self) -> Option { + let duration_ns = 1_000_000_000u64 + .checked_mul(self.data.len() as u64) + .unwrap() + / self.sample_rate.get() as u64 + / self.channels.get() as u64; + let duration = Duration::new( + duration_ns / 1_000_000_000, + (duration_ns % 1_000_000_000) as u32, + ); + + Some(duration) + } + // /// This jumps in memory till the sample for `pos`. + // #[inline] + // fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + // // This is fast because all the samples are in memory already + // // and due to the constant sample_rate we can jump to the right + // // sample directly. + // let curr_channel = self.pos % self.channels() as usize; + // let new_pos = pos.as_secs_f32() * self.sample_rate() as f32 * self.channels() as f32; + // // saturate pos at the end of the source + // let new_pos = new_pos as usize; + // let new_pos = new_pos.min(self.data.len()); + // // make sure the next sample is for the right channel + // let new_pos = new_pos.next_multiple_of(self.channels() as usize); + // let new_pos = new_pos - curr_channel; + // self.pos = new_pos; + // Ok(()) + // } +} + +impl Iterator for SamplesBuffer { + type Item = Sample; + #[inline] + fn next(&mut self) -> Option { + let sample = self.data.get(self.pos)?; + self.pos += 1; + Some(*sample) + } + #[inline] + fn size_hint(&self) -> (usize, Option) { + (self.data.len(), Some(self.data.len())) + } +} diff --git a/src/fixed_source/chain.rs b/src/fixed_source/chain.rs new file mode 100644 index 000000000..c34a947b1 --- /dev/null +++ b/src/fixed_source/chain.rs @@ -0,0 +1,86 @@ +use std::fmt::Display; + +use crate::FixedSource; +use crate::Sample; +use crate::{ChannelCount, SampleRate}; + +/// Two chained sources, the one played after the other. +#[derive(Clone)] +pub struct SourceChain { + inner: S1, + next: S2, + playing_inner: bool, +} + +#[derive(Debug, Clone, Copy, thiserror::Error, PartialEq, Eq)] +pub struct ParamsMismatch { + sample_rate_self: SampleRate, + channel_count_self: ChannelCount, + sample_rate_adding: SampleRate, + channel_count_adding: ChannelCount, +} + +impl Display for ParamsMismatch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let ParamsMismatch { + sample_rate_self, + channel_count_self, + sample_rate_adding, + channel_count_adding, + } = self; + f.write_fmt(format_args!("Parameters mismatch the source chained onto Self does not have the same parameters. Self has sample rate: {sample_rate_self} and channel count: {channel_count_self} while the source being chained has sample rate: {sample_rate_adding} and channel count: {channel_count_adding}.")) + } +} + +impl SourceChain { + pub(crate) fn new(s1: S1, s2: S2) -> Result { + if s1.sample_rate() != s2.sample_rate() || s1.channels() != s2.channels() { + Err(ParamsMismatch { + sample_rate_self: s1.sample_rate(), + channel_count_self: s1.channels(), + sample_rate_adding: s2.sample_rate(), + channel_count_adding: s2.channels(), + }) + } else { + Ok(SourceChain { + inner: s1, + next: s2, + playing_inner: true, + }) + } + } +} + +impl FixedSource for SourceChain { + fn channels(&self) -> ChannelCount { + self.inner.channels() + } + + fn sample_rate(&self) -> SampleRate { + self.inner.sample_rate() + } + + fn total_duration(&self) -> Option { + self.inner + .total_duration() + .and_then(|d| self.next.total_duration().map(|d2| d2 + d)) + } +} + +impl Iterator for SourceChain { + type Item = Sample; + + fn next(&mut self) -> Option { + if self.playing_inner { + match self.inner.next() { + Some(sample) => Some(sample), + None => { + self.playing_inner = false; + self.next.next() + } + } + } else { + self.next.next() + } + } +} diff --git a/src/generators.rs b/src/generators.rs index bb69047f1..c0fed4a50 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -8,3 +8,8 @@ mod silence; pub mod const_source { pub use super::silence::const_source::Silence; } + +/// Generators where the sample rate and channel may be passed in at runtime +pub mod fixed_source { + pub use super::silence::fixed_source::Silence; +} diff --git a/src/generators/silence.rs b/src/generators/silence.rs index a223f444b..eb64b1482 100644 --- a/src/generators/silence.rs +++ b/src/generators/silence.rs @@ -1,3 +1,66 @@ +pub mod fixed_source { + use crate::{ChannelCount, SampleRate}; + use crate::{FixedSource, nz}; + + /// A source producing an infinite amount of Silence. Like all generators you + /// probably want to limit the duration of this source. + /// + /// # Example + /// Padding a [`TakeDuration`](crate::fixed_source::Placeholder) to + /// guarantee an exact playtime: + /// + /// ```rust,no_run + /// # use std::time::Duration; + /// # use rodio::nz; + /// # use rodio::generators::fixed_source::Silence; + /// # use rodio::{ConstSource, FixedSource}; + /// # fn main() -> Result<(), Box> { + /// # let unknown_length = Silence::new(nz!(44100)); + /// let silence = Silence::new(nz!(44100)); + /// let two_seconds = unknown_length + /// .try_chain_source(silence)? + /// .take_duration(Duration::from_secs(2)); + /// # Ok(()) + /// # } + /// ``` + pub struct Silence { + sample_rate: SampleRate, + } + + impl Silence { + /// Create an infinite silence + pub fn new(sample_rate: SampleRate) -> Self { + Self { sample_rate } + } + } + + impl FixedSource for Silence { + fn channels(&self) -> ChannelCount { + nz!(1) + } + + fn sample_rate(&self) -> SampleRate { + self.sample_rate + } + + fn total_duration(&self) -> Option { + None + } + } + + impl Iterator for Silence { + type Item = crate::Sample; + + fn next(&mut self) -> Option { + Some(0.0) + } + + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } + } +} + pub mod const_source { use std::time::Duration; @@ -20,6 +83,7 @@ pub mod const_source { /// # use rodio::generators::const_source::Silence; /// # use rodio::ConstSource; /// # let unknown_length = Silence::new(); + /// /// let silence: Silence<44100> = Silence::new(); /// let two_seconds = unknown_length /// .chain_source(silence) From 900d34d860182bf7fba5fe97c299e27d4a93f6a8 Mon Sep 17 00:00:00 2001 From: Yara Date: Wed, 22 Jul 2026 17:51:42 +0200 Subject: [PATCH 10/11] add seeking and deduplicate --- CHANGELOG.md | 5 ++++ Cargo.toml | 2 +- examples/microphone.rs | 4 +-- src/const_source.rs | 54 +++++++++++++++++++------------------- src/fixed_source.rs | 50 +++++++++++++++++++++++++++++++++++ src/fixed_source/buffer.rs | 50 +++-------------------------------- src/fixed_source/chain.rs | 39 +++++---------------------- src/generators/silence.rs | 12 +++++++-- src/lib.rs | 2 +- src/microphone.rs | 23 ++-------------- src/microphone/builder.rs | 2 +- 11 files changed, 110 insertions(+), 133 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0f149dec..94b004b8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added `FixedSource` which mirrors `Source` but does not allow the sample rate or + channel count to change. +- Added `ConstSource` which is like `FixedSource` except the sample rate and + channel count are fixed at compile time. - Added `Skippable::skipped` function to check if the inner source was skipped. - All sources now implement `ExactSizeIterator` when their inner source does. - All sources now implement `Iterator::size_hint()`. @@ -21,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Breaking: `Microphone` now implements `FixedSource` - Breaking: `Done` now calls a callback instead of decrementing an `Arc`. - Updated `cpal` to v0.18. - Clarified `Source::current_span_len()` documentation to specify it returns total span length. diff --git a/Cargo.toml b/Cargo.toml index 1dfac553e..88ce817d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -250,7 +250,7 @@ required-features = ["playback", "wav"] [[example]] name = "microphone" -required-features = ["playback", "recording", "wav_output"] +required-features = ["playback", "recording"] [[example]] name = "mix_multiple_sources" diff --git a/examples/microphone.rs b/examples/microphone.rs index e196208a0..9568116a1 100644 --- a/examples/microphone.rs +++ b/examples/microphone.rs @@ -1,6 +1,6 @@ use inquire::Select; use rodio::microphone::{self, MicrophoneBuilder}; -use rodio::Source; +use rodio::FixedSource; use std::error::Error; use std::thread; use std::time::Duration; @@ -22,7 +22,7 @@ fn main() -> Result<(), Box> { println!("Playing the recording"); let mut output = rodio::DeviceSinkBuilder::open_default_sink()?; - output.mixer().add(recording); + output.mixer().add(recording.into_dynamic_source()); thread::sleep(Duration::from_secs(5)); diff --git a/src/const_source.rs b/src/const_source.rs index 0c215512c..99acbe483 100644 --- a/src/const_source.rs +++ b/src/const_source.rs @@ -58,33 +58,33 @@ pub trait ConstSource: Iterator { IntoDynamicSource { inner: self } } - /// Use this const source as if it's a fixed source which is generally - /// easier to work with since it drops the generics. The same effects are - /// available for both. - /// - /// # Example - /// - /// ```rust - /// # struct CustomEffect(S); - /// # use rodio::{FixedSource, ConstSource}; - /// # use rodio::generators::const_source; - /// - /// // Note custom effect can only wrap a FixedSource - /// fn apply_custom_effect(source: S) -> CustomEffect { - /// CustomEffect(source) - /// } - /// - /// let source = const_source::Silence::<44100>::new(); - /// let source = source.into_fixed_source(); - /// apply_custom_effect(source); - /// ``` - fn into_fixed_source(self) -> IntoFixedSource - where - Self: Sized, - { - IntoFixedSource { inner: self } - } - + /// Use this const source as if it's a fixed source which is generally + /// easier to work with since it drops the generics. The same effects are + /// available for both. + /// + /// # Example + /// + /// ```rust + /// # struct CustomEffect(S); + /// # use rodio::{FixedSource, ConstSource}; + /// # use rodio::generators::const_source; + /// + /// // Note custom effect can only wrap a FixedSource + /// fn apply_custom_effect(source: S) -> CustomEffect { + /// CustomEffect(source) + /// } + /// + /// let source = const_source::Silence::<44100>::new(); + /// let source = source.into_fixed_source(); + /// apply_custom_effect(source); + /// ``` + fn into_fixed_source(self) -> IntoFixedSource + where + Self: Sized, + { + IntoFixedSource { inner: self } + } + #[doc = include_str!("docs/collect_into_buffer.md")] fn collect_into_buffer(self) -> SamplesBuffer where diff --git a/src/fixed_source.rs b/src/fixed_source.rs index ecb9d2a9f..5a8a1b25d 100644 --- a/src/fixed_source.rs +++ b/src/fixed_source.rs @@ -2,6 +2,7 @@ //! channel count. use std::time::Duration; +use crate::source::SeekError; use crate::{ChannelCount, ConstSource, Sample, SampleRate}; mod buffer; @@ -23,6 +24,14 @@ pub trait FixedSource: Iterator { /// `None` indicates at the same time "infinite" or "unknown". fn total_duration(&self) -> Option; + #[allow(unused_variables)] + #[doc = include_str!("docs/try_seek.md")] + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + Err(SeekError::NotSupported { + underlying_source: std::any::type_name::(), + }) + } + /// Tries to convert from a fixed source to a const one assuming /// the parameters already match. If they do not this returns an error. /// @@ -56,6 +65,18 @@ pub trait FixedSource: Iterator { IntoDynamicSource(self) } + #[doc = include_str!("docs/collect_into_buffer.md")] + fn collect_into_buffer(self) -> SamplesBuffer + where + Self: Sized, + { + SamplesBuffer::new( + self.channels(), + self.sample_rate(), + self.collect::>(), + ) + } + /// Add another source to play directly after this one. /// /// # Example @@ -106,6 +127,35 @@ where inner: std::marker::PhantomData, } +impl Placeholder +where + S: FixedSource, +{ + /// placeholder + pub fn record(self) -> Placeholder { + self + } +} + +impl FixedSource for Placeholder { + fn channels(&self) -> ChannelCount { + unimplemented!("placeholder") + } + fn sample_rate(&self) -> SampleRate { + unimplemented!("placeholder") + } + fn total_duration(&self) -> Option { + unimplemented!("placeholder") + } +} + +impl Iterator for Placeholder { + type Item = Sample; + fn next(&mut self) -> Option { + unimplemented!("placeholder") + } +} + /// A [`ConstSource`] adapted from a [`FixedSource`]. pub struct IntoConstSource(S); diff --git a/src/fixed_source/buffer.rs b/src/fixed_source/buffer.rs index dcd5bfaa2..70618e6bd 100644 --- a/src/fixed_source/buffer.rs +++ b/src/fixed_source/buffer.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use std::time::Duration; +use crate::source::SeekError; use crate::FixedSource; use crate::{ChannelCount, Sample, SampleRate}; @@ -19,7 +20,7 @@ impl SamplesBuffer { where D: Into>, { - let data: Arc<[f32]> = data.into().into(); + let data: Arc<[Sample]> = data.into().into(); SamplesBuffer { data, pos: 0, @@ -38,52 +39,9 @@ impl FixedSource for SamplesBuffer { fn sample_rate(&self) -> SampleRate { self.sample_rate } - /// # Panics - /// If the length of the buffer is larger than approximately 16 billion elements. - /// This is because the calculation of the duration would overflow. - #[inline] - fn total_duration(&self) -> Option { - let duration_ns = 1_000_000_000u64 - .checked_mul(self.data.len() as u64) - .unwrap() - / self.sample_rate.get() as u64 - / self.channels.get() as u64; - let duration = Duration::new( - duration_ns / 1_000_000_000, - (duration_ns % 1_000_000_000) as u32, - ); - - Some(duration) - } - // /// This jumps in memory till the sample for `pos`. - // #[inline] - // fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - // // This is fast because all the samples are in memory already - // // and due to the constant sample_rate we can jump to the right - // // sample directly. - // let curr_channel = self.pos % self.channels() as usize; - // let new_pos = pos.as_secs_f32() * self.sample_rate() as f32 * self.channels() as f32; - // // saturate pos at the end of the source - // let new_pos = new_pos as usize; - // let new_pos = new_pos.min(self.data.len()); - // // make sure the next sample is for the right channel - // let new_pos = new_pos.next_multiple_of(self.channels() as usize); - // let new_pos = new_pos - curr_channel; - // self.pos = new_pos; - // Ok(()) - // } + crate::common::source::buffer::source_impl! {} } impl Iterator for SamplesBuffer { - type Item = Sample; - #[inline] - fn next(&mut self) -> Option { - let sample = self.data.get(self.pos)?; - self.pos += 1; - Some(*sample) - } - #[inline] - fn size_hint(&self) -> (usize, Option) { - (self.data.len(), Some(self.data.len())) - } + crate::common::source::buffer::iter_impl! {} } diff --git a/src/fixed_source/chain.rs b/src/fixed_source/chain.rs index c34a947b1..27fb5b3c1 100644 --- a/src/fixed_source/chain.rs +++ b/src/fixed_source/chain.rs @@ -7,8 +7,8 @@ use crate::{ChannelCount, SampleRate}; /// Two chained sources, the one played after the other. #[derive(Clone)] pub struct SourceChain { - inner: S1, - next: S2, + first: S1, + second: S2, playing_inner: bool, } @@ -43,44 +43,19 @@ impl SourceChain { }) } else { Ok(SourceChain { - inner: s1, - next: s2, + first: s1, + second: s2, playing_inner: true, }) } } } +pub use crate::common::source::chain::ChainSeekError; impl FixedSource for SourceChain { - fn channels(&self) -> ChannelCount { - self.inner.channels() - } - - fn sample_rate(&self) -> SampleRate { - self.inner.sample_rate() - } - - fn total_duration(&self) -> Option { - self.inner - .total_duration() - .and_then(|d| self.next.total_duration().map(|d2| d2 + d)) - } + crate::common::source::chain::source_impl! {} } impl Iterator for SourceChain { - type Item = Sample; - - fn next(&mut self) -> Option { - if self.playing_inner { - match self.inner.next() { - Some(sample) => Some(sample), - None => { - self.playing_inner = false; - self.next.next() - } - } - } else { - self.next.next() - } - } + crate::common::source::chain::iter_impl! {} } diff --git a/src/generators/silence.rs b/src/generators/silence.rs index eb64b1482..c0704bdf6 100644 --- a/src/generators/silence.rs +++ b/src/generators/silence.rs @@ -1,6 +1,9 @@ pub mod fixed_source { + use std::time::Duration; + + use crate::source::SeekError; + use crate::{nz, FixedSource}; use crate::{ChannelCount, SampleRate}; - use crate::{FixedSource, nz}; /// A source producing an infinite amount of Silence. Like all generators you /// probably want to limit the duration of this source. @@ -46,6 +49,11 @@ pub mod fixed_source { fn total_duration(&self) -> Option { None } + + /// This does nothing since all silence is equal :3 + fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { + Ok(()) + } } impl Iterator for Silence { @@ -105,7 +113,7 @@ pub mod const_source { } impl ConstSource for Silence { - fn total_duration(&self) -> Option { + fn total_duration(&self) -> Option { None } diff --git a/src/lib.rs b/src/lib.rs index 229f0e6af..034912b87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -224,10 +224,10 @@ pub mod stream; mod wav_output; pub mod buffer; -pub mod const_source; pub mod conversions; pub mod decoder; +pub mod const_source; pub mod fixed_source; pub mod generators; diff --git a/src/microphone.rs b/src/microphone.rs index 77b9b4839..c1b3b2502 100644 --- a/src/microphone.rs +++ b/src/microphone.rs @@ -4,7 +4,7 @@ //! //! ```no_run //! use rodio::microphone::MicrophoneBuilder; -//! use rodio::Source; +//! use rodio::FixedSource; //! use std::time::Duration; //! //! # fn main() -> Result<(), Box> { @@ -106,7 +106,7 @@ use std::time::Duration; use crate::common::assert_error_traits; use crate::conversions::SampleTypeConverter; -use crate::{Sample, Source}; +use crate::Sample; mod builder; mod config; @@ -188,25 +188,6 @@ pub struct Microphone { config: InputConfig, } -impl Source for Microphone { - fn current_span_len(&self) -> Option { - None - } - - fn channels(&self) -> crate::ChannelCount { - self.config.channel_count - } - - fn sample_rate(&self) -> crate::SampleRate { - self.config.sample_rate - } - - fn total_duration(&self) -> Option { - None - } -} - -#[cfg(feature = "experimental")] impl crate::FixedSource for Microphone { fn channels(&self) -> crate::ChannelCount { self.config.channel_count diff --git a/src/microphone/builder.rs b/src/microphone/builder.rs index a698424bd..69afe1646 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -538,7 +538,7 @@ where /// # Example /// ```no_run /// # use rodio::microphone::MicrophoneBuilder; - /// # use rodio::Source; + /// # use rodio::FixedSource; /// # use std::time::Duration; /// let mic = MicrophoneBuilder::new() /// .default_device()? From ddfa50fabe6c297e11459358b9a06ac17d43dd15 Mon Sep 17 00:00:00 2001 From: Yara Date: Sun, 23 Aug 2026 21:28:24 +0200 Subject: [PATCH 11/11] review feedback --- src/const_source.rs | 4 ++++ src/docs/collect_into_buffer.md | 2 +- src/fixed_source.rs | 10 +++++++++- src/fixed_source/chain.rs | 6 ++++-- src/generators/silence.rs | 18 ++++++++---------- src/source/chain.rs | 2 +- 6 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/const_source.rs b/src/const_source.rs index 99acbe483..98d131924 100644 --- a/src/const_source.rs +++ b/src/const_source.rs @@ -211,4 +211,8 @@ where fn total_duration(&self) -> Option { self.inner.total_duration() } + + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + self.inner.try_seek(pos) + } } diff --git a/src/docs/collect_into_buffer.md b/src/docs/collect_into_buffer.md index d97b115bc..04f512c4c 100644 --- a/src/docs/collect_into_buffer.md +++ b/src/docs/collect_into_buffer.md @@ -1,4 +1,4 @@ -Builds a new `SamplesBuffer`. +Consume the source and store all the samples in RAM using a `SamplesBuffer`. # Panics - Panics if the length of the buffer is larger than approximately 16 billion elements. diff --git a/src/fixed_source.rs b/src/fixed_source.rs index 5a8a1b25d..1ae72edc0 100644 --- a/src/fixed_source.rs +++ b/src/fixed_source.rs @@ -9,7 +9,7 @@ mod buffer; mod chain; pub use buffer::SamplesBuffer; -pub use chain::SourceChain; +pub use chain::{ParamsMismatch, SourceChain}; /// Similar to `Source`, something that can produce interleaved samples for a /// fixed amount of channels at a fixed sample rate. Those parameters never @@ -165,6 +165,10 @@ impl ConstSource fn total_duration(&self) -> Option { self.0.total_duration() } + + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + self.0.try_seek(pos) + } } impl Iterator for IntoConstSource { @@ -218,6 +222,10 @@ impl crate::DynamicSource for IntoDynamicSource { fn total_duration(&self) -> Option { self.0.total_duration() } + + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + self.0.try_seek(pos) + } } impl Iterator for IntoDynamicSource { diff --git a/src/fixed_source/chain.rs b/src/fixed_source/chain.rs index 27fb5b3c1..e269d0550 100644 --- a/src/fixed_source/chain.rs +++ b/src/fixed_source/chain.rs @@ -9,9 +9,11 @@ use crate::{ChannelCount, SampleRate}; pub struct SourceChain { first: S1, second: S2, - playing_inner: bool, + playing_first: bool, } +/// Error returned by [`FixedSource::try_chain_source`] when chaining two sources with +/// mismatching parameters. #[derive(Debug, Clone, Copy, thiserror::Error, PartialEq, Eq)] pub struct ParamsMismatch { sample_rate_self: SampleRate, @@ -45,7 +47,7 @@ impl SourceChain { Ok(SourceChain { first: s1, second: s2, - playing_inner: true, + playing_first: true, }) } } diff --git a/src/generators/silence.rs b/src/generators/silence.rs index c0704bdf6..ff8d27479 100644 --- a/src/generators/silence.rs +++ b/src/generators/silence.rs @@ -1,7 +1,10 @@ -pub mod fixed_source { - use std::time::Duration; +use crate::source::SeekError; +use crate::Sample; +use dasp_sample::Sample as _; +use std::time::Duration; - use crate::source::SeekError; +pub mod fixed_source { + use super::*; use crate::{nz, FixedSource}; use crate::{ChannelCount, SampleRate}; @@ -60,7 +63,7 @@ pub mod fixed_source { type Item = crate::Sample; fn next(&mut self) -> Option { - Some(0.0) + Some(Sample::EQUILIBRIUM) } fn size_hint(&self) -> (usize, Option) { @@ -70,13 +73,8 @@ pub mod fixed_source { } pub mod const_source { - use std::time::Duration; - - use crate::source::SeekError; + use super::*; use crate::ConstSource; - use crate::Sample; - - use dasp_sample::Sample as _; /// A source producing an infinite amount of Silence. Like all generators you /// probably want to limit the duration of this source. diff --git a/src/source/chain.rs b/src/source/chain.rs index e64ab88cd..310c929b7 100644 --- a/src/source/chain.rs +++ b/src/source/chain.rs @@ -78,7 +78,7 @@ where // The transition between sources must be a span boundary. We propagate the current // source's span length directly. When the source is exhausted it already returns Some(0), // which correctly signals end-of-span. The None case (empty iterator) is likewise - // signalled as Some(0). + // signaled as Some(0). match &self.current_source { None => Some(0), Some(src) => src.current_span_len(),