diff --git a/CHANGELOG.md b/CHANGELOG.md index ff36459..63faa80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for *ALAC/AIFF* +- New sources: + - `fade_out` fades an input out using a linear gain fade. + - `linear_gain_ramp` applies a linear gain change to a sound over a + given duration. `fade_out` is implemented as a `linear_gain_ramp` and + `fade_in` has been refactored to use the `linear_gain_ramp` + implementation. ### Changed - `SamplesBuffer` is now `Clone` diff --git a/Cargo.toml b/Cargo.toml index 6d53723..aa75167 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ symphonia-aiff = ["symphonia/aiff", "symphonia/pcm"] quickcheck = "0.9.2" rstest = "0.18.2" rstest_reuse = "0.6.0" +approx = "0.5.1" [[example]] name = "music_m4a" diff --git a/src/conversions/sample.rs b/src/conversions/sample.rs index 7ee9762..7f593f7 100644 --- a/src/conversions/sample.rs +++ b/src/conversions/sample.rs @@ -67,7 +67,7 @@ where /// - For `u16`, silence corresponds to the value `u16::max_value() / 2`. The minimum and maximum /// amplitudes are represented by `0` and `u16::max_value()` respectively. /// - For `f32`, silence corresponds to the value `0.0`. The minimum and maximum amplitudes are -/// represented by `-1.0` and `1.0` respectively. +/// represented by `-1.0` and `1.0` respectively. /// /// You can implement this trait on your own type as well if you wish so. /// diff --git a/src/source/crossfade.rs b/src/source/crossfade.rs index d9d29db..a8ea20c 100644 --- a/src/source/crossfade.rs +++ b/src/source/crossfade.rs @@ -5,9 +5,11 @@ use cpal::FromSample; use crate::source::{FadeIn, Mix, TakeDuration}; use crate::{Sample, Source}; -/// Mixes one sound fading out with another sound fading in for the given duration. +/// Mixes one sound fading out with another sound fading in for the given +/// duration. /// -/// Only the crossfaded portion (beginning of fadeout, beginning of fadein) is returned. +/// Only the crossfaded portion (beginning of fadeout, beginning of fadein) is +/// returned. pub fn crossfade( input_fadeout: I1, input_fadein: I2, @@ -37,7 +39,7 @@ mod tests { } #[test] - fn test_crossfade() { + fn test_crossfade_with_self() { let source1 = dummysource(10); let source2 = dummysource(10); let mut mixed = crossfade( @@ -51,7 +53,10 @@ mod tests { assert_eq!(mixed.next(), Some(4.0)); assert_eq!(mixed.next(), Some(5.0)); assert_eq!(mixed.next(), None); + } + #[test] + fn test_crossfade() { let source1 = dummysource(10); let source2 = dummysource(10).amplify(0.0); let mut mixed = crossfade( diff --git a/src/source/fadein.rs b/src/source/fadein.rs index 68cf429..eac958f 100644 --- a/src/source/fadein.rs +++ b/src/source/fadein.rs @@ -2,7 +2,7 @@ use std::time::Duration; use crate::{Sample, Source}; -use super::SeekError; +use super::{linear_ramp::linear_gain_ramp, LinearGainRamp, SeekError}; /// Internal function that builds a `FadeIn` object. pub fn fadein(input: I, duration: Duration) -> FadeIn @@ -10,21 +10,15 @@ where I: Source, I::Item: Sample, { - let duration = duration.as_secs() * 1000000000 + duration.subsec_nanos() as u64; - FadeIn { - input, - remaining_ns: duration as f32, - total_ns: duration as f32, + input: linear_gain_ramp(input, duration, 0.0f32, 1.0f32, false), } } /// Filter that modifies raises the volume from silence over a time period. #[derive(Clone, Debug)] pub struct FadeIn { - input: I, - remaining_ns: f32, - total_ns: f32, + input: LinearGainRamp, } impl FadeIn @@ -35,19 +29,19 @@ where /// Returns a reference to the inner source. #[inline] pub fn inner(&self) -> &I { - &self.input + self.input.inner() } /// Returns a mutable reference to the inner source. #[inline] pub fn inner_mut(&mut self) -> &mut I { - &mut self.input + self.input.inner_mut() } /// Returns the inner source. #[inline] pub fn into_inner(self) -> I { - self.input + self.input.into_inner() } } @@ -60,14 +54,7 @@ where #[inline] fn next(&mut self) -> Option { - if self.remaining_ns <= 0.0 { - return self.input.next(); - } - - let factor = 1.0 - self.remaining_ns / self.total_ns; - self.remaining_ns -= - 1000000000.0 / (self.input.sample_rate() as f32 * self.channels() as f32); - self.input.next().map(|value| value.amplify(factor)) + self.input.next() } #[inline] @@ -90,26 +77,26 @@ where { #[inline] fn current_frame_len(&self) -> Option { - self.input.current_frame_len() + self.inner().current_frame_len() } #[inline] fn channels(&self) -> u16 { - self.input.channels() + self.inner().channels() } #[inline] fn sample_rate(&self) -> u32 { - self.input.sample_rate() + self.inner().sample_rate() } #[inline] fn total_duration(&self) -> Option { - self.input.total_duration() + self.inner().total_duration() } #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - self.input.try_seek(pos) + self.inner_mut().try_seek(pos) } } diff --git a/src/source/fadeout.rs b/src/source/fadeout.rs new file mode 100644 index 0000000..b71de92 --- /dev/null +++ b/src/source/fadeout.rs @@ -0,0 +1,102 @@ +use std::time::Duration; + +use crate::{Sample, Source}; + +use super::{linear_ramp::linear_gain_ramp, LinearGainRamp, SeekError}; + +/// Internal function that builds a `FadeOut` object. +pub fn fadeout(input: I, duration: Duration) -> FadeOut +where + I: Source, + I::Item: Sample, +{ + FadeOut { + input: linear_gain_ramp(input, duration, 1.0f32, 0.0f32, true), + } +} + +/// Filter that modifies lowers the volume to silence over a time period. +#[derive(Clone, Debug)] +pub struct FadeOut { + input: LinearGainRamp, +} + +impl FadeOut +where + I: Source, + I::Item: Sample, +{ + /// Returns a reference to the inner source. + #[inline] + pub fn inner(&self) -> &I { + self.input.inner() + } + + /// Returns a mutable reference to the inner source. + #[inline] + pub fn inner_mut(&mut self) -> &mut I { + self.input.inner_mut() + } + + /// Returns the inner source. + #[inline] + pub fn into_inner(self) -> I { + self.input.into_inner() + } +} + +impl Iterator for FadeOut +where + I: Source, + I::Item: Sample, +{ + type Item = I::Item; + + #[inline] + fn next(&mut self) -> Option { + self.input.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.input.size_hint() + } +} + +impl ExactSizeIterator for FadeOut +where + I: Source + ExactSizeIterator, + I::Item: Sample, +{ +} + +impl Source for FadeOut +where + I: Source, + I::Item: Sample, +{ + #[inline] + fn current_frame_len(&self) -> Option { + self.inner().current_frame_len() + } + + #[inline] + fn channels(&self) -> u16 { + self.inner().channels() + } + + #[inline] + fn sample_rate(&self) -> u32 { + self.inner().sample_rate() + } + + #[inline] + fn total_duration(&self) -> Option { + self.inner().total_duration() + } + + #[inline] + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + self.inner_mut().try_seek(pos) + } +} diff --git a/src/source/linear_ramp.rs b/src/source/linear_ramp.rs new file mode 100644 index 0000000..0e46a68 --- /dev/null +++ b/src/source/linear_ramp.rs @@ -0,0 +1,238 @@ +use std::time::Duration; + +use super::SeekError; +use crate::{Sample, Source}; + +/// Internal function that builds a `LinearRamp` object. +pub fn linear_gain_ramp( + input: I, + duration: Duration, + start_gain: f32, + end_gain: f32, + clamp_end: bool, +) -> LinearGainRamp +where + I: Source, + I::Item: Sample, +{ + let duration_nanos = duration.as_nanos() as f32; + assert!(duration_nanos > 0.0f32); + + LinearGainRamp { + input, + elapsed_ns: 0.0f32, + total_ns: duration_nanos, + start_gain, + end_gain, + clamp_end, + sample_idx: 0u64, + } +} + +/// Filter that adds a linear gain ramp to the source over a given time range. +#[derive(Clone, Debug)] +pub struct LinearGainRamp { + input: I, + elapsed_ns: f32, + total_ns: f32, + start_gain: f32, + end_gain: f32, + clamp_end: bool, + sample_idx: u64, +} + +impl LinearGainRamp +where + I: Source, + I::Item: Sample, +{ + /// Returns a reference to the innner 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 Iterator for LinearGainRamp +where + I: Source, + I::Item: Sample, +{ + type Item = I::Item; + + #[inline] + fn next(&mut self) -> Option { + let factor: f32; + let remaining_ns = self.total_ns - self.elapsed_ns; + + if remaining_ns < 0.0 { + if self.clamp_end { + factor = self.end_gain; + } else { + factor = 1.0f32; + } + } else { + self.sample_idx += 1; + + let p = self.elapsed_ns / self.total_ns; + factor = self.start_gain * (1.0f32 - p) + self.end_gain * p; + } + + if self.sample_idx % (self.channels() as u64) == 0 { + self.elapsed_ns += 1000000000.0 / (self.input.sample_rate() as f32); + } + + self.input.next().map(|value| value.amplify(factor)) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.input.size_hint() + } +} + +impl ExactSizeIterator for LinearGainRamp +where + I: Source + ExactSizeIterator, + I::Item: Sample, +{ +} + +impl Source for LinearGainRamp +where + I: Source, + I::Item: Sample, +{ + #[inline] + fn current_frame_len(&self) -> Option { + self.input.current_frame_len() + } + + #[inline] + fn channels(&self) -> u16 { + self.input.channels() + } + + #[inline] + fn sample_rate(&self) -> u32 { + 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.elapsed_ns = pos.as_nanos() as f32; + self.input.try_seek(pos) + } +} + +#[cfg(test)] +mod tests { + use approx::assert_abs_diff_eq; + + use super::*; + use crate::buffer::SamplesBuffer; + + /// Create a SamplesBuffer of identical samples with value `value`. + /// Returned buffer is one channel and has a sample rate of 1 hz. + fn const_source(length: u8, value: f32) -> SamplesBuffer { + let data: Vec = (1..=length).map(|_| value).collect(); + SamplesBuffer::new(1, 1, data) + } + + /// Create a SamplesBuffer of repeating sample values from `values`. + fn cycle_source(length: u8, values: Vec) -> SamplesBuffer { + let data: Vec = (1..=length) + .enumerate() + .map(|(i, _)| values[i % values.len()]) + .collect(); + + SamplesBuffer::new(1, 1, data) + } + + #[test] + fn test_linear_ramp() { + let source1 = const_source(10, 1.0f32); + let mut faded = linear_gain_ramp(source1, Duration::from_secs(4), 0.0, 1.0, true); + + assert_eq!(faded.next(), Some(0.0)); + assert_eq!(faded.next(), Some(0.25)); + assert_eq!(faded.next(), Some(0.5)); + assert_eq!(faded.next(), Some(0.75)); + assert_eq!(faded.next(), Some(1.0)); + assert_eq!(faded.next(), Some(1.0)); + assert_eq!(faded.next(), Some(1.0)); + assert_eq!(faded.next(), Some(1.0)); + assert_eq!(faded.next(), Some(1.0)); + assert_eq!(faded.next(), Some(1.0)); + assert_eq!(faded.next(), None); + } + + #[test] + fn test_linear_ramp_clamped() { + let source1 = const_source(10, 1.0f32); + let mut faded = linear_gain_ramp(source1, Duration::from_secs(4), 0.0, 0.5, true); + + assert_eq!(faded.next(), Some(0.0)); // fading in... + assert_eq!(faded.next(), Some(0.125)); + assert_eq!(faded.next(), Some(0.25)); + assert_eq!(faded.next(), Some(0.375)); + assert_eq!(faded.next(), Some(0.5)); // fade is done + assert_eq!(faded.next(), Some(0.5)); + assert_eq!(faded.next(), Some(0.5)); + assert_eq!(faded.next(), Some(0.5)); + assert_eq!(faded.next(), Some(0.5)); + assert_eq!(faded.next(), Some(0.5)); + assert_eq!(faded.next(), None); + } + + #[test] + fn test_linear_ramp_seek() { + let source1 = cycle_source(20, vec![0.0f32, 0.4f32, 0.8f32]); + let mut faded = linear_gain_ramp(source1, Duration::from_secs(10), 0.0, 1.0, true); + + assert_abs_diff_eq!(faded.next().unwrap(), 0.0); // source value 0 + assert_abs_diff_eq!(faded.next().unwrap(), 0.04); // source value 0.4, ramp gain 0.1 + assert_abs_diff_eq!(faded.next().unwrap(), 0.16); // source value 0.8, ramp gain 0.2 + + if let Ok(_result) = faded.try_seek(Duration::from_secs(5)) { + assert_abs_diff_eq!(faded.next().unwrap(), 0.40); // source value 0.8, ramp gain 0.5 + assert_abs_diff_eq!(faded.next().unwrap(), 0.0); // source value 0, ramp gain 0.6 + assert_abs_diff_eq!(faded.next().unwrap(), 0.28); // source value 0.4. ramp gain 0.7 + } else { + panic!("try_seek() failed!"); + } + + if let Ok(_result) = faded.try_seek(Duration::from_secs(0)) { + assert_abs_diff_eq!(faded.next().unwrap(), 0.0); // source value 0, ramp gain 0.0 + assert_abs_diff_eq!(faded.next().unwrap(), 0.04); // source value 0.4, ramp gain 0.1 + assert_abs_diff_eq!(faded.next().unwrap(), 0.16); // source value 0.8. ramp gain 0.2 + } else { + panic!("try_seek() failed!"); + } + + if let Ok(_result) = faded.try_seek(Duration::from_secs(10)) { + assert_abs_diff_eq!(faded.next().unwrap(), 0.4); // source value 0.4, ramp gain 1.0 + assert_abs_diff_eq!(faded.next().unwrap(), 0.8); // source value 0.8, ramp gain 1.0 + assert_abs_diff_eq!(faded.next().unwrap(), 0.0); // source value 0. ramp gain 1.0 + } else { + panic!("try_seek() failed!"); + } + } +} diff --git a/src/source/mod.rs b/src/source/mod.rs index c173ea8..2da1067 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -16,8 +16,10 @@ pub use self::done::Done; pub use self::empty::Empty; pub use self::empty_callback::EmptyCallback; pub use self::fadein::FadeIn; +pub use self::fadeout::FadeOut; pub use self::from_factory::{from_factory, FromFactoryIter}; pub use self::from_iter::{from_iter, FromIter}; +pub use self::linear_ramp::LinearGainRamp; pub use self::mix::Mix; pub use self::pausable::Pausable; pub use self::periodic::PeriodicAccess; @@ -45,8 +47,10 @@ mod done; mod empty; mod empty_callback; mod fadein; +mod fadeout; mod from_factory; mod from_iter; +mod linear_ramp; mod mix; mod pausable; mod periodic; @@ -251,6 +255,34 @@ where fadein::fadein(self, duration) } + /// Fades out the sound. + #[inline] + fn fade_out(self, duration: Duration) -> FadeOut + where + Self: Sized, + { + fadeout::fadeout(self, duration) + } + + /// Applies a linear gain ramp to the sound. + /// + /// If `clamp_end` is `true`, all samples subsequent to the end of the ramp + /// will be scaled by the `end_value`. If `clamp_end` is `false`, all + /// subsequent samples will not have any scaling applied. + #[inline] + fn linear_gain_ramp( + self, + duration: Duration, + start_value: f32, + end_value: f32, + clamp_end: bool, + ) -> LinearGainRamp + where + Self: Sized, + { + linear_ramp::linear_gain_ramp(self, duration, start_value, end_value, clamp_end) + } + /// Calls the `access` closure on `Self` the first time the source is iterated and every /// time `period` elapses. ///