From a365e10a5c12cd4d76e502086b7b23bb1c44a2bd Mon Sep 17 00:00:00 2001 From: Yara Date: Tue, 21 Jul 2026 17:58:11 +0200 Subject: [PATCH 01/14] 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/14] 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/14] 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/14] 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/14] 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 1aa5d673bb8e91aa52f944329950b285c1527b8d Mon Sep 17 00:00:00 2001 From: Yara Date: Tue, 21 Jul 2026 17:58:11 +0200 Subject: [PATCH 06/14] 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 0a1a89b8f305d05c1e701f0afe18cf99a88522d0 Mon Sep 17 00:00:00 2001 From: Yara Date: Wed, 22 Jul 2026 17:51:42 +0200 Subject: [PATCH 07/14] 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 296965088f18a95628df610f3a0f366f8ffadcbe Mon Sep 17 00:00:00 2001 From: Yara Date: Tue, 21 Jul 2026 17:58:11 +0200 Subject: [PATCH 08/14] 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 fa0ed7d8e..79fe8fa8c 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; @@ -17,6 +80,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 429345abb3ad80be846acf135ad9b89d64923cdc Mon Sep 17 00:00:00 2001 From: Yara Date: Wed, 22 Jul 2026 17:51:42 +0200 Subject: [PATCH 09/14] 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 79fe8fa8c..baa7516f8 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 { @@ -102,7 +110,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 4594830fa3767b664bea9b4cd77b4cb86006e843 Mon Sep 17 00:00:00 2001 From: Yara Date: Thu, 23 Jul 2026 22:18:18 +0200 Subject: [PATCH 10/14] Replace inner, inner_mut & into_inner impl with macro --- src/common/source.rs | 24 ++++++++++++++++++++++ src/conversions/channels.rs | 25 +++++------------------ src/conversions/sample.rs | 22 +++----------------- src/source/amplify.rs | 20 ++---------------- src/source/automatic_gain_control/agc.rs | 22 +++++++------------- src/source/channel_volume.rs | 26 +++++------------------- src/source/delay.rs | 22 +++----------------- src/source/distortion.rs | 20 ++---------------- src/source/done.rs | 20 +++--------------- src/source/from_iter.rs | 22 +++----------------- src/source/linear_ramp.rs | 22 +++----------------- src/source/pausable.rs | 22 +++----------------- src/source/periodic.rs | 24 ++++------------------ src/source/position.rs | 20 ++---------------- src/source/skip.rs | 22 +++----------------- src/source/skippable.rs | 20 ++---------------- src/source/speed.rs | 22 +++----------------- src/source/stoppable.rs | 20 ++---------------- src/source/take.rs | 24 ++++------------------ 19 files changed, 83 insertions(+), 336 deletions(-) diff --git a/src/common/source.rs b/src/common/source.rs index 6798c3555..ec6b65347 100644 --- a/src/common/source.rs +++ b/src/common/source.rs @@ -18,3 +18,27 @@ pub(crate) mod buffer; pub(crate) mod chain; + +macro_rules! add_inner_accessors { + ($inner:ident) => { + /// Get immutable access to the input to this source + #[inline] + pub fn inner(&self) -> &S { + &self.$inner + } + + /// Returns a mutable reference to the inner source. + #[inline] + pub fn inner_mut(&mut self) -> &mut S { + &mut self.$inner + } + + /// Returns the inner source. + #[inline] + pub fn into_inner(self) -> S { + self.$inner + } + }; +} + +pub(crate) use add_inner_accessors; diff --git a/src/conversions/channels.rs b/src/conversions/channels.rs index 94bcf4428..909f2fb43 100644 --- a/src/conversions/channels.rs +++ b/src/conversions/channels.rs @@ -1,3 +1,4 @@ +use crate::common::source::add_inner_accessors; use crate::common::ChannelCount; use crate::{Sample, Source}; use dasp_sample::Sample as _; @@ -18,13 +19,13 @@ where next_output_sample_pos: u16, } -impl ChannelCountConverter +impl ChannelCountConverter where - I: Iterator, + S: Iterator, { /// Initializes the iterator. #[inline] - pub fn new(input: I, from: ChannelCount, to: ChannelCount) -> ChannelCountConverter { + pub fn new(input: S, from: ChannelCount, to: ChannelCount) -> ChannelCountConverter { ChannelCountConverter { input, from, @@ -34,23 +35,7 @@ where } } - /// Destroys this iterator and returns the underlying iterator. - #[inline] - pub fn into_inner(self) -> I { - self.input - } - - /// Get immutable access to the underlying iterator. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Get mutable access to the underlying iterator. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } + add_inner_accessors! {input} } impl Iterator for ChannelCountConverter diff --git a/src/conversions/sample.rs b/src/conversions/sample.rs index cf7094308..0a1080214 100644 --- a/src/conversions/sample.rs +++ b/src/conversions/sample.rs @@ -11,33 +11,17 @@ pub struct SampleTypeConverter { marker: PhantomData, } -impl SampleTypeConverter { +impl SampleTypeConverter { /// Builds a new converter. #[inline] - pub fn new(input: I) -> SampleTypeConverter { + pub fn new(input: S) -> SampleTypeConverter { SampleTypeConverter { input, marker: PhantomData, } } - /// Destroys this iterator and returns the underlying iterator. - #[inline] - pub fn into_inner(self) -> I { - self.input - } - - /// Get immutable access to the underlying iterator. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Get mutable access to the underlying iterator. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } + crate::common::source::add_inner_accessors! {input} } impl Iterator for SampleTypeConverter diff --git a/src/source/amplify.rs b/src/source/amplify.rs index c9dcec486..fb8ab8b47 100644 --- a/src/source/amplify.rs +++ b/src/source/amplify.rs @@ -21,7 +21,7 @@ pub struct Amplify { factor: Float, } -impl Amplify { +impl Amplify { /// Modifies the amplification factor. #[inline] pub fn set_factor(&mut self, factor: Float) { @@ -34,23 +34,7 @@ impl Amplify { self.factor = math::db_to_linear(factor); } - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } + crate::common::source::add_inner_accessors! {input} } impl Iterator for Amplify diff --git a/src/source/automatic_gain_control/agc.rs b/src/source/automatic_gain_control/agc.rs index 2afec0202..54f4db8ac 100644 --- a/src/source/automatic_gain_control/agc.rs +++ b/src/source/automatic_gain_control/agc.rs @@ -112,9 +112,9 @@ pub struct AutomaticGainControl { slow_down_state: SlowDownState, } -impl AutomaticGainControl +impl AutomaticGainControl where - I: Source, + S: Source, { /// Constructs an `AutomaticGainControl` object with specified parameters. /// @@ -129,16 +129,16 @@ where /// `floor` - The minimum output level (gain floor) that the AGC will not go below #[inline] pub(crate) fn new( - input: I, + input: S, target_level: Float, attack_time: Duration, release_time: Duration, absolute_max_gain: Float, peak_tracking_window: Duration, floor: Float, - ) -> AutomaticGainControl + ) -> AutomaticGainControl where - I: Source, + S: Source, { let sample_rate = input.sample_rate(); let attack_duration = duration_to_float(attack_time); @@ -377,7 +377,7 @@ where } #[inline] - fn process_sample(&mut self, sample: I::Item) -> I::Item { + fn process_sample(&mut self, sample: S::Item) -> S::Item { // Cache atomic loads at the start - avoids repeated atomic operations let target_level = self.target_level(); let absolute_max_gain = self.absolute_max_gain(); @@ -468,15 +468,7 @@ where sample * self.current_gain } - /// Returns an immutable reference to the inner source. - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } + crate::common::source::add_inner_accessors! {input} } impl Iterator for AutomaticGainControl diff --git a/src/source/channel_volume.rs b/src/source/channel_volume.rs index 29d5a59ba..97367ccf6 100644 --- a/src/source/channel_volume.rs +++ b/src/source/channel_volume.rs @@ -19,14 +19,14 @@ where current_sample: Option, } -impl ChannelVolume +impl ChannelVolume where - I: Source, + S: Source, { /// Wrap the input source and make it mono. Play that mono sound to each /// channel at the volume set by the user. The volume can be changed using /// [`ChannelVolume::set_volume`]. - pub fn new(input: I, channel_volumes: Vec) -> ChannelVolume { + pub fn new(input: S, channel_volumes: Vec) -> ChannelVolume { let channel_count = channel_volumes.len(); // See next() implementation. ChannelVolume { input, @@ -42,28 +42,12 @@ where self.channel_volumes[channel] = volume; } - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } - /// Gets the volume for a given channel number. Returns `None` if channel number is invalid. pub fn volume(&self, channel: usize) -> Option { self.channel_volumes.get(channel).copied() } + + crate::common::source::add_inner_accessors! {input} } impl Iterator for ChannelVolume diff --git a/src/source/delay.rs b/src/source/delay.rs index 5c835dc55..25d37f3d2 100644 --- a/src/source/delay.rs +++ b/src/source/delay.rs @@ -35,27 +35,11 @@ pub struct Delay { requested_duration: Duration, } -impl Delay +impl Delay where - I: Source, + S: Source, { - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } + crate::common::source::add_inner_accessors! {input} } impl Iterator for Delay diff --git a/src/source/distortion.rs b/src/source/distortion.rs index 381e9a0df..f01aa68ec 100644 --- a/src/source/distortion.rs +++ b/src/source/distortion.rs @@ -24,7 +24,7 @@ pub struct Distortion { threshold: Float, } -impl Distortion { +impl Distortion { /// Modifies the distortion gain. #[inline] pub fn set_gain(&mut self, gain: Float) { @@ -37,23 +37,7 @@ impl Distortion { self.threshold = threshold; } - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } + crate::common::source::add_inner_accessors! {input} } impl Iterator for Distortion diff --git a/src/source/done.rs b/src/source/done.rs index 64a51a500..80f1f70e2 100644 --- a/src/source/done.rs +++ b/src/source/done.rs @@ -30,24 +30,10 @@ where signal_sent: false, } } +} - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } +impl Done { + crate::common::source::add_inner_accessors! {input} } impl Iterator for Done diff --git a/src/source/from_iter.rs b/src/source/from_iter.rs index 0ade85bc6..1801cbee7 100644 --- a/src/source/from_iter.rs +++ b/src/source/from_iter.rs @@ -40,10 +40,10 @@ pub struct FromIter { sample_rate: SampleRate, } -impl FromIter { +impl FromIter { /// Creates a new `FromIter` from an iterator and audio parameters. #[inline] - pub fn new(iter: I, channels: ChannelCount, sample_rate: SampleRate) -> Self { + pub fn new(iter: S, channels: ChannelCount, sample_rate: SampleRate) -> Self { Self { iter, channels, @@ -51,23 +51,7 @@ impl FromIter { } } - /// Destroys this source and returns the underlying iterator. - #[inline] - pub fn into_inner(self) -> I { - self.iter - } - - /// Get immutable access to the underlying iterator. - #[inline] - pub fn inner(&self) -> &I { - &self.iter - } - - /// Get mutable access to the underlying iterator. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.iter - } + crate::common::source::add_inner_accessors! {iter} } impl Iterator for FromIter diff --git a/src/source/linear_ramp.rs b/src/source/linear_ramp.rs index 4467e730d..68d65f6e3 100644 --- a/src/source/linear_ramp.rs +++ b/src/source/linear_ramp.rs @@ -46,27 +46,11 @@ pub struct LinearGainRamp { span: SpanTracker, } -impl LinearGainRamp +impl LinearGainRamp where - I: Source, + S: Source, { - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } + crate::common::source::add_inner_accessors! {input} } impl Iterator for LinearGainRamp diff --git a/src/source/pausable.rs b/src/source/pausable.rs index 1b05ab0d7..32fd2c720 100644 --- a/src/source/pausable.rs +++ b/src/source/pausable.rs @@ -34,9 +34,9 @@ pub struct Pausable { remaining_paused_samples: u16, } -impl Pausable +impl Pausable where - I: Source, + S: Source, { /// Sets whether the filter applies. /// @@ -56,23 +56,7 @@ where self.paused_channels.is_some() } - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } + crate::common::source::add_inner_accessors! {input} } impl Iterator for Pausable diff --git a/src/source/periodic.rs b/src/source/periodic.rs index b251ea316..a6ba5317f 100644 --- a/src/source/periodic.rs +++ b/src/source/periodic.rs @@ -41,29 +41,13 @@ pub struct PeriodicAccess { samples_until_update: usize, } -impl PeriodicAccess +impl PeriodicAccess where - I: Source, + S: Source, - F: FnMut(&mut I), + F: FnMut(&mut S), { - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } + crate::common::source::add_inner_accessors! {input} } impl Iterator for PeriodicAccess diff --git a/src/source/position.rs b/src/source/position.rs index f41848eb6..5b85f9539 100644 --- a/src/source/position.rs +++ b/src/source/position.rs @@ -28,24 +28,8 @@ pub struct TrackPosition { span: SpanTracker, } -impl TrackPosition { - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } +impl TrackPosition { + crate::common::source::add_inner_accessors! {input} } impl TrackPosition diff --git a/src/source/skip.rs b/src/source/skip.rs index e1656aee0..900a164e4 100644 --- a/src/source/skip.rs +++ b/src/source/skip.rs @@ -89,27 +89,11 @@ pub struct SkipDuration { skipped_duration: Duration, } -impl SkipDuration +impl SkipDuration where - I: Source, + S: Source, { - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } + crate::common::source::add_inner_accessors! {input} } impl Iterator for SkipDuration diff --git a/src/source/skippable.rs b/src/source/skippable.rs index bfa973998..73eb73358 100644 --- a/src/source/skippable.rs +++ b/src/source/skippable.rs @@ -23,7 +23,7 @@ pub struct Skippable { do_skip: bool, } -impl Skippable { +impl Skippable { /// Skips the current source #[inline] pub fn skip(&mut self) { @@ -36,23 +36,7 @@ impl Skippable { self.do_skip } - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } + crate::common::source::add_inner_accessors! {input} } impl Iterator for Skippable diff --git a/src/source/speed.rs b/src/source/speed.rs index 5d599a132..a9bcb5dea 100644 --- a/src/source/speed.rs +++ b/src/source/speed.rs @@ -64,9 +64,9 @@ pub struct Speed { factor: f32, } -impl Speed +impl Speed where - I: Source, + S: Source, { /// Modifies the speed factor. #[inline] @@ -74,23 +74,7 @@ where self.factor = factor; } - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } + crate::common::source::add_inner_accessors! {input} } impl Iterator for Speed diff --git a/src/source/stoppable.rs b/src/source/stoppable.rs index f89e0017c..12c412e90 100644 --- a/src/source/stoppable.rs +++ b/src/source/stoppable.rs @@ -19,30 +19,14 @@ pub struct Stoppable { stopped: bool, } -impl Stoppable { +impl Stoppable { /// Stops the sound. #[inline] pub fn stop(&mut self) { self.stopped = true; } - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } + crate::common::source::add_inner_accessors! {input} } impl Iterator for Stoppable diff --git a/src/source/take.rs b/src/source/take.rs index fb6ef2864..631a2f4f2 100644 --- a/src/source/take.rs +++ b/src/source/take.rs @@ -56,35 +56,19 @@ pub struct TakeDuration { silence_samples_remaining: usize, } -impl TakeDuration +impl TakeDuration where - I: Source, + S: Source, { /// Returns the duration elapsed for each sample extracted. #[inline] - fn get_duration_per_sample(input: &I) -> Duration { + fn get_duration_per_sample(input: &S) -> Duration { let ns = NANOS_PER_SEC / (input.sample_rate().get() as u64 * input.channels().get() as u64); // \|/ the maximum value of `ns` is one billion, so this can't fail Duration::new(0, ns as u32) } - /// Returns a reference to the inner source. - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Returns the inner source. - #[inline] - pub fn into_inner(self) -> I { - self.input - } + crate::common::source::add_inner_accessors! {input} /// Make the truncated source end with a FadeOut. The fadeout covers the /// entire length of the take source. From 1d975e254730e36a5a375bd94c55cdab8029ac93 Mon Sep 17 00:00:00 2001 From: Yara Date: Thu, 23 Jul 2026 22:18:18 +0200 Subject: [PATCH 11/14] add channel conversions to fixed and const source --- src/const_source.rs | 10 +++ src/const_source/conversions.rs | 1 + src/const_source/conversions/channel_count.rs | 78 +++++++++++++++++ src/fixed_source.rs | 12 ++- src/fixed_source/conversions.rs | 1 + src/fixed_source/conversions/channel_count.rs | 84 +++++++++++++++++++ 6 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/const_source/conversions.rs create mode 100644 src/const_source/conversions/channel_count.rs create mode 100644 src/fixed_source/conversions.rs create mode 100644 src/fixed_source/conversions/channel_count.rs diff --git a/src/const_source.rs b/src/const_source.rs index 99acbe483..29120d1d2 100644 --- a/src/const_source.rs +++ b/src/const_source.rs @@ -21,9 +21,11 @@ use crate::Source as DynamicSource; // Source will (probably) be renamed to this mod buffer; mod chain; +mod conversions; pub use buffer::SamplesBuffer; pub use chain::SourceChain; +pub use conversions::channel_count::ChannelConvertor; /// A source which sample rate and channel count are fixed at compile time. pub trait ConstSource: Iterator { @@ -47,6 +49,14 @@ pub trait ConstSource: Iterator { }) } + /// Convert from the current channel count to `CH_OUT`. + fn with_channel_count(self) -> ChannelConvertor + where + Self: Sized, + { + ChannelConvertor::new(self) + } + /// 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/conversions.rs b/src/const_source/conversions.rs new file mode 100644 index 000000000..f648f8500 --- /dev/null +++ b/src/const_source/conversions.rs @@ -0,0 +1 @@ +pub mod channel_count; diff --git a/src/const_source/conversions/channel_count.rs b/src/const_source/conversions/channel_count.rs new file mode 100644 index 000000000..9c6ed55de --- /dev/null +++ b/src/const_source/conversions/channel_count.rs @@ -0,0 +1,78 @@ +use std::time::Duration; + +use crate::source::SeekError; +use crate::{ConstSource, Sample}; + +/// Converts between two channel counts, created using +/// [with_channel_count](ConstSource::with_channel_count) +pub struct ChannelConvertor { + input: S, + sample_repeat: Option, + next_output_sample_pos: u16, +} + +impl> + ChannelConvertor +{ + pub(crate) fn new(input: S) -> Self { + Self { + input, + sample_repeat: None, + next_output_sample_pos: 0, + } + } + + crate::common::source::add_inner_accessors! {input} +} + +impl> + ConstSource for ChannelConvertor +{ + fn total_duration(&self) -> Option { + self.input.total_duration() + } + + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + self.input.try_seek(pos) + } +} + +impl> Iterator + for ChannelConvertor +{ + type Item = Sample; + + fn next(&mut self) -> Option { + let result = match self.next_output_sample_pos { + 0 => { + // save first sample for mono -> stereo conversion + let value = self.input.next(); + self.sample_repeat = value; + value + } + x if x < CH_IN => { + // make sure we always end on a frame boundary + let value = self.input.next(); + assert!(value.is_some(), "Sources may not emit half frames"); + value + } + 1 => self.sample_repeat, + _ => Some(0.0), // all other added channels are empty + }; + + if result.is_some() { + self.next_output_sample_pos += 1; + } + + if self.next_output_sample_pos == CH_OUT { + self.next_output_sample_pos = 0; + + if CH_IN > CH_OUT { + for _ in CH_OUT..CH_IN { + self.input.next(); // discarding extra input + } + } + } + result + } +} diff --git a/src/fixed_source.rs b/src/fixed_source.rs index 5a8a1b25d..e0bcbaa0c 100644 --- a/src/fixed_source.rs +++ b/src/fixed_source.rs @@ -7,9 +7,11 @@ use crate::{ChannelCount, ConstSource, Sample, SampleRate}; mod buffer; mod chain; +mod conversions; pub use buffer::SamplesBuffer; pub use chain::SourceChain; +pub use conversions::channel_count::ChannelConverter; /// Similar to `Source`, something that can produce interleaved samples for a /// fixed amount of channels at a fixed sample rate. Those parameters never @@ -32,12 +34,20 @@ pub trait FixedSource: Iterator { }) } + /// Convert from the current channel count to `channel_count`. + fn with_channel_count(self, channel_count: ChannelCount) -> ChannelConverter + where + Self: Sized, + { + ChannelConverter::new(self, channel_count) + } + /// 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::placeholder) and - /// [`with_channel_count`](Self::placeholder). + /// [`with_channel_count`](Self::with_channel_count). fn try_into_const_source( self, ) -> Result, ParameterMismatch> diff --git a/src/fixed_source/conversions.rs b/src/fixed_source/conversions.rs new file mode 100644 index 000000000..f648f8500 --- /dev/null +++ b/src/fixed_source/conversions.rs @@ -0,0 +1 @@ +pub mod channel_count; diff --git a/src/fixed_source/conversions/channel_count.rs b/src/fixed_source/conversions/channel_count.rs new file mode 100644 index 000000000..0c6070489 --- /dev/null +++ b/src/fixed_source/conversions/channel_count.rs @@ -0,0 +1,84 @@ +use std::time::Duration; + +use crate::source::SeekError; +use crate::{ChannelCount, Sample}; +use crate::{FixedSource, SampleRate}; + +/// Converts between two channel counts, created using +/// [with_channel_count](FixedSource::with_channel_count) +pub struct ChannelConverter { + input: S, + pub(crate) target: ChannelCount, + sample_repeat: Option, + next_output_sample_pos: u16, +} + +impl ChannelConverter { + pub(crate) fn new(input: S, target: ChannelCount) -> Self { + Self { + input, + target, + sample_repeat: None, + next_output_sample_pos: 0, + } + } + + crate::common::source::add_inner_accessors! {input} +} + +impl FixedSource for ChannelConverter { + fn channels(&self) -> ChannelCount { + self.target + } + + fn sample_rate(&self) -> SampleRate { + self.input.sample_rate() + } + + fn total_duration(&self) -> Option { + self.input.total_duration() + } + + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + self.input.try_seek(pos) + } +} + +// TODO optimize (still assumes dynamicsource) +impl Iterator for ChannelConverter { + type Item = crate::Sample; + + fn next(&mut self) -> Option { + let result = match self.next_output_sample_pos { + 0 => { + // save first sample for mono -> stereo conversion + let value = self.input.next(); + self.sample_repeat = value; + value + } + x if x < self.input.channels().get() => { + // make sure we always end on a frame boundary + let value = self.input.next(); + assert!(value.is_some(), "Sources may not emit half frames"); + value + } + 1 => self.sample_repeat, + _ => Some(0.0), // all other added channels are empty + }; + + if result.is_some() { + self.next_output_sample_pos += 1; + } + + if self.next_output_sample_pos == self.target.get() { + self.next_output_sample_pos = 0; + + if self.input.channels() > self.target { + for _ in self.target.get()..self.input.channels().get() { + self.input.next(); // discarding extra input + } + } + } + result + } +} From 52aaf5c0bda3b06c90b5657a99b2cbc53bf9525a Mon Sep 17 00:00:00 2001 From: Yara Date: Sat, 25 Jul 2026 00:39:30 +0200 Subject: [PATCH 12/14] add resampler for fixed source --- codebook.toml | 1 + src/common/source.rs | 7 +- src/conversions/sample_rate/buffer.rs | 8 +- src/conversions/sample_rate/builder.rs | 4 +- src/conversions/sample_rate/mod.rs | 11 +- src/conversions/sample_rate/rubato.rs | 58 ++++-- src/fixed_source.rs | 14 +- src/fixed_source/conversions.rs | 1 + src/fixed_source/conversions/sample_rate.rs | 204 ++++++++++++++++++++ 9 files changed, 271 insertions(+), 37 deletions(-) create mode 100644 src/fixed_source/conversions/sample_rate.rs diff --git a/codebook.toml b/codebook.toml index f4122a49c..f1b9bee53 100644 --- a/codebook.toml +++ b/codebook.toml @@ -17,5 +17,6 @@ words = [ "stddev", "tpdf", "tpdf's", + "unsafelessly", "voss", ] diff --git a/src/common/source.rs b/src/common/source.rs index ec6b65347..d9c48ca7b 100644 --- a/src/common/source.rs +++ b/src/common/source.rs @@ -19,21 +19,22 @@ pub(crate) mod buffer; pub(crate) mod chain; +/// TODO(yara) this should become a trait really. macro_rules! add_inner_accessors { ($inner:ident) => { - /// Get immutable access to the input to this source + /// placeholder #[inline] pub fn inner(&self) -> &S { &self.$inner } - /// Returns a mutable reference to the inner source. + /// placeholder #[inline] pub fn inner_mut(&mut self) -> &mut S { &mut self.$inner } - /// Returns the inner source. + /// placeholder #[inline] pub fn into_inner(self) -> S { self.$inner diff --git a/src/conversions/sample_rate/buffer.rs b/src/conversions/sample_rate/buffer.rs index 844b6493f..2b890dc2d 100644 --- a/src/conversions/sample_rate/buffer.rs +++ b/src/conversions/sample_rate/buffer.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Write}; use super::{InSamples, OutFrameCount, OutSamples}; -use crate::{ChannelCount, Sample, SampleRate}; +use crate::{ChannelCount, Sample}; pub(crate) struct Input { pub samples: Box<[Sample]>, @@ -61,7 +61,6 @@ pub(crate) struct Output { pub samples: Box<[Sample]>, pub channels: ChannelCount, - pub source_rate: SampleRate, } impl Debug for Output { @@ -75,7 +74,6 @@ impl Debug for Output { &LimitLength(&self.samples[self.pos.raw()..self.end.raw()]), ) .field("channels", &self.channels) - .field("source_rate", &self.source_rate) .finish() } } @@ -114,7 +112,6 @@ impl Debug for LimitLength<'_> { impl Output { pub(super) fn new( - source_rate: SampleRate, channels: ChannelCount, capacity: OutFrameCount, ) -> Self { @@ -127,7 +124,6 @@ impl Output { end: OutSamples::ZERO, samples: samples.into_boxed_slice(), channels, - source_rate, } } @@ -135,7 +131,7 @@ impl Output { OutSamples(self.samples.len()).frames(self.channels) } - pub(super) fn len(&self) -> OutSamples { + pub(crate) fn len(&self) -> OutSamples { self.end - self.pos } diff --git a/src/conversions/sample_rate/builder.rs b/src/conversions/sample_rate/builder.rs index b30c0e4c0..6fe5af953 100644 --- a/src/conversions/sample_rate/builder.rs +++ b/src/conversions/sample_rate/builder.rs @@ -208,7 +208,7 @@ impl Default for SincConfigBuilder { /// let config = ResampleConfig::sinc().chunk_size(nz!(512)); /// let config = ResampleConfig::poly().degree(Poly::Cubic); /// ``` -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum ResampleConfig { /// Polynomial resampling (fast, no anti-aliasing) Poly { @@ -221,7 +221,7 @@ pub enum ResampleConfig { Sinc(Sinc), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct Sinc { /// Length of the windowed sinc interpolation filter pub sinc_len: usize, diff --git a/src/conversions/sample_rate/mod.rs b/src/conversions/sample_rate/mod.rs index 7df49d64c..d210cf7fd 100644 --- a/src/conversions/sample_rate/mod.rs +++ b/src/conversions/sample_rate/mod.rs @@ -84,10 +84,10 @@ use crate::{ Source, }; -mod buffer; +pub(crate) mod buffer; mod builder; -mod rubato; -mod types; +pub(crate) mod rubato; +pub(crate) mod types; pub(crate) use types::{InFrameCount, InSamples, OutFrameCount, OutSamples}; #[cfg(test)] mod tests; @@ -127,7 +127,7 @@ where fn clone(&self) -> Self { // Shallow clone: this resets filter state let source = self.inner().clone(); - SampleRateConverter::new(source, self.target_rate, self.config.clone()) + SampleRateConverter::new(source, self.target_rate, self.config) } } @@ -191,8 +191,7 @@ where .expect("Failed to create polynomial resampler"); ResampleInner::Poly(resampler) } - ResampleConfig::Sinc(sinc) => { - let mut sinc = sinc.clone(); + ResampleConfig::Sinc(mut sinc) => { #[cfg(feature = "rubato-fft")] if sinc.is_supported_fixed_ratio(target_rate, source_rate) { let resampler = RubatoFftResample::new( diff --git a/src/conversions/sample_rate/rubato.rs b/src/conversions/sample_rate/rubato.rs index df1ee0161..21ef3b6a9 100644 --- a/src/conversions/sample_rate/rubato.rs +++ b/src/conversions/sample_rate/rubato.rs @@ -45,13 +45,36 @@ pub enum ResampleInner { } impl ResampleInner { + pub fn can_reconfigure(&self) -> bool { + match self { + ResampleInner::Passthrough { .. } => false, + ResampleInner::Poly(resampler) | ResampleInner::Sinc(resampler) => { + resampler.output_delay_remaining == output_delay(&resampler.resampler) + } + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => { + resampler.output_delay_remaining == output_delay(&resampler.resampler) + } + } + } + /// Get a reference to the inner input source #[inline] pub fn input(&self) -> &I { match self { ResampleInner::Passthrough { source, .. } => source, - ResampleInner::Poly(resampler) => &resampler.input, - ResampleInner::Sinc(resampler) => &resampler.input, + ResampleInner::Poly(resampler) | ResampleInner::Sinc(resampler) => &resampler.input, + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => &resampler.input, + } + } + + /// Extract the inner input source, consuming the resampler + #[inline] + pub fn inner(&self) -> &I { + match self { + ResampleInner::Passthrough { source, .. } => source, + ResampleInner::Poly(resampler) | ResampleInner::Sinc(resampler) => &resampler.input, #[cfg(feature = "rubato-fft")] ResampleInner::Fft(resampler) => &resampler.input, } @@ -76,7 +99,7 @@ pub struct RubatoResample> { pub input: I, pub resampler: R, - pub input_buffer: super::buffer::Input, + pub(crate) input_buffer: super::buffer::Input, pub(crate) output: super::buffer::Output, /// The following are cached at construction for parameter-change detection. @@ -88,15 +111,14 @@ pub struct RubatoResample> { pub frames_being_resampled: OutFrameCount, } -impl> RubatoResample { - /// Calculate the number of output samples to skip for delay compensation. - pub fn output_delay(resampler: &R) -> OutFrameCount { - // Skip delay-1 frames to align the first output frame with input position 0. - let delay_frames = resampler.output_delay(); - let delay_frames = delay_frames.saturating_sub(1); - OutFrameCount(delay_frames) - } +pub fn output_delay(resampler: &impl rubato::Resampler) -> OutFrameCount { + // Skip delay-1 frames to align the first output frame with input position 0. + let delay_frames = resampler.output_delay(); + let delay_frames = delay_frames.saturating_sub(1); + OutFrameCount(delay_frames) +} +impl> RubatoResample { pub fn span_length(&self) -> Option { if !self.output.is_empty() { // rest of the output buffer is rest of span @@ -119,7 +141,7 @@ impl> RubatoResample { self.resampler.reset(); self.output.reset(); self.pos_in_current_span = InSamples::ZERO; - self.output_delay_remaining = Self::output_delay(&self.resampler); + self.output_delay_remaining = output_delay(&self.resampler); } pub fn next_sample(&mut self) -> Option { @@ -239,14 +261,13 @@ impl RubatoAsyncResample { let input_buf_size = InFrameCount(resampler.input_frames_max()); let output_buf_size = OutFrameCount(resampler.output_frames_max()); - let initial_output_delay = - RubatoResample::>::output_delay(&resampler); + let initial_output_delay = output_delay(&resampler); Ok(Self { input, resampler, input_buffer: Input::new(input_buf_size.samples(channels)), - output: Output::new(source_rate, channels, output_buf_size), + output: Output::new(channels, output_buf_size), pos_in_current_span: InSamples::ZERO, output_delay_remaining: initial_output_delay, resample_ratio, @@ -290,14 +311,13 @@ impl RubatoAsyncResample { let input_buf_size = InFrameCount(resampler.input_frames_max()); let output_buf_size = OutFrameCount(resampler.output_frames_max()); - let initial_output_delay = - RubatoResample::>::output_delay(&resampler); + let initial_output_delay = output_delay(&resampler); Ok(Self { input, resampler, input_buffer: Input::new(input_buf_size.samples(channels)), - output: Output::new(source_rate, channels, output_buf_size), + output: Output::new(channels, output_buf_size), pos_in_current_span: InSamples::ZERO, output_delay_remaining: initial_output_delay, resample_ratio, @@ -341,7 +361,7 @@ impl RubatoFftResample { let output_buf_size = OutFrameCount(resampler.output_frames_max()); let resample_ratio = target_rate.get() as Float / source_rate.get() as Float; - let output_delay_remaining = RubatoFftResample::::output_delay(&resampler); + let output_delay_remaining = output_delay(&resampler); Ok(Self { input, diff --git a/src/fixed_source.rs b/src/fixed_source.rs index e0bcbaa0c..0e7b2158a 100644 --- a/src/fixed_source.rs +++ b/src/fixed_source.rs @@ -12,6 +12,7 @@ mod conversions; pub use buffer::SamplesBuffer; pub use chain::SourceChain; pub use conversions::channel_count::ChannelConverter; +pub use conversions::sample_rate::SampleRateConvertor; /// Similar to `Source`, something that can produce interleaved samples for a /// fixed amount of channels at a fixed sample rate. Those parameters never @@ -34,6 +35,17 @@ pub trait FixedSource: Iterator { }) } + /// Convert from the current channel count to `channel count`. + /// + /// Though the defaults cover most use-cases you can configure + /// the resampler using [`with_config`](SampleRateConvertor::with_config). + fn with_sample_rate(self, sample_rate: SampleRate) -> SampleRateConvertor + where + Self: Sized, + { + SampleRateConvertor::new(self, sample_rate) + } + /// Convert from the current channel count to `channel_count`. fn with_channel_count(self, channel_count: ChannelCount) -> ChannelConverter where @@ -46,7 +58,7 @@ 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::placeholder) and + /// [`with_sample_rate`](Self::with_sample_rate) and /// [`with_channel_count`](Self::with_channel_count). fn try_into_const_source( self, diff --git a/src/fixed_source/conversions.rs b/src/fixed_source/conversions.rs index f648f8500..334551742 100644 --- a/src/fixed_source/conversions.rs +++ b/src/fixed_source/conversions.rs @@ -1 +1,2 @@ pub mod channel_count; +pub mod sample_rate; diff --git a/src/fixed_source/conversions/sample_rate.rs b/src/fixed_source/conversions/sample_rate.rs new file mode 100644 index 000000000..72da4083a --- /dev/null +++ b/src/fixed_source/conversions/sample_rate.rs @@ -0,0 +1,204 @@ +use crate::conversions::sample_rate::rubato::{ResampleInner, RubatoAsyncResample}; +use crate::conversions::Interpolation; +use crate::math::gcd; +use crate::source::ResampleConfig; +use crate::{FixedSource, Sample, SampleRate, Source}; + +use crate::conversions::sample_rate::{InSamples, OutSamples}; +use crate::fixed_source::IntoDynamicSource; + +/// Resamples an audio source to a target sample rate using Rubato. +pub struct SampleRateConvertor { + // Option so we can take out the source and rebuild the resampler without unsafe + inner: Option>>, + target_rate: SampleRate, +} + +#[derive(thiserror::Error)] +#[error("The resampler was already running")] +pub struct ResamplerRunning(SampleRateConvertor); + +impl SampleRateConvertor { + pub(crate) fn new(source: S, target_rate: SampleRate) -> Self { + Self { + inner: Some(Self::create_resampler( + source.into_dynamic_source(), + target_rate, + ResampleConfig::default(), + )), + target_rate, + } + } + + /// Further configure the resampler created with [`with_sample_rate`](FixedSource::with_sample_rate). + /// You usually do not need to do this unless you have a specific use-case that requires very + /// fast or extremely high quality resampling. The [`ResampleConfig`] has a number of factories + /// with good defaults for such use-cases. + /// + /// # Errors + /// If the resampler can no longer be reconfigured, usually after yielding + /// the first sample. + /// + /// # Example + /// ``` + /// # use rodio::generators::fixed_source::Silence; + /// # use rodio::SampleRate; + /// # fn hi() -> Option<()> { // to enable ? in the example + /// use rodio::FixedSource; + /// use rodio::conversions::ResampleConfig; + /// + /// let source = Silence::new(SampleRate::new(44_100)?); + /// let resampled = source + /// .with_sample_rate(SampleRate::new(48_000)?) + /// .with_config(ResampleConfig::fast()); + /// # Some(()) + /// # } + /// # hi().unwrap(); + /// ``` + #[allow(clippy::result_large_err, reason = "the Ok variant is the same size")] + pub fn with_config(mut self, config: ResampleConfig) -> Result> { + if !self.resampler().can_reconfigure() { + return Err(ResamplerRunning(self)); + } + + let source = self + .inner + .take() + .expect( + "we are the only ones who set this to none and we set it to \ + some at the end of this fn", + ) + .into_inner(); + + Ok(Self { + inner: Some(Self::create_resampler(source, self.target_rate, config)), + target_rate: self.target_rate, + }) + } + + fn resampler(&self) -> &ResampleInner> { + self.inner + .as_ref() + .expect("never none outside `with_config`") + } + + fn resampler_mut(&mut self) -> &mut ResampleInner> { + self.inner + .as_mut() + .expect("never none outside `with_config`") + } + + fn create_resampler( + source: IntoDynamicSource, + target_rate: SampleRate, + config: ResampleConfig, + ) -> ResampleInner> { + if source.sample_rate() == target_rate { + let channels = source.channels(); + ResampleInner::Passthrough { + source_rate: source.sample_rate(), + source, + input_span_pos: InSamples::ZERO, + channels, + } + } else { + match config { + ResampleConfig::Poly { degree, chunk_size } => { + let resampler = + RubatoAsyncResample::new_poly(source, target_rate, chunk_size, degree) + .expect("Failed to create polynomial resampler"); + ResampleInner::Poly(resampler) + } + ResampleConfig::Sinc(mut sinc) => { + #[cfg(feature = "rubato-fft")] + if sinc.is_supported_fixed_ratio(target_rate, source_rate) { + let resampler = RubatoFftResample::new( + source, + target_rate, + sinc.chunk_size, + sinc.sub_chunks, + ) + .expect("Failed to create FFT resampler"); + return ResampleInner::Fft(resampler); + } + + if sinc.is_supported_fixed_ratio(target_rate, source.sample_rate()) { + sinc.interpolation = Interpolation::Nearest; + let g = gcd(target_rate.get(), source.sample_rate().get()); + let numer = target_rate.get() / g; + let denom = source.sample_rate().get() / g; + let ratio = numer.max(denom) as usize; + sinc.oversampling_factor = ratio; + } + ResampleInner::Sinc(sinc.build(source, target_rate)) + } + } + } + } +} + +impl FixedSource for SampleRateConvertor { + fn channels(&self) -> crate::ChannelCount { + self.resampler().inner().channels() + } + + fn sample_rate(&self) -> SampleRate { + self.target_rate + } + + fn total_duration(&self) -> Option { + self.resampler().inner().total_duration() + } +} + +impl Iterator for SampleRateConvertor +where + S: FixedSource, +{ + type Item = Sample; + + #[inline] + fn next(&mut self) -> Option { + match self.resampler_mut() { + ResampleInner::Passthrough { source, .. } => source.next(), + ResampleInner::Poly(resampler) => resampler.next_sample(), + ResampleInner::Sinc(resampler) => resampler.next_sample(), + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => resampler.next_sample(), + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + match self.resampler() { + ResampleInner::Passthrough { source, .. } => source.size_hint(), + ResampleInner::Poly(resampler) | ResampleInner::Sinc(resampler) => { + let adjusted_for_resampling = |samples| { + InSamples(samples).resampled_by(resampler.resample_ratio) + + resampler.output.len() + + resampler + .frames_being_resampled + .samples(resampler.output.channels) + }; + let (lower, upper) = resampler.input.size_hint(); + let lower = adjusted_for_resampling(lower); + let upper = upper.map(adjusted_for_resampling); + (lower.raw(), upper.as_ref().map(OutSamples::raw)) + } + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => { + let adjusted_for_resampling = |samples| { + InSamples(samples).resampled_by(resampler.resample_ratio) + + resampler.output.len() + + resampler + .frames_being_resampled + .samples(resampler.output.channels) + }; + let (lower, upper) = resampler.input.size_hint(); + let lower = adjusted_for_resampling(lower); + let upper = upper.map(adjusted_for_resampling); + (lower.raw(), upper.as_ref().map(OutSamples::raw)) + } + } + } +} From 6ba8b725d573cb32d73103c6af884795cd1f9af2 Mon Sep 17 00:00:00 2001 From: Yara Date: Mon, 27 Jul 2026 18:23:55 +0200 Subject: [PATCH 13/14] add resampler for const source --- src/const_source.rs | 12 ++ src/const_source/conversions.rs | 1 + src/const_source/conversions/sample_rate.rs | 213 ++++++++++++++++++++ src/conversions/sample_rate/buffer.rs | 5 +- src/conversions/sample_rate/rubato.rs | 2 +- src/fixed_source.rs | 2 +- src/fixed_source/conversions/sample_rate.rs | 9 +- 7 files changed, 235 insertions(+), 9 deletions(-) create mode 100644 src/const_source/conversions/sample_rate.rs diff --git a/src/const_source.rs b/src/const_source.rs index 29120d1d2..2f9962a9b 100644 --- a/src/const_source.rs +++ b/src/const_source.rs @@ -26,6 +26,7 @@ mod conversions; pub use buffer::SamplesBuffer; pub use chain::SourceChain; pub use conversions::channel_count::ChannelConvertor; +pub use conversions::sample_rate::SampleRateConvertor; /// A source which sample rate and channel count are fixed at compile time. pub trait ConstSource: Iterator { @@ -49,6 +50,17 @@ pub trait ConstSource: Iterator { }) } + /// Convert from `SR` (the current sample rate) to `SR_OUT`. + /// + /// Though the defaults cover most use-cases you can configure + /// the resampler using [`with_config`](SampleRateConvertor::with_config). + fn with_sample_rate(self) -> SampleRateConvertor + where + Self: Sized, + { + SampleRateConvertor::new(self) + } + /// Convert from the current channel count to `CH_OUT`. fn with_channel_count(self) -> ChannelConvertor where diff --git a/src/const_source/conversions.rs b/src/const_source/conversions.rs index f648f8500..334551742 100644 --- a/src/const_source/conversions.rs +++ b/src/const_source/conversions.rs @@ -1 +1,2 @@ pub mod channel_count; +pub mod sample_rate; diff --git a/src/const_source/conversions/sample_rate.rs b/src/const_source/conversions/sample_rate.rs new file mode 100644 index 000000000..551a908e9 --- /dev/null +++ b/src/const_source/conversions/sample_rate.rs @@ -0,0 +1,213 @@ +use crate::conversions::sample_rate::rubato::{ResampleInner, RubatoAsyncResample}; +use crate::conversions::Interpolation; +use crate::math::gcd; +use crate::source::ResampleConfig; +use crate::{ConstSource, Sample, SampleRate, Source}; + +use crate::const_source::IntoDynamicSource; +#[cfg(feature = "rubato-fft")] +use crate::conversions::sample_rate::rubato::RubatoFftResample; +use crate::conversions::sample_rate::{InSamples, OutSamples}; + +/// Resamples an audio source to a target sample rate using Rubato. +pub struct SampleRateConvertor< + const SR_IN: u32, + const SR_OUT: u32, + const CH: u16, + S: ConstSource, +> { + // Option so we can take out the source and rebuild the resampler without unsafe + inner: Option>>, +} + +#[derive(thiserror::Error)] +#[error("The resampler was already running")] +pub struct ResamplerRunning< + const SR_IN: u32, + const SR_OUT: u32, + const CH: u16, + S: ConstSource, +>(SampleRateConvertor); + +impl> + SampleRateConvertor +{ + pub(crate) fn new(source: S) -> Self { + Self { + inner: Some(Self::create_resampler( + source.into_dynamic_source(), + ResampleConfig::default(), + )), + } + } + + /// Further configure the resampler created with [`with_sample_rate`](ConstSource::with_sample_rate). + /// You usually do not need to do this unless you have a specific use-case that requires very + /// fast or extremely high quality resampling. The [`ResampleConfig`] has a number of factories + /// with good defaults for such use-cases. + /// + /// # Errors + /// If the resampler can no longer be reconfigured, usually after yielding + /// the first sample. + /// + /// # Example + /// ``` + /// # use rodio::generators::const_source::Silence; + /// # use rodio::SampleRate; + /// # fn hi() -> Option<()> { // to enable ? in the example + /// use rodio::ConstSource; + /// use rodio::conversions::ResampleConfig; + /// + /// let source: Silence<44100> = Silence::new(); + /// let resampled = source + /// .with_sample_rate::<48000>() + /// .with_config(ResampleConfig::fast()); + /// # Some(()) + /// # } + /// # hi().unwrap(); + /// ``` + #[allow(clippy::result_large_err, reason = "the Ok variant is the same size")] + pub fn with_config( + mut self, + config: ResampleConfig, + ) -> Result> { + if !self.resampler().can_reconfigure() { + return Err(ResamplerRunning(self)); + } + + let source = self + .inner + .take() + .expect( + "we are the only ones who set this to none and we set it to \ + some at the end of this fn", + ) + .into_inner(); + + Ok(Self { + inner: Some(Self::create_resampler(source, config)), + }) + } + + fn resampler(&self) -> &ResampleInner> { + self.inner + .as_ref() + .expect("never none outside `with_config`") + } + + fn resampler_mut(&mut self) -> &mut ResampleInner> { + self.inner + .as_mut() + .expect("never none outside `with_config`") + } + + fn create_resampler( + source: IntoDynamicSource, + config: ResampleConfig, + ) -> ResampleInner> { + let source_rate = const { SampleRate::new(SR_IN).expect("checked in 'new'") }; + if SR_IN == SR_OUT { + let channels = source.channels(); + ResampleInner::Passthrough { + source_rate, + source, + input_span_pos: InSamples::ZERO, + channels, + } + } else { + let target_rate = const { SampleRate::new(SR_OUT).expect("checked in 'new'") }; + match config { + ResampleConfig::Poly { degree, chunk_size } => { + let resampler = + RubatoAsyncResample::new_poly(source, target_rate, chunk_size, degree) + .expect("Failed to create polynomial resampler"); + ResampleInner::Poly(resampler) + } + ResampleConfig::Sinc(mut sinc) => { + #[cfg(feature = "rubato-fft")] + if sinc.is_supported_fixed_ratio(target_rate, source_rate) { + let resampler = RubatoFftResample::new( + source, + target_rate, + sinc.chunk_size, + sinc.sub_chunks, + ) + .expect("Failed to create FFT resampler"); + return ResampleInner::Fft(resampler); + } + + if sinc.is_supported_fixed_ratio(target_rate, source.sample_rate()) { + sinc.interpolation = Interpolation::Nearest; + let g = gcd(target_rate.get(), source.sample_rate().get()); + let numer = target_rate.get() / g; + let denom = source.sample_rate().get() / g; + let ratio = numer.max(denom) as usize; + sinc.oversampling_factor = ratio; + } + ResampleInner::Sinc(sinc.build(source, target_rate)) + } + } + } + } +} + +impl> + ConstSource for SampleRateConvertor +{ + fn total_duration(&self) -> Option { + self.resampler().inner().total_duration() + } +} + +impl Iterator + for SampleRateConvertor +where + S: ConstSource, +{ + type Item = Sample; + + #[inline] + fn next(&mut self) -> Option { + match self.resampler_mut() { + ResampleInner::Passthrough { source, .. } => source.next(), + ResampleInner::Poly(resampler) => resampler.next_sample(), + ResampleInner::Sinc(resampler) => resampler.next_sample(), + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => resampler.next_sample(), + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + match self.resampler() { + ResampleInner::Passthrough { source, .. } => source.size_hint(), + ResampleInner::Poly(resampler) | ResampleInner::Sinc(resampler) => { + let adjusted_for_resampling = |samples| { + InSamples(samples).resampled_by(resampler.resample_ratio) + + resampler.output.len() + + resampler + .frames_being_resampled + .samples(resampler.output.channels) + }; + let (lower, upper) = resampler.input.size_hint(); + let lower = adjusted_for_resampling(lower); + let upper = upper.map(adjusted_for_resampling); + (lower.raw(), upper.as_ref().map(OutSamples::raw)) + } + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => { + let adjusted_for_resampling = |samples| { + InSamples(samples).resampled_by(resampler.resample_ratio) + + resampler.output.len() + + resampler + .frames_being_resampled + .samples(resampler.output.channels) + }; + let (lower, upper) = resampler.input.size_hint(); + let lower = adjusted_for_resampling(lower); + let upper = upper.map(adjusted_for_resampling); + (lower.raw(), upper.as_ref().map(OutSamples::raw)) + } + } + } +} diff --git a/src/conversions/sample_rate/buffer.rs b/src/conversions/sample_rate/buffer.rs index 2b890dc2d..737fcf98d 100644 --- a/src/conversions/sample_rate/buffer.rs +++ b/src/conversions/sample_rate/buffer.rs @@ -111,10 +111,7 @@ impl Debug for LimitLength<'_> { } impl Output { - pub(super) fn new( - channels: ChannelCount, - capacity: OutFrameCount, - ) -> Self { + pub(super) fn new(channels: ChannelCount, capacity: OutFrameCount) -> Self { let mut samples = Vec::new(); samples.reserve_exact(capacity.samples(channels).raw()); samples.resize(samples.capacity(), 0.0); diff --git a/src/conversions/sample_rate/rubato.rs b/src/conversions/sample_rate/rubato.rs index 21ef3b6a9..8ca2b52ea 100644 --- a/src/conversions/sample_rate/rubato.rs +++ b/src/conversions/sample_rate/rubato.rs @@ -367,7 +367,7 @@ impl RubatoFftResample { input, resampler, input_buffer: Input::new(input_buf_size.samples(channels)), - output: Output::new(source_rate, channels, output_buf_size), + output: Output::new(channels, output_buf_size), pos_in_current_span: InSamples::ZERO, output_delay_remaining, resample_ratio, diff --git a/src/fixed_source.rs b/src/fixed_source.rs index 0e7b2158a..e9e03bead 100644 --- a/src/fixed_source.rs +++ b/src/fixed_source.rs @@ -37,7 +37,7 @@ pub trait FixedSource: Iterator { /// Convert from the current channel count to `channel count`. /// - /// Though the defaults cover most use-cases you can configure + /// Though the defaults cover most use-cases you can configure /// the resampler using [`with_config`](SampleRateConvertor::with_config). fn with_sample_rate(self, sample_rate: SampleRate) -> SampleRateConvertor where diff --git a/src/fixed_source/conversions/sample_rate.rs b/src/fixed_source/conversions/sample_rate.rs index 72da4083a..ee1c93a65 100644 --- a/src/fixed_source/conversions/sample_rate.rs +++ b/src/fixed_source/conversions/sample_rate.rs @@ -1,9 +1,12 @@ -use crate::conversions::sample_rate::rubato::{ResampleInner, RubatoAsyncResample}; use crate::conversions::Interpolation; use crate::math::gcd; use crate::source::ResampleConfig; use crate::{FixedSource, Sample, SampleRate, Source}; +#[cfg(feature = "rubato-fft")] +use crate::conversions::sample_rate::rubato::RubatoFftResample; +use crate::conversions::sample_rate::rubato::{ResampleInner, RubatoAsyncResample}; + use crate::conversions::sample_rate::{InSamples, OutSamples}; use crate::fixed_source::IntoDynamicSource; @@ -46,7 +49,7 @@ impl SampleRateConvertor { /// # fn hi() -> Option<()> { // to enable ? in the example /// use rodio::FixedSource; /// use rodio::conversions::ResampleConfig; - /// + /// /// let source = Silence::new(SampleRate::new(44_100)?); /// let resampled = source /// .with_sample_rate(SampleRate::new(48_000)?) @@ -111,7 +114,7 @@ impl SampleRateConvertor { } ResampleConfig::Sinc(mut sinc) => { #[cfg(feature = "rubato-fft")] - if sinc.is_supported_fixed_ratio(target_rate, source_rate) { + if sinc.is_supported_fixed_ratio(target_rate, source.sample_rate()) { let resampler = RubatoFftResample::new( source, target_rate, From 2a26325b489445fe4f10a4e861f9cf2357c3fd94 Mon Sep 17 00:00:00 2001 From: Yara Date: Wed, 29 Jul 2026 17:24:49 +0200 Subject: [PATCH 14/14] Add amplify effects using new pure_effect macro The macro allows us to specify an effect once and have it be implemented for `Source`, `FixedSource` and `ConstSource`. --- CHANGELOG.md | 3 + benches/effects.rs | 3 +- benches/pipeline.rs | 5 +- examples/basic.rs | 3 +- examples/custom_config.rs | 3 +- examples/distortion.rs | 3 +- examples/error_callback.rs | 3 +- examples/limit_settings.rs | 9 +- examples/limit_wav.rs | 3 +- examples/mix_multiple_sources.rs | 9 +- examples/signal_generator.rs | 14 +- examples/stereo.rs | 3 +- src/const_source.rs | 12 ++ src/const_source/macros.rs | 22 +++ src/docs/amplify.md | 3 + src/effects.rs | 221 +++++++++++++++++++++++++++++++ src/effects/amplify.rs | 71 ++++++++++ src/fixed_source.rs | 12 ++ src/fixed_source/macros.rs | 28 ++++ src/lib.rs | 6 +- src/math.rs | 21 +++ src/player.rs | 10 +- src/source/amplify.rs | 87 ------------ src/source/limit.rs | 35 +++-- src/source/macros.rs | 32 +++++ src/source/mod.rs | 69 +++------- src/source/speed.rs | 4 +- src/wav_output.rs | 2 +- tests/limit.rs | 7 +- 29 files changed, 522 insertions(+), 181 deletions(-) create mode 100644 src/const_source/macros.rs create mode 100644 src/docs/amplify.md create mode 100644 src/effects.rs create mode 100644 src/effects/amplify.rs create mode 100644 src/fixed_source/macros.rs delete mode 100644 src/source/amplify.rs create mode 100644 src/source/macros.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 94b004b8a..d34d92249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Breaking: `amplify` now takes a `Factor` which can be linear, decibels or + normalized. +- Breaking: removed `amplify_decibel` and `amplify_normalized` - Breaking: `Microphone` now implements `FixedSource` - Breaking: `Done` now calls a callback instead of decrementing an `Arc`. - Updated `cpal` to v0.18. diff --git a/benches/effects.rs b/benches/effects.rs index ac4239117..fa2fe6997 100644 --- a/benches/effects.rs +++ b/benches/effects.rs @@ -4,6 +4,7 @@ use divan::Bencher; use rodio::Source; mod shared; +use rodio::effects::amplify::Factor; use shared::music_wav; fn main() { @@ -40,7 +41,7 @@ fn fade_out(bencher: Bencher) { fn amplify(bencher: Bencher) { bencher .with_inputs(music_wav) - .bench_values(|source| source.amplify(0.8).for_each(divan::black_box_drop)) + .bench_values(|source| source.amplify(Factor::Linear(0.8)).for_each(divan::black_box_drop)) } #[divan::bench] diff --git a/benches/pipeline.rs b/benches/pipeline.rs index c7d67cfae..a48637919 100644 --- a/benches/pipeline.rs +++ b/benches/pipeline.rs @@ -3,6 +3,7 @@ use std::time::Duration; use divan::Bencher; use rodio::ChannelCount; +use rodio::effects::amplify::Factor; use rodio::{source::UniformSourceIterator, Source}; mod shared; @@ -17,7 +18,7 @@ fn long(bencher: Bencher) { bencher.with_inputs(music_wav).bench_values(|source| { let mut take_dur = source .high_pass(300) - .amplify(1.2) + .amplify(Factor::Linear(1.2)) .speed(0.9) .automatic_gain_control(Default::default()) .delay(Duration::from_secs_f32(0.5)) @@ -41,7 +42,7 @@ fn long(bencher: Bencher) { fn short(bencher: Bencher) { bencher.with_inputs(music_wav).bench_values(|source| { source - .amplify(1.2) + .amplify(Factor::Linear(1.2)) .low_pass(200) .for_each(divan::black_box_drop) }) diff --git a/examples/basic.rs b/examples/basic.rs index f9719017b..7c5fa1930 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,3 +1,4 @@ +use rodio::effects::amplify::Factor; use rodio::source::SineWave; use rodio::Source; use std::error::Error; @@ -22,7 +23,7 @@ fn main() -> Result<(), Box> { { // Generate sine wave. let wave = SineWave::new(740.0) - .amplify(0.2) + .amplify(Factor::Linear(0.2)) .take_duration(Duration::from_secs(3)); mixer.add(wave); } diff --git a/examples/custom_config.rs b/examples/custom_config.rs index d7a52bd4e..5e7005429 100644 --- a/examples/custom_config.rs +++ b/examples/custom_config.rs @@ -1,5 +1,6 @@ use cpal::traits::HostTrait; use cpal::{BufferSize, SampleFormat}; +use rodio::effects::amplify::Factor; use rodio::source::SineWave; use rodio::Source; use std::error::Error; @@ -25,7 +26,7 @@ fn main() -> Result<(), Box> { let mixer = stream_handle.mixer(); let wave = SineWave::new(740.0) - .amplify(0.1) + .amplify(Factor::Linear(0.1)) .take_duration(Duration::from_secs(1)); mixer.add(wave); diff --git a/examples/distortion.rs b/examples/distortion.rs index a9653a4b2..5a6155456 100644 --- a/examples/distortion.rs +++ b/examples/distortion.rs @@ -1,3 +1,4 @@ +use rodio::effects::amplify::Factor; use rodio::source::{SineWave, Source}; use std::error::Error; use std::thread; @@ -10,7 +11,7 @@ fn main() -> Result<(), Box> { // Create a sine wave source and apply distortion let distorted = SineWave::new(440.0) - .amplify(0.2) + .amplify(Factor::Linear(0.2)) .distortion(4.0, 0.3) .take_duration(Duration::from_secs(3)); diff --git a/examples/error_callback.rs b/examples/error_callback.rs index 25ff28683..007eebab1 100644 --- a/examples/error_callback.rs +++ b/examples/error_callback.rs @@ -1,4 +1,5 @@ use cpal::traits::HostTrait; +use rodio::effects::amplify::Factor; use rodio::source::SineWave; use rodio::Source; use std::error::Error; @@ -29,7 +30,7 @@ fn main() -> Result<(), Box> { let mixer = stream_handle.mixer(); let wave = SineWave::new(740.0) - .amplify(0.1) + .amplify(Factor::Linear(0.1)) .take_duration(Duration::from_secs(30)); mixer.add(wave); diff --git a/examples/limit_settings.rs b/examples/limit_settings.rs index 4047a6e32..70563634b 100644 --- a/examples/limit_settings.rs +++ b/examples/limit_settings.rs @@ -3,6 +3,7 @@ //! This example shows how to use the LimitSettings struct with the builder //! to configure audio limiting parameters. +use rodio::effects::amplify::Factor; use rodio::source::{LimitSettings, SineWave, Source}; use rodio::Sample; use std::time::Duration; @@ -33,7 +34,7 @@ fn main() { // Create a sine wave at 440 Hz let sine_wave = SineWave::new(440.0) - .amplify(2.0) // Amplify to cause limiting + .amplify(Factor::Linear(2.0)) // Amplify to cause limiting .take_duration(Duration::from_millis(100)); // Apply limiting with default settings (simplest usage) @@ -52,7 +53,7 @@ fn main() { // Create another sine wave for custom limiting let sine_wave2 = SineWave::new(880.0) - .amplify(1.8) + .amplify(Factor::Linear(1.8)) .take_duration(Duration::from_millis(50)); // Apply the custom settings from Example 2 @@ -102,7 +103,7 @@ fn main() { println!("Example 6: Limiting with -6dB threshold"); // Create a sine wave that will definitely trigger limiting - const AMPLITUDE: Sample = 2.5; // High amplitude to ensure limiting occurs + const AMPLITUDE: Factor = Factor::Linear(2.5); // High amplitude to ensure limiting occurs let test_sine = SineWave::new(440.0) .amplify(AMPLITUDE) .take_duration(Duration::from_millis(100)); // 100ms = ~4410 samples @@ -138,7 +139,7 @@ fn main() { " {}dB threshold limiting results:", strict_limiting.threshold ); - println!(" Original max amplitude: {AMPLITUDE}"); + println!(" Original max amplitude: {AMPLITUDE:?}"); println!(" Target threshold: {target_linear:.3}"); println!(" Early peak (0-500 samples): {early_peak:.3}"); println!(" Mid peak (1000-1500 samples): {mid_peak:.3}"); diff --git a/examples/limit_wav.rs b/examples/limit_wav.rs index 9217dfd2c..f6cd45610 100644 --- a/examples/limit_wav.rs +++ b/examples/limit_wav.rs @@ -1,3 +1,4 @@ +use rodio::effects::amplify::Factor; use rodio::{source::LimitSettings, Source}; use std::error::Error; @@ -7,7 +8,7 @@ fn main() -> Result<(), Box> { let file = std::fs::File::open("assets/music.wav")?; let source = rodio::Decoder::try_from(file)? - .amplify(3.0) + .amplify(Factor::Linear(3.0)) .limit(LimitSettings::default()); player.append(source); diff --git a/examples/mix_multiple_sources.rs b/examples/mix_multiple_sources.rs index 4341330b5..9794ae644 100644 --- a/examples/mix_multiple_sources.rs +++ b/examples/mix_multiple_sources.rs @@ -1,3 +1,4 @@ +use rodio::effects::amplify::Factor; use rodio::mixer; use rodio::source::{SineWave, Source}; use rodio::Float; @@ -19,16 +20,16 @@ fn main() -> Result<(), Box> { // E4, G4, and A4 respectively. let source_c = SineWave::new(261.63) .take_duration(NOTE_DURATION) - .amplify(NOTE_AMPLITUDE); + .amplify(Factor::Linear(NOTE_AMPLITUDE)); let source_e = SineWave::new(329.63) .take_duration(NOTE_DURATION) - .amplify(NOTE_AMPLITUDE); + .amplify(Factor::Linear(NOTE_AMPLITUDE)); let source_g = SineWave::new(392.0) .take_duration(NOTE_DURATION) - .amplify(NOTE_AMPLITUDE); + .amplify(Factor::Linear(NOTE_AMPLITUDE)); let source_a = SineWave::new(440.0) .take_duration(NOTE_DURATION) - .amplify(NOTE_AMPLITUDE); + .amplify(Factor::Linear(NOTE_AMPLITUDE)); // Add sources C, E, G, and A to the mixer controller. controller.add(source_c); diff --git a/examples/signal_generator.rs b/examples/signal_generator.rs index 09738af92..a845c8cff 100644 --- a/examples/signal_generator.rs +++ b/examples/signal_generator.rs @@ -3,6 +3,8 @@ use std::error::Error; use std::num::NonZero; +use rodio::effects::amplify::Factor; + fn main() -> Result<(), Box> { use rodio::source::{chirp, Function, SignalGenerator, Source}; use std::thread; @@ -17,7 +19,7 @@ fn main() -> Result<(), Box> { println!("Playing 1000 Hz tone"); stream_handle.mixer().add( SignalGenerator::new(sample_rate, 1000.0, Function::Sine) - .amplify(0.1) + .amplify(Factor::Linear(0.1)) .take_duration(test_signal_duration), ); @@ -26,7 +28,7 @@ fn main() -> Result<(), Box> { println!("Playing 10,000 Hz tone"); stream_handle.mixer().add( SignalGenerator::new(sample_rate, 10000.0, Function::Sine) - .amplify(0.1) + .amplify(Factor::Linear(0.1)) .take_duration(test_signal_duration), ); @@ -35,7 +37,7 @@ fn main() -> Result<(), Box> { println!("Playing 440 Hz Triangle Wave"); stream_handle.mixer().add( SignalGenerator::new(sample_rate, 440.0, Function::Triangle) - .amplify(0.1) + .amplify(Factor::Linear(0.1)) .take_duration(test_signal_duration), ); @@ -44,7 +46,7 @@ fn main() -> Result<(), Box> { println!("Playing 440 Hz Sawtooth Wave"); stream_handle.mixer().add( SignalGenerator::new(sample_rate, 440.0, Function::Sawtooth) - .amplify(0.1) + .amplify(Factor::Linear(0.1)) .take_duration(test_signal_duration), ); @@ -53,7 +55,7 @@ fn main() -> Result<(), Box> { println!("Playing 440 Hz Square Wave"); stream_handle.mixer().add( SignalGenerator::new(sample_rate, 440.0, Function::Square) - .amplify(0.1) + .amplify(Factor::Linear(0.1)) .take_duration(test_signal_duration), ); @@ -62,7 +64,7 @@ fn main() -> Result<(), Box> { println!("Playing 20-10000 Hz Sweep"); stream_handle.mixer().add( chirp(sample_rate, 20.0, 10000.0, Duration::from_secs(1)) - .amplify(0.1) + .amplify(Factor::Linear(0.1)) .take_duration(test_signal_duration), ); diff --git a/examples/stereo.rs b/examples/stereo.rs index 2c2c3d4a1..e341e5a1a 100644 --- a/examples/stereo.rs +++ b/examples/stereo.rs @@ -1,5 +1,6 @@ //! Plays a tone alternating between right and left ears, with right being first. +use rodio::effects::amplify::Factor; use rodio::Source; use std::error::Error; @@ -8,7 +9,7 @@ fn main() -> Result<(), Box> { let player = rodio::Player::connect_new(stream_handle.mixer()); let file = std::fs::File::open("assets/RL.ogg")?; - player.append(rodio::Decoder::try_from(file)?.amplify(0.2)); + player.append(rodio::Decoder::try_from(file)?.amplify(Factor::Linear(0.2))); player.sleep_until_end(); diff --git a/src/const_source.rs b/src/const_source.rs index 2f9962a9b..393937098 100644 --- a/src/const_source.rs +++ b/src/const_source.rs @@ -12,6 +12,8 @@ use std::num::NonZeroU16; use std::num::NonZeroU32; use std::time::Duration; +use crate::effects::amplify::Factor; +use crate::effects::const_source::Amplify; use crate::source::SeekError; use crate::ChannelCount; use crate::FixedSource; @@ -23,6 +25,8 @@ mod buffer; mod chain; mod conversions; +pub(crate) mod macros; + pub use buffer::SamplesBuffer; pub use chain::SourceChain; pub use conversions::channel_count::ChannelConvertor; @@ -144,6 +148,14 @@ pub trait ConstSource: Iterator { { todo!() } + + #[doc = include_str!("docs/amplify.md")] + fn amplify(self, factor: Factor) -> Amplify + where + Self: Sized, + { + Amplify::new(self, factor) + } } // placeholder until effects land (need this for some examples) diff --git a/src/const_source/macros.rs b/src/const_source/macros.rs new file mode 100644 index 000000000..b6035a3b2 --- /dev/null +++ b/src/const_source/macros.rs @@ -0,0 +1,22 @@ +macro_rules! add_inner_methods { + ($name:ident$(<$t:ident$(:$bound:path)?>)?) => { + impl$(,$t$(:$bound)?)?> $name { + crate::common::source::add_inner_accessors!{inner} + } + }; +} + +pub(crate) use add_inner_methods; + +macro_rules! impl_wrapper { + ($name:ident$(<$t:ident$(:$bound:path)?>)?) => { + impl$(,$t$(:$bound)?)?> crate::ConstSource + for $name + { + fn total_duration(&self) -> Option { + self.inner.total_duration() + } + } + }; +} +pub(crate) use impl_wrapper; diff --git a/src/docs/amplify.md b/src/docs/amplify.md new file mode 100644 index 000000000..c851b305a --- /dev/null +++ b/src/docs/amplify.md @@ -0,0 +1,3 @@ +Amplifies the sound by either linearly, logarithmically or perceptually + +See the docs on [`Factor`](crate::effects::amplify::Factor) for details. diff --git a/src/effects.rs b/src/effects.rs new file mode 100644 index 000000000..6f0dd2666 --- /dev/null +++ b/src/effects.rs @@ -0,0 +1,221 @@ +//! All the effects that can be applied to rodio sources. To apply an effect to +//! a source use the method on the source directly. You should only need this +//! module for effects settings and to specify the full type of the source. + +/// Amplify effect +pub mod amplify; + +// we can only get the structure: effects::effect::source_type::Struct with a macro +// so we re-export the structs here to get the nicer structure: +// effects::source_type::Struct; + +pub mod fixed_source { + //! Effects that work on fixed sources + pub use super::amplify::fixed_source::Amplify; +} +pub mod const_source { + //! Effects that work on const sources + pub use super::amplify::const_source::Amplify; +} +pub mod dynamic_source { + //! Effects that work on dynamic sources + pub use super::amplify::dynamic_source::Amplify; +} + +/// Write the minimal Rust needed to define the needed source implementations +/// for a pure effect. A pure effect is one which does not create or modify +/// spans. Start with `supports_dynamic_source` on a single line to generate an +/// implementation for Dynamic-, Fixed- and ConstSource. Leave that line out to +/// generate only implementations for Fixed- and ConstSource. +/// +/// For example usage see src/effects/amplify.rs +macro_rules! pure_effect { + ( + supports_dynamic_source + #[$struct_doc:meta] + struct $name:ident$(<$t:ident$(:$bound:path)?>)? { + $($field:ident: $field_ty:ty,)* + } + // like `struct` above the `fn`, `&mut` and `-> Option` are just there + // to make the macro input seem regular rust code + fn next(&mut $self:ident) -> Option $body:block + fn new$(<$new_generic:ident : $new_bound:path>)?($($factory_args:tt)*) -> $factory_name:ident $factory_body:block + // m stands for method + $($(#[$m_meta:meta])* $m_vis:vis fn $m_name:ident($($args:tt)*) $(-> $m_ret:ty)? $m_body:block)* + ) => { + pub(crate) mod dynamic_source { + #[allow(unused)] + use super::*; + #[derive(Clone)] + #[$struct_doc] + pub struct $name { + pub(crate) inner: S, + $(pub(crate) $field: $field_ty),* + } + + crate::source::macros::add_inner_methods!{$name$(<$t$(:$bound)?>)?} + crate::source::macros::impl_wrapper!{$name$(<$t$(:$bound)?>)?} + } + + impl dynamic_source::$name { + #[must_use] + pub(crate) fn new($($factory_args)*) -> dynamic_source::$name { + $factory_body + } + $($(#[$m_meta])* $m_vis fn $m_name($($args)*) $(-> $m_ret)? $m_body)* + } + + impl Iterator for dynamic_source::$name { + type Item = crate::Sample; + + fn next(&mut $self) -> Option { + $body + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } + } + + impl ExactSizeIterator for dynamic_source::$name where S: ExactSizeIterator {} + + crate::effects::inner!{ + #[$struct_doc] + struct $name$(<$t$(:$bound)?>)? { + $($field: $field_ty,)* + } + fn next(&mut $self) -> Option $body + fn new($($factory_args)*) -> $factory_name $factory_body + $($(#[$m_meta])* $m_vis fn $m_name($($args)*) $(-> $m_ret)? $m_body)* + } + }; + + ( + #[$struct_doc:meta] + struct $name:ident$(<$t:ident$(:$bound:path)?>)? { + $($field:ident: $field_ty:ty,)* + } + // like `struct` above the `fn`, `&mut` and `-> Option` are just there + // to make the macro input seem regular rust code + fn next(&mut $self:ident) -> Option $body:block + fn new$(<$new_generic:ident : $new_bound:path>)?($($factory_args:tt)*) + -> $factory_name:ident $factory_body:block + // m stands for method + $($(#[$m_meta:meta])* $m_vis:vis fn $m_name:ident($($args:tt)*) $(-> $m_ret:ty)? $m_body:block)* + ) => { + crate::effects::inner!{ + struct $name$(<$t$(:$bound)?>)? { + $($field: $field_ty,)* + } + fn next(&mut $self) -> Option $body + fn new$(<$new_generic: $new_bound>)?($($factory_args)*) + -> $factory_name $factory_body + $($(#[$m_meta])* $m_vis fn $m_name($($args)*) $(-> $m_ret)? $m_body)* + } + } +} + +macro_rules! inner { +( + #[$struct_doc:meta] + struct $name:ident$(<$t:ident$(:$bound:path)?>)? { + $($field:ident: $field_ty:ty,)* + } + // like `struct` above the `fn`, `&mut` and `-> Option` are just there + // to make the macro input seem regular rust code + fn next(&mut $self:ident) -> Option $body:block + fn new$(<$new_generic:ident: $new_bound:path>)?($($factory_args:tt)*) -> $factory_name:ident $factory_body:block + // m stands for method + $($(#[$m_meta:meta])* $m_vis:vis fn $m_name:ident($($args:tt)*) $(-> $m_ret:ty)? $m_body:block)* + ) => { + pub(crate) mod fixed_source { + #[allow(unused)] + use super::*; + + #[derive(Clone)] + #[$struct_doc] + pub struct $name { + pub(crate) inner: S, + $(pub(crate) $field: $field_ty),* + } + + crate::fixed_source::macros::add_inner_methods!{$name$(<$t$(:$bound)?>)?} + crate::fixed_source::macros::impl_wrapper!{$name$(<$t$(:$bound)?>)?} + } + + impl fixed_source::$name { + #[must_use] + pub(crate) fn new$(<$new_generic: $new_bound>)?($($factory_args)*) + -> fixed_source::$name { + $factory_body + } + $($(#[$m_meta])* $m_vis fn $m_name($($args)*) $(-> $m_ret)? $m_body)* + } + + impl + ExactSizeIterator for fixed_source::$name + where S: ExactSizeIterator {} + + impl + Iterator for fixed_source::$name { + + type Item = crate::Sample; + + fn next(&mut $self) -> Option { + $body + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } + } + + pub(crate) mod const_source { + #[allow(unused)] + use super::*; + + #[derive(Clone)] + #[$struct_doc] + pub struct $name + $(,$t$(:$bound)?)?> { + pub(crate) inner: S, + $(pub(crate) $field: $field_ty),* + } + + crate::const_source::macros::add_inner_methods!{$name$(<$t$(:$bound)?>)?} + crate::const_source::macros::impl_wrapper!{$name$(<$t$(:$bound)?>)?} + } + + impl$(,$t$(:$bound)?)?> + const_source::$name { + + #[must_use] + pub(crate) fn new$(<$new_generic: $new_bound>)?($($factory_args)*) + -> const_source::$name { + $factory_body + } + $($(#[$m_meta])* $m_vis fn $m_name($($args)*) $(-> $m_ret)? $m_body)* + } + + + impl$(,$t$(:$bound)?)?> + ExactSizeIterator for const_source::$name + where S: ExactSizeIterator {} + + impl$(,$t$(:$bound)?)?> + Iterator for const_source::$name { + type Item = crate::Sample; + + fn next(&mut $self) -> Option { + $body + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } + } + } +} + +pub(crate) use inner; +pub(crate) use pure_effect; diff --git a/src/effects/amplify.rs b/src/effects/amplify.rs new file mode 100644 index 000000000..d0f9f960b --- /dev/null +++ b/src/effects/amplify.rs @@ -0,0 +1,71 @@ +use crate::math::{db_to_linear, normalized_to_linear}; + +use crate::effects::pure_effect; + +/// Amplification factor. +/// +/// Recommended for volume control: [`Normalized`](Factor::Normalized). +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Factor { + /// Makes the sound exactly this times louder or softer. Note human hearing + /// is logarithmic so something that is N times louder does not sound N + /// times louder. + Linear(f32), + /// Amplifies the sound logarithmically by the given value. + /// - 0 dB = linear value of 1.0 (no change) + /// - Positive dB values represent amplification (> 1.0) + /// - Negative dB values represent attenuation (< 1.0) + /// - -60 dB ≈ 0.001 (barely audible) + /// - +20 dB = 10.0 (10x amplification) + /// + Decibel(f32), + /// Normalized amplification in `[0.0, 1.0]` range. This method better + /// matches the perceived loudness of sounds in human hearing and is + /// recommended to use when you want to change volume in `[0.0, 1.0]` range. + /// based on article: + /// + /// **note: it clamps values outside this range.** + Normalized(f32), +} + +impl Factor { + pub(crate) fn as_linear(&self) -> f32 { + match self { + Factor::Linear(v) => *v, + Factor::Decibel(db) => db_to_linear(*db), + Factor::Normalized(normalized) => normalized_to_linear(*normalized), + } + } +} + +impl Default for Factor { + /// Keep the volume unchanged + fn default() -> Self { + Self::Linear(1.0) + } +} + +pure_effect! { + supports_dynamic_source + /// An effect that changes how loud the sound is. + struct Amplify { + factor: f32, + } + + fn next(&mut self) -> Option { + self.inner.next().map(|value| value * self.factor) + } + + fn new(source: S, factor: Factor) -> Amplify { + Self { + inner: source, + factor: factor.as_linear(), + } + } + + /// Change the current amplification factor. Note this takes immediate + /// effect without any smoothing. This can sound jarring. + pub fn set_factor(&mut self, factor: Factor) { + self.factor = factor.as_linear() + } +} diff --git a/src/fixed_source.rs b/src/fixed_source.rs index e9e03bead..c4f7309ac 100644 --- a/src/fixed_source.rs +++ b/src/fixed_source.rs @@ -2,9 +2,13 @@ //! channel count. use std::time::Duration; +use crate::effects::amplify::Factor; +use crate::effects::fixed_source::Amplify; use crate::source::SeekError; use crate::{ChannelCount, ConstSource, Sample, SampleRate}; +pub(crate) mod macros; + mod buffer; mod chain; mod conversions; @@ -138,6 +142,14 @@ pub trait FixedSource: Iterator { /// here to make docs links work without the linked item being in /// remove before next release fn placeholder(&self) {} + + #[doc = include_str!("docs/amplify.md")] + fn amplify(self, factor: Factor) -> Amplify + where + Self: Sized, + { + Amplify::new(self, factor) + } } // placeholder until effects land (need this for some examples) diff --git a/src/fixed_source/macros.rs b/src/fixed_source/macros.rs new file mode 100644 index 000000000..2ce6426b3 --- /dev/null +++ b/src/fixed_source/macros.rs @@ -0,0 +1,28 @@ +macro_rules! add_inner_methods { + ($name:ident$(<$t:ident$(:$bound:path)?>)?) => { + impl $name { + crate::common::source::add_inner_accessors!{inner} + } + }; +} + +pub(crate) use add_inner_methods; + +macro_rules! impl_wrapper { + ($name:ident$(<$t:ident$(:$bound:path)?>)?) => { + impl crate::FixedSource for $name { + fn channels(&self) -> crate::ChannelCount { + self.inner.channels() + } + + fn sample_rate(&self) -> crate::SampleRate { + self.inner.sample_rate() + } + + fn total_duration(&self) -> Option { + self.inner.total_duration() + } + } + }; +} +pub(crate) use impl_wrapper; diff --git a/src/lib.rs b/src/lib.rs index 034912b87..86c799caa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -70,6 +70,7 @@ #![cfg_attr(feature = "playback", doc = "```no_run")] //! use std::time::Duration; //! use rodio::{MixerDeviceSink, Player}; +//! use rodio::effects::amplify::Factor; //! use rodio::source::{SineWave, Source}; //! //! // _stream must live as long as the sink @@ -78,7 +79,9 @@ //! 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); +//! let source = SineWave::new(440.0) +//! .take_duration(Duration::from_secs_f32(0.25)) +//! .amplify(Factor::Linear(0.20)); //! player.append(source); //! //! // The sound plays in a separate thread. This call will block the current thread until the @@ -230,6 +233,7 @@ pub mod decoder; pub mod const_source; pub mod fixed_source; +pub mod effects; pub mod generators; pub mod math; diff --git a/src/math.rs b/src/math.rs index 375021354..6ef081435 100644 --- a/src/math.rs +++ b/src/math.rs @@ -43,6 +43,27 @@ pub fn db_to_linear(decibels: Float) -> Float { Float::powf(2.0, decibels * 0.05 * LOG2_10) } +/// Normalized amplification in `[0.0, 1.0]` range. This method better matches the perceived +/// loudness of sounds in human hearing and is recommended to use when you want to change +/// volume in `[0.0, 1.0]` range. +/// based on article: +/// +/// **note: it clamps values outside this range.** +pub(crate) fn normalized_to_linear(normalized: f32) -> f32 { + const NORMALIZATION_MIN: f32 = 0.0; + const NORMALIZATION_MAX: f32 = 1.0; + const LOG_VOLUME_GROWTH_RATE: f32 = 6.907_755_4; + const LOG_VOLUME_SCALE_FACTOR: f32 = 1000.0; + + let normalized = normalized.clamp(NORMALIZATION_MIN, NORMALIZATION_MAX); + + let mut amplitude = f32::exp(LOG_VOLUME_GROWTH_RATE * normalized) / LOG_VOLUME_SCALE_FACTOR; + if normalized < 0.1 { + amplitude *= normalized * 10.0; + } + amplitude +} + /// Converts linear amplitude scale to decibels. /// /// This function converts a linear amplitude value to its corresponding decibel value diff --git a/src/player.rs b/src/player.rs index c3547be00..6ca34c848 100644 --- a/src/player.rs +++ b/src/player.rs @@ -8,6 +8,7 @@ use dasp_sample::FromSample; #[cfg(not(feature = "crossbeam-channel"))] use std::sync::mpsc::{Receiver, Sender}; +use crate::effects::amplify; use crate::mixer::Mixer; use crate::source::SeekError; use crate::Float; @@ -125,7 +126,7 @@ impl Player { // Must be placed before pausable but after speed & delay .track_position() .pausable(false) - .amplify(1.0) + .amplify(amplify::Factor::Linear(1.0)) .skippable() .stoppable(), move |src| { @@ -152,7 +153,7 @@ impl Player { } } let amp = src.inner_mut().inner_mut().inner_mut(); - amp.set_factor(*controls.volume.lock().unwrap()); + amp.set_factor(amplify::Factor::Linear(*controls.volume.lock().unwrap())); amp.inner_mut() .set_paused(controls.pause.load(Ordering::SeqCst)); amp.inner_mut() @@ -367,7 +368,8 @@ mod tests { use std::sync::atomic::Ordering; use crate::buffer::SamplesBuffer; - use crate::math::nz; + use crate::effects::amplify; +use crate::math::nz; use crate::{Player, Source}; #[test] @@ -461,7 +463,7 @@ mod tests { 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); + let mut src = src.amplify(amplify::Factor::Linear(0.5)); player.set_volume(0.5); for _ in 0..v.len() { diff --git a/src/source/amplify.rs b/src/source/amplify.rs deleted file mode 100644 index fb8ab8b47..000000000 --- a/src/source/amplify.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::time::Duration; - -use super::SeekError; -use crate::{ - common::{ChannelCount, Float, SampleRate}, - math, Source, -}; - -/// Internal function that builds a `Amplify` object. -pub fn amplify(input: I, factor: Float) -> Amplify -where - I: Source, -{ - Amplify { input, factor } -} - -/// Filter that modifies each sample by a given value. -#[derive(Clone, Debug)] -pub struct Amplify { - input: I, - factor: Float, -} - -impl Amplify { - /// Modifies the amplification factor. - #[inline] - pub fn set_factor(&mut self, factor: Float) { - self.factor = factor; - } - - /// Modifies the amplification factor logarithmically. - #[inline] - pub fn set_log_factor(&mut self, factor: Float) { - self.factor = math::db_to_linear(factor); - } - - crate::common::source::add_inner_accessors! {input} -} - -impl Iterator for Amplify -where - I: Source, -{ - type Item = I::Item; - - #[inline] - fn next(&mut self) -> Option { - self.input.next().map(|value| value * self.factor) - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - self.input.size_hint() - } -} - -impl ExactSizeIterator for Amplify where I: Source + ExactSizeIterator {} - -impl Source for Amplify -where - I: Source, -{ - #[inline] - fn current_span_len(&self) -> Option { - self.input.current_span_len() - } - - #[inline] - fn channels(&self) -> ChannelCount { - self.input.channels() - } - - #[inline] - fn sample_rate(&self) -> SampleRate { - self.input.sample_rate() - } - - #[inline] - fn total_duration(&self) -> Option { - self.input.total_duration() - } - - #[inline] - fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - self.input.try_seek(pos) - } -} diff --git a/src/source/limit.rs b/src/source/limit.rs index 1d466e5ab..97d069552 100644 --- a/src/source/limit.rs +++ b/src/source/limit.rs @@ -25,10 +25,11 @@ //! //! ```rust //! use rodio::source::{SineWave, Source, LimitSettings}; +//! use rodio::effects::amplify::Factor; //! use std::time::Duration; //! //! // Create a loud sine wave -//! let source = SineWave::new(440.0).amplify(2.0); +//! let source = SineWave::new(440.0).amplify(Factor::Linear(2.0)); //! //! // Apply limiting with -6dB threshold //! let settings = LimitSettings::default().with_threshold(-6.0); @@ -48,13 +49,14 @@ //! //! ```rust //! use rodio::source::{SineWave, Source, LimitSettings}; +//! use rodio::effects::amplify::Factor; //! //! // Use preset optimized for music -//! let music = SineWave::new(440.0).amplify(1.5); +//! let music = SineWave::new(440.0).amplify(Factor::Linear(1.5)); //! let limited_music = music.limit(LimitSettings::dynamic_content()); //! //! // Use preset optimized for streaming -//! let stream = SineWave::new(440.0).amplify(2.0); +//! let stream = SineWave::new(440.0).amplify(Factor::Linear(2.0)); //! let limited_stream = stream.limit(LimitSettings::broadcast()); //! ``` @@ -86,8 +88,9 @@ use crate::{ /// /// ```rust /// use rodio::source::{SineWave, Source, LimitSettings}; +/// use rodio::effects::amplify::Factor; /// -/// let source = SineWave::new(440.0).amplify(2.0); +/// let source = SineWave::new(440.0).amplify(Factor::Linear(2.0)); /// let settings = LimitSettings::default().with_threshold(-6.0); /// let limited = source.limit(settings); /// ``` @@ -148,10 +151,11 @@ pub(crate) fn limit(input: I, settings: LimitSettings) -> Limit { /// /// ```rust /// use rodio::source::{SineWave, Source, LimitSettings}; +/// use rodio::effects::amplify::Factor; /// use std::time::Duration; /// /// // Use default settings (-1 dB threshold, 4 dB knee, 5ms attack, 100ms release) -/// let source = SineWave::new(440.0).amplify(2.0); +/// let source = SineWave::new(440.0).amplify(Factor::Linear(2.0)); /// let limited = source.limit(LimitSettings::default()); /// ``` /// @@ -159,9 +163,10 @@ pub(crate) fn limit(input: I, settings: LimitSettings) -> Limit { /// /// ```rust /// use rodio::source::{SineWave, Source, LimitSettings}; +/// use rodio::effects::amplify::Factor; /// use std::time::Duration; /// -/// let source = SineWave::new(440.0).amplify(3.0); +/// let source = SineWave::new(440.0).amplify(Factor::Linear(3.0)); /// let settings = LimitSettings::new() /// .with_threshold(-6.0) // Limit peaks above -6dB /// .with_knee_width(2.0) // 2dB soft knee for smooth limiting @@ -276,8 +281,9 @@ impl LimitSettings { /// /// ``` /// use rodio::source::{SineWave, Source, LimitSettings}; + /// use rodio::effects::amplify::Factor; /// - /// let music = SineWave::new(440.0).amplify(1.5); + /// let music = SineWave::new(440.0).amplify(Factor::Linear(1.5)); /// let limited = music.limit(LimitSettings::dynamic_content()); /// ``` #[inline] @@ -311,8 +317,9 @@ impl LimitSettings { /// /// ``` /// use rodio::source::{SineWave, Source, LimitSettings}; + /// use rodio::effects::amplify::Factor; /// - /// let voice_chat = SineWave::new(440.0).amplify(2.0); + /// let voice_chat = SineWave::new(440.0).amplify(Factor::Linear(2.0)); /// let limited = voice_chat.limit(LimitSettings::broadcast()); /// ``` #[inline] @@ -346,8 +353,9 @@ impl LimitSettings { /// /// ``` /// use rodio::source::{SineWave, Source, LimitSettings}; + /// use rodio::effects::amplify::Factor; /// - /// let master_track = SineWave::new(440.0).amplify(3.0); + /// let master_track = SineWave::new(440.0).amplify(Factor::Linear(3.0)); /// let mastered = master_track.limit(LimitSettings::mastering()); /// ``` #[inline] @@ -384,8 +392,9 @@ impl LimitSettings { /// /// ``` /// use rodio::source::{SineWave, Source, LimitSettings}; + /// use rodio::effects::amplify::Factor; /// - /// let live_input = SineWave::new(440.0).amplify(2.5); + /// let live_input = SineWave::new(440.0).amplify(Factor::Linear(2.5)); /// let protected = live_input.limit(LimitSettings::live_performance()); /// ``` #[inline] @@ -423,8 +432,9 @@ impl LimitSettings { /// /// ``` /// use rodio::source::{SineWave, Source, LimitSettings}; + /// use rodio::effects::amplify::Factor; /// - /// let game_audio = SineWave::new(440.0).amplify(2.0); + /// let game_audio = SineWave::new(440.0).amplify(Factor::Linear(2.0)); /// let limited = game_audio.limit(LimitSettings::gaming()); /// ``` #[inline] @@ -540,9 +550,10 @@ impl LimitSettings { /// ``` /// use rodio::source::{SineWave, Source}; /// use rodio::source::LimitSettings; +/// use rodio::effects::amplify::Factor; /// use std::time::Duration; /// -/// let source = SineWave::new(440.0).amplify(2.0); +/// let source = SineWave::new(440.0).amplify(Factor::Linear(2.0)); /// let settings = LimitSettings::default() /// .with_threshold(-6.0) // -6 dBFS threshold /// .with_attack(Duration::from_millis(5)) diff --git a/src/source/macros.rs b/src/source/macros.rs new file mode 100644 index 000000000..41846c686 --- /dev/null +++ b/src/source/macros.rs @@ -0,0 +1,32 @@ +macro_rules! add_inner_methods { + ($name:ident$(<$t:ident>)?) => { + impl $name { + crate::common::source::add_inner_accessors!{inner} + } + }; +} + +macro_rules! impl_wrapper { + ($name:ident$(<$t:ident>)?) => { + impl crate::Source for $name { + fn current_span_len(&self) -> Option { + self.inner.current_span_len() + } + + fn channels(&self) -> crate::ChannelCount { + self.inner.channels() + } + + fn sample_rate(&self) -> crate::SampleRate { + self.inner.sample_rate() + } + + fn total_duration(&self) -> Option { + self.inner.total_duration() + } + } + }; +} + +pub(crate) use add_inner_methods; +pub(crate) use impl_wrapper; diff --git a/src/source/mod.rs b/src/source/mod.rs index 237735bee..9eb14cc55 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -7,12 +7,12 @@ use crate::{ buffer::SamplesBuffer, common::{assert_error_traits, ChannelCount, SampleRate}, conversions::SampleRateConverter, - math, Float, Sample, + effects::{amplify::Factor, dynamic_source::Amplify}, + Float, Sample, }; use dasp_sample::FromSample; -pub use self::amplify::Amplify; pub use self::automatic_gain_control::{AutomaticGainControl, AutomaticGainControlSettings}; pub use self::blt::BltFilter; pub use self::buffered::Buffered; @@ -51,9 +51,10 @@ pub use self::triangle::TriangleWave; pub use self::uniform::UniformSourceIterator; pub use self::zero::{Zero, ZeroError}; pub use crate::conversions::ResampleConfig; - -mod amplify; mod automatic_gain_control; + +pub(crate) mod macros; + mod blt; mod buffered; mod chain; @@ -241,7 +242,7 @@ pub trait Source: Iterator { /// use rodio::BitDepth; /// /// let source = SineWave::new(440.0) - /// .amplify(0.5) + /// .amplify(Factor::Linear(0.5)) /// .dither(BitDepth::new(16).unwrap(), DitherAlgorithm::default()); /// ``` #[cfg(feature = "dither")] @@ -307,48 +308,12 @@ pub trait Source: Iterator { skip::skip_duration(self, duration) } - /// Amplifies the sound by the given value. - #[inline] - fn amplify(self, value: Float) -> Amplify - where - Self: Sized, - { - amplify::amplify(self, value) - } - - /// Amplifies the sound logarithmically by the given value. - #[inline] - fn amplify_decibel(self, value: Float) -> Amplify - where - Self: Sized, - { - amplify::amplify(self, math::db_to_linear(value)) - } - - /// Normalized amplification in `[0.0, 1.0]` range. This method better matches the perceived - /// loudness of sounds in human hearing and is recommended to use when you want to change - /// volume in `[0.0, 1.0]` range. - /// based on article: - /// - /// **note: it clamps values outside this range.** - #[inline] - fn amplify_normalized(self, value: Float) -> Amplify + #[doc = include_str!("../docs/amplify.md")] + fn amplify(self, factor: Factor) -> Amplify where - Self: Sized, + Self: Source + Sized, { - const NORMALIZATION_MIN: Float = 0.0; - const NORMALIZATION_MAX: Float = 1.0; - const LOG_VOLUME_GROWTH_RATE: Float = 6.907_755_4; - const LOG_VOLUME_SCALE_FACTOR: Float = 1000.0; - - let value = value.clamp(NORMALIZATION_MIN, NORMALIZATION_MAX); - - let mut amplitude = Float::exp(LOG_VOLUME_GROWTH_RATE * value) / LOG_VOLUME_SCALE_FACTOR; - if value < 0.1 { - amplitude *= value * 10.0; - } - - amplify::amplify(self, amplitude) + Amplify::new(self, factor) } /// Applies automatic gain control to the sound. @@ -433,10 +398,11 @@ pub trait Source: Iterator { /// /// ``` /// use rodio::source::{SineWave, Source, LimitSettings}; + /// use rodio::effects::amplify::Factor; /// use std::time::Duration; /// /// // Create a loud sine wave and apply default limiting (-1dB threshold) - /// let source = SineWave::new(440.0).amplify(2.0); + /// let source = SineWave::new(440.0).amplify(Factor::Linear(2.0)); /// let limited = source.limit(LimitSettings::default()); /// ``` /// @@ -444,9 +410,10 @@ pub trait Source: Iterator { /// /// ``` /// use rodio::source::{SineWave, Source, LimitSettings}; + /// use rodio::effects::amplify::Factor; /// use std::time::Duration; /// - /// let source = SineWave::new(440.0).amplify(3.0); + /// let source = SineWave::new(440.0).amplify(Factor::Linear(3.0)); /// let settings = LimitSettings::default() /// .with_threshold(-6.0) // Limit at -6dB /// .with_knee_width(2.0) // 2dB soft knee @@ -536,9 +503,10 @@ pub trait Source: Iterator { /// /// # use rodio::source::SineWave; /// # use rodio::Source; + /// # use rodio::effects::amplify::Factor; /// # use std::time::Duration; /// let wave = SineWave::new(740.0) - /// .amplify(0.2) + /// .amplify(Factor::Linear(0.2)) /// .take_duration(Duration::from_secs(3)); /// let wave = wave.record(); /// ``` @@ -566,7 +534,10 @@ pub trait Source: Iterator { where Self: Sized + Clone, { - let echo = self.clone().amplify(amplitude).delay(duration); + let echo = self + .clone() + .amplify(Factor::Linear(amplitude)) + .delay(duration); self.mix(echo) } diff --git a/src/source/speed.rs b/src/source/speed.rs index a9bcb5dea..d883fc0ff 100644 --- a/src/source/speed.rs +++ b/src/source/speed.rs @@ -32,9 +32,11 @@ #![cfg_attr(not(feature = "playback"), doc = "```ignore")] #![cfg_attr(feature = "playback", doc = "```no_run")] //! use rodio::source::{Source, SineWave}; +//! use rodio::effects::amplify::Factor; +//! //! let source = SineWave::new(440.0) //! .take_duration(std::time::Duration::from_secs_f32(20.25)) -//! .amplify(0.20); +//! .amplify(Factor::Linear(0.20)); //! let handle = rodio::DeviceSinkBuilder::open_default_sink() //! .expect("open default audio sink"); //! let player = rodio::Player::connect_new(&handle.mixer()); diff --git a/src/wav_output.rs b/src/wav_output.rs index 5f8f5cedc..febf0f787 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -145,7 +145,7 @@ mod test { fn test_wav_to_file() { let make_source = || { crate::source::SineWave::new(745.0) - .amplify(0.1) + .amplify(Factor::Linear(0.1)) .take_duration(Duration::from_secs(1)) }; let wav_file_path = "target/tmp/save-to-wav-test.wav"; diff --git a/tests/limit.rs b/tests/limit.rs index 629fb209d..d1e0a36d9 100644 --- a/tests/limit.rs +++ b/tests/limit.rs @@ -1,3 +1,4 @@ +use rodio::effects::amplify::Factor; use rodio::source::Source; use rodio::Sample; use std::num::NonZero; @@ -7,7 +8,7 @@ use std::time::Duration; fn test_limiting_works() { // High amplitude sine wave limited to -6dB let sine_wave = rodio::source::SineWave::new(440.0) - .amplify(3.0) // 3.0 linear = ~9.5dB + .amplify(Factor::Linear(3.0)) // 3.0 linear = ~9.5dB .take_duration(Duration::from_millis(60)); // ~2600 samples let settings = rodio::source::LimitSettings::default() @@ -43,7 +44,7 @@ fn test_limiting_works() { fn test_passthrough_below_threshold() { // Low amplitude signal should pass through unchanged let sine_wave = rodio::source::SineWave::new(1000.0) - .amplify(0.2) // 0.2 linear, well below -6dB threshold + .amplify(Factor::Linear(0.2)) // 0.2 linear, well below -6dB threshold .take_duration(Duration::from_millis(20)); let settings = rodio::source::LimitSettings::default().with_threshold(-6.0); @@ -73,7 +74,7 @@ fn test_limiter_with_different_settings() { for (threshold_db, expected_peak) in test_cases { let sine_wave = rodio::source::SineWave::new(440.0) - .amplify(2.0) // Ensure signal exceeds all thresholds + .amplify(Factor::Linear(2.0)) // Ensure signal exceeds all thresholds .take_duration(Duration::from_millis(50)); let settings = rodio::source::LimitSettings::default()