From fc22b37f1b3641dc161bf91436ac51b94330ddff Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Wed, 7 Aug 2024 20:30:57 -0700 Subject: [PATCH 01/65] Implemented synthesizer waveform source --- src/source/mod.rs | 2 + src/source/synth_waveforms.rs | 156 ++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 src/source/synth_waveforms.rs diff --git a/src/source/mod.rs b/src/source/mod.rs index 528e6fe8..f44df89f 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -30,6 +30,7 @@ pub use self::skippable::Skippable; pub use self::spatial::Spatial; pub use self::speed::Speed; pub use self::stoppable::Stoppable; +pub use self::synth_waveforms::SynthWaveform; pub use self::take::TakeDuration; pub use self::uniform::UniformSourceIterator; pub use self::zero::Zero; @@ -58,6 +59,7 @@ mod skippable; mod spatial; mod speed; mod stoppable; +mod synth_waveforms; mod take; mod uniform; mod zero; diff --git a/src/source/synth_waveforms.rs b/src/source/synth_waveforms.rs new file mode 100644 index 00000000..b335b91a --- /dev/null +++ b/src/source/synth_waveforms.rs @@ -0,0 +1,156 @@ +use std::f32::consts::TAU; +use std::time::Duration; + +use super::SeekError; +use crate::{buffer::SamplesBuffer, source::Repeat, Source}; + +/// Syntheizer waveform functions. All of the synth waveforms are in the +/// codomain [-1.0, 1.0]. +#[derive(Clone, Debug)] +pub enum SynthWaveformFunction { + Sine, + Triangle, + Square, + Sawtooth, +} + +impl SynthWaveformFunction { + /// Create a `SamplesBuffer` containing one period of `self` with the given + /// sample rate and frequency. + pub fn create_buffer(&self, sample_rate: u32, frequency: u32) -> SamplesBuffer { + let p: usize = (sample_rate / frequency) as usize; + let mut samples_vec = vec![0.0f32; p]; + + fn _pwm_impl(duty: f32, t: f32) -> f32 { + if t < duty.abs() { + 1f32 + } else { + -1f32 + } + } + + for i in 0..p { + let i_div_p: f32 = i as f32 / p as f32; + samples_vec[i] = match self { + Self::Sine => (TAU * i_div_p).sin(), + Self::Triangle => 4.0f32 * (i_div_p - (i_div_p + 0.5f32).floor()).abs() - 1f32, + Self::Square => { + if i_div_p < 0.5f32 { + 1.0f32 + } else { + -1.0f32 + } + } + Self::Sawtooth => 2.0f32 * (i_div_p - (i_div_p + 0.5f32).floor()), + } + } + + SamplesBuffer::new(1, sample_rate, samples_vec) + } +} + +/// An infinite source that produces one of a selection of synthesizer +/// waveforms from a buffered source. +#[derive(Clone)] +pub struct SynthWaveform { + input: Repeat>, +} + +impl SynthWaveform { + /// Create a new `SynthWaveform` object that generates an endless waveform + /// `f`. + #[inline] + pub fn new(sample_rate: u32, frequency: u32, f: SynthWaveformFunction) -> SynthWaveform { + let buffer = f.create_buffer(sample_rate, frequency); + SynthWaveform { + input: buffer.repeat_infinite(), + } + } +} + +impl Iterator for SynthWaveform { + type Item = f32; + + #[inline] + fn next(&mut self) -> Option { + self.input.next() + } +} + +impl Source for SynthWaveform { + #[inline] + fn current_frame_len(&self) -> Option { + self.input.current_frame_len() + } + + #[inline] + fn channels(&self) -> u16 { + 1 + } + + #[inline] + fn sample_rate(&self) -> u32 { + self.input.sample_rate() + } + + #[inline] + fn total_duration(&self) -> Option { + None + } + + #[inline] + fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { + self.input.try_seek(duration) + } +} + +#[cfg(test)] +mod tests { + use crate::source::synth_waveforms::*; + + #[test] + fn square() { + let mut wf = SynthWaveform::new(1000, 250, SynthWaveformFunction::Square); + assert_eq!(wf.next(), Some(1.0f32)); + assert_eq!(wf.next(), Some(1.0f32)); + assert_eq!(wf.next(), Some(-1.0f32)); + assert_eq!(wf.next(), Some(-1.0f32)); + assert_eq!(wf.next(), Some(1.0f32)); + assert_eq!(wf.next(), Some(1.0f32)); + assert_eq!(wf.next(), Some(-1.0f32)); + assert_eq!(wf.next(), Some(-1.0f32)); + } + + #[test] + fn triangle() { + let mut wf = SynthWaveform::new(8000, 1000, SynthWaveformFunction::Triangle); + assert_eq!(wf.next(), Some(-1.0f32)); + assert_eq!(wf.next(), Some(-0.5f32)); + assert_eq!(wf.next(), Some(0.0f32)); + assert_eq!(wf.next(), Some(0.5f32)); + assert_eq!(wf.next(), Some(1.0f32)); + assert_eq!(wf.next(), Some(0.5f32)); + assert_eq!(wf.next(), Some(0.0f32)); + assert_eq!(wf.next(), Some(-0.5f32)); + assert_eq!(wf.next(), Some(-1.0f32)); + assert_eq!(wf.next(), Some(-0.5f32)); + assert_eq!(wf.next(), Some(0.0f32)); + assert_eq!(wf.next(), Some(0.5f32)); + assert_eq!(wf.next(), Some(1.0f32)); + assert_eq!(wf.next(), Some(0.5f32)); + assert_eq!(wf.next(), Some(0.0f32)); + assert_eq!(wf.next(), Some(-0.5f32)); + } + + #[test] + fn saw() { + let mut wf = SynthWaveform::new(200, 50, SynthWaveformFunction::Sawtooth); + assert_eq!(wf.next(), Some(0.0f32)); + assert_eq!(wf.next(), Some(0.5f32)); + assert_eq!(wf.next(), Some(-1.0f32)); + assert_eq!(wf.next(), Some(-0.5f32)); + assert_eq!(wf.next(), Some(0.0f32)); + assert_eq!(wf.next(), Some(0.5f32)); + assert_eq!(wf.next(), Some(-1.0f32)); + } +} From 128fa745545595cb29019aad077e00c8b6e17cb0 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Thu, 8 Aug 2024 16:39:35 -0700 Subject: [PATCH 02/65] More synth work, but think I'm going to startover --- src/source/mod.rs | 2 +- src/source/synth_waveforms.rs | 89 ++++++++++++++++++++++------------- 2 files changed, 57 insertions(+), 34 deletions(-) diff --git a/src/source/mod.rs b/src/source/mod.rs index f44df89f..c173ea88 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -30,7 +30,7 @@ pub use self::skippable::Skippable; pub use self::spatial::Spatial; pub use self::speed::Speed; pub use self::stoppable::Stoppable; -pub use self::synth_waveforms::SynthWaveform; +pub use self::synth_waveforms::{BufferedSynthWaveform, SynthWaveformFunction}; pub use self::take::TakeDuration; pub use self::uniform::UniformSourceIterator; pub use self::zero::Zero; diff --git a/src/source/synth_waveforms.rs b/src/source/synth_waveforms.rs index b335b91a..ad910c03 100644 --- a/src/source/synth_waveforms.rs +++ b/src/source/synth_waveforms.rs @@ -4,6 +4,16 @@ use std::time::Duration; use super::SeekError; use crate::{buffer::SamplesBuffer, source::Repeat, Source}; + +/// Express a frequency as a rational number. +/// .0 Cycles per time quanta +/// .1 Time quanta per second +/// +/// Examples: +/// 1000,1 1000 Hz +/// 12345,100 123.45 Hz +pub struct RationalFrequency(u32, u32); + /// Syntheizer waveform functions. All of the synth waveforms are in the /// codomain [-1.0, 1.0]. #[derive(Clone, Debug)] @@ -15,60 +25,67 @@ pub enum SynthWaveformFunction { } impl SynthWaveformFunction { - /// Create a `SamplesBuffer` containing one period of `self` with the given - /// sample rate and frequency. - pub fn create_buffer(&self, sample_rate: u32, frequency: u32) -> SamplesBuffer { - let p: usize = (sample_rate / frequency) as usize; - let mut samples_vec = vec![0.0f32; p]; - - fn _pwm_impl(duty: f32, t: f32) -> f32 { - if t < duty.abs() { - 1f32 - } else { - -1f32 - } - } - - for i in 0..p { - let i_div_p: f32 = i as f32 / p as f32; - samples_vec[i] = match self { - Self::Sine => (TAU * i_div_p).sin(), - Self::Triangle => 4.0f32 * (i_div_p - (i_div_p + 0.5f32).floor()).abs() - 1f32, - Self::Square => { + /// Create a single sample for the given waveform + #[inline] + pub fn render(&self, sample: u32, period: u32) -> f32 { + let i_div_p: f32 = sample as f32 / period as f32; + + match self { + Self::Sine => (TAU * i_div_p).sin(), + Self::Triangle => 04.0f32 * (i_div_p - (i_div_p + 0.5f32).floor()).abs() - 1f32, + Self::Square => { if i_div_p < 0.5f32 { 1.0f32 } else { -1.0f32 } - } - Self::Sawtooth => 2.0f32 * (i_div_p - (i_div_p + 0.5f32).floor()), - } + }, + Self::Sawtooth => 2.0f32 * (i_div_p - (i_div_p + 0.5f32).floor()), + } + } + + /// Create a `SamplesBuffer` containing one period of `self` with the given + /// sample rate and frequency. + pub fn create_buffer( + &self, + sample_rate: cpal::SampleRate, + frequency: u32, + ) -> SamplesBuffer { + let period: usize = (sample_rate.0 / frequency) as usize; + let mut samples_vec = vec![0.0f32; period]; + + for i in 0..period { + samples_vec[i] = self.render(i as u32, period as u32); } - SamplesBuffer::new(1, sample_rate, samples_vec) + SamplesBuffer::new(1, sample_rate.0, samples_vec) } } /// An infinite source that produces one of a selection of synthesizer /// waveforms from a buffered source. #[derive(Clone)] -pub struct SynthWaveform { +pub struct BufferedSynthWaveform { input: Repeat>, } -impl SynthWaveform { +impl BufferedSynthWaveform { /// Create a new `SynthWaveform` object that generates an endless waveform /// `f`. #[inline] - pub fn new(sample_rate: u32, frequency: u32, f: SynthWaveformFunction) -> SynthWaveform { + pub fn new( + sample_rate: cpal::SampleRate, + frequency: u32, + f: SynthWaveformFunction, + ) -> BufferedSynthWaveform { let buffer = f.create_buffer(sample_rate, frequency); - SynthWaveform { + BufferedSynthWaveform { input: buffer.repeat_infinite(), } } } -impl Iterator for SynthWaveform { +impl Iterator for BufferedSynthWaveform { type Item = f32; #[inline] @@ -77,7 +94,7 @@ impl Iterator for SynthWaveform { } } -impl Source for SynthWaveform { +impl Source for BufferedSynthWaveform { #[inline] fn current_frame_len(&self) -> Option { self.input.current_frame_len() @@ -110,7 +127,8 @@ mod tests { #[test] fn square() { - let mut wf = SynthWaveform::new(1000, 250, SynthWaveformFunction::Square); + let mut wf = + BufferedSynthWaveform::new(cpal::SampleRate(1000), 250, SynthWaveformFunction::Square); assert_eq!(wf.next(), Some(1.0f32)); assert_eq!(wf.next(), Some(1.0f32)); assert_eq!(wf.next(), Some(-1.0f32)); @@ -123,7 +141,11 @@ mod tests { #[test] fn triangle() { - let mut wf = SynthWaveform::new(8000, 1000, SynthWaveformFunction::Triangle); + let mut wf = BufferedSynthWaveform::new( + cpal::SampleRate(8000), + 1000, + SynthWaveformFunction::Triangle, + ); assert_eq!(wf.next(), Some(-1.0f32)); assert_eq!(wf.next(), Some(-0.5f32)); assert_eq!(wf.next(), Some(0.0f32)); @@ -144,7 +166,8 @@ mod tests { #[test] fn saw() { - let mut wf = SynthWaveform::new(200, 50, SynthWaveformFunction::Sawtooth); + let mut wf = + BufferedSynthWaveform::new(cpal::SampleRate(200), 50, SynthWaveformFunction::Sawtooth); assert_eq!(wf.next(), Some(0.0f32)); assert_eq!(wf.next(), Some(0.5f32)); assert_eq!(wf.next(), Some(-1.0f32)); From eef2edaa3deb87e3af932825aa98b69a3c2983ec Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 9 Aug 2024 12:40:46 -0700 Subject: [PATCH 03/65] Recent synth changes caught up with master ...and upstream. --- src/source/mod.rs | 4 +- src/source/{synth_waveforms.rs => synth.rs} | 113 ++++++++++---------- 2 files changed, 56 insertions(+), 61 deletions(-) rename src/source/{synth_waveforms.rs => synth.rs} (60%) diff --git a/src/source/mod.rs b/src/source/mod.rs index 2da10677..84ce2f35 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -32,7 +32,7 @@ pub use self::skippable::Skippable; pub use self::spatial::Spatial; pub use self::speed::Speed; pub use self::stoppable::Stoppable; -pub use self::synth_waveforms::{BufferedSynthWaveform, SynthWaveformFunction}; +pub use self::synth::{SynthWaveform, SynthWaveformFunction}; pub use self::take::TakeDuration; pub use self::uniform::UniformSourceIterator; pub use self::zero::Zero; @@ -63,7 +63,7 @@ mod skippable; mod spatial; mod speed; mod stoppable; -mod synth_waveforms; +mod synth; mod take; mod uniform; mod zero; diff --git a/src/source/synth_waveforms.rs b/src/source/synth.rs similarity index 60% rename from src/source/synth_waveforms.rs rename to src/source/synth.rs index ad910c03..1531b6d1 100644 --- a/src/source/synth_waveforms.rs +++ b/src/source/synth.rs @@ -2,17 +2,7 @@ use std::f32::consts::TAU; use std::time::Duration; use super::SeekError; -use crate::{buffer::SamplesBuffer, source::Repeat, Source}; - - -/// Express a frequency as a rational number. -/// .0 Cycles per time quanta -/// .1 Time quanta per second -/// -/// Examples: -/// 1000,1 1000 Hz -/// 12345,100 123.45 Hz -pub struct RationalFrequency(u32, u32); +use crate::Source; /// Syntheizer waveform functions. All of the synth waveforms are in the /// codomain [-1.0, 1.0]. @@ -27,77 +17,68 @@ pub enum SynthWaveformFunction { impl SynthWaveformFunction { /// Create a single sample for the given waveform #[inline] - pub fn render(&self, sample: u32, period: u32) -> f32 { - let i_div_p: f32 = sample as f32 / period as f32; - + pub fn render(&self, i: u64, period: f32) -> f32 { + let i_div_p: f32 = i as f32 / period; + match self { Self::Sine => (TAU * i_div_p).sin(), Self::Triangle => 04.0f32 * (i_div_p - (i_div_p + 0.5f32).floor()).abs() - 1f32, Self::Square => { - if i_div_p < 0.5f32 { - 1.0f32 - } else { - -1.0f32 - } - }, + if i_div_p % 1.0f32 < 0.5f32 { + 1.0f32 + } else { + -1.0f32 + } + } Self::Sawtooth => 2.0f32 * (i_div_p - (i_div_p + 0.5f32).floor()), } } - - /// Create a `SamplesBuffer` containing one period of `self` with the given - /// sample rate and frequency. - pub fn create_buffer( - &self, - sample_rate: cpal::SampleRate, - frequency: u32, - ) -> SamplesBuffer { - let period: usize = (sample_rate.0 / frequency) as usize; - let mut samples_vec = vec![0.0f32; period]; - - for i in 0..period { - samples_vec[i] = self.render(i as u32, period as u32); - } - - SamplesBuffer::new(1, sample_rate.0, samples_vec) - } } /// An infinite source that produces one of a selection of synthesizer -/// waveforms from a buffered source. +/// waveforms. #[derive(Clone)] -pub struct BufferedSynthWaveform { - input: Repeat>, +pub struct SynthWaveform { + sample_rate: cpal::SampleRate, + period: f32, + f: SynthWaveformFunction, + i: u64, } -impl BufferedSynthWaveform { +impl SynthWaveform { /// Create a new `SynthWaveform` object that generates an endless waveform /// `f`. #[inline] pub fn new( sample_rate: cpal::SampleRate, - frequency: u32, + frequency: f32, f: SynthWaveformFunction, - ) -> BufferedSynthWaveform { - let buffer = f.create_buffer(sample_rate, frequency); - BufferedSynthWaveform { - input: buffer.repeat_infinite(), + ) -> SynthWaveform { + let period = sample_rate.0 as f32 / frequency; + SynthWaveform { + sample_rate, + period, + f, + i: 0, } } } -impl Iterator for BufferedSynthWaveform { +impl Iterator for SynthWaveform { type Item = f32; #[inline] fn next(&mut self) -> Option { - self.input.next() + let this_i = self.i; + self.i += 1; + Some(self.f.render(this_i, self.period)) } } -impl Source for BufferedSynthWaveform { +impl Source for SynthWaveform { #[inline] fn current_frame_len(&self) -> Option { - self.input.current_frame_len() + None } #[inline] @@ -107,7 +88,7 @@ impl Source for BufferedSynthWaveform { #[inline] fn sample_rate(&self) -> u32 { - self.input.sample_rate() + self.sample_rate.0 } #[inline] @@ -117,18 +98,22 @@ impl Source for BufferedSynthWaveform { #[inline] fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { - self.input.try_seek(duration) + self.i = (self.sample_rate.0 as f32 * duration.as_secs_f32()) as u64; + Ok(()) } } #[cfg(test)] mod tests { - use crate::source::synth_waveforms::*; + use crate::source::synth::*; #[test] fn square() { - let mut wf = - BufferedSynthWaveform::new(cpal::SampleRate(1000), 250, SynthWaveformFunction::Square); + let mut wf = SynthWaveform::new( + cpal::SampleRate(2000), + 500.0f32, + SynthWaveformFunction::Square, + ); assert_eq!(wf.next(), Some(1.0f32)); assert_eq!(wf.next(), Some(1.0f32)); assert_eq!(wf.next(), Some(-1.0f32)); @@ -141,9 +126,9 @@ mod tests { #[test] fn triangle() { - let mut wf = BufferedSynthWaveform::new( + let mut wf = SynthWaveform::new( cpal::SampleRate(8000), - 1000, + 1000.0f32, SynthWaveformFunction::Triangle, ); assert_eq!(wf.next(), Some(-1.0f32)); @@ -166,8 +151,11 @@ mod tests { #[test] fn saw() { - let mut wf = - BufferedSynthWaveform::new(cpal::SampleRate(200), 50, SynthWaveformFunction::Sawtooth); + let mut wf = SynthWaveform::new( + cpal::SampleRate(200), + 50.0f32, + SynthWaveformFunction::Sawtooth, + ); assert_eq!(wf.next(), Some(0.0f32)); assert_eq!(wf.next(), Some(0.5f32)); assert_eq!(wf.next(), Some(-1.0f32)); @@ -176,4 +164,11 @@ mod tests { assert_eq!(wf.next(), Some(0.5f32)); assert_eq!(wf.next(), Some(-1.0f32)); } + + // #[test] + // fn sine() { + // let mut wf = + // SynthWaveform::new(cpal::SampleRate(1000), 100f32, SynthWaveformFunction::Sine); + // + // } } From 107454efded065091990fe32ea921a155ce143bd Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 9 Aug 2024 12:54:41 -0700 Subject: [PATCH 04/65] Implementation of synth waveforms, tests --- src/source/synth.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/source/synth.rs b/src/source/synth.rs index 1531b6d1..499827c6 100644 --- a/src/source/synth.rs +++ b/src/source/synth.rs @@ -106,6 +106,7 @@ impl Source for SynthWaveform { #[cfg(test)] mod tests { use crate::source::synth::*; + use approx::assert_abs_diff_eq; #[test] fn square() { @@ -165,10 +166,17 @@ mod tests { assert_eq!(wf.next(), Some(-1.0f32)); } - // #[test] - // fn sine() { - // let mut wf = - // SynthWaveform::new(cpal::SampleRate(1000), 100f32, SynthWaveformFunction::Sine); - // - // } + #[test] + fn sine() { + let mut wf = + SynthWaveform::new(cpal::SampleRate(1000), 100f32, SynthWaveformFunction::Sine); + + assert_abs_diff_eq!(wf.next().unwrap(), 0.0f32); + assert_abs_diff_eq!(wf.next().unwrap(), 0.58778525f32); + assert_abs_diff_eq!(wf.next().unwrap(), 0.95105652f32); + assert_abs_diff_eq!(wf.next().unwrap(), 0.95105652f32); + assert_abs_diff_eq!(wf.next().unwrap(), 0.58778525f32); + assert_abs_diff_eq!(wf.next().unwrap(), 0.0f32); + assert_abs_diff_eq!(wf.next().unwrap(), -0.58778554f32); + } } From 7d4c9d78fa84ce4298e541fe18542c53caf9bae3 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 9 Aug 2024 18:46:44 -0700 Subject: [PATCH 05/65] Implemented `sine` source as a Synth --- src/source/sine.rs | 26 ++++++++++---------------- src/source/synth.rs | 2 +- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/source/sine.rs b/src/source/sine.rs index 44aa48ee..f263dccd 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -1,26 +1,27 @@ -use std::f32::consts::PI; use std::time::Duration; +use crate::source::{SynthWaveform, SynthWaveformFunction}; use crate::Source; use super::SeekError; +const SAMPLE_RATE: u32 = 46000; + /// An infinite source that produces a sine. /// /// Always has a rate of 48kHz and one channel. #[derive(Clone, Debug)] pub struct SineWave { - freq: f32, - num_sample: usize, + synth: SynthWaveform, } impl SineWave { /// The frequency of the sine. #[inline] pub fn new(freq: f32) -> SineWave { + let sr = cpal::SampleRate(SAMPLE_RATE); SineWave { - freq, - num_sample: 0, + synth: SynthWaveform::new(sr, freq, SynthWaveformFunction::Sine), } } } @@ -30,10 +31,7 @@ impl Iterator for SineWave { #[inline] fn next(&mut self) -> Option { - self.num_sample = self.num_sample.wrapping_add(1); - - let value = 2.0 * PI * self.freq * self.num_sample as f32 / 48000.0; - Some(value.sin()) + self.synth.next() } } @@ -50,7 +48,7 @@ impl Source for SineWave { #[inline] fn sample_rate(&self) -> u32 { - 48000 + SAMPLE_RATE } #[inline] @@ -59,11 +57,7 @@ impl Source for SineWave { } #[inline] - fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { - // This is a constant sound, normal seeking would not have any effect. - // While changing the phase of the sine wave could change how it sounds in - // combination with another sound (beating) such precision is not the intend - // of seeking - Ok(()) + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + self.synth.try_seek(pos) } } diff --git a/src/source/synth.rs b/src/source/synth.rs index 499827c6..e87dfe9e 100644 --- a/src/source/synth.rs +++ b/src/source/synth.rs @@ -37,7 +37,7 @@ impl SynthWaveformFunction { /// An infinite source that produces one of a selection of synthesizer /// waveforms. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct SynthWaveform { sample_rate: cpal::SampleRate, period: f32, From 278390471538cdd26b3428bd303a77b17dcab68e Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 9 Aug 2024 20:17:42 -0700 Subject: [PATCH 06/65] Dumb typo on sine sample rate --- src/source/sine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/source/sine.rs b/src/source/sine.rs index f263dccd..6e78ec69 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -5,7 +5,7 @@ use crate::Source; use super::SeekError; -const SAMPLE_RATE: u32 = 46000; +const SAMPLE_RATE: u32 = 48000; /// An infinite source that produces a sine. /// From b6c8a7ee48b5f87ae0653e217992599a5c68a416 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Sun, 11 Aug 2024 14:13:42 -0700 Subject: [PATCH 07/65] Implemented White and Pink noise generators Need to write an example app to test them. --- Cargo.toml | 1 + src/source/mod.rs | 1 + src/source/noise.rs | 106 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 src/source/noise.rs diff --git a/Cargo.toml b/Cargo.toml index aa75167a..2c5ed858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ minimp3_fixed = { version = "0.5.4", optional = true} symphonia = { version = "0.5.4", optional = true, default-features = false } crossbeam-channel = { version = "0.5.8", optional = true } thiserror = "1.0.49" +rand = "0.8.5" [features] default = ["flac", "vorbis", "wav", "mp3"] diff --git a/src/source/mod.rs b/src/source/mod.rs index 84ce2f35..d86c8334 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -52,6 +52,7 @@ mod from_factory; mod from_iter; mod linear_ramp; mod mix; +mod noise; mod pausable; mod periodic; mod position; diff --git a/src/source/noise.rs b/src/source/noise.rs new file mode 100644 index 00000000..cbbbbd12 --- /dev/null +++ b/src/source/noise.rs @@ -0,0 +1,106 @@ +use crate::Source; + +use rand::prelude::*; + +/// Generates an infinite stream of random samples in [=1.0, 1.0] +#[derive(Clone, Debug)] +pub struct WhiteNoise { + sample_rate: cpal::SampleRate, + rng: rand::rngs::ThreadRng, +} + +impl WhiteNoise { + /// Create a new white noise generator. + pub fn new(sample_rate: cpal::SampleRate) -> Self { + Self { + sample_rate, + rng: rand::thread_rng(), + } + } +} + +impl Iterator for WhiteNoise { + type Item = f32; + + #[inline] + fn next(&mut self) -> Option { + let ru32: i64 = self.rng.next_u32().into(); + let scaled: i64 = 2 * ru32 - (u32::MAX / 2) as i64; + let sample: f32 = scaled as f32 / u32::MAX as f32; + Some(sample) + } +} + +impl Source for WhiteNoise { + fn current_frame_len(&self) -> Option { + None + } + + fn channels(&self) -> u16 { + 1 + } + + fn sample_rate(&self) -> u32 { + self.sample_rate.0 + } + + fn total_duration(&self) -> Option { + None + } +} + +// https://www.musicdsp.org/en/latest/Filters/76-pink-noise-filter.html +// +/// Generate an infinite stream of pink noise samples in [-1.0, 1.0]. +struct PinkNoise { + noise: WhiteNoise, + b: [f32; 7], +} + +impl PinkNoise { + pub fn new(sample_rate: cpal::SampleRate) -> Self { + Self { + noise: WhiteNoise::new(sample_rate), + b: [0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32], + } + } +} + +impl Iterator for PinkNoise { + type Item = f32; + + fn next(&mut self) -> Option { + let white = self.noise.next().unwrap(); + self.b[0] = 0.99886 * self.b[0] + white * 0.0555179; + self.b[1] = 0.99332 * self.b[1] + white * 0.0750759; + self.b[2] = 0.96900 * self.b[2] + white * 0.1538520; + self.b[3] = 0.86650 * self.b[3] + white * 0.3104856; + self.b[4] = 0.55000 * self.b[4] + white * 0.5329522; + self.b[5] = -0.7616 * self.b[5] - white * 0.0168980; + + let pink = self.b[0] + self.b[1] + self.b[2] + self.b[3] + self.b[4] + self.b[5] + + self.b[6] + white * 0.5362; + + self.b[6] = white * 0.115926; + + Some(pink) + } +} + +impl Source for PinkNoise { + fn current_frame_len(&self) -> Option { + None + } + + fn channels(&self) -> u16 { + 1 + } + + fn sample_rate(&self) -> u32 { + self.noise.sample_rate() + } + + fn total_duration(&self) -> Option { + None + } +} From 1eabb212ac21b7ac7d303eff09c3bb1463c9537f Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Sun, 11 Aug 2024 14:29:24 -0700 Subject: [PATCH 08/65] Some clippy and style changes --- src/source/mod.rs | 1 + src/source/noise.rs | 38 ++++++++++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/source/mod.rs b/src/source/mod.rs index d86c8334..059c84ed 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -21,6 +21,7 @@ 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::noise::{pink, white, PinkNoise, WhiteNoise}; pub use self::pausable::Pausable; pub use self::periodic::PeriodicAccess; pub use self::position::TrackPosition; diff --git a/src/source/noise.rs b/src/source/noise.rs index cbbbbd12..f8b55aa9 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -2,6 +2,18 @@ use crate::Source; use rand::prelude::*; +/// Create a new white noise source. +#[inline] +pub fn white(sample_rate: cpal::SampleRate) -> WhiteNoise { + WhiteNoise::new(sample_rate) +} + +/// Create a new pink noise source. +#[inline] +pub fn pink(sample_rate: cpal::SampleRate) -> PinkNoise { + PinkNoise::new(sample_rate) +} + /// Generates an infinite stream of random samples in [=1.0, 1.0] #[derive(Clone, Debug)] pub struct WhiteNoise { @@ -32,18 +44,22 @@ impl Iterator for WhiteNoise { } impl Source for WhiteNoise { + #[inline] fn current_frame_len(&self) -> Option { None } + #[inline] fn channels(&self) -> u16 { 1 } + #[inline] fn sample_rate(&self) -> u32 { self.sample_rate.0 } + #[inline] fn total_duration(&self) -> Option { None } @@ -52,7 +68,7 @@ impl Source for WhiteNoise { // https://www.musicdsp.org/en/latest/Filters/76-pink-noise-filter.html // /// Generate an infinite stream of pink noise samples in [-1.0, 1.0]. -struct PinkNoise { +pub struct PinkNoise { noise: WhiteNoise, b: [f32; 7], } @@ -73,13 +89,19 @@ impl Iterator for PinkNoise { let white = self.noise.next().unwrap(); self.b[0] = 0.99886 * self.b[0] + white * 0.0555179; self.b[1] = 0.99332 * self.b[1] + white * 0.0750759; - self.b[2] = 0.96900 * self.b[2] + white * 0.1538520; - self.b[3] = 0.86650 * self.b[3] + white * 0.3104856; - self.b[4] = 0.55000 * self.b[4] + white * 0.5329522; - self.b[5] = -0.7616 * self.b[5] - white * 0.0168980; - - let pink = self.b[0] + self.b[1] + self.b[2] + self.b[3] + self.b[4] + self.b[5] + - self.b[6] + white * 0.5362; + self.b[2] = 0.969 * self.b[2] + white * 0.153852; + self.b[3] = 0.8665 * self.b[3] + white * 0.3104856; + self.b[4] = 0.550 * self.b[4] + white * 0.5329522; + self.b[5] = -0.7616 * self.b[5] - white * 0.016898; + + let pink = self.b[0] + + self.b[1] + + self.b[2] + + self.b[3] + + self.b[4] + + self.b[5] + + self.b[6] + + white * 0.5362; self.b[6] = white * 0.115926; From 84a008dd2743531afc9c0ba1fbd255ff38ec0da1 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Sun, 11 Aug 2024 15:28:15 -0700 Subject: [PATCH 09/65] Added noise_generator example --- examples/noise_generator.rs | 33 +++++++++++++++++++++++++++++++++ src/source/noise.rs | 27 +++++++++++++++++---------- 2 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 examples/noise_generator.rs diff --git a/examples/noise_generator.rs b/examples/noise_generator.rs new file mode 100644 index 00000000..49f1f88e --- /dev/null +++ b/examples/noise_generator.rs @@ -0,0 +1,33 @@ +use std::thread; +use std::time::Duration; + +use rodio::source::{pink, white, Source}; + +fn main() { + let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap(); + + let noise_duration = Duration::from_millis(1000); + let interval_duration = Duration::from_millis(1500); + + stream_handle + .play_raw( + white(cpal::SampleRate(48000)) + .amplify(0.1) + .take_duration(noise_duration), + ) + .unwrap(); + println!("Playing white noise"); + + thread::sleep(interval_duration); + + stream_handle + .play_raw( + pink(cpal::SampleRate(48000)) + .amplify(0.1) + .take_duration(noise_duration), + ) + .unwrap(); + println!("Playing pink noise"); + + thread::sleep(interval_duration); +} diff --git a/src/source/noise.rs b/src/source/noise.rs index f8b55aa9..25f42266 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -1,6 +1,6 @@ use crate::Source; -use rand::prelude::*; +use super::SeekError; /// Create a new white noise source. #[inline] @@ -18,16 +18,12 @@ pub fn pink(sample_rate: cpal::SampleRate) -> PinkNoise { #[derive(Clone, Debug)] pub struct WhiteNoise { sample_rate: cpal::SampleRate, - rng: rand::rngs::ThreadRng, } impl WhiteNoise { /// Create a new white noise generator. pub fn new(sample_rate: cpal::SampleRate) -> Self { - Self { - sample_rate, - rng: rand::thread_rng(), - } + Self { sample_rate } } } @@ -36,10 +32,9 @@ impl Iterator for WhiteNoise { #[inline] fn next(&mut self) -> Option { - let ru32: i64 = self.rng.next_u32().into(); - let scaled: i64 = 2 * ru32 - (u32::MAX / 2) as i64; - let sample: f32 = scaled as f32 / u32::MAX as f32; - Some(sample) + let randf = rand::random::(); + let scaled = randf * 2.0 - 1.0; + Some(scaled) } } @@ -63,6 +58,12 @@ impl Source for WhiteNoise { fn total_duration(&self) -> Option { None } + + #[inline] + fn try_seek(&mut self, _: std::time::Duration) -> Result<(), SeekError> { + // Does nothing, should do nothing + Ok(()) + } } // https://www.musicdsp.org/en/latest/Filters/76-pink-noise-filter.html @@ -125,4 +126,10 @@ impl Source for PinkNoise { fn total_duration(&self) -> Option { None } + + #[inline] + fn try_seek(&mut self, _: std::time::Duration) -> Result<(), SeekError> { + // Does nothing, should do nothing + Ok(()) + } } From a015eb49a20bf851fad0150f72709282ecd6933c Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 12 Aug 2024 20:28:25 -0700 Subject: [PATCH 10/65] Using SmallRng for white noise generation This resolves a PR code review note and should make the source run much faster. --- Cargo.toml | 2 +- src/source/noise.rs | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2c5ed858..a75b74c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ minimp3_fixed = { version = "0.5.4", optional = true} symphonia = { version = "0.5.4", optional = true, default-features = false } crossbeam-channel = { version = "0.5.8", optional = true } thiserror = "1.0.49" -rand = "0.8.5" +rand = {version = "0.8.5", features = ["small_rng"]} [features] default = ["flac", "vorbis", "wav", "mp3"] diff --git a/src/source/noise.rs b/src/source/noise.rs index 25f42266..dfee9172 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -2,6 +2,9 @@ use crate::Source; use super::SeekError; +use rand::rngs::SmallRng; +use rand::{RngCore, SeedableRng}; + /// Create a new white noise source. #[inline] pub fn white(sample_rate: cpal::SampleRate) -> WhiteNoise { @@ -18,12 +21,16 @@ pub fn pink(sample_rate: cpal::SampleRate) -> PinkNoise { #[derive(Clone, Debug)] pub struct WhiteNoise { sample_rate: cpal::SampleRate, + rng: SmallRng, } impl WhiteNoise { /// Create a new white noise generator. pub fn new(sample_rate: cpal::SampleRate) -> Self { - Self { sample_rate } + Self { + sample_rate, + rng: SmallRng::from_entropy(), + } } } @@ -32,7 +39,7 @@ impl Iterator for WhiteNoise { #[inline] fn next(&mut self) -> Option { - let randf = rand::random::(); + let randf = self.rng.next_u32() as f32 / u32::MAX as f32; let scaled = randf * 2.0 - 1.0; Some(scaled) } From 56d087afbc8b1accdbf6a5090f88f81ad2d20f2b Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Thu, 15 Aug 2024 00:33:02 -0700 Subject: [PATCH 11/65] Renamed Synth to Test Modules, files and types renamed --- src/source/mod.rs | 4 +-- src/source/sine.rs | 6 ++-- src/source/{synth.rs => test_waveform.rs} | 36 +++++++++++------------ 3 files changed, 23 insertions(+), 23 deletions(-) rename src/source/{synth.rs => test_waveform.rs} (86%) diff --git a/src/source/mod.rs b/src/source/mod.rs index 059c84ed..6043318c 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -33,7 +33,7 @@ pub use self::skippable::Skippable; pub use self::spatial::Spatial; pub use self::speed::Speed; pub use self::stoppable::Stoppable; -pub use self::synth::{SynthWaveform, SynthWaveformFunction}; +pub use self::test_waveform::{TestWaveform, TestWaveformFunction}; pub use self::take::TakeDuration; pub use self::uniform::UniformSourceIterator; pub use self::zero::Zero; @@ -65,8 +65,8 @@ mod skippable; mod spatial; mod speed; mod stoppable; -mod synth; mod take; +mod test_waveform; mod uniform; mod zero; diff --git a/src/source/sine.rs b/src/source/sine.rs index 6e78ec69..b9c11a05 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use crate::source::{SynthWaveform, SynthWaveformFunction}; +use crate::source::{TestWaveform, TestWaveformFunction}; use crate::Source; use super::SeekError; @@ -12,7 +12,7 @@ const SAMPLE_RATE: u32 = 48000; /// Always has a rate of 48kHz and one channel. #[derive(Clone, Debug)] pub struct SineWave { - synth: SynthWaveform, + synth: TestWaveform, } impl SineWave { @@ -21,7 +21,7 @@ impl SineWave { pub fn new(freq: f32) -> SineWave { let sr = cpal::SampleRate(SAMPLE_RATE); SineWave { - synth: SynthWaveform::new(sr, freq, SynthWaveformFunction::Sine), + synth: TestWaveform::new(sr, freq, TestWaveformFunction::Sine), } } } diff --git a/src/source/synth.rs b/src/source/test_waveform.rs similarity index 86% rename from src/source/synth.rs rename to src/source/test_waveform.rs index e87dfe9e..6919bead 100644 --- a/src/source/synth.rs +++ b/src/source/test_waveform.rs @@ -7,14 +7,14 @@ use crate::Source; /// Syntheizer waveform functions. All of the synth waveforms are in the /// codomain [-1.0, 1.0]. #[derive(Clone, Debug)] -pub enum SynthWaveformFunction { +pub enum TestWaveformFunction { Sine, Triangle, Square, Sawtooth, } -impl SynthWaveformFunction { +impl TestWaveformFunction { /// Create a single sample for the given waveform #[inline] pub fn render(&self, i: u64, period: f32) -> f32 { @@ -38,24 +38,24 @@ impl SynthWaveformFunction { /// An infinite source that produces one of a selection of synthesizer /// waveforms. #[derive(Clone, Debug)] -pub struct SynthWaveform { +pub struct TestWaveform { sample_rate: cpal::SampleRate, period: f32, - f: SynthWaveformFunction, + f: TestWaveformFunction, i: u64, } -impl SynthWaveform { +impl TestWaveform { /// Create a new `SynthWaveform` object that generates an endless waveform /// `f`. #[inline] pub fn new( sample_rate: cpal::SampleRate, frequency: f32, - f: SynthWaveformFunction, - ) -> SynthWaveform { + f: TestWaveformFunction, + ) -> TestWaveform { let period = sample_rate.0 as f32 / frequency; - SynthWaveform { + TestWaveform { sample_rate, period, f, @@ -64,7 +64,7 @@ impl SynthWaveform { } } -impl Iterator for SynthWaveform { +impl Iterator for TestWaveform { type Item = f32; #[inline] @@ -75,7 +75,7 @@ impl Iterator for SynthWaveform { } } -impl Source for SynthWaveform { +impl Source for TestWaveform { #[inline] fn current_frame_len(&self) -> Option { None @@ -105,15 +105,15 @@ impl Source for SynthWaveform { #[cfg(test)] mod tests { - use crate::source::synth::*; + use crate::source::{TestWaveform, TestWaveformFunction}; use approx::assert_abs_diff_eq; #[test] fn square() { - let mut wf = SynthWaveform::new( + let mut wf = TestWaveform::new( cpal::SampleRate(2000), 500.0f32, - SynthWaveformFunction::Square, + TestWaveformFunction::Square, ); assert_eq!(wf.next(), Some(1.0f32)); assert_eq!(wf.next(), Some(1.0f32)); @@ -127,10 +127,10 @@ mod tests { #[test] fn triangle() { - let mut wf = SynthWaveform::new( + let mut wf = TestWaveform::new( cpal::SampleRate(8000), 1000.0f32, - SynthWaveformFunction::Triangle, + TestWaveformFunction::Triangle, ); assert_eq!(wf.next(), Some(-1.0f32)); assert_eq!(wf.next(), Some(-0.5f32)); @@ -152,10 +152,10 @@ mod tests { #[test] fn saw() { - let mut wf = SynthWaveform::new( + let mut wf = TestWaveform::new( cpal::SampleRate(200), 50.0f32, - SynthWaveformFunction::Sawtooth, + TestWaveformFunction::Sawtooth, ); assert_eq!(wf.next(), Some(0.0f32)); assert_eq!(wf.next(), Some(0.5f32)); @@ -169,7 +169,7 @@ mod tests { #[test] fn sine() { let mut wf = - SynthWaveform::new(cpal::SampleRate(1000), 100f32, SynthWaveformFunction::Sine); + TestWaveform::new(cpal::SampleRate(1000), 100f32, TestWaveformFunction::Sine); assert_abs_diff_eq!(wf.next().unwrap(), 0.0f32); assert_abs_diff_eq!(wf.next().unwrap(), 0.58778525f32); From 0907857501f68aa0e9b020e13232a35fd6c6ba62 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Thu, 15 Aug 2024 01:01:53 -0700 Subject: [PATCH 12/65] Documentation updates --- src/source/mod.rs | 2 +- src/source/test_waveform.rs | 29 +++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/source/mod.rs b/src/source/mod.rs index 6043318c..3448529e 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -33,8 +33,8 @@ pub use self::skippable::Skippable; pub use self::spatial::Spatial; pub use self::speed::Speed; pub use self::stoppable::Stoppable; -pub use self::test_waveform::{TestWaveform, TestWaveformFunction}; pub use self::take::TakeDuration; +pub use self::test_waveform::{TestWaveform, TestWaveformFunction}; pub use self::uniform::UniformSourceIterator; pub use self::zero::Zero; diff --git a/src/source/test_waveform.rs b/src/source/test_waveform.rs index 6919bead..1b2214bd 100644 --- a/src/source/test_waveform.rs +++ b/src/source/test_waveform.rs @@ -1,23 +1,38 @@ +//! Generator sources for various periodic test waveforms. +//! +//! This module provides several periodic, deterministic waveforms for testing other sources. Every +//! source oscillates in the codomain `[-1.0f32, 1.0f32]`. +//! +//! # Example +//! +//! ``` +//! use rodio::source::{TestWaveform,TestWaveformFunction}; +//! +//! let tone = TestWaveform::new(cpal::SampleRate(48000), 440.0, TestWaveformFunction::Sine); +//! ``` use std::f32::consts::TAU; use std::time::Duration; use super::SeekError; use crate::Source; -/// Syntheizer waveform functions. All of the synth waveforms are in the -/// codomain [-1.0, 1.0]. +/// Test waveform functions. #[derive(Clone, Debug)] pub enum TestWaveformFunction { + /// A sinusoidal waveform. Sine, + /// A triangle wave. Triangle, + /// A square wave, rising edge at t=0. Square, + /// A rising swatooth wave. Sawtooth, } impl TestWaveformFunction { /// Create a single sample for the given waveform #[inline] - pub fn render(&self, i: u64, period: f32) -> f32 { + fn render(&self, i: u64, period: f32) -> f32 { let i_div_p: f32 = i as f32 / period; match self { @@ -35,8 +50,7 @@ impl TestWaveformFunction { } } -/// An infinite source that produces one of a selection of synthesizer -/// waveforms. +/// An infinite source that produces one of a selection of test waveforms. #[derive(Clone, Debug)] pub struct TestWaveform { sample_rate: cpal::SampleRate, @@ -46,7 +60,7 @@ pub struct TestWaveform { } impl TestWaveform { - /// Create a new `SynthWaveform` object that generates an endless waveform + /// Create a new `TestWaveform` object that generates an endless waveform /// `f`. #[inline] pub fn new( @@ -168,8 +182,7 @@ mod tests { #[test] fn sine() { - let mut wf = - TestWaveform::new(cpal::SampleRate(1000), 100f32, TestWaveformFunction::Sine); + let mut wf = TestWaveform::new(cpal::SampleRate(1000), 100f32, TestWaveformFunction::Sine); assert_abs_diff_eq!(wf.next().unwrap(), 0.0f32); assert_abs_diff_eq!(wf.next().unwrap(), 0.58778525f32); From 87cd605d73060181eb5a8b3129028b5d3ef6f114 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Thu, 15 Aug 2024 01:32:46 -0700 Subject: [PATCH 13/65] More documentation, WhiteNoise creation option. --- src/source/noise.rs | 25 +++++++++++++++++++------ src/source/test_waveform.rs | 5 +++-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/source/noise.rs b/src/source/noise.rs index dfee9172..04b450ed 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -1,3 +1,6 @@ +//! Noise sources. +//! +//! use crate::Source; use super::SeekError; @@ -5,19 +8,20 @@ use super::SeekError; use rand::rngs::SmallRng; use rand::{RngCore, SeedableRng}; -/// Create a new white noise source. +/// Create a new `WhiteNoise` noise source. #[inline] pub fn white(sample_rate: cpal::SampleRate) -> WhiteNoise { WhiteNoise::new(sample_rate) } -/// Create a new pink noise source. +/// Create a new `PinkNoise` noise source. #[inline] pub fn pink(sample_rate: cpal::SampleRate) -> PinkNoise { PinkNoise::new(sample_rate) } -/// Generates an infinite stream of random samples in [=1.0, 1.0] +/// Generates an infinite stream of random samples in [-1.0, 1.0]. This source generates random +/// samples as provided by the `rand::rngs::SmallRng` randomness source. #[derive(Clone, Debug)] pub struct WhiteNoise { sample_rate: cpal::SampleRate, @@ -25,7 +29,15 @@ pub struct WhiteNoise { } impl WhiteNoise { - /// Create a new white noise generator. + /// Create a new white noise generator, seeding the RNG with `seed`. + pub fn new_with_seed(sample_rate: cpal::SampleRate, seed: u64) -> Self { + Self { + sample_rate, + rng: SmallRng::seed_from_u64(seed), + } + } + + /// Create a new white noise generator, seeding the RNG with system entropy. pub fn new(sample_rate: cpal::SampleRate) -> Self { Self { sample_rate, @@ -73,9 +85,10 @@ impl Source for WhiteNoise { } } -// https://www.musicdsp.org/en/latest/Filters/76-pink-noise-filter.html -// /// Generate an infinite stream of pink noise samples in [-1.0, 1.0]. +/// +/// The output of this source is the result of taking the output of the `WhiteNoise` source and +/// filtering it according to a weighted-sum of seven FIR filters after [Paul Kellett](https://www.musicdsp.org/en/latest/Filters/76-pink-noise-filter.html). pub struct PinkNoise { noise: WhiteNoise, b: [f32; 7], diff --git a/src/source/test_waveform.rs b/src/source/test_waveform.rs index 1b2214bd..b9615aea 100644 --- a/src/source/test_waveform.rs +++ b/src/source/test_waveform.rs @@ -1,7 +1,8 @@ //! Generator sources for various periodic test waveforms. //! -//! This module provides several periodic, deterministic waveforms for testing other sources. Every -//! source oscillates in the codomain `[-1.0f32, 1.0f32]`. +//! This module provides several periodic, deterministic waveforms for testing other sources and +//! for simple additive sound synthesis. Every source is monoaural and in the codomain `[-1.0f32, +//! 1.0f32]` //! //! # Example //! From 59a5b1236738888699b32b161a894479b50741ca Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 19 Aug 2024 17:14:06 -0500 Subject: [PATCH 14/65] Several code review fixes --- src/source/chirp.rs | 79 +++++++++++++++++++++++++++++++++++++ src/source/mod.rs | 2 + src/source/noise.rs | 25 ++++++------ src/source/sine.rs | 8 ++-- src/source/test_waveform.rs | 37 +++++++++-------- 5 files changed, 120 insertions(+), 31 deletions(-) create mode 100644 src/source/chirp.rs diff --git a/src/source/chirp.rs b/src/source/chirp.rs new file mode 100644 index 00000000..43d2b30d --- /dev/null +++ b/src/source/chirp.rs @@ -0,0 +1,79 @@ +//! Chirp/sweep source. + +use std::time::Duration; + +use crate::Source; + +/// Convenience function to create a new `Chirp` source. +#[inline] +pub fn chirp( + sample_rate: cpal::SampleRate, + start_frequency: f32, + end_frequency: f32, + duration: Duration, +) -> Chirp { + Chirp::new(sample_rate, start_frequency, end_frequency, duration) +} + +/// Generate a sine wave with an instantaneous frequency that changes/sweeps linearly over time. +/// At the end of the chirp, once the `end_frequency` is reached, the source is exhausted. +#[derive(Clone, Debug)] +pub struct Chirp { + start_frequency: f32, + end_frequency: f32, + sample_rate: cpal::SampleRate, + total_samples: u64, + elapsed_samples: u64, +} + +impl Chirp { + fn new( + sample_rate: cpal::SampleRate, + start_frequency: f32, + end_frequency: f32, + duration: Duration, + ) -> Self { + Self { + sample_rate, + start_frequency, + end_frequency, + total_samples: (duration.as_secs_f64() * (sample_rate.0 as f64)) as u64, + elapsed_samples: 0, + } + } +} + +impl Iterator for Chirp { + type Item = f32; + + fn next(&mut self) -> Option { + let i = self.elapsed_samples; + self.elapsed_samples += 1; + + + todo!() + } +} + +impl Source for Chirp { + fn current_frame_len(&self) -> Option { + None + } + + fn channels(&self) -> u16 { + 1 + } + + fn sample_rate(&self) -> u32 { + self.sample_rate.0 + } + + fn total_duration(&self) -> Option { + let secs: f64 = self.total_samples as f64 / self.sample_rate.0 as f64; + Some(Duration::new(1,0).mul_f64(secs)) + } + + fn try_seek(&mut self, pos: Duration) -> Result<(), super::SeekError> { + todo!() + } +} diff --git a/src/source/mod.rs b/src/source/mod.rs index 3448529e..23f058de 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -10,6 +10,7 @@ pub use self::amplify::Amplify; pub use self::blt::BltFilter; pub use self::buffered::Buffered; pub use self::channel_volume::ChannelVolume; +pub use self::chirp::{chirp, Chirp}; pub use self::crossfade::Crossfade; pub use self::delay::Delay; pub use self::done::Done; @@ -41,6 +42,7 @@ pub use self::zero::Zero; mod amplify; mod blt; mod buffered; +mod chirp; mod channel_volume; mod crossfade; mod delay; diff --git a/src/source/noise.rs b/src/source/noise.rs index 04b450ed..434cc998 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -8,13 +8,13 @@ use super::SeekError; use rand::rngs::SmallRng; use rand::{RngCore, SeedableRng}; -/// Create a new `WhiteNoise` noise source. +/// Convenience function to create a new `WhiteNoise` noise source. #[inline] pub fn white(sample_rate: cpal::SampleRate) -> WhiteNoise { WhiteNoise::new(sample_rate) } -/// Create a new `PinkNoise` noise source. +/// Convenience function to create a new `PinkNoise` noise source. #[inline] pub fn pink(sample_rate: cpal::SampleRate) -> PinkNoise { PinkNoise::new(sample_rate) @@ -51,8 +51,8 @@ impl Iterator for WhiteNoise { #[inline] fn next(&mut self) -> Option { - let randf = self.rng.next_u32() as f32 / u32::MAX as f32; - let scaled = randf * 2.0 - 1.0; + let rand = self.rng.next_u32() as f32 / u32::MAX as f32; + let scaled = rand * 2.0 - 1.0; Some(scaled) } } @@ -85,19 +85,22 @@ impl Source for WhiteNoise { } } -/// Generate an infinite stream of pink noise samples in [-1.0, 1.0]. +/// Generates an infinite stream of pink noise samples in [-1.0, 1.0]. /// -/// The output of this source is the result of taking the output of the `WhiteNoise` source and -/// filtering it according to a weighted-sum of seven FIR filters after [Paul Kellett](https://www.musicdsp.org/en/latest/Filters/76-pink-noise-filter.html). +/// The output of the source is the result of taking the output of the `WhiteNoise` source and +/// filtering it according to a weighted-sum of seven FIR filters after [Paul Kellett's +/// method][pk_method] from *musicdsp.org*. +/// +/// [pk_method]: https://www.musicdsp.org/en/latest/Filters/76-pink-noise-filter.html pub struct PinkNoise { - noise: WhiteNoise, + white_noise: WhiteNoise, b: [f32; 7], } impl PinkNoise { pub fn new(sample_rate: cpal::SampleRate) -> Self { Self { - noise: WhiteNoise::new(sample_rate), + white_noise: WhiteNoise::new(sample_rate), b: [0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32], } } @@ -107,7 +110,7 @@ impl Iterator for PinkNoise { type Item = f32; fn next(&mut self) -> Option { - let white = self.noise.next().unwrap(); + let white = self.white_noise.next().unwrap(); self.b[0] = 0.99886 * self.b[0] + white * 0.0555179; self.b[1] = 0.99332 * self.b[1] + white * 0.0750759; self.b[2] = 0.969 * self.b[2] + white * 0.153852; @@ -140,7 +143,7 @@ impl Source for PinkNoise { } fn sample_rate(&self) -> u32 { - self.noise.sample_rate() + self.white_noise.sample_rate() } fn total_duration(&self) -> Option { diff --git a/src/source/sine.rs b/src/source/sine.rs index b9c11a05..0fafcf00 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -12,7 +12,7 @@ const SAMPLE_RATE: u32 = 48000; /// Always has a rate of 48kHz and one channel. #[derive(Clone, Debug)] pub struct SineWave { - synth: TestWaveform, + test_sine: TestWaveform, } impl SineWave { @@ -21,7 +21,7 @@ impl SineWave { pub fn new(freq: f32) -> SineWave { let sr = cpal::SampleRate(SAMPLE_RATE); SineWave { - synth: TestWaveform::new(sr, freq, TestWaveformFunction::Sine), + test_sine: TestWaveform::new(sr, freq, TestWaveformFunction::Sine), } } } @@ -31,7 +31,7 @@ impl Iterator for SineWave { #[inline] fn next(&mut self) -> Option { - self.synth.next() + self.test_sine.next() } } @@ -58,6 +58,6 @@ impl Source for SineWave { #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - self.synth.try_seek(pos) + self.test_sine.try_seek(pos) } } diff --git a/src/source/test_waveform.rs b/src/source/test_waveform.rs index b9615aea..420eac58 100644 --- a/src/source/test_waveform.rs +++ b/src/source/test_waveform.rs @@ -1,8 +1,8 @@ //! Generator sources for various periodic test waveforms. //! //! This module provides several periodic, deterministic waveforms for testing other sources and -//! for simple additive sound synthesis. Every source is monoaural and in the codomain `[-1.0f32, -//! 1.0f32]` +//! for simple additive sound synthesis. Every source is monoaural and in the codomain [-1.0f32, +//! 1.0f32]. //! //! # Example //! @@ -17,12 +17,12 @@ use std::time::Duration; use super::SeekError; use crate::Source; -/// Test waveform functions. +/// Waveform functions. #[derive(Clone, Debug)] -pub enum TestWaveformFunction { +pub enum Function { /// A sinusoidal waveform. Sine, - /// A triangle wave. + /// A triangle waveform. Triangle, /// A square wave, rising edge at t=0. Square, @@ -30,7 +30,7 @@ pub enum TestWaveformFunction { Sawtooth, } -impl TestWaveformFunction { +impl Function { /// Create a single sample for the given waveform #[inline] fn render(&self, i: u64, period: f32) -> f32 { @@ -38,7 +38,7 @@ impl TestWaveformFunction { match self { Self::Sine => (TAU * i_div_p).sin(), - Self::Triangle => 04.0f32 * (i_div_p - (i_div_p + 0.5f32).floor()).abs() - 1f32, + Self::Triangle => 4.0f32 * (i_div_p - (i_div_p + 0.5f32).floor()).abs() - 1f32, Self::Square => { if i_div_p % 1.0f32 < 0.5f32 { 1.0f32 @@ -56,24 +56,29 @@ impl TestWaveformFunction { pub struct TestWaveform { sample_rate: cpal::SampleRate, period: f32, - f: TestWaveformFunction, + function: Function, i: u64, } impl TestWaveform { /// Create a new `TestWaveform` object that generates an endless waveform /// `f`. + /// + /// # Panics + /// + /// Will panic if `frequency` is equal to zero. #[inline] pub fn new( sample_rate: cpal::SampleRate, frequency: f32, - f: TestWaveformFunction, + f: Function, ) -> TestWaveform { + assert!(frequency != 0.0, "frequency must be greater than zero"); let period = sample_rate.0 as f32 / frequency; TestWaveform { sample_rate, period, - f, + function: f, i: 0, } } @@ -86,7 +91,7 @@ impl Iterator for TestWaveform { fn next(&mut self) -> Option { let this_i = self.i; self.i += 1; - Some(self.f.render(this_i, self.period)) + Some(self.function.render(this_i, self.period)) } } @@ -120,7 +125,7 @@ impl Source for TestWaveform { #[cfg(test)] mod tests { - use crate::source::{TestWaveform, TestWaveformFunction}; + use crate::source::{TestWaveform, Function}; use approx::assert_abs_diff_eq; #[test] @@ -128,7 +133,7 @@ mod tests { let mut wf = TestWaveform::new( cpal::SampleRate(2000), 500.0f32, - TestWaveformFunction::Square, + Function::Square, ); assert_eq!(wf.next(), Some(1.0f32)); assert_eq!(wf.next(), Some(1.0f32)); @@ -145,7 +150,7 @@ mod tests { let mut wf = TestWaveform::new( cpal::SampleRate(8000), 1000.0f32, - TestWaveformFunction::Triangle, + Function::Triangle, ); assert_eq!(wf.next(), Some(-1.0f32)); assert_eq!(wf.next(), Some(-0.5f32)); @@ -170,7 +175,7 @@ mod tests { let mut wf = TestWaveform::new( cpal::SampleRate(200), 50.0f32, - TestWaveformFunction::Sawtooth, + Function::Sawtooth, ); assert_eq!(wf.next(), Some(0.0f32)); assert_eq!(wf.next(), Some(0.5f32)); @@ -183,7 +188,7 @@ mod tests { #[test] fn sine() { - let mut wf = TestWaveform::new(cpal::SampleRate(1000), 100f32, TestWaveformFunction::Sine); + let mut wf = TestWaveform::new(cpal::SampleRate(1000), 100f32, Function::Sine); assert_abs_diff_eq!(wf.next().unwrap(), 0.0f32); assert_abs_diff_eq!(wf.next().unwrap(), 0.58778525f32); From a012ac1a48ae5e96b118384d8f820b15037e30ab Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 19 Aug 2024 17:15:27 -0500 Subject: [PATCH 15/65] Changes for code reviews --- Cargo.toml | 3 ++- src/source/mod.rs | 2 +- src/source/sine.rs | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a75b74c0..b93973d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ minimp3_fixed = { version = "0.5.4", optional = true} symphonia = { version = "0.5.4", optional = true, default-features = false } crossbeam-channel = { version = "0.5.8", optional = true } thiserror = "1.0.49" -rand = {version = "0.8.5", features = ["small_rng"]} +rand = { version = "0.8.5", features = ["small_rng"], optional = true } [features] default = ["flac", "vorbis", "wav", "mp3"] @@ -28,6 +28,7 @@ vorbis = ["lewton"] wav = ["hound"] mp3 = ["symphonia-mp3"] minimp3 = ["dep:minimp3_fixed"] +noise = ["rand"] wasm-bindgen = ["cpal/wasm-bindgen"] cpal-shared-stdcxx = ["cpal/oboe-shared-stdcxx"] symphonia-aac = ["symphonia/aac"] diff --git a/src/source/mod.rs b/src/source/mod.rs index 23f058de..14e252e7 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -35,7 +35,7 @@ pub use self::spatial::Spatial; pub use self::speed::Speed; pub use self::stoppable::Stoppable; pub use self::take::TakeDuration; -pub use self::test_waveform::{TestWaveform, TestWaveformFunction}; +pub use self::test_waveform::{TestWaveform, Function}; pub use self::uniform::UniformSourceIterator; pub use self::zero::Zero; diff --git a/src/source/sine.rs b/src/source/sine.rs index 0fafcf00..9a92b20b 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use crate::source::{TestWaveform, TestWaveformFunction}; +use crate::source::{TestWaveform, Function}; use crate::Source; use super::SeekError; @@ -21,7 +21,7 @@ impl SineWave { pub fn new(freq: f32) -> SineWave { let sr = cpal::SampleRate(SAMPLE_RATE); SineWave { - test_sine: TestWaveform::new(sr, freq, TestWaveformFunction::Sine), + test_sine: TestWaveform::new(sr, freq, Function::Sine), } } } From 0c2c1941d6edec698e6b7baeb59275081a9f8051 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 19 Aug 2024 17:39:25 -0500 Subject: [PATCH 16/65] Made noise feature Noise source and example guarded by it. --- Cargo.toml | 4 ++++ examples/noise_generator.rs | 16 ++++++++++++---- src/source/chirp.rs | 3 +-- src/source/mod.rs | 11 +++++++---- src/source/noise.rs | 4 ++-- src/source/sine.rs | 2 +- src/source/test_waveform.rs | 34 +++++++++------------------------- 7 files changed, 36 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b93973d1..fc29e7fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,3 +50,7 @@ approx = "0.5.1" [[example]] name = "music_m4a" required-features = ["symphonia-isomp4", "symphonia-aac"] + +[[example]] +name = "noise_generator" +required-features = ["noise"] diff --git a/examples/noise_generator.rs b/examples/noise_generator.rs index 49f1f88e..a72bcb7c 100644 --- a/examples/noise_generator.rs +++ b/examples/noise_generator.rs @@ -1,9 +1,11 @@ -use std::thread; -use std::time::Duration; - -use rodio::source::{pink, white, Source}; +//! Noise generator example. Use the "noise" feature to enable the noise generator sources. +#[cfg(feature = "noise")] fn main() { + use rodio::source::{pink, white, Source}; + use std::thread; + use std::time::Duration; + let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap(); let noise_duration = Duration::from_millis(1000); @@ -31,3 +33,9 @@ fn main() { thread::sleep(interval_duration); } + +#[cfg(not(feature = "noise"))] +fn main() { + println!("rodio has not been compiled with noise sources, use `--features noise` to enable this feature."); + println!("Exiting..."); +} diff --git a/src/source/chirp.rs b/src/source/chirp.rs index 43d2b30d..8fabc292 100644 --- a/src/source/chirp.rs +++ b/src/source/chirp.rs @@ -50,7 +50,6 @@ impl Iterator for Chirp { let i = self.elapsed_samples; self.elapsed_samples += 1; - todo!() } } @@ -70,7 +69,7 @@ impl Source for Chirp { fn total_duration(&self) -> Option { let secs: f64 = self.total_samples as f64 / self.sample_rate.0 as f64; - Some(Duration::new(1,0).mul_f64(secs)) + Some(Duration::new(1, 0).mul_f64(secs)) } fn try_seek(&mut self, pos: Duration) -> Result<(), super::SeekError> { diff --git a/src/source/mod.rs b/src/source/mod.rs index 14e252e7..5e9b6f39 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -22,7 +22,6 @@ 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::noise::{pink, white, PinkNoise, WhiteNoise}; pub use self::pausable::Pausable; pub use self::periodic::PeriodicAccess; pub use self::position::TrackPosition; @@ -35,15 +34,15 @@ pub use self::spatial::Spatial; pub use self::speed::Speed; pub use self::stoppable::Stoppable; pub use self::take::TakeDuration; -pub use self::test_waveform::{TestWaveform, Function}; +pub use self::test_waveform::{Function, TestWaveform}; pub use self::uniform::UniformSourceIterator; pub use self::zero::Zero; mod amplify; mod blt; mod buffered; -mod chirp; mod channel_volume; +mod chirp; mod crossfade; mod delay; mod done; @@ -55,7 +54,6 @@ mod from_factory; mod from_iter; mod linear_ramp; mod mix; -mod noise; mod pausable; mod periodic; mod position; @@ -72,6 +70,11 @@ mod test_waveform; mod uniform; mod zero; +#[cfg(feature = "noise")] +mod noise; +#[cfg(feature = "noise")] +pub use self::noise::{pink, white, PinkNoise, WhiteNoise}; + /// A source of samples. /// /// # A quick lesson about sounds diff --git a/src/source/noise.rs b/src/source/noise.rs index 434cc998..42ae0e61 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -1,12 +1,12 @@ //! Noise sources. //! //! + use crate::Source; use super::SeekError; -use rand::rngs::SmallRng; -use rand::{RngCore, SeedableRng}; +use rand::{rngs::SmallRng, RngCore, SeedableRng}; /// Convenience function to create a new `WhiteNoise` noise source. #[inline] diff --git a/src/source/sine.rs b/src/source/sine.rs index 9a92b20b..73db5eba 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use crate::source::{TestWaveform, Function}; +use crate::source::{Function, TestWaveform}; use crate::Source; use super::SeekError; diff --git a/src/source/test_waveform.rs b/src/source/test_waveform.rs index 420eac58..fea4f508 100644 --- a/src/source/test_waveform.rs +++ b/src/source/test_waveform.rs @@ -7,9 +7,9 @@ //! # Example //! //! ``` -//! use rodio::source::{TestWaveform,TestWaveformFunction}; +//! use rodio::source::{TestWaveform,Function}; //! -//! let tone = TestWaveform::new(cpal::SampleRate(48000), 440.0, TestWaveformFunction::Sine); +//! let tone = TestWaveform::new(cpal::SampleRate(48000), 440.0, Function::Sine); //! ``` use std::f32::consts::TAU; use std::time::Duration; @@ -68,11 +68,7 @@ impl TestWaveform { /// /// Will panic if `frequency` is equal to zero. #[inline] - pub fn new( - sample_rate: cpal::SampleRate, - frequency: f32, - f: Function, - ) -> TestWaveform { + pub fn new(sample_rate: cpal::SampleRate, frequency: f32, f: Function) -> TestWaveform { assert!(frequency != 0.0, "frequency must be greater than zero"); let period = sample_rate.0 as f32 / frequency; TestWaveform { @@ -89,9 +85,9 @@ impl Iterator for TestWaveform { #[inline] fn next(&mut self) -> Option { - let this_i = self.i; + let val = Some(self.function.render(self.i, self.period)); self.i += 1; - Some(self.function.render(this_i, self.period)) + val } } @@ -125,16 +121,12 @@ impl Source for TestWaveform { #[cfg(test)] mod tests { - use crate::source::{TestWaveform, Function}; + use crate::source::{Function, TestWaveform}; use approx::assert_abs_diff_eq; #[test] fn square() { - let mut wf = TestWaveform::new( - cpal::SampleRate(2000), - 500.0f32, - Function::Square, - ); + let mut wf = TestWaveform::new(cpal::SampleRate(2000), 500.0f32, Function::Square); assert_eq!(wf.next(), Some(1.0f32)); assert_eq!(wf.next(), Some(1.0f32)); assert_eq!(wf.next(), Some(-1.0f32)); @@ -147,11 +139,7 @@ mod tests { #[test] fn triangle() { - let mut wf = TestWaveform::new( - cpal::SampleRate(8000), - 1000.0f32, - Function::Triangle, - ); + let mut wf = TestWaveform::new(cpal::SampleRate(8000), 1000.0f32, Function::Triangle); assert_eq!(wf.next(), Some(-1.0f32)); assert_eq!(wf.next(), Some(-0.5f32)); assert_eq!(wf.next(), Some(0.0f32)); @@ -172,11 +160,7 @@ mod tests { #[test] fn saw() { - let mut wf = TestWaveform::new( - cpal::SampleRate(200), - 50.0f32, - Function::Sawtooth, - ); + let mut wf = TestWaveform::new(cpal::SampleRate(200), 50.0f32, Function::Sawtooth); assert_eq!(wf.next(), Some(0.0f32)); assert_eq!(wf.next(), Some(0.5f32)); assert_eq!(wf.next(), Some(-1.0f32)); From e060bdd35c9a5fa5ce91bdf08a3cbacac0454b5d Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Sat, 14 Sep 2024 09:32:51 -0700 Subject: [PATCH 17/65] Implemented chirp generator --- CHANGELOG.md | 13 +++++++++++-- src/lib.rs | 9 +++++++++ src/source/chirp.rs | 8 +++++--- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63faa801..03cf51f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Support for *ALAC/AIFF* -- New sources: +- Support for *ALAC/AIFF* +- New test signal generator sources: + - `TestSignal` source generates a sine, triangle, square wave or sawtooth + of a given frequency and sample rate. + - `Chirp` source generates a sine wave with linearly period over a given + frequency range and duration. + - `white` and `pink` generate white or pink noise, respectively. These + sources depend on the `rand` crate and are guarded with the "noise" + feature. + - Documentation for the "noise" feature has been added to `lib.rs`. +- New Fade and Crossfade 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 diff --git a/src/lib.rs b/src/lib.rs index 6f6c505b..c47eafab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -112,6 +112,15 @@ //! the number of sinks that can be created (except for the fact that creating too many will slow //! down your program). //! +//! +//! ## Features +//! +//! Rodio provides several optional features that are guarded with feature gates. +//! +//! ### Feature "Noise" +//! +//! The "noise" feature adds support for white and pink noise sources. This feature requires the +//! "rand" crate. #![cfg_attr(test, deny(missing_docs))] pub use cpal::{ self, traits::DeviceTrait, Device, Devices, DevicesError, InputDevices, OutputDevices, diff --git a/src/source/chirp.rs b/src/source/chirp.rs index 8fabc292..1971c70f 100644 --- a/src/source/chirp.rs +++ b/src/source/chirp.rs @@ -1,6 +1,6 @@ //! Chirp/sweep source. -use std::time::Duration; +use std::{f32::consts::TAU, time::Duration}; use crate::Source; @@ -48,9 +48,11 @@ impl Iterator for Chirp { fn next(&mut self) -> Option { let i = self.elapsed_samples; + let ratio = self.elapsed_samples as f32 / self.total_samples as f32; self.elapsed_samples += 1; - - todo!() + let freq = self.start_frequency * (1.0 - ratio) + self.end_frequency * ratio; + let t = (i as f32 / self.sample_rate() as f32) * TAU * freq; + Some( t.sin() ) } } From f3e77f43ec52dac082d0a362508d01706a767f0c Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Sat, 14 Sep 2024 09:39:02 -0700 Subject: [PATCH 18/65] Removed `try_seek()` from chirp. --- src/source/chirp.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/source/chirp.rs b/src/source/chirp.rs index 1971c70f..9942cab4 100644 --- a/src/source/chirp.rs +++ b/src/source/chirp.rs @@ -52,7 +52,7 @@ impl Iterator for Chirp { self.elapsed_samples += 1; let freq = self.start_frequency * (1.0 - ratio) + self.end_frequency * ratio; let t = (i as f32 / self.sample_rate() as f32) * TAU * freq; - Some( t.sin() ) + Some(t.sin()) } } @@ -73,8 +73,4 @@ impl Source for Chirp { let secs: f64 = self.total_samples as f64 / self.sample_rate.0 as f64; Some(Duration::new(1, 0).mul_f64(secs)) } - - fn try_seek(&mut self, pos: Duration) -> Result<(), super::SeekError> { - todo!() - } } From c912f567a66bd2a0aa21a0b058087e31c1e47878 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Sun, 15 Sep 2024 13:26:43 -0700 Subject: [PATCH 19/65] Removed white space for rustfmt --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index c47eafab..6889bd62 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,7 +113,7 @@ //! down your program). //! //! -//! ## Features +//! ## Features //! //! Rodio provides several optional features that are guarded with feature gates. //! From cde724a429a45345200ae654c67644f900077a49 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Sun, 15 Sep 2024 13:29:15 -0700 Subject: [PATCH 20/65] Removed white space for rustfmt --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 6889bd62..f9dde615 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -112,7 +112,7 @@ //! the number of sinks that can be created (except for the fact that creating too many will slow //! down your program). //! -//! +//! //! ## Features //! //! Rodio provides several optional features that are guarded with feature gates. From 85bfcbd40cca5296e520511ca1026c0542453cf4 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Fri, 27 Sep 2024 06:39:09 +1200 Subject: [PATCH 21/65] Init commit for automatic_gain_control --- src/conversions/sample.rs | 21 +++++ src/source/agc.rs | 175 ++++++++++++++++++++++++++++++++++++++ src/source/mod.rs | 18 +++- 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src/source/agc.rs diff --git a/src/conversions/sample.rs b/src/conversions/sample.rs index 7f593f7a..a78f5c14 100644 --- a/src/conversions/sample.rs +++ b/src/conversions/sample.rs @@ -80,6 +80,9 @@ pub trait Sample: CpalSample { /// Multiplies the value of this sample by the given amount. fn amplify(self, value: f32) -> Self; + /// Converts the sample to an f32 value. + fn to_f32(self) -> f32; + /// Calls `saturating_add` on the sample. fn saturating_add(self, other: Self) -> Self; @@ -102,6 +105,12 @@ impl Sample for u16 { ((self as f32) * value) as u16 } + #[inline] + fn to_f32(self) -> f32 { + // Convert u16 to f32 in the range [-1.0, 1.0] + (self as f32 - 32768.0) / 32768.0 + } + #[inline] fn saturating_add(self, other: u16) -> u16 { self.saturating_add(other) @@ -125,6 +134,12 @@ impl Sample for i16 { ((self as f32) * value) as i16 } + #[inline] + fn to_f32(self) -> f32 { + // Convert i16 to f32 in the range [-1.0, 1.0] + self as f32 / 32768.0 + } + #[inline] fn saturating_add(self, other: i16) -> i16 { self.saturating_add(other) @@ -147,6 +162,12 @@ impl Sample for f32 { self * value } + #[inline] + fn to_f32(self) -> f32 { + // f32 is already in the correct format + self + } + #[inline] fn saturating_add(self, other: f32) -> f32 { self + other diff --git a/src/source/agc.rs b/src/source/agc.rs new file mode 100644 index 00000000..89342eab --- /dev/null +++ b/src/source/agc.rs @@ -0,0 +1,175 @@ +use super::SeekError; +use crate::{Sample, Source}; +use std::time::Duration; + +/// Constructs an `AutomaticGainControl` object with specified parameters. +/// +/// # Arguments +/// +/// * `input` - The input audio source +/// * `target_level` - The desired output level +/// * `attack_time` - Time constant for gain adjustment +/// * `absolute_max_gain` - Maximum allowable gain +pub fn automatic_gain_control( + input: I, + target_level: f32, + attack_time: f32, + absolute_max_gain: f32, +) -> AutomaticGainControl +where + I: Source, + I::Item: Sample, +{ + let sample_rate = input.sample_rate(); + + AutomaticGainControl { + input, + target_level, + absolute_max_gain, + current_gain: 1.0, + attack_coeff: (-1.0 / (attack_time * sample_rate as f32)).exp(), + peak_level: 0.0, + rms_level: 0.0, + rms_window: vec![0.0; 1024], + rms_index: 0, + } +} + +/// Automatic Gain Control filter for maintaining consistent output levels. +#[derive(Clone, Debug)] +pub struct AutomaticGainControl { + input: I, + target_level: f32, + absolute_max_gain: f32, + current_gain: f32, + attack_coeff: f32, + peak_level: f32, + rms_level: f32, + rms_window: Vec, + rms_index: usize, +} + +impl AutomaticGainControl +where + I: Source, + I::Item: Sample, +{ + // Sets a new target output level. + #[inline] + pub fn set_target_level(&mut self, level: f32) { + self.target_level = level; + } + + // Add this method to allow changing the attack coefficient + pub fn set_attack_coeff(&mut self, attack_time: f32) { + let sample_rate = self.input.sample_rate(); + self.attack_coeff = (-1.0 / (attack_time * sample_rate as f32)).exp(); + } +} + +impl Iterator for AutomaticGainControl +where + I: Source, + I::Item: Sample, +{ + type Item = I::Item; + + #[inline] + fn next(&mut self) -> Option { + self.input.next().map(|value| { + let sample_value = value.to_f32().abs(); + + // Update peak level with adaptive attack coefficient + let attack_coeff = if sample_value > self.peak_level { + self.attack_coeff.min(0.1) // Faster response to sudden increases + } else { + self.attack_coeff + }; + self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; + + // Update RMS level using a sliding window + self.rms_level -= self.rms_window[self.rms_index] / self.rms_window.len() as f32; + self.rms_window[self.rms_index] = sample_value * sample_value; + self.rms_level += self.rms_window[self.rms_index] / self.rms_window.len() as f32; + self.rms_index = (self.rms_index + 1) % self.rms_window.len(); + + let rms = self.rms_level.sqrt(); + + // Calculate gain adjustments based on peak and RMS levels + let peak_gain = if self.peak_level > 0.0 { + self.target_level / self.peak_level + } else { + 1.0 + }; + + let rms_gain = if rms > 0.0 { + self.target_level / rms + } else { + 1.0 + }; + + // Choose the more conservative gain adjustment + let desired_gain = peak_gain.min(rms_gain); + + // Set target gain to the middle of the allowable range + let target_gain = 1.0; // Midpoint between 0.1 and 3.0 + + // Smoothly adjust current gain towards the target + let adjustment_speed = 0.05; // Balance between responsiveness and stability + self.current_gain = self.current_gain * (1.0 - adjustment_speed) + + (desired_gain * target_gain) * adjustment_speed; + + // Constrain gain within predefined limits + self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain); + + // Uncomment for debugging: + println!("Current gain: {}", self.current_gain); + + // Apply calculated gain to the sample + value.amplify(self.current_gain) + }) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.input.size_hint() + } +} + +impl ExactSizeIterator for AutomaticGainControl +where + I: Source + ExactSizeIterator, + I::Item: Sample, +{ +} + +impl Source for AutomaticGainControl +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.input.try_seek(pos) + } +} diff --git a/src/source/mod.rs b/src/source/mod.rs index 1e85270b..ceb49e8c 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -6,6 +6,7 @@ use cpal::FromSample; use crate::Sample; +pub use self::agc::AutomaticGainControl; pub use self::amplify::Amplify; pub use self::blt::BltFilter; pub use self::buffered::Buffered; @@ -36,6 +37,7 @@ pub use self::take::TakeDuration; pub use self::uniform::UniformSourceIterator; pub use self::zero::Zero; +mod agc; mod amplify; mod blt; mod buffered; @@ -232,6 +234,20 @@ where amplify::amplify(self, value) } + /// Applies automatic gain control to the sound. + #[inline] + fn automatic_gain_control( + self, + target_level: f32, + attack_time: f32, + absolute_max_gain: f32, + ) -> AutomaticGainControl + where + Self: Sized, + { + agc::automatic_gain_control(self, target_level, attack_time, absolute_max_gain) + } + /// Mixes this sound fading out with another sound fading in for the given duration. /// /// Only the crossfaded portion (beginning of self, beginning of other) is returned. @@ -445,7 +461,7 @@ where /// sources does not support seeking. /// /// It will return an error if an implementation ran - /// into one during the seek. + /// 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. From 625d0f27b56a777af40e0dd2bb33fb622f29a1cd Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:35:44 +1200 Subject: [PATCH 22/65] Updated comments, refactored logic & added more member functions for simplicity --- src/source/agc.rs | 121 +++++++++++++++++++++++++++++++++------------- src/source/mod.rs | 15 ++++++ 2 files changed, 102 insertions(+), 34 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 89342eab..0eb4270d 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -26,6 +26,7 @@ where input, target_level, absolute_max_gain, + attack_time, current_gain: 1.0, attack_coeff: (-1.0 / (attack_time * sample_rate as f32)).exp(), peak_level: 0.0, @@ -41,6 +42,7 @@ pub struct AutomaticGainControl { input: I, target_level: f32, absolute_max_gain: f32, + attack_time: f32, current_gain: f32, attack_coeff: f32, peak_level: f32, @@ -54,17 +56,82 @@ where I: Source, I::Item: Sample, { - // Sets a new target output level. + /// Sets a new target output level. + /// + /// This method allows dynamic adjustment of the target output level + /// for the Automatic Gain Control. The target level determines the + /// desired amplitude of the processed audio signal. #[inline] pub fn set_target_level(&mut self, level: f32) { self.target_level = level; } - // Add this method to allow changing the attack coefficient + /// Sets a new absolute maximum gain limit. + #[inline] + pub fn set_absolute_max_gain(&mut self, max_gain: f32) { + self.absolute_max_gain = max_gain; + } + + /// This method allows changing the attack coefficient dynamically. + /// The attack coefficient determines how quickly the AGC responds to level changes. + /// A smaller value results in faster response, while a larger value gives a slower response. + #[inline] pub fn set_attack_coeff(&mut self, attack_time: f32) { let sample_rate = self.input.sample_rate(); self.attack_coeff = (-1.0 / (attack_time * sample_rate as f32)).exp(); } + + /// Updates the peak level with an adaptive attack coefficient + /// + /// This method adjusts the peak level using a variable attack coefficient. + /// It responds faster to sudden increases in signal level by using a + /// minimum attack coefficient of 0.1 when the sample value exceeds the + /// current peak level. This adaptive behavior helps capture transients + /// more accurately while maintaining smoother behavior for gradual changes. + #[inline] + fn update_peak_level(&mut self, sample_value: f32) { + let attack_coeff = if sample_value > self.peak_level { + self.attack_coeff.min(0.1) // Faster response to sudden increases + } else { + self.attack_coeff + }; + self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; + } + + /// Calculate gain adjustments based on peak and RMS levels + /// This method determines the appropriate gain level to apply to the audio + /// signal, considering both peak and RMS (Root Mean Square) levels. + /// The peak level helps prevent sudden spikes, while the RMS level + /// provides a measure of the overall signal power over time. + #[inline] + fn calculate_peak_gain(&self) -> f32 { + if self.peak_level > 0.0 { + self.target_level / self.peak_level + } else { + 1.0 + } + } + + /// Updates the RMS (Root Mean Square) level using a sliding window approach. + /// This method calculates a moving average of the squared input samples, + /// providing a measure of the signal's average power over time. + #[inline] + fn update_rms(&mut self, sample_value: f32) -> f32 { + // Remove the oldest sample from the RMS calculation + self.rms_level -= self.rms_window[self.rms_index] / self.rms_window.len() as f32; + + // Add the new sample to the window + self.rms_window[self.rms_index] = sample_value * sample_value; + + // Add the new sample to the RMS calculation + self.rms_level += self.rms_window[self.rms_index] / self.rms_window.len() as f32; + + // Move the index to the next position + self.rms_index = (self.rms_index + 1) % self.rms_window.len(); + + // Calculate and return the RMS value + self.rms_level.sqrt() + } } impl Iterator for AutomaticGainControl @@ -77,55 +144,41 @@ where #[inline] fn next(&mut self) -> Option { self.input.next().map(|value| { + // Convert the sample to its absolute float value for level calculations let sample_value = value.to_f32().abs(); - // Update peak level with adaptive attack coefficient - let attack_coeff = if sample_value > self.peak_level { - self.attack_coeff.min(0.1) // Faster response to sudden increases - } else { - self.attack_coeff - }; - self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; - - // Update RMS level using a sliding window - self.rms_level -= self.rms_window[self.rms_index] / self.rms_window.len() as f32; - self.rms_window[self.rms_index] = sample_value * sample_value; - self.rms_level += self.rms_window[self.rms_index] / self.rms_window.len() as f32; - self.rms_index = (self.rms_index + 1) % self.rms_window.len(); + // Dynamically adjust peak level using an adaptive attack coefficient + self.update_peak_level(sample_value); - let rms = self.rms_level.sqrt(); + // Calculate the current RMS (Root Mean Square) level using a sliding window approach + let rms = self.update_rms(sample_value); - // Calculate gain adjustments based on peak and RMS levels - let peak_gain = if self.peak_level > 0.0 { - self.target_level / self.peak_level - } else { - 1.0 - }; + // Determine the gain adjustment needed based on the current peak level + let peak_gain = self.calculate_peak_gain(); + // Compute the gain adjustment required to reach the target level based on RMS let rms_gain = if rms > 0.0 { self.target_level / rms } else { - 1.0 + 1.0 // Default to unity gain if RMS is zero to avoid division by zero }; - // Choose the more conservative gain adjustment + // Select the lower of peak and RMS gains to ensure conservative adjustment let desired_gain = peak_gain.min(rms_gain); - // Set target gain to the middle of the allowable range - let target_gain = 1.0; // Midpoint between 0.1 and 3.0 - - // Smoothly adjust current gain towards the target - let adjustment_speed = 0.05; // Balance between responsiveness and stability - self.current_gain = self.current_gain * (1.0 - adjustment_speed) - + (desired_gain * target_gain) * adjustment_speed; + // Gradually adjust the current gain towards the desired gain for smooth transitions + let adjustment_speed = self.attack_time; // Controls the trade-off between quick response and stability + self.current_gain = + self.current_gain * (1.0 - adjustment_speed) + desired_gain * adjustment_speed; - // Constrain gain within predefined limits + // Ensure the calculated gain stays within the defined operational range self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain); - // Uncomment for debugging: + // Output current gain value for monitoring and debugging purposes + // Must be deleted before merge: println!("Current gain: {}", self.current_gain); - // Apply calculated gain to the sample + // Apply the computed gain to the input sample and return the result value.amplify(self.current_gain) }) } diff --git a/src/source/mod.rs b/src/source/mod.rs index ceb49e8c..71441804 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -235,6 +235,21 @@ where } /// Applies automatic gain control to the sound. + /// + /// Automatic Gain Control (AGC) adjusts the amplitude of the audio signal + /// to maintain a consistent output level. + /// + /// # Parameters + /// + /// * `target_level`: The desired output level, typically between 0.9 and 1.0. + /// This is the level that the AGC will try to maintain. + /// + /// * `attack_time`: The time (in seconds) it takes for the AGC to respond to + /// an increase in input level. A shorter attack time means faster response + /// but may lead to more abrupt changes. + /// + /// * `absolute_max_gain`: The maximum gain that can be applied to the signal. + /// This prevents excessive amplification of quiet signals or background noise. #[inline] fn automatic_gain_control( self, From 6b62544cdeea28e26c9f5994c28b169922e3644c Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:04:48 +1200 Subject: [PATCH 23/65] Added simple flag to enable the debug temporarily during development --- src/source/agc.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 0eb4270d..71e82289 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -176,7 +176,10 @@ where // Output current gain value for monitoring and debugging purposes // Must be deleted before merge: - println!("Current gain: {}", self.current_gain); + // Added flag so its usable without the debug temporarily during development + if std::env::args().any(|arg| arg == "--debug-gain") { + println!("Current gain: {}", self.current_gain); + } // Apply the computed gain to the input sample and return the result value.amplify(self.current_gain) From 611055c62df0d621d8a19d00e46c2dc264cdcc26 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:27:35 +1200 Subject: [PATCH 24/65] Enhance AGC with asymmetric attack/release and safety limits - Implement asymmetric attack/release - Introduce MIN_ATTACK_TIME limit to prevent AGC instability - Clamp attack_time to prevent instability - Faster decrease, slower increase for smoother sound - Safeguard against extreme gain fluctuations --- src/source/agc.rs | 40 ++++++++++++++++++++++++++++++++++++++-- src/source/mod.rs | 4 ++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 71e82289..acb01bfd 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -166,10 +166,46 @@ where // Select the lower of peak and RMS gains to ensure conservative adjustment let desired_gain = peak_gain.min(rms_gain); + // Adaptive attack/release speed for AGC (Automatic Gain Control) + // + // This mechanism implements an asymmetric approach to gain adjustment: + // 1. Slow increase: Prevents abrupt amplification of noise during quiet periods. + // 2. Fast decrease: Rapidly attenuates sudden loud signals to avoid distortion. + // + // The asymmetry is crucial because: + // - Gradual gain increases sound more natural and less noticeable to listeners. + // - Quick gain reductions are necessary to prevent clipping and maintain audio quality. + // + // This approach addresses several challenges associated with high attack times: + // 1. Slow response: With a high attack time, the AGC responds very slowly to changes in input level. + // This means it takes longer for the gain to adjust to new signal levels. + // 2. Initial gain calculation: When the audio starts or after a period of silence, the initial gain + // calculation might result in a very high gain value, especially if the input signal starts quietly. + // 3. Overshooting: As the gain slowly increases (due to the high attack time), it might overshoot + // the desired level, causing the signal to become too loud. + // 4. Overcorrection: The AGC then tries to correct this by reducing the gain, but due to the slow response, + // it might reduce the gain too much, causing the sound to drop to near-zero levels. + // 5. Slow recovery: Again, due to the high attack time, it takes a while for the gain to increase + // back to the appropriate level. + // + // By using a faster release time for decreasing gain, we can mitigate these issues and provide + // more responsive control over sudden level increases while maintaining smooth gain increases. + let attack_speed = if desired_gain > self.current_gain { + // Slower attack for increasing gain to avoid sudden amplification + self.attack_time.min(10.0) + } else { + // Faster release for decreasing gain to prevent overamplification + // Cap release time at 1.0 to ensure responsiveness + // This prevents issues with very high attack times: + // - Avoids overcorrection and near-zero sound levels + // - Ensures AGC can always correct itself in reasonable time + // - Maintains ability to quickly attenuate sudden loud signals + (self.attack_time * 0.1).min(1.0) // Capped faster release time + }; + // Gradually adjust the current gain towards the desired gain for smooth transitions - let adjustment_speed = self.attack_time; // Controls the trade-off between quick response and stability self.current_gain = - self.current_gain * (1.0 - adjustment_speed) + desired_gain * adjustment_speed; + self.current_gain * (1.0 - attack_speed) + desired_gain * attack_speed; // Ensure the calculated gain stays within the defined operational range self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain); diff --git a/src/source/mod.rs b/src/source/mod.rs index 71441804..65a38ce9 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -260,6 +260,10 @@ where where Self: Sized, { + // Added Limits to prevent the AGC from blowing up. ;) + const MIN_ATTACK_TIME: f32 = 10.0; + let attack_time = attack_time.min(MIN_ATTACK_TIME); + agc::automatic_gain_control(self, target_level, attack_time, absolute_max_gain) } From 97636d163eb30e6652e5a6b4ca988b27c2638e49 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sat, 28 Sep 2024 11:10:50 +1200 Subject: [PATCH 25/65] Add author credit to AGC implementation --- src/source/agc.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/source/agc.rs b/src/source/agc.rs index acb01bfd..cda5e816 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -1,3 +1,17 @@ +// +// Automatic Gain Control (AGC) Algorithm +// Designed by @UnknownSuperficialNight +// +// Features: +// • Adaptive peak detection +// • RMS-based level estimation +// • Asymmetric attack/release +// +// Optimized for smooth and responsive gain control +// +// Crafted with love. Enjoy! :) +// + use super::SeekError; use crate::{Sample, Source}; use std::time::Duration; From 1b27bcd335b852f0ac2687d332e1e54f7b9611c4 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sat, 28 Sep 2024 13:52:42 +1200 Subject: [PATCH 26/65] Add debug logging for AGC current gain value --- src/source/agc.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index cda5e816..98e6ef55 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -16,6 +16,9 @@ use super::SeekError; use crate::{Sample, Source}; use std::time::Duration; +#[cfg(feature = "tracing")] +use tracing; + /// Constructs an `AutomaticGainControl` object with specified parameters. /// /// # Arguments @@ -224,12 +227,9 @@ where // Ensure the calculated gain stays within the defined operational range self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain); - // Output current gain value for monitoring and debugging purposes - // Must be deleted before merge: - // Added flag so its usable without the debug temporarily during development - if std::env::args().any(|arg| arg == "--debug-gain") { - println!("Current gain: {}", self.current_gain); - } + // Output current gain value for developers to fine tune their inputs to automatic_gain_control + #[cfg(feature = "tracing")] + tracing::debug!("AGC gain: {}", self.current_gain); // Apply the computed gain to the input sample and return the result value.amplify(self.current_gain) From d9f7967fd287396935d391b937b6e1d9c8b091e1 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:19:13 +1200 Subject: [PATCH 27/65] Better document comments for docs.rs --- src/source/mod.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/source/mod.rs b/src/source/mod.rs index 65a38ce9..55847118 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -241,15 +241,28 @@ where /// /// # Parameters /// - /// * `target_level`: The desired output level, typically between 0.9 and 1.0. - /// This is the level that the AGC will try to maintain. + /// * `target_level`: The desired output level, where 1.0 represents the original sound level. + /// Values above 1.0 will amplify the sound, while values below 1.0 will lower it. + /// For example, a target_level of 1.4 means that at normal sound levels, the AGC + /// will aim to increase the gain by a factor of 1.4, resulting in a minimum 40% amplification. + /// A recommended level is 1.0, which maintains the original sound level. /// - /// * `attack_time`: The time (in seconds) it takes for the AGC to respond to - /// an increase in input level. A shorter attack time means faster response - /// but may lead to more abrupt changes. + /// * `attack_time`: The time (in seconds) for the AGC to respond to input level increases. + /// Shorter times mean faster response but may cause abrupt changes. Longer times result + /// in smoother transitions but slower reactions to sudden volume changes. Too short can + /// lead to overreaction to peaks, causing unnecessary adjustments. Too long can make the + /// AGC miss important volume changes or react too slowly to sudden loud passages. Very + /// high values might result in excessively loud output or sluggish response, as the AGC's + /// adjustment speed is limited by the attack time. Balance is key for optimal performance. + /// A recommended attack_time of 2.0 seconds provides a sweet spot for most applications. /// /// * `absolute_max_gain`: The maximum gain that can be applied to the signal. - /// This prevents excessive amplification of quiet signals or background noise. + /// This parameter acts as a safeguard against excessive amplification of quiet signals + /// or background noise. It establishes an upper boundary for the AGC's signal boost, + /// effectively preventing distortion or overamplification of low-level sounds. + /// This is crucial for maintaining audio quality and preventing unexpected volume spikes. + /// A recommended value for `absolute_max_gain` is 4, which provides a good balance between + /// amplification capability and protection against distortion in most scenarios. #[inline] fn automatic_gain_control( self, From ce3d7e0bd2a81b9414aa4eb70d08da9443b9ef68 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sat, 28 Sep 2024 23:47:34 +1200 Subject: [PATCH 28/65] Optimize AGC with CircularBuffer and enhance functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Vec-based RMS calculation with efficient CircularBuffer - Add separate release_time for asymmetric gain control - Implement MAX_PEAK_LEVEL constant to prevent clipping - Revise gain calculation logic: • Separate RMS and peak gain calculations • Use RMS for general adjustments, peak for limiting • Implement smoother transitions between gain levels • Improve handling of edge cases (e.g., zero RMS) - Improve code organization and documentation --- src/source/agc.rs | 169 +++++++++++++++++++++++++++++----------------- src/source/mod.rs | 42 ++++++++++-- 2 files changed, 144 insertions(+), 67 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 98e6ef55..c82088bf 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -6,6 +6,7 @@ // • Adaptive peak detection // • RMS-based level estimation // • Asymmetric attack/release +// • RMS-based general adjustments with peak limiting // // Optimized for smooth and responsive gain control // @@ -19,18 +20,89 @@ use std::time::Duration; #[cfg(feature = "tracing")] use tracing; +/// Size of the circular buffer used for RMS calculation. +/// A larger size provides more stable RMS values but increases latency. +const RMS_WINDOW_SIZE: usize = 1024; + +/// Minimum attack coefficient for rapid response to sudden level increases. +/// Balances between responsiveness and stability. +const MIN_ATTACK_COEFF: f32 = 0.05; + +/// Maximum allowed peak level to prevent clipping +const MAX_PEAK_LEVEL: f32 = 0.99; + +/// Automatic Gain Control filter for maintaining consistent output levels. +/// +/// This struct implements an AGC algorithm that dynamically adjusts audio levels +/// based on both peak and RMS (Root Mean Square) measurements. +#[derive(Clone, Debug)] +pub struct AutomaticGainControl { + input: I, + target_level: f32, + absolute_max_gain: f32, + current_gain: f32, + attack_coeff: f32, + release_coeff: f32, + peak_level: f32, + rms_window: CircularBuffer, +} + +/// A circular buffer for efficient RMS calculation over a sliding window. +/// +/// This structure allows for constant-time updates and mean calculations, +/// which is crucial for real-time audio processing. +#[derive(Clone, Debug)] +struct CircularBuffer { + buffer: [f32; RMS_WINDOW_SIZE], + index: usize, + sum: f32, +} + +impl CircularBuffer { + /// Creates a new CircularBuffer with a fixed size determined at compile time. + /// + /// The `_size` parameter is ignored as the buffer size is set by `RMS_WINDOW_SIZE`. + fn new(_size: usize) -> Self { + CircularBuffer { + buffer: [0.0; RMS_WINDOW_SIZE], + index: 0, + sum: 0.0, + } + } + + /// Pushes a new value into the buffer and returns the old value. + /// + /// This method maintains a running sum for efficient mean calculation. + fn push(&mut self, value: f32) -> f32 { + let old_value = self.buffer[self.index]; + self.buffer[self.index] = value; + self.sum += value - old_value; + self.index = (self.index + 1) % self.buffer.len(); + old_value + } + + /// Calculates the mean of all values in the buffer. + /// + /// This operation is O(1) due to the maintained running sum. + fn mean(&self) -> f32 { + self.sum / self.buffer.len() as f32 + } +} + /// Constructs an `AutomaticGainControl` object with specified parameters. /// /// # Arguments /// /// * `input` - The input audio source /// * `target_level` - The desired output level -/// * `attack_time` - Time constant for gain adjustment +/// * `attack_time` - Time constant for gain increase +/// * `release_time` - Time constant for gain decrease /// * `absolute_max_gain` - Maximum allowable gain pub fn automatic_gain_control( input: I, target_level: f32, attack_time: f32, + release_time: f32, absolute_max_gain: f32, ) -> AutomaticGainControl where @@ -43,31 +115,14 @@ where input, target_level, absolute_max_gain, - attack_time, current_gain: 1.0, attack_coeff: (-1.0 / (attack_time * sample_rate as f32)).exp(), + release_coeff: (-1.0 / (release_time * sample_rate as f32)).exp(), peak_level: 0.0, - rms_level: 0.0, - rms_window: vec![0.0; 1024], - rms_index: 0, + rms_window: CircularBuffer::new(RMS_WINDOW_SIZE), } } -/// Automatic Gain Control filter for maintaining consistent output levels. -#[derive(Clone, Debug)] -pub struct AutomaticGainControl { - input: I, - target_level: f32, - absolute_max_gain: f32, - attack_time: f32, - current_gain: f32, - attack_coeff: f32, - peak_level: f32, - rms_level: f32, - rms_window: Vec, - rms_index: usize, -} - impl AutomaticGainControl where I: Source, @@ -90,7 +145,7 @@ where } /// This method allows changing the attack coefficient dynamically. - /// The attack coefficient determines how quickly the AGC responds to level changes. + /// The attack coefficient determines how quickly the AGC responds to level increases. /// A smaller value results in faster response, while a larger value gives a slower response. #[inline] pub fn set_attack_coeff(&mut self, attack_time: f32) { @@ -98,56 +153,53 @@ where self.attack_coeff = (-1.0 / (attack_time * sample_rate as f32)).exp(); } + /// This method allows changing the release coefficient dynamically. + /// The release coefficient determines how quickly the AGC responds to level decreases. + /// A smaller value results in faster response, while a larger value gives a slower response. + #[inline] + pub fn set_release_coeff(&mut self, release_time: f32) { + let sample_rate = self.input.sample_rate(); + self.release_coeff = (-1.0 / (release_time * sample_rate as f32)).exp(); + } + /// Updates the peak level with an adaptive attack coefficient /// /// This method adjusts the peak level using a variable attack coefficient. /// It responds faster to sudden increases in signal level by using a - /// minimum attack coefficient of 0.1 when the sample value exceeds the + /// minimum attack coefficient of MIN_ATTACK_COEFF when the sample value exceeds the /// current peak level. This adaptive behavior helps capture transients /// more accurately while maintaining smoother behavior for gradual changes. #[inline] fn update_peak_level(&mut self, sample_value: f32) { let attack_coeff = if sample_value > self.peak_level { - self.attack_coeff.min(0.1) // Faster response to sudden increases + self.attack_coeff.min(MIN_ATTACK_COEFF) // Faster response to sudden increases } else { - self.attack_coeff + self.release_coeff }; self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; } - /// Calculate gain adjustments based on peak and RMS levels + /// Calculate gain adjustments based on peak levels /// This method determines the appropriate gain level to apply to the audio - /// signal, considering both peak and RMS (Root Mean Square) levels. - /// The peak level helps prevent sudden spikes, while the RMS level - /// provides a measure of the overall signal power over time. + /// signal, considering the peak level. + /// The peak level helps prevent sudden spikes in the output signal. #[inline] fn calculate_peak_gain(&self) -> f32 { if self.peak_level > 0.0 { - self.target_level / self.peak_level + (MAX_PEAK_LEVEL / self.peak_level).min(self.absolute_max_gain) } else { - 1.0 + self.absolute_max_gain } } - /// Updates the RMS (Root Mean Square) level using a sliding window approach. + /// Updates the RMS (Root Mean Square) level using a circular buffer approach. /// This method calculates a moving average of the squared input samples, /// providing a measure of the signal's average power over time. #[inline] fn update_rms(&mut self, sample_value: f32) -> f32 { - // Remove the oldest sample from the RMS calculation - self.rms_level -= self.rms_window[self.rms_index] / self.rms_window.len() as f32; - - // Add the new sample to the window - self.rms_window[self.rms_index] = sample_value * sample_value; - - // Add the new sample to the RMS calculation - self.rms_level += self.rms_window[self.rms_index] / self.rms_window.len() as f32; - - // Move the index to the next position - self.rms_index = (self.rms_index + 1) % self.rms_window.len(); - - // Calculate and return the RMS value - self.rms_level.sqrt() + let squared_sample = sample_value * sample_value; + self.rms_window.push(squared_sample); + self.rms_window.mean().sqrt() } } @@ -170,18 +222,18 @@ where // Calculate the current RMS (Root Mean Square) level using a sliding window approach let rms = self.update_rms(sample_value); - // Determine the gain adjustment needed based on the current peak level - let peak_gain = self.calculate_peak_gain(); - // Compute the gain adjustment required to reach the target level based on RMS let rms_gain = if rms > 0.0 { self.target_level / rms } else { - 1.0 // Default to unity gain if RMS is zero to avoid division by zero + self.absolute_max_gain // Default to max gain if RMS is zero }; - // Select the lower of peak and RMS gains to ensure conservative adjustment - let desired_gain = peak_gain.min(rms_gain); + // Calculate the peak limiting gain + let peak_gain = self.calculate_peak_gain(); + + // Use RMS for general adjustments, but limit by peak gain to prevent clipping + let desired_gain = rms_gain.min(peak_gain); // Adaptive attack/release speed for AGC (Automatic Gain Control) // @@ -208,28 +260,21 @@ where // By using a faster release time for decreasing gain, we can mitigate these issues and provide // more responsive control over sudden level increases while maintaining smooth gain increases. let attack_speed = if desired_gain > self.current_gain { - // Slower attack for increasing gain to avoid sudden amplification - self.attack_time.min(10.0) + self.attack_coeff } else { - // Faster release for decreasing gain to prevent overamplification - // Cap release time at 1.0 to ensure responsiveness - // This prevents issues with very high attack times: - // - Avoids overcorrection and near-zero sound levels - // - Ensures AGC can always correct itself in reasonable time - // - Maintains ability to quickly attenuate sudden loud signals - (self.attack_time * 0.1).min(1.0) // Capped faster release time + self.release_coeff }; // Gradually adjust the current gain towards the desired gain for smooth transitions self.current_gain = - self.current_gain * (1.0 - attack_speed) + desired_gain * attack_speed; + self.current_gain * attack_speed + desired_gain * (1.0 - attack_speed); // Ensure the calculated gain stays within the defined operational range self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain); // Output current gain value for developers to fine tune their inputs to automatic_gain_control #[cfg(feature = "tracing")] - tracing::debug!("AGC gain: {}", self.current_gain); + tracing::debug!("AGC gain: {}", self.current_gain,); // Apply the computed gain to the input sample and return the result value.amplify(self.current_gain) diff --git a/src/source/mod.rs b/src/source/mod.rs index 55847118..1aa5565e 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -241,13 +241,19 @@ where /// /// # Parameters /// - /// * `target_level`: The desired output level, where 1.0 represents the original sound level. + /// * `target_level`: + /// TL;DR: Desired output level. 1.0 = original level, > 1.0 amplifies, < 1.0 reduces. + /// + /// The desired output level, where 1.0 represents the original sound level. /// Values above 1.0 will amplify the sound, while values below 1.0 will lower it. /// For example, a target_level of 1.4 means that at normal sound levels, the AGC /// will aim to increase the gain by a factor of 1.4, resulting in a minimum 40% amplification. /// A recommended level is 1.0, which maintains the original sound level. /// - /// * `attack_time`: The time (in seconds) for the AGC to respond to input level increases. + /// * `attack_time`: + /// TL;DR: Response time for volume increases. Shorter = faster but may cause abrupt changes. Recommended: 2.0 seconds. + /// + /// The time (in seconds) for the AGC to respond to input level increases. /// Shorter times mean faster response but may cause abrupt changes. Longer times result /// in smoother transitions but slower reactions to sudden volume changes. Too short can /// lead to overreaction to peaks, causing unnecessary adjustments. Too long can make the @@ -256,7 +262,24 @@ where /// adjustment speed is limited by the attack time. Balance is key for optimal performance. /// A recommended attack_time of 2.0 seconds provides a sweet spot for most applications. /// - /// * `absolute_max_gain`: The maximum gain that can be applied to the signal. + /// * `release_time`: + /// TL;DR: Response time for volume decreases. Shorter = faster gain reduction. Recommended: 0.01 seconds. + /// + /// The time (in seconds) for the AGC to respond to input level decreases. + /// This parameter controls how quickly the gain is reduced when the signal level drops. + /// Shorter release times result in faster gain reduction, which can be useful for quick + /// adaptation to quieter passages but may lead to pumping effects. Longer release times + /// provide smoother transitions but may be slower to respond to sudden decreases in volume. + /// However, if the release_time is too high, the AGC may not be able to lower the gain + /// quickly enough, potentially leading to clipping and distorted sound before it can adjust. + /// Finding the right balance is crucial for maintaining natural-sounding dynamics and + /// preventing distortion. A recommended release_time of 0.01 seconds often works well for + /// general use, providing a good balance between responsiveness and smooth transitions. + /// + /// * `absolute_max_gain`: + /// TL;DR: Maximum allowed gain. Prevents over-amplification. Recommended: 4.0. + /// + /// The maximum gain that can be applied to the signal. /// This parameter acts as a safeguard against excessive amplification of quiet signals /// or background noise. It establishes an upper boundary for the AGC's signal boost, /// effectively preventing distortion or overamplification of low-level sounds. @@ -268,6 +291,7 @@ where self, target_level: f32, attack_time: f32, + release_time: f32, absolute_max_gain: f32, ) -> AutomaticGainControl where @@ -275,9 +299,17 @@ where { // Added Limits to prevent the AGC from blowing up. ;) const MIN_ATTACK_TIME: f32 = 10.0; + const MIN_RELEASE_TIME: f32 = 10.0; let attack_time = attack_time.min(MIN_ATTACK_TIME); - - agc::automatic_gain_control(self, target_level, attack_time, absolute_max_gain) + let release_time = release_time.min(MIN_RELEASE_TIME); + + agc::automatic_gain_control( + self, + target_level, + attack_time, + release_time, + absolute_max_gain, + ) } /// Mixes this sound fading out with another sound fading in for the given duration. From 28b3c4b80c4a4d99c3b42bbc844802d0f30862c6 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sun, 29 Sep 2024 13:36:55 +1300 Subject: [PATCH 29/65] Removed MAX_PEAK_LEVEL now uses target_level as intended and styled documentation --- src/source/agc.rs | 5 +---- src/source/mod.rs | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index c82088bf..02777dd9 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -28,9 +28,6 @@ const RMS_WINDOW_SIZE: usize = 1024; /// Balances between responsiveness and stability. const MIN_ATTACK_COEFF: f32 = 0.05; -/// Maximum allowed peak level to prevent clipping -const MAX_PEAK_LEVEL: f32 = 0.99; - /// Automatic Gain Control filter for maintaining consistent output levels. /// /// This struct implements an AGC algorithm that dynamically adjusts audio levels @@ -186,7 +183,7 @@ where #[inline] fn calculate_peak_gain(&self) -> f32 { if self.peak_level > 0.0 { - (MAX_PEAK_LEVEL / self.peak_level).min(self.absolute_max_gain) + (self.target_level / self.peak_level).min(self.absolute_max_gain) } else { self.absolute_max_gain } diff --git a/src/source/mod.rs b/src/source/mod.rs index 1aa5565e..5ac1d724 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -241,17 +241,17 @@ where /// /// # Parameters /// - /// * `target_level`: - /// TL;DR: Desired output level. 1.0 = original level, > 1.0 amplifies, < 1.0 reduces. + ///* `target_level`: + /// **TL;DR**: Desired output level. 1.0 = original level, > 1.0 amplifies, < 1.0 reduces. /// /// The desired output level, where 1.0 represents the original sound level. /// Values above 1.0 will amplify the sound, while values below 1.0 will lower it. /// For example, a target_level of 1.4 means that at normal sound levels, the AGC /// will aim to increase the gain by a factor of 1.4, resulting in a minimum 40% amplification. - /// A recommended level is 1.0, which maintains the original sound level. + /// A recommended level is `1.0`, which maintains the original sound level. /// - /// * `attack_time`: - /// TL;DR: Response time for volume increases. Shorter = faster but may cause abrupt changes. Recommended: 2.0 seconds. + ///* `attack_time`: + /// **TL;DR**: Response time for volume increases. Shorter = faster but may cause abrupt changes. **Recommended: `4.0` seconds**. /// /// The time (in seconds) for the AGC to respond to input level increases. /// Shorter times mean faster response but may cause abrupt changes. Longer times result @@ -260,10 +260,10 @@ where /// AGC miss important volume changes or react too slowly to sudden loud passages. Very /// high values might result in excessively loud output or sluggish response, as the AGC's /// adjustment speed is limited by the attack time. Balance is key for optimal performance. - /// A recommended attack_time of 2.0 seconds provides a sweet spot for most applications. + /// A recommended attack_time of `4.0` seconds provides a sweet spot for most applications. /// - /// * `release_time`: - /// TL;DR: Response time for volume decreases. Shorter = faster gain reduction. Recommended: 0.01 seconds. + ///* `release_time`: + /// **TL;DR**: Response time for volume decreases. Shorter = faster gain reduction. **Recommended: `0.005` seconds**. /// /// The time (in seconds) for the AGC to respond to input level decreases. /// This parameter controls how quickly the gain is reduced when the signal level drops. @@ -273,18 +273,18 @@ where /// However, if the release_time is too high, the AGC may not be able to lower the gain /// quickly enough, potentially leading to clipping and distorted sound before it can adjust. /// Finding the right balance is crucial for maintaining natural-sounding dynamics and - /// preventing distortion. A recommended release_time of 0.01 seconds often works well for + /// preventing distortion. A recommended release_time of `0.005` seconds often works well for /// general use, providing a good balance between responsiveness and smooth transitions. /// - /// * `absolute_max_gain`: - /// TL;DR: Maximum allowed gain. Prevents over-amplification. Recommended: 4.0. + ///* `absolute_max_gain`: + /// **TL;DR**: Maximum allowed gain. Prevents over-amplification. **Recommended: `5.0`**. /// /// The maximum gain that can be applied to the signal. /// This parameter acts as a safeguard against excessive amplification of quiet signals /// or background noise. It establishes an upper boundary for the AGC's signal boost, /// effectively preventing distortion or overamplification of low-level sounds. /// This is crucial for maintaining audio quality and preventing unexpected volume spikes. - /// A recommended value for `absolute_max_gain` is 4, which provides a good balance between + /// A recommended value for `absolute_max_gain` is `5`, which provides a good balance between /// amplification capability and protection against distortion in most scenarios. #[inline] fn automatic_gain_control( From f4bb729235259d185d68b79d35c850f854d6a230 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:03:58 +1300 Subject: [PATCH 30/65] Added benchmark for agc and inlines --- benches/effects.rs | 16 ++++++++++++++++ src/source/agc.rs | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/benches/effects.rs b/benches/effects.rs index 5f100112..eba38fae 100644 --- a/benches/effects.rs +++ b/benches/effects.rs @@ -46,3 +46,19 @@ fn amplify(bencher: Bencher) { .with_inputs(|| TestSource::music_wav().to_f32s()) .bench_values(|source| source.amplify(0.8).for_each(divan::black_box_drop)) } + +#[divan::bench] +fn agc(bencher: Bencher) { + bencher + .with_inputs(|| TestSource::music_wav().to_f32s()) + .bench_values(|source| { + source + .automatic_gain_control( + 1.0, // target_level + 2.0, // attack_time (in seconds) + 0.01, // release_time (in seconds) + 5.0, // absolute_max_gain + ) + .for_each(divan::black_box_drop) + }) +} diff --git a/src/source/agc.rs b/src/source/agc.rs index 02777dd9..495c1b3c 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -59,6 +59,7 @@ impl CircularBuffer { /// Creates a new CircularBuffer with a fixed size determined at compile time. /// /// The `_size` parameter is ignored as the buffer size is set by `RMS_WINDOW_SIZE`. + #[inline] fn new(_size: usize) -> Self { CircularBuffer { buffer: [0.0; RMS_WINDOW_SIZE], @@ -70,6 +71,7 @@ impl CircularBuffer { /// Pushes a new value into the buffer and returns the old value. /// /// This method maintains a running sum for efficient mean calculation. + #[inline] fn push(&mut self, value: f32) -> f32 { let old_value = self.buffer[self.index]; self.buffer[self.index] = value; @@ -81,6 +83,7 @@ impl CircularBuffer { /// Calculates the mean of all values in the buffer. /// /// This operation is O(1) due to the maintained running sum. + #[inline] fn mean(&self) -> f32 { self.sum / self.buffer.len() as f32 } @@ -95,6 +98,7 @@ impl CircularBuffer { /// * `attack_time` - Time constant for gain increase /// * `release_time` - Time constant for gain decrease /// * `absolute_max_gain` - Maximum allowable gain +#[inline] pub fn automatic_gain_control( input: I, target_level: f32, From 1d2a6fdc94a057e2f573824328ea6687c6e4cdbf Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:52:33 +1300 Subject: [PATCH 31/65] Removed bullet point from docs --- src/source/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/source/mod.rs b/src/source/mod.rs index 5ac1d724..da7c04bc 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -241,7 +241,7 @@ where /// /// # Parameters /// - ///* `target_level`: + /// `target_level`: /// **TL;DR**: Desired output level. 1.0 = original level, > 1.0 amplifies, < 1.0 reduces. /// /// The desired output level, where 1.0 represents the original sound level. @@ -250,7 +250,7 @@ where /// will aim to increase the gain by a factor of 1.4, resulting in a minimum 40% amplification. /// A recommended level is `1.0`, which maintains the original sound level. /// - ///* `attack_time`: + /// `attack_time`: /// **TL;DR**: Response time for volume increases. Shorter = faster but may cause abrupt changes. **Recommended: `4.0` seconds**. /// /// The time (in seconds) for the AGC to respond to input level increases. @@ -262,7 +262,7 @@ where /// adjustment speed is limited by the attack time. Balance is key for optimal performance. /// A recommended attack_time of `4.0` seconds provides a sweet spot for most applications. /// - ///* `release_time`: + /// `release_time`: /// **TL;DR**: Response time for volume decreases. Shorter = faster gain reduction. **Recommended: `0.005` seconds**. /// /// The time (in seconds) for the AGC to respond to input level decreases. @@ -276,7 +276,7 @@ where /// preventing distortion. A recommended release_time of `0.005` seconds often works well for /// general use, providing a good balance between responsiveness and smooth transitions. /// - ///* `absolute_max_gain`: + /// `absolute_max_gain`: /// **TL;DR**: Maximum allowed gain. Prevents over-amplification. **Recommended: `5.0`**. /// /// The maximum gain that can be applied to the signal. From beeacf6e4a53fbfbc7f14bc4669659b26454003d Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sun, 29 Sep 2024 19:07:30 +1300 Subject: [PATCH 32/65] Added agc to CHANGELOG.md --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bcc2ace..cdcf20b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,17 +8,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Support for *ALAC/AIFF* +- Support for *ALAC/AIFF* +- Add `automatic_gain_control` source for dynamic audio level adjustment. - 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` + `fade_in` has been refactored to use the `linear_gain_ramp` implementation. ### Fixed - `Sink.try_seek` now updates `controls.position` before returning. Calls to `Sink.get_pos` - done immediately after a seek will now return the correct value. + done immediately after a seek will now return the correct value. ### Changed - `SamplesBuffer` is now `Clone` @@ -44,7 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Source` trait is now also implemented for `Box` and `&mut Source` - `fn new_vorbis` is now also available when the `symphonia-vorbis` feature is enabled -### Added +### Added - Adds a new method `try_seek` to all sources. It returns either an error or seeks to the given position. A few sources are "unsupported" they return the error `Unsupported`. @@ -52,7 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - channel upscaling now follows the 'WAVEFORMATEXTENSIBLE' format and no longer - repeats the last source channel on all extra output channels. + repeats the last source channel on all extra output channels. Stereo content playing on a 5.1 speaker set will now only use the front left and front right speaker instead of repeating the right sample on all speakers except the front left one. From 56fcce74f3d93517c45886097eafdd588ac99a4f Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Sun, 29 Sep 2024 15:40:36 -0700 Subject: [PATCH 33/65] Fixed a typo from @UnknownSuperficialNight --- src/source/test_waveform.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/source/test_waveform.rs b/src/source/test_waveform.rs index fea4f508..ec8db6c2 100644 --- a/src/source/test_waveform.rs +++ b/src/source/test_waveform.rs @@ -26,7 +26,7 @@ pub enum Function { Triangle, /// A square wave, rising edge at t=0. Square, - /// A rising swatooth wave. + /// A rising sawtooth wave. Sawtooth, } From b373f5253afdfeb51e14b409abc3fcc0cc561aa7 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Sun, 29 Sep 2024 16:02:07 -0700 Subject: [PATCH 34/65] Moved "noise" feature documentation ...into the previous "Optional Features" block --- src/lib.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d6742eab..ecff98a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,6 +110,11 @@ //! The "tracing" feature replaces the print to stderr when a stream error happens with a //! recording an error event with tracing. //! +//! ### Feature "Noise" +//! +//! The "noise" feature adds support for white and pink noise sources. This feature requires the +//! "rand" crate. +//! //! ## How it works under the hood //! //! Rodio spawns a background thread that is dedicated to reading from the sources and sending @@ -121,15 +126,8 @@ //! the number of sinks that can be created (except for the fact that creating too many will slow //! down your program). //! -//! -//! ## Features -//! -//! Rodio provides several optional features that are guarded with feature gates. -//! -//! ### Feature "Noise" -//! -//! The "noise" feature adds support for white and pink noise sources. This feature requires the -//! "rand" crate. + + #![cfg_attr(test, deny(missing_docs))] pub use cpal::{ self, traits::DeviceTrait, Device, Devices, DevicesError, InputDevices, OutputDevices, From 5fa0188cc361a7a36b511b6781d49b4496dad648 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Sun, 29 Sep 2024 16:15:41 -0700 Subject: [PATCH 35/65] Added signal_generator.rs example --- examples/signal_generator.rs | 83 ++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 examples/signal_generator.rs diff --git a/examples/signal_generator.rs b/examples/signal_generator.rs new file mode 100644 index 00000000..ca761937 --- /dev/null +++ b/examples/signal_generator.rs @@ -0,0 +1,83 @@ +//! Noise generator example. Use the "noise" feature to enable the noise generator sources. + +fn main() { + use rodio::source::{chirp, Function, Source, TestWaveform}; + use std::thread; + use std::time::Duration; + + let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap(); + + let noise_duration = Duration::from_millis(1000); + let interval_duration = Duration::from_millis(1500); + + println!("Playing 1000 Hz tone"); + stream_handle + .play_raw( + TestWaveform::new(cpal::SampleRate(48000), 1000.0, Function::Sine) + .amplify(0.1) + .take_duration(noise_duration), + ) + .unwrap(); + + thread::sleep(interval_duration); + + println!("Playing 10,000 Hz tone"); + stream_handle + .play_raw( + TestWaveform::new(cpal::SampleRate(48000), 10000.0, Function::Sine) + .amplify(0.1) + .take_duration(noise_duration), + ) + .unwrap(); + + thread::sleep(interval_duration); + + println!("Playing 440 Hz Triangle Wave"); + stream_handle + .play_raw( + TestWaveform::new(cpal::SampleRate(48000), 440.0, Function::Triangle) + .amplify(0.1) + .take_duration(noise_duration), + ) + .unwrap(); + + thread::sleep(interval_duration); + + println!("Playing 440 Hz Sawtooth Wave"); + stream_handle + .play_raw( + TestWaveform::new(cpal::SampleRate(48000), 440.0, Function::Sawtooth) + .amplify(0.1) + .take_duration(noise_duration), + ) + .unwrap(); + + thread::sleep(interval_duration); + + println!("Playing 440 Hz Square Wave"); + stream_handle + .play_raw( + TestWaveform::new(cpal::SampleRate(48000), 440.0, Function::Square) + .amplify(0.1) + .take_duration(noise_duration), + ) + .unwrap(); + + thread::sleep(interval_duration); + + println!("Playing 20-10000 Hz Sweep"); + stream_handle + .play_raw( + chirp( + cpal::SampleRate(48000), + 20.0, + 10000.0, + Duration::from_secs(1), + ) + .amplify(0.1) + .take_duration(noise_duration), + ) + .unwrap(); + + thread::sleep(interval_duration); +} From 371bbda49e4df18276b7fe91be2525cd5bc40065 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Sun, 29 Sep 2024 16:18:42 -0700 Subject: [PATCH 36/65] Some cosmetic changes for rustfmt --- src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ecff98a8..822d5e35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,7 +114,7 @@ //! //! The "noise" feature adds support for white and pink noise sources. This feature requires the //! "rand" crate. -//! +//! //! ## How it works under the hood //! //! Rodio spawns a background thread that is dedicated to reading from the sources and sending @@ -125,7 +125,6 @@ //! hardware. Therefore there is no restriction on the number of sounds that play simultaneously or //! the number of sinks that can be created (except for the fact that creating too many will slow //! down your program). -//! #![cfg_attr(test, deny(missing_docs))] From 9d20ea21f197753f60d6d1ac2f22ba6360beede3 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Sun, 29 Sep 2024 16:23:52 -0700 Subject: [PATCH 37/65] One more tweak for rustfmt --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 822d5e35..10ef60dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,7 +126,6 @@ //! the number of sinks that can be created (except for the fact that creating too many will slow //! down your program). - #![cfg_attr(test, deny(missing_docs))] pub use cpal::{ self, traits::DeviceTrait, Device, Devices, DevicesError, InputDevices, OutputDevices, From 9bf97acfa678f21113d460a48aa81d4fa262cde7 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:44:55 +1300 Subject: [PATCH 38/65] Update benchmark to new default values --- benches/effects.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/benches/effects.rs b/benches/effects.rs index eba38fae..91c48b14 100644 --- a/benches/effects.rs +++ b/benches/effects.rs @@ -54,10 +54,10 @@ fn agc(bencher: Bencher) { .bench_values(|source| { source .automatic_gain_control( - 1.0, // target_level - 2.0, // attack_time (in seconds) - 0.01, // release_time (in seconds) - 5.0, // absolute_max_gain + 1.0, // target_level + 4.0, // attack_time (in seconds) + 0.005, // release_time (in seconds) + 5.0, // absolute_max_gain ) .for_each(divan::black_box_drop) }) From 44cb217d48e65f3a2484b865a2f3b0b94ea45333 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Sun, 29 Sep 2024 16:45:29 -0700 Subject: [PATCH 39/65] Code review changes --- CHANGELOG.md | 6 +++--- examples/signal_generator.rs | 16 ++++++++-------- src/source/sine.rs | 8 ++++---- src/source/test_waveform.rs | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 390bfa03..b1567d48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New test signal generator sources: - `TestSignal` source generates a sine, triangle, square wave or sawtooth of a given frequency and sample rate. - - `Chirp` source generates a sine wave with linearly period over a given - frequency range and duration. + - `Chirp` source generates a sine wave with a linearly-increasing + frequency over a given frequency range and duration. - `white` and `pink` generate white or pink noise, respectively. These - sources depend on the `rand` crate and are guarded with the "noise" + sources depend on the `rand` crate and are guarded with the "noise" feature. - Documentation for the "noise" feature has been added to `lib.rs`. - New Fade and Crossfade sources: diff --git a/examples/signal_generator.rs b/examples/signal_generator.rs index ca761937..d17f1675 100644 --- a/examples/signal_generator.rs +++ b/examples/signal_generator.rs @@ -1,4 +1,4 @@ -//! Noise generator example. Use the "noise" feature to enable the noise generator sources. +//! Test signal generator example. fn main() { use rodio::source::{chirp, Function, Source, TestWaveform}; @@ -7,7 +7,7 @@ fn main() { let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap(); - let noise_duration = Duration::from_millis(1000); + let test_signal_duration = Duration::from_millis(1000); let interval_duration = Duration::from_millis(1500); println!("Playing 1000 Hz tone"); @@ -15,7 +15,7 @@ fn main() { .play_raw( TestWaveform::new(cpal::SampleRate(48000), 1000.0, Function::Sine) .amplify(0.1) - .take_duration(noise_duration), + .take_duration(test_signal_duration), ) .unwrap(); @@ -26,7 +26,7 @@ fn main() { .play_raw( TestWaveform::new(cpal::SampleRate(48000), 10000.0, Function::Sine) .amplify(0.1) - .take_duration(noise_duration), + .take_duration(test_signal_duration), ) .unwrap(); @@ -37,7 +37,7 @@ fn main() { .play_raw( TestWaveform::new(cpal::SampleRate(48000), 440.0, Function::Triangle) .amplify(0.1) - .take_duration(noise_duration), + .take_duration(test_signal_duration), ) .unwrap(); @@ -48,7 +48,7 @@ fn main() { .play_raw( TestWaveform::new(cpal::SampleRate(48000), 440.0, Function::Sawtooth) .amplify(0.1) - .take_duration(noise_duration), + .take_duration(test_signal_duration), ) .unwrap(); @@ -59,7 +59,7 @@ fn main() { .play_raw( TestWaveform::new(cpal::SampleRate(48000), 440.0, Function::Square) .amplify(0.1) - .take_duration(noise_duration), + .take_duration(test_signal_duration), ) .unwrap(); @@ -75,7 +75,7 @@ fn main() { Duration::from_secs(1), ) .amplify(0.1) - .take_duration(noise_duration), + .take_duration(test_signal_duration), ) .unwrap(); diff --git a/src/source/sine.rs b/src/source/sine.rs index 73db5eba..b9b97caa 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -5,8 +5,6 @@ use crate::Source; use super::SeekError; -const SAMPLE_RATE: u32 = 48000; - /// An infinite source that produces a sine. /// /// Always has a rate of 48kHz and one channel. @@ -16,10 +14,12 @@ pub struct SineWave { } impl SineWave { + const SAMPLE_RATE: u32 = 48000; + /// The frequency of the sine. #[inline] pub fn new(freq: f32) -> SineWave { - let sr = cpal::SampleRate(SAMPLE_RATE); + let sr = cpal::SampleRate(Self::SAMPLE_RATE); SineWave { test_sine: TestWaveform::new(sr, freq, Function::Sine), } @@ -48,7 +48,7 @@ impl Source for SineWave { #[inline] fn sample_rate(&self) -> u32 { - SAMPLE_RATE + Self::SAMPLE_RATE } #[inline] diff --git a/src/source/test_waveform.rs b/src/source/test_waveform.rs index ec8db6c2..bcd4799c 100644 --- a/src/source/test_waveform.rs +++ b/src/source/test_waveform.rs @@ -34,19 +34,19 @@ impl Function { /// Create a single sample for the given waveform #[inline] fn render(&self, i: u64, period: f32) -> f32 { - let i_div_p: f32 = i as f32 / period; + let cycle_pos: f32 = i as f32 / period; match self { - Self::Sine => (TAU * i_div_p).sin(), - Self::Triangle => 4.0f32 * (i_div_p - (i_div_p + 0.5f32).floor()).abs() - 1f32, + Self::Sine => (TAU * cycle_pos).sin(), + Self::Triangle => 4.0f32 * (cycle_pos - (cycle_pos + 0.5f32).floor()).abs() - 1f32, Self::Square => { - if i_div_p % 1.0f32 < 0.5f32 { + if cycle_pos % 1.0f32 < 0.5f32 { 1.0f32 } else { -1.0f32 } } - Self::Sawtooth => 2.0f32 * (i_div_p - (i_div_p + 0.5f32).floor()), + Self::Sawtooth => 2.0f32 * (cycle_pos - (cycle_pos + 0.5f32).floor()), } } } From a8a443ba511f1237235f67d23a4a5d1f4e9ed4bd Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:46:46 +1300 Subject: [PATCH 40/65] Enhance AGC stability and flexibility - Increase RMS_WINDOW_SIZE for more stable measurements of very low frequencies - Replace MIN_ATTACK_COEFF with release_coeff for improved customizability These changes provide better handling of low-frequency content and allow developers more control over AGC behavior, particularly in attack/release time adjustments. --- src/source/agc.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 495c1b3c..01344a1d 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -22,11 +22,7 @@ use tracing; /// Size of the circular buffer used for RMS calculation. /// A larger size provides more stable RMS values but increases latency. -const RMS_WINDOW_SIZE: usize = 1024; - -/// Minimum attack coefficient for rapid response to sudden level increases. -/// Balances between responsiveness and stability. -const MIN_ATTACK_COEFF: f32 = 0.05; +const RMS_WINDOW_SIZE: usize = 8192; /// Automatic Gain Control filter for maintaining consistent output levels. /// @@ -40,6 +36,7 @@ pub struct AutomaticGainControl { current_gain: f32, attack_coeff: f32, release_coeff: f32, + min_attack_coeff: f32, peak_level: f32, rms_window: CircularBuffer, } @@ -119,6 +116,7 @@ where current_gain: 1.0, attack_coeff: (-1.0 / (attack_time * sample_rate as f32)).exp(), release_coeff: (-1.0 / (release_time * sample_rate as f32)).exp(), + min_attack_coeff: release_time, peak_level: 0.0, rms_window: CircularBuffer::new(RMS_WINDOW_SIZE), } @@ -171,9 +169,9 @@ where /// current peak level. This adaptive behavior helps capture transients /// more accurately while maintaining smoother behavior for gradual changes. #[inline] - fn update_peak_level(&mut self, sample_value: f32) { + fn update_peak_level(&mut self, sample_value: f32, release_time: f32) { let attack_coeff = if sample_value > self.peak_level { - self.attack_coeff.min(MIN_ATTACK_COEFF) // Faster response to sudden increases + self.attack_coeff.min(release_time) // Faster response to sudden increases } else { self.release_coeff }; @@ -218,7 +216,7 @@ where let sample_value = value.to_f32().abs(); // Dynamically adjust peak level using an adaptive attack coefficient - self.update_peak_level(sample_value); + self.update_peak_level(sample_value, self.min_attack_coeff); // Calculate the current RMS (Root Mean Square) level using a sliding window approach let rms = self.update_rms(sample_value); From 68e1bd21c0244d0279c5e8b89857ea4e84a729b0 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Mon, 30 Sep 2024 20:01:57 +1300 Subject: [PATCH 41/65] Pass min_attack_coeff directly --- src/source/agc.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 01344a1d..d54b5972 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -165,13 +165,13 @@ where /// /// This method adjusts the peak level using a variable attack coefficient. /// It responds faster to sudden increases in signal level by using a - /// minimum attack coefficient of MIN_ATTACK_COEFF when the sample value exceeds the + /// minimum attack coefficient of min_attack_coeff when the sample value exceeds the /// current peak level. This adaptive behavior helps capture transients /// more accurately while maintaining smoother behavior for gradual changes. #[inline] - fn update_peak_level(&mut self, sample_value: f32, release_time: f32) { + fn update_peak_level(&mut self, sample_value: f32) { let attack_coeff = if sample_value > self.peak_level { - self.attack_coeff.min(release_time) // Faster response to sudden increases + self.attack_coeff.min(self.min_attack_coeff) // User-defined attack time limited via release_time } else { self.release_coeff }; @@ -216,7 +216,7 @@ where let sample_value = value.to_f32().abs(); // Dynamically adjust peak level using an adaptive attack coefficient - self.update_peak_level(sample_value, self.min_attack_coeff); + self.update_peak_level(sample_value); // Calculate the current RMS (Root Mean Square) level using a sliding window approach let rms = self.update_rms(sample_value); From 2442aa09e8eb3e48e44534b7108b0e3061e7f2a8 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Tue, 1 Oct 2024 00:45:48 +1300 Subject: [PATCH 42/65] Add real-time toggle for AGC processing Implement get_agc_control() to allow dynamic enabling/disabling of AGC during audio playback. --- src/source/agc.rs | 164 ++++++++++++++++++++++++++-------------------- 1 file changed, 92 insertions(+), 72 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index d54b5972..1aa1ae1a 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -15,6 +15,8 @@ use super::SeekError; use crate::{Sample, Source}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::Duration; #[cfg(feature = "tracing")] @@ -39,6 +41,7 @@ pub struct AutomaticGainControl { min_attack_coeff: f32, peak_level: f32, rms_window: CircularBuffer, + is_enabled: Arc, } /// A circular buffer for efficient RMS calculation over a sliding window. @@ -119,6 +122,7 @@ where min_attack_coeff: release_time, peak_level: 0.0, rms_window: CircularBuffer::new(RMS_WINDOW_SIZE), + is_enabled: Arc::new(AtomicBool::new(true)), } } @@ -161,6 +165,14 @@ where self.release_coeff = (-1.0 / (release_time * sample_rate as f32)).exp(); } + /// Returns a handle to control AGC on/off state. + /// + /// This allows real-time toggling of the AGC processing. + #[inline] + pub fn get_agc_control(&self) -> Arc { + Arc::clone(&self.is_enabled) + } + /// Updates the peak level with an adaptive attack coefficient /// /// This method adjusts the peak level using a variable attack coefficient. @@ -178,6 +190,16 @@ where self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; } + /// Updates the RMS (Root Mean Square) level using a circular buffer approach. + /// This method calculates a moving average of the squared input samples, + /// providing a measure of the signal's average power over time. + #[inline] + fn update_rms(&mut self, sample_value: f32) -> f32 { + let squared_sample = sample_value * sample_value; + self.rms_window.push(squared_sample); + self.rms_window.mean().sqrt() + } + /// Calculate gain adjustments based on peak levels /// This method determines the appropriate gain level to apply to the audio /// signal, considering the peak level. @@ -191,14 +213,72 @@ where } } - /// Updates the RMS (Root Mean Square) level using a circular buffer approach. - /// This method calculates a moving average of the squared input samples, - /// providing a measure of the signal's average power over time. #[inline] - fn update_rms(&mut self, sample_value: f32) -> f32 { - let squared_sample = sample_value * sample_value; - self.rms_window.push(squared_sample); - self.rms_window.mean().sqrt() + fn process_sample(&mut self, sample: I::Item) -> I::Item { + // Convert the sample to its absolute float value for level calculations + let sample_value = sample.to_f32().abs(); + + // Dynamically adjust peak level using an adaptive attack coefficient + self.update_peak_level(sample_value); + + // Calculate the current RMS (Root Mean Square) level using a sliding window approach + let rms = self.update_rms(sample_value); + + // Compute the gain adjustment required to reach the target level based on RMS + let rms_gain = if rms > 0.0 { + self.target_level / rms + } else { + self.absolute_max_gain // Default to max gain if RMS is zero + }; + + // Calculate the peak limiting gain + let peak_gain = self.calculate_peak_gain(); + + // Use RMS for general adjustments, but limit by peak gain to prevent clipping + let desired_gain = rms_gain.min(peak_gain); + + // Adaptive attack/release speed for AGC (Automatic Gain Control) + // + // This mechanism implements an asymmetric approach to gain adjustment: + // 1. Slow increase: Prevents abrupt amplification of noise during quiet periods. + // 2. Fast decrease: Rapidly attenuates sudden loud signals to avoid distortion. + // + // The asymmetry is crucial because: + // - Gradual gain increases sound more natural and less noticeable to listeners. + // - Quick gain reductions are necessary to prevent clipping and maintain audio quality. + // + // This approach addresses several challenges associated with high attack times: + // 1. Slow response: With a high attack time, the AGC responds very slowly to changes in input level. + // This means it takes longer for the gain to adjust to new signal levels. + // 2. Initial gain calculation: When the audio starts or after a period of silence, the initial gain + // calculation might result in a very high gain value, especially if the input signal starts quietly. + // 3. Overshooting: As the gain slowly increases (due to the high attack time), it might overshoot + // the desired level, causing the signal to become too loud. + // 4. Overcorrection: The AGC then tries to correct this by reducing the gain, but due to the slow response, + // it might reduce the gain too much, causing the sound to drop to near-zero levels. + // 5. Slow recovery: Again, due to the high attack time, it takes a while for the gain to increase + // back to the appropriate level. + // + // By using a faster release time for decreasing gain, we can mitigate these issues and provide + // more responsive control over sudden level increases while maintaining smooth gain increases. + let attack_speed = if desired_gain > self.current_gain { + self.attack_coeff + } else { + self.release_coeff + }; + + // Gradually adjust the current gain towards the desired gain for smooth transitions + self.current_gain = self.current_gain * attack_speed + desired_gain * (1.0 - attack_speed); + + // Ensure the calculated gain stays within the defined operational range + self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain); + + // Output current gain value for developers to fine tune their inputs to automatic_gain_control + #[cfg(feature = "tracing")] + tracing::debug!("AGC gain: {}", self.current_gain,); + + // Apply the computed gain to the input sample and return the result + sample.amplify(self.current_gain) } } @@ -211,72 +291,12 @@ where #[inline] fn next(&mut self) -> Option { - self.input.next().map(|value| { - // Convert the sample to its absolute float value for level calculations - let sample_value = value.to_f32().abs(); - - // Dynamically adjust peak level using an adaptive attack coefficient - self.update_peak_level(sample_value); - - // Calculate the current RMS (Root Mean Square) level using a sliding window approach - let rms = self.update_rms(sample_value); - - // Compute the gain adjustment required to reach the target level based on RMS - let rms_gain = if rms > 0.0 { - self.target_level / rms + self.input.next().map(|sample| { + if self.is_enabled.load(Ordering::Relaxed) { + self.process_sample(sample) } else { - self.absolute_max_gain // Default to max gain if RMS is zero - }; - - // Calculate the peak limiting gain - let peak_gain = self.calculate_peak_gain(); - - // Use RMS for general adjustments, but limit by peak gain to prevent clipping - let desired_gain = rms_gain.min(peak_gain); - - // Adaptive attack/release speed for AGC (Automatic Gain Control) - // - // This mechanism implements an asymmetric approach to gain adjustment: - // 1. Slow increase: Prevents abrupt amplification of noise during quiet periods. - // 2. Fast decrease: Rapidly attenuates sudden loud signals to avoid distortion. - // - // The asymmetry is crucial because: - // - Gradual gain increases sound more natural and less noticeable to listeners. - // - Quick gain reductions are necessary to prevent clipping and maintain audio quality. - // - // This approach addresses several challenges associated with high attack times: - // 1. Slow response: With a high attack time, the AGC responds very slowly to changes in input level. - // This means it takes longer for the gain to adjust to new signal levels. - // 2. Initial gain calculation: When the audio starts or after a period of silence, the initial gain - // calculation might result in a very high gain value, especially if the input signal starts quietly. - // 3. Overshooting: As the gain slowly increases (due to the high attack time), it might overshoot - // the desired level, causing the signal to become too loud. - // 4. Overcorrection: The AGC then tries to correct this by reducing the gain, but due to the slow response, - // it might reduce the gain too much, causing the sound to drop to near-zero levels. - // 5. Slow recovery: Again, due to the high attack time, it takes a while for the gain to increase - // back to the appropriate level. - // - // By using a faster release time for decreasing gain, we can mitigate these issues and provide - // more responsive control over sudden level increases while maintaining smooth gain increases. - let attack_speed = if desired_gain > self.current_gain { - self.attack_coeff - } else { - self.release_coeff - }; - - // Gradually adjust the current gain towards the desired gain for smooth transitions - self.current_gain = - self.current_gain * attack_speed + desired_gain * (1.0 - attack_speed); - - // Ensure the calculated gain stays within the defined operational range - self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain); - - // Output current gain value for developers to fine tune their inputs to automatic_gain_control - #[cfg(feature = "tracing")] - tracing::debug!("AGC gain: {}", self.current_gain,); - - // Apply the computed gain to the input sample and return the result - value.amplify(self.current_gain) + sample + } }) } From b59533e3820f1181b1469d721795429efd56eb0c Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Tue, 1 Oct 2024 00:50:45 +1300 Subject: [PATCH 43/65] Add new benchmark for disabled_agc --- benches/effects.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/benches/effects.rs b/benches/effects.rs index 91c48b14..1101bd3a 100644 --- a/benches/effects.rs +++ b/benches/effects.rs @@ -48,7 +48,7 @@ fn amplify(bencher: Bencher) { } #[divan::bench] -fn agc(bencher: Bencher) { +fn agc_enabled(bencher: Bencher) { bencher .with_inputs(|| TestSource::music_wav().to_f32s()) .bench_values(|source| { @@ -62,3 +62,25 @@ fn agc(bencher: Bencher) { .for_each(divan::black_box_drop) }) } + +#[divan::bench] +fn agc_disabled(bencher: Bencher) { + bencher + .with_inputs(|| TestSource::music_wav().to_f32s()) + .bench_values(|source| { + // Create the AGC source + let amplified_source = source.automatic_gain_control( + 1.0, // target_level + 4.0, // attack_time (in seconds) + 0.005, // release_time (in seconds) + 5.0, // absolute_max_gain + ); + + // Get the control handle and disable AGC + let agc_control = amplified_source.get_agc_control(); + agc_control.store(false, std::sync::atomic::Ordering::Relaxed); + + // Process the audio stream with AGC disabled + amplified_source.for_each(divan::black_box_drop) + }) +} From 42fe8320aa24b3b4f62e5bf9a1003aac6c559761 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Tue, 1 Oct 2024 01:46:55 +1300 Subject: [PATCH 44/65] Enhance automatic_gain_control documentation - Add references to get_agc_control method in automatic_gain_control docs - Include a quick start example demonstrating usage of get_agc_control --- src/source/mod.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/source/mod.rs b/src/source/mod.rs index da7c04bc..75dd7e20 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -286,6 +286,27 @@ where /// This is crucial for maintaining audio quality and preventing unexpected volume spikes. /// A recommended value for `absolute_max_gain` is `5`, which provides a good balance between /// amplification capability and protection against distortion in most scenarios. + /// + /// Use `get_agc_control` to obtain a handle for real-time enabling/disabling of the AGC. + /// + /// # Example (Quick start) + /// + /// ```rust + /// // Apply Automatic Gain Control to the source (AGC is on by default) + /// let agc_source = source.automatic_gain_control(1.0, 4.0, 0.005, 5.0); + /// + /// // Get a handle to control the AGC's enabled state (optional) + /// let agc_control = agc_source.get_agc_control(); + /// + /// // You can toggle AGC on/off at any time (optional) + /// agc_control.store(false, std::sync::atomic::Ordering::Relaxed); + /// + /// // Add the AGC-controlled source to the sink + /// sink.append(agc_source); + /// + /// // Note: Using agc_control is optional. If you don't need to toggle AGC, + /// // you can simply use the agc_source directly without getting agc_control. + /// ``` #[inline] fn automatic_gain_control( self, From 15cca7338859dfa08fbff22ae6bec996974876a2 Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Tue, 1 Oct 2024 02:35:16 +0200 Subject: [PATCH 45/65] Add dependency notice (libalsa on linux) to readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 614ab1fb..91b03693 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,10 @@ See [the docs](https://docs.rs/rodio/latest/rodio/#alternative-decoder-backends) [The documentation](http://docs.rs/rodio) contains an introduction to the library. +## Dependencies(Linux only) + +Rodio uses `cpal` to send audio to the OS for playback. On Linux `cpal` needs the ALSA development files. These are provided as part of the libasound2-dev package on Debian and Ubuntu distributions and alsa-lib-devel on Fedora. + ## License [License]: #license From 86cb156e47bb0165a8463e75c70b284903189337 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:56:28 +1300 Subject: [PATCH 46/65] Refactor CircularBuffer to use heap allocation to avoid large stack usage - Updated CircularBuffer implementation to allocate on the heap instead of the stack to prevent excessive stack usage (32KB) which can lead to stack overflow issues. - Optimized index wrapping logic using bitwise operations, leveraging the fact that RMS_WINDOW_SIZE is a power of two for improved efficiency. --- src/source/agc.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 1aa1ae1a..9d24e4ca 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -50,21 +50,19 @@ pub struct AutomaticGainControl { /// which is crucial for real-time audio processing. #[derive(Clone, Debug)] struct CircularBuffer { - buffer: [f32; RMS_WINDOW_SIZE], - index: usize, + buffer: Box<[f32; RMS_WINDOW_SIZE]>, sum: f32, + index: usize, } impl CircularBuffer { /// Creates a new CircularBuffer with a fixed size determined at compile time. - /// - /// The `_size` parameter is ignored as the buffer size is set by `RMS_WINDOW_SIZE`. #[inline] - fn new(_size: usize) -> Self { + fn new() -> Self { CircularBuffer { - buffer: [0.0; RMS_WINDOW_SIZE], - index: 0, + buffer: Box::new([0.0; RMS_WINDOW_SIZE]), sum: 0.0, + index: 0, } } @@ -74,9 +72,11 @@ impl CircularBuffer { #[inline] fn push(&mut self, value: f32) -> f32 { let old_value = self.buffer[self.index]; + // Update the sum by first subtracting the old value and then adding the new value; this is more accurate. + self.sum = self.sum - old_value + value; self.buffer[self.index] = value; - self.sum += value - old_value; - self.index = (self.index + 1) % self.buffer.len(); + // Use bitwise AND for efficient index wrapping since RMS_WINDOW_SIZE is a power of two. + self.index = (self.index + 1) & (RMS_WINDOW_SIZE - 1); old_value } @@ -85,7 +85,7 @@ impl CircularBuffer { /// This operation is O(1) due to the maintained running sum. #[inline] fn mean(&self) -> f32 { - self.sum / self.buffer.len() as f32 + self.sum / RMS_WINDOW_SIZE as f32 } } @@ -121,7 +121,7 @@ where release_coeff: (-1.0 / (release_time * sample_rate as f32)).exp(), min_attack_coeff: release_time, peak_level: 0.0, - rms_window: CircularBuffer::new(RMS_WINDOW_SIZE), + rms_window: CircularBuffer::new(), is_enabled: Arc::new(AtomicBool::new(true)), } } From 8899e6709fa50e7d70136ea8029217e9feafefd2 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 30 Sep 2024 21:45:30 -0700 Subject: [PATCH 47/65] Renamed TestWaveform to SignalGenerator --- examples/signal_generator.rs | 12 +-- src/source/mod.rs | 4 +- src/source/sine.rs | 6 +- src/source/test_waveform.rs | 185 ----------------------------------- 4 files changed, 11 insertions(+), 196 deletions(-) delete mode 100644 src/source/test_waveform.rs diff --git a/examples/signal_generator.rs b/examples/signal_generator.rs index d17f1675..802fd582 100644 --- a/examples/signal_generator.rs +++ b/examples/signal_generator.rs @@ -1,7 +1,7 @@ //! Test signal generator example. fn main() { - use rodio::source::{chirp, Function, Source, TestWaveform}; + use rodio::source::{chirp, Function, Source, SignalGenerator}; use std::thread; use std::time::Duration; @@ -13,7 +13,7 @@ fn main() { println!("Playing 1000 Hz tone"); stream_handle .play_raw( - TestWaveform::new(cpal::SampleRate(48000), 1000.0, Function::Sine) + SignalGenerator::new(cpal::SampleRate(48000), 1000.0, Function::Sine) .amplify(0.1) .take_duration(test_signal_duration), ) @@ -24,7 +24,7 @@ fn main() { println!("Playing 10,000 Hz tone"); stream_handle .play_raw( - TestWaveform::new(cpal::SampleRate(48000), 10000.0, Function::Sine) + SignalGenerator::new(cpal::SampleRate(48000), 10000.0, Function::Sine) .amplify(0.1) .take_duration(test_signal_duration), ) @@ -35,7 +35,7 @@ fn main() { println!("Playing 440 Hz Triangle Wave"); stream_handle .play_raw( - TestWaveform::new(cpal::SampleRate(48000), 440.0, Function::Triangle) + SignalGenerator::new(cpal::SampleRate(48000), 440.0, Function::Triangle) .amplify(0.1) .take_duration(test_signal_duration), ) @@ -46,7 +46,7 @@ fn main() { println!("Playing 440 Hz Sawtooth Wave"); stream_handle .play_raw( - TestWaveform::new(cpal::SampleRate(48000), 440.0, Function::Sawtooth) + SignalGenerator::new(cpal::SampleRate(48000), 440.0, Function::Sawtooth) .amplify(0.1) .take_duration(test_signal_duration), ) @@ -57,7 +57,7 @@ fn main() { println!("Playing 440 Hz Square Wave"); stream_handle .play_raw( - TestWaveform::new(cpal::SampleRate(48000), 440.0, Function::Square) + SignalGenerator::new(cpal::SampleRate(48000), 440.0, Function::Square) .amplify(0.1) .take_duration(test_signal_duration), ) diff --git a/src/source/mod.rs b/src/source/mod.rs index 8a2f728f..d91cabf2 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -34,7 +34,7 @@ pub use self::spatial::Spatial; pub use self::speed::Speed; pub use self::stoppable::Stoppable; pub use self::take::TakeDuration; -pub use self::test_waveform::{Function, TestWaveform}; +pub use self::signal_generator::{Function, SignalGenerator}; pub use self::uniform::UniformSourceIterator; pub use self::zero::Zero; @@ -66,7 +66,7 @@ mod spatial; mod speed; mod stoppable; mod take; -mod test_waveform; +mod signal_generator; mod uniform; mod zero; diff --git a/src/source/sine.rs b/src/source/sine.rs index b9b97caa..b7591900 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use crate::source::{Function, TestWaveform}; +use crate::source::{Function, SignalGenerator}; use crate::Source; use super::SeekError; @@ -10,7 +10,7 @@ use super::SeekError; /// Always has a rate of 48kHz and one channel. #[derive(Clone, Debug)] pub struct SineWave { - test_sine: TestWaveform, + test_sine: SignalGenerator, } impl SineWave { @@ -21,7 +21,7 @@ impl SineWave { pub fn new(freq: f32) -> SineWave { let sr = cpal::SampleRate(Self::SAMPLE_RATE); SineWave { - test_sine: TestWaveform::new(sr, freq, Function::Sine), + test_sine: SignalGenerator::new(sr, freq, Function::Sine), } } } diff --git a/src/source/test_waveform.rs b/src/source/test_waveform.rs deleted file mode 100644 index bcd4799c..00000000 --- a/src/source/test_waveform.rs +++ /dev/null @@ -1,185 +0,0 @@ -//! Generator sources for various periodic test waveforms. -//! -//! This module provides several periodic, deterministic waveforms for testing other sources and -//! for simple additive sound synthesis. Every source is monoaural and in the codomain [-1.0f32, -//! 1.0f32]. -//! -//! # Example -//! -//! ``` -//! use rodio::source::{TestWaveform,Function}; -//! -//! let tone = TestWaveform::new(cpal::SampleRate(48000), 440.0, Function::Sine); -//! ``` -use std::f32::consts::TAU; -use std::time::Duration; - -use super::SeekError; -use crate::Source; - -/// Waveform functions. -#[derive(Clone, Debug)] -pub enum Function { - /// A sinusoidal waveform. - Sine, - /// A triangle waveform. - Triangle, - /// A square wave, rising edge at t=0. - Square, - /// A rising sawtooth wave. - Sawtooth, -} - -impl Function { - /// Create a single sample for the given waveform - #[inline] - fn render(&self, i: u64, period: f32) -> f32 { - let cycle_pos: f32 = i as f32 / period; - - match self { - Self::Sine => (TAU * cycle_pos).sin(), - Self::Triangle => 4.0f32 * (cycle_pos - (cycle_pos + 0.5f32).floor()).abs() - 1f32, - Self::Square => { - if cycle_pos % 1.0f32 < 0.5f32 { - 1.0f32 - } else { - -1.0f32 - } - } - Self::Sawtooth => 2.0f32 * (cycle_pos - (cycle_pos + 0.5f32).floor()), - } - } -} - -/// An infinite source that produces one of a selection of test waveforms. -#[derive(Clone, Debug)] -pub struct TestWaveform { - sample_rate: cpal::SampleRate, - period: f32, - function: Function, - i: u64, -} - -impl TestWaveform { - /// Create a new `TestWaveform` object that generates an endless waveform - /// `f`. - /// - /// # Panics - /// - /// Will panic if `frequency` is equal to zero. - #[inline] - pub fn new(sample_rate: cpal::SampleRate, frequency: f32, f: Function) -> TestWaveform { - assert!(frequency != 0.0, "frequency must be greater than zero"); - let period = sample_rate.0 as f32 / frequency; - TestWaveform { - sample_rate, - period, - function: f, - i: 0, - } - } -} - -impl Iterator for TestWaveform { - type Item = f32; - - #[inline] - fn next(&mut self) -> Option { - let val = Some(self.function.render(self.i, self.period)); - self.i += 1; - val - } -} - -impl Source for TestWaveform { - #[inline] - fn current_frame_len(&self) -> Option { - None - } - - #[inline] - fn channels(&self) -> u16 { - 1 - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.sample_rate.0 - } - - #[inline] - fn total_duration(&self) -> Option { - None - } - - #[inline] - fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { - self.i = (self.sample_rate.0 as f32 * duration.as_secs_f32()) as u64; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use crate::source::{Function, TestWaveform}; - use approx::assert_abs_diff_eq; - - #[test] - fn square() { - let mut wf = TestWaveform::new(cpal::SampleRate(2000), 500.0f32, Function::Square); - assert_eq!(wf.next(), Some(1.0f32)); - assert_eq!(wf.next(), Some(1.0f32)); - assert_eq!(wf.next(), Some(-1.0f32)); - assert_eq!(wf.next(), Some(-1.0f32)); - assert_eq!(wf.next(), Some(1.0f32)); - assert_eq!(wf.next(), Some(1.0f32)); - assert_eq!(wf.next(), Some(-1.0f32)); - assert_eq!(wf.next(), Some(-1.0f32)); - } - - #[test] - fn triangle() { - let mut wf = TestWaveform::new(cpal::SampleRate(8000), 1000.0f32, Function::Triangle); - assert_eq!(wf.next(), Some(-1.0f32)); - assert_eq!(wf.next(), Some(-0.5f32)); - assert_eq!(wf.next(), Some(0.0f32)); - assert_eq!(wf.next(), Some(0.5f32)); - assert_eq!(wf.next(), Some(1.0f32)); - assert_eq!(wf.next(), Some(0.5f32)); - assert_eq!(wf.next(), Some(0.0f32)); - assert_eq!(wf.next(), Some(-0.5f32)); - assert_eq!(wf.next(), Some(-1.0f32)); - assert_eq!(wf.next(), Some(-0.5f32)); - assert_eq!(wf.next(), Some(0.0f32)); - assert_eq!(wf.next(), Some(0.5f32)); - assert_eq!(wf.next(), Some(1.0f32)); - assert_eq!(wf.next(), Some(0.5f32)); - assert_eq!(wf.next(), Some(0.0f32)); - assert_eq!(wf.next(), Some(-0.5f32)); - } - - #[test] - fn saw() { - let mut wf = TestWaveform::new(cpal::SampleRate(200), 50.0f32, Function::Sawtooth); - assert_eq!(wf.next(), Some(0.0f32)); - assert_eq!(wf.next(), Some(0.5f32)); - assert_eq!(wf.next(), Some(-1.0f32)); - assert_eq!(wf.next(), Some(-0.5f32)); - assert_eq!(wf.next(), Some(0.0f32)); - assert_eq!(wf.next(), Some(0.5f32)); - assert_eq!(wf.next(), Some(-1.0f32)); - } - - #[test] - fn sine() { - let mut wf = TestWaveform::new(cpal::SampleRate(1000), 100f32, Function::Sine); - - assert_abs_diff_eq!(wf.next().unwrap(), 0.0f32); - assert_abs_diff_eq!(wf.next().unwrap(), 0.58778525f32); - assert_abs_diff_eq!(wf.next().unwrap(), 0.95105652f32); - assert_abs_diff_eq!(wf.next().unwrap(), 0.95105652f32); - assert_abs_diff_eq!(wf.next().unwrap(), 0.58778525f32); - assert_abs_diff_eq!(wf.next().unwrap(), 0.0f32); - assert_abs_diff_eq!(wf.next().unwrap(), -0.58778554f32); - } -} From a1fa144f579243e052e71b399907c0b46adbae0b Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 30 Sep 2024 21:50:30 -0700 Subject: [PATCH 48/65] Adding renamed signal_generator.rs --- src/source/signal_generator.rs | 185 +++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 src/source/signal_generator.rs diff --git a/src/source/signal_generator.rs b/src/source/signal_generator.rs new file mode 100644 index 00000000..54969ed5 --- /dev/null +++ b/src/source/signal_generator.rs @@ -0,0 +1,185 @@ +//! Generator sources for various periodic test waveforms. +//! +//! This module provides several periodic, deterministic waveforms for testing other sources and +//! for simple additive sound synthesis. Every source is monoaural and in the codomain [-1.0f32, +//! 1.0f32]. +//! +//! # Example +//! +//! ``` +//! use rodio::source::{SignalGenerator,Function}; +//! +//! let tone = SignalGenerator::new(cpal::SampleRate(48000), 440.0, Function::Sine); +//! ``` +use std::f32::consts::TAU; +use std::time::Duration; + +use super::SeekError; +use crate::Source; + +/// Waveform functions. +#[derive(Clone, Debug)] +pub enum Function { + /// A sinusoidal waveform. + Sine, + /// A triangle waveform. + Triangle, + /// A square wave, rising edge at t=0. + Square, + /// A rising sawtooth wave. + Sawtooth, +} + +impl Function { + /// Create a single sample for the given waveform + #[inline] + fn render(&self, i: u64, period: f32) -> f32 { + let cycle_pos: f32 = i as f32 / period; + + match self { + Self::Sine => (TAU * cycle_pos).sin(), + Self::Triangle => 4.0f32 * (cycle_pos - (cycle_pos + 0.5f32).floor()).abs() - 1f32, + Self::Square => { + if cycle_pos % 1.0f32 < 0.5f32 { + 1.0f32 + } else { + -1.0f32 + } + } + Self::Sawtooth => 2.0f32 * (cycle_pos - (cycle_pos + 0.5f32).floor()), + } + } +} + +/// An infinite source that produces one of a selection of test waveforms. +#[derive(Clone, Debug)] +pub struct SignalGenerator { + sample_rate: cpal::SampleRate, + period: f32, + function: Function, + i: u64, +} + +impl SignalGenerator { + /// Create a new `TestWaveform` object that generates an endless waveform + /// `f`. + /// + /// # Panics + /// + /// Will panic if `frequency` is equal to zero. + #[inline] + pub fn new(sample_rate: cpal::SampleRate, frequency: f32, f: Function) -> SignalGenerator { + assert!(frequency != 0.0, "frequency must be greater than zero"); + let period = sample_rate.0 as f32 / frequency; + SignalGenerator { + sample_rate, + period, + function: f, + i: 0, + } + } +} + +impl Iterator for SignalGenerator { + type Item = f32; + + #[inline] + fn next(&mut self) -> Option { + let val = Some(self.function.render(self.i, self.period)); + self.i += 1; + val + } +} + +impl Source for SignalGenerator { + #[inline] + fn current_frame_len(&self) -> Option { + None + } + + #[inline] + fn channels(&self) -> u16 { + 1 + } + + #[inline] + fn sample_rate(&self) -> u32 { + self.sample_rate.0 + } + + #[inline] + fn total_duration(&self) -> Option { + None + } + + #[inline] + fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { + self.i = (self.sample_rate.0 as f32 * duration.as_secs_f32()) as u64; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::source::{Function, SignalGenerator}; + use approx::assert_abs_diff_eq; + + #[test] + fn square() { + let mut wf = SignalGenerator::new(cpal::SampleRate(2000), 500.0f32, Function::Square); + assert_eq!(wf.next(), Some(1.0f32)); + assert_eq!(wf.next(), Some(1.0f32)); + assert_eq!(wf.next(), Some(-1.0f32)); + assert_eq!(wf.next(), Some(-1.0f32)); + assert_eq!(wf.next(), Some(1.0f32)); + assert_eq!(wf.next(), Some(1.0f32)); + assert_eq!(wf.next(), Some(-1.0f32)); + assert_eq!(wf.next(), Some(-1.0f32)); + } + + #[test] + fn triangle() { + let mut wf = SignalGenerator::new(cpal::SampleRate(8000), 1000.0f32, Function::Triangle); + assert_eq!(wf.next(), Some(-1.0f32)); + assert_eq!(wf.next(), Some(-0.5f32)); + assert_eq!(wf.next(), Some(0.0f32)); + assert_eq!(wf.next(), Some(0.5f32)); + assert_eq!(wf.next(), Some(1.0f32)); + assert_eq!(wf.next(), Some(0.5f32)); + assert_eq!(wf.next(), Some(0.0f32)); + assert_eq!(wf.next(), Some(-0.5f32)); + assert_eq!(wf.next(), Some(-1.0f32)); + assert_eq!(wf.next(), Some(-0.5f32)); + assert_eq!(wf.next(), Some(0.0f32)); + assert_eq!(wf.next(), Some(0.5f32)); + assert_eq!(wf.next(), Some(1.0f32)); + assert_eq!(wf.next(), Some(0.5f32)); + assert_eq!(wf.next(), Some(0.0f32)); + assert_eq!(wf.next(), Some(-0.5f32)); + } + + #[test] + fn saw() { + let mut wf = SignalGenerator::new(cpal::SampleRate(200), 50.0f32, Function::Sawtooth); + assert_eq!(wf.next(), Some(0.0f32)); + assert_eq!(wf.next(), Some(0.5f32)); + assert_eq!(wf.next(), Some(-1.0f32)); + assert_eq!(wf.next(), Some(-0.5f32)); + assert_eq!(wf.next(), Some(0.0f32)); + assert_eq!(wf.next(), Some(0.5f32)); + assert_eq!(wf.next(), Some(-1.0f32)); + } + + #[test] + fn sine() { + let mut wf = SignalGenerator::new(cpal::SampleRate(1000), 100f32, Function::Sine); + + assert_abs_diff_eq!(wf.next().unwrap(), 0.0f32); + assert_abs_diff_eq!(wf.next().unwrap(), 0.58778525f32); + assert_abs_diff_eq!(wf.next().unwrap(), 0.95105652f32); + assert_abs_diff_eq!(wf.next().unwrap(), 0.95105652f32); + assert_abs_diff_eq!(wf.next().unwrap(), 0.58778525f32); + assert_abs_diff_eq!(wf.next().unwrap(), 0.0f32); + assert_abs_diff_eq!(wf.next().unwrap(), -0.58778554f32); + } +} From 322bdd5f5f66d1930f09e193fc1f48314ec11626 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 30 Sep 2024 21:59:44 -0700 Subject: [PATCH 49/65] Rustfmt and a doc comment --- src/source/mod.rs | 4 ++-- src/source/sine.rs | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/source/mod.rs b/src/source/mod.rs index d91cabf2..932bec4c 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -27,6 +27,7 @@ pub use self::periodic::PeriodicAccess; pub use self::position::TrackPosition; pub use self::repeat::Repeat; pub use self::samples_converter::SamplesConverter; +pub use self::signal_generator::{Function, SignalGenerator}; pub use self::sine::SineWave; pub use self::skip::SkipDuration; pub use self::skippable::Skippable; @@ -34,7 +35,6 @@ pub use self::spatial::Spatial; pub use self::speed::Speed; pub use self::stoppable::Stoppable; pub use self::take::TakeDuration; -pub use self::signal_generator::{Function, SignalGenerator}; pub use self::uniform::UniformSourceIterator; pub use self::zero::Zero; @@ -59,6 +59,7 @@ mod periodic; mod position; mod repeat; mod samples_converter; +mod signal_generator; mod sine; mod skip; mod skippable; @@ -66,7 +67,6 @@ mod spatial; mod speed; mod stoppable; mod take; -mod signal_generator; mod uniform; mod zero; diff --git a/src/source/sine.rs b/src/source/sine.rs index b7591900..da4f8b2c 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -56,8 +56,11 @@ impl Source for SineWave { None } + /// `try_seek()` does nothing on the sine generator. If you need to + /// generate a sine tone with a precise phase or sample offset, consider + /// using `skip::skip_samples()`. #[inline] - fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - self.test_sine.try_seek(pos) + fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { + Ok(()) } } From 135024650ebeac215a36e0876a8d52dde4c597fa Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 30 Sep 2024 22:02:46 -0700 Subject: [PATCH 50/65] rustfmt --- examples/signal_generator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/signal_generator.rs b/examples/signal_generator.rs index 802fd582..08fd4769 100644 --- a/examples/signal_generator.rs +++ b/examples/signal_generator.rs @@ -1,7 +1,7 @@ //! Test signal generator example. fn main() { - use rodio::source::{chirp, Function, Source, SignalGenerator}; + use rodio::source::{chirp, Function, SignalGenerator, Source}; use std::thread; use std::time::Duration; From 3e4bf8b12b28893d2f1b4c99b53850071cf1604c Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Tue, 1 Oct 2024 20:29:22 +1300 Subject: [PATCH 51/65] Implement thread-safe parameter control for AGC using AtomicF32 - Replace static parameters with AtomicF32 for thread-safe access - Add methods to get Arc for release_coeff, attack_coeff, absolute_max_gain, and target_level - Enable real-time modification of AGC parameters during playback - Use Ordering::Relaxed for optimal low-latency performance - Remove set_* methods in favor of direct atomic access - Update internal methods to use atomic loads consistently This change allows for dynamic adjustment of AGC parameters without interrupting audio playback, improving real-time control and responsiveness of the Automatic Gain Control system. --- Cargo.toml | 1 + src/sink.rs | 4 +-- src/source/agc.rs | 65 ++++++++++++++++++++++++++--------------------- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1a0035bc..59811c2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ lewton = { version = "0.10", optional = true } minimp3_fixed = { version = "0.5.4", optional = true} symphonia = { version = "0.5.4", optional = true, default-features = false } crossbeam-channel = { version = "0.5.8", optional = true } +atomic_float = "1.1.0" thiserror = "1.0.49" tracing = { version = "0.1.40", optional = true } diff --git a/src/sink.rs b/src/sink.rs index 148a1588..c63ff72f 100644 --- a/src/sink.rs +++ b/src/sink.rs @@ -221,10 +221,10 @@ impl Sink { /// /// # Errors /// This function will return [`SeekError::NotSupported`] if one of the underlying - /// sources does not support seeking. + /// sources does not support seeking. /// /// It will return an error if an implementation ran - /// into one during the seek. + /// into one during the seek. /// /// When seeking beyond the end of a source this /// function might return an error if the duration of the source is not known. diff --git a/src/source/agc.rs b/src/source/agc.rs index 9d24e4ca..103ab299 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -15,6 +15,7 @@ use super::SeekError; use crate::{Sample, Source}; +use atomic_float::AtomicF32; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -33,11 +34,11 @@ const RMS_WINDOW_SIZE: usize = 8192; #[derive(Clone, Debug)] pub struct AutomaticGainControl { input: I, - target_level: f32, - absolute_max_gain: f32, + target_level: Arc, + absolute_max_gain: Arc, current_gain: f32, - attack_coeff: f32, - release_coeff: f32, + attack_coeff: Arc, + release_coeff: Arc, min_attack_coeff: f32, peak_level: f32, rms_window: CircularBuffer, @@ -99,7 +100,7 @@ impl CircularBuffer { /// * `release_time` - Time constant for gain decrease /// * `absolute_max_gain` - Maximum allowable gain #[inline] -pub fn automatic_gain_control( +pub(crate) fn automatic_gain_control( input: I, target_level: f32, attack_time: f32, @@ -111,14 +112,16 @@ where I::Item: Sample, { let sample_rate = input.sample_rate(); + let attack_coeff = (-1.0 / (attack_time * sample_rate as f32)).exp(); + let release_coeff = (-1.0 / (release_time * sample_rate as f32)).exp(); AutomaticGainControl { input, - target_level, - absolute_max_gain, + target_level: Arc::new(AtomicF32::new(target_level)), + absolute_max_gain: Arc::new(AtomicF32::new(absolute_max_gain)), current_gain: 1.0, - attack_coeff: (-1.0 / (attack_time * sample_rate as f32)).exp(), - release_coeff: (-1.0 / (release_time * sample_rate as f32)).exp(), + attack_coeff: Arc::new(AtomicF32::new(attack_coeff)), + release_coeff: Arc::new(AtomicF32::new(release_coeff)), min_attack_coeff: release_time, peak_level: 0.0, rms_window: CircularBuffer::new(), @@ -137,32 +140,30 @@ where /// for the Automatic Gain Control. The target level determines the /// desired amplitude of the processed audio signal. #[inline] - pub fn set_target_level(&mut self, level: f32) { - self.target_level = level; + pub fn get_target_level(&self) -> Arc { + Arc::clone(&self.target_level) } /// Sets a new absolute maximum gain limit. #[inline] - pub fn set_absolute_max_gain(&mut self, max_gain: f32) { - self.absolute_max_gain = max_gain; + pub fn get_absolute_max_gain(&self) -> Arc { + Arc::clone(&self.absolute_max_gain) } /// This method allows changing the attack coefficient dynamically. /// The attack coefficient determines how quickly the AGC responds to level increases. /// A smaller value results in faster response, while a larger value gives a slower response. #[inline] - pub fn set_attack_coeff(&mut self, attack_time: f32) { - let sample_rate = self.input.sample_rate(); - self.attack_coeff = (-1.0 / (attack_time * sample_rate as f32)).exp(); + pub fn get_attack_coeff(&self) -> Arc { + Arc::clone(&self.attack_coeff) } /// This method allows changing the release coefficient dynamically. /// The release coefficient determines how quickly the AGC responds to level decreases. /// A smaller value results in faster response, while a larger value gives a slower response. #[inline] - pub fn set_release_coeff(&mut self, release_time: f32) { - let sample_rate = self.input.sample_rate(); - self.release_coeff = (-1.0 / (release_time * sample_rate as f32)).exp(); + pub fn get_release_coeff(&self) -> Arc { + Arc::clone(&self.release_coeff) } /// Returns a handle to control AGC on/off state. @@ -183,9 +184,11 @@ where #[inline] fn update_peak_level(&mut self, sample_value: f32) { let attack_coeff = if sample_value > self.peak_level { - self.attack_coeff.min(self.min_attack_coeff) // User-defined attack time limited via release_time + self.attack_coeff + .load(Ordering::Relaxed) + .min(self.min_attack_coeff) // User-defined attack time limited via release_time } else { - self.release_coeff + self.release_coeff.load(Ordering::Relaxed) }; self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; } @@ -207,9 +210,10 @@ where #[inline] fn calculate_peak_gain(&self) -> f32 { if self.peak_level > 0.0 { - (self.target_level / self.peak_level).min(self.absolute_max_gain) + (self.target_level.load(Ordering::Relaxed) / self.peak_level) + .min(self.absolute_max_gain.load(Ordering::Relaxed)) } else { - self.absolute_max_gain + self.absolute_max_gain.load(Ordering::Relaxed) } } @@ -226,9 +230,9 @@ where // Compute the gain adjustment required to reach the target level based on RMS let rms_gain = if rms > 0.0 { - self.target_level / rms + self.target_level.load(Ordering::Relaxed) / rms } else { - self.absolute_max_gain // Default to max gain if RMS is zero + self.absolute_max_gain.load(Ordering::Relaxed) // Default to max gain if RMS is zero }; // Calculate the peak limiting gain @@ -262,16 +266,19 @@ where // By using a faster release time for decreasing gain, we can mitigate these issues and provide // more responsive control over sudden level increases while maintaining smooth gain increases. let attack_speed = if desired_gain > self.current_gain { - self.attack_coeff + &self.attack_coeff } else { - self.release_coeff + &self.release_coeff }; // Gradually adjust the current gain towards the desired gain for smooth transitions - self.current_gain = self.current_gain * attack_speed + desired_gain * (1.0 - attack_speed); + self.current_gain = self.current_gain * attack_speed.load(Ordering::Relaxed) + + desired_gain * (1.0 - attack_speed.load(Ordering::Relaxed)); // Ensure the calculated gain stays within the defined operational range - self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain); + self.current_gain = self + .current_gain + .clamp(0.1, self.absolute_max_gain.load(Ordering::Relaxed)); // Output current gain value for developers to fine tune their inputs to automatic_gain_control #[cfg(feature = "tracing")] From cb85bce7ae442984d2b4bdb34c12468026f5559a Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:20:10 +1300 Subject: [PATCH 52/65] Enforce RMS_WINDOW_SIZE is a power of two at compile time --- src/source/agc.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 103ab299..f0a2498e 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -23,9 +23,18 @@ use std::time::Duration; #[cfg(feature = "tracing")] use tracing; +// Ensures RMS_WINDOW_SIZE is a power of two +const fn power_of_two(n: usize) -> usize { + assert!( + n.is_power_of_two(), + "RMS_WINDOW_SIZE must be a power of two" + ); + n +} + /// Size of the circular buffer used for RMS calculation. /// A larger size provides more stable RMS values but increases latency. -const RMS_WINDOW_SIZE: usize = 8192; +const RMS_WINDOW_SIZE: usize = power_of_two(8192); /// Automatic Gain Control filter for maintaining consistent output levels. /// From db0bfb09d38559bc5272bd7d9a8ffdd5ae308302 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Wed, 2 Oct 2024 00:53:22 +1300 Subject: [PATCH 53/65] Add better documentation for AutomaticGainControl's Implementations --- src/source/agc.rs | 57 ++++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index f0a2498e..ef451084 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -23,7 +23,7 @@ use std::time::Duration; #[cfg(feature = "tracing")] use tracing; -// Ensures RMS_WINDOW_SIZE is a power of two +/// Ensures `RMS_WINDOW_SIZE` is a power of two const fn power_of_two(n: usize) -> usize { assert!( n.is_power_of_two(), @@ -39,7 +39,7 @@ const RMS_WINDOW_SIZE: usize = power_of_two(8192); /// Automatic Gain Control filter for maintaining consistent output levels. /// /// This struct implements an AGC algorithm that dynamically adjusts audio levels -/// based on both peak and RMS (Root Mean Square) measurements. +/// based on both **peak** and **RMS** (Root Mean Square) measurements. #[derive(Clone, Debug)] pub struct AutomaticGainControl { input: I, @@ -66,7 +66,7 @@ struct CircularBuffer { } impl CircularBuffer { - /// Creates a new CircularBuffer with a fixed size determined at compile time. + /// Creates a new `CircularBuffer` with a fixed size determined at compile time. #[inline] fn new() -> Self { CircularBuffer { @@ -92,7 +92,7 @@ impl CircularBuffer { /// Calculates the mean of all values in the buffer. /// - /// This operation is O(1) due to the maintained running sum. + /// This operation is `O(1)` due to the maintained running sum. #[inline] fn mean(&self) -> f32 { self.sum / RMS_WINDOW_SIZE as f32 @@ -143,41 +143,48 @@ where I: Source, I::Item: Sample, { - /// Sets a new target output level. + /// Access the target output level for real-time adjustment. /// - /// This method allows dynamic adjustment of the target output level - /// for the Automatic Gain Control. The target level determines the - /// desired amplitude of the processed audio signal. + /// Use this to dynamically modify the AGC's target level while audio is processing. + /// Adjust this value to control the overall output amplitude of the processed signal. #[inline] pub fn get_target_level(&self) -> Arc { Arc::clone(&self.target_level) } - /// Sets a new absolute maximum gain limit. + /// Access the maximum gain limit for real-time adjustment. + /// + /// Use this to dynamically modify the AGC's maximum allowable gain during runtime. + /// Adjusting this value helps prevent excessive amplification in low-level signals. #[inline] pub fn get_absolute_max_gain(&self) -> Arc { Arc::clone(&self.absolute_max_gain) } - /// This method allows changing the attack coefficient dynamically. - /// The attack coefficient determines how quickly the AGC responds to level increases. - /// A smaller value results in faster response, while a larger value gives a slower response. + /// Access the attack coefficient for real-time adjustment. + /// + /// Use this to dynamically modify how quickly the AGC responds to level increases. + /// Smaller values result in faster response, larger values in slower response. + /// Adjust during runtime to fine-tune AGC behavior for different audio content. #[inline] pub fn get_attack_coeff(&self) -> Arc { Arc::clone(&self.attack_coeff) } - /// This method allows changing the release coefficient dynamically. - /// The release coefficient determines how quickly the AGC responds to level decreases. - /// A smaller value results in faster response, while a larger value gives a slower response. + /// Access the release coefficient for real-time adjustment. + /// + /// Use this to dynamically modify how quickly the AGC responds to level decreases. + /// Smaller values result in faster response, larger values in slower response. + /// Adjust during runtime to optimize AGC behavior for varying audio dynamics. #[inline] pub fn get_release_coeff(&self) -> Arc { Arc::clone(&self.release_coeff) } - /// Returns a handle to control AGC on/off state. + /// Access the AGC on/off control for real-time adjustment. /// - /// This allows real-time toggling of the AGC processing. + /// Use this to dynamically enable or disable AGC processing during runtime. + /// Useful for comparing processed and unprocessed audio or for disabling/enabling AGC at runtime. #[inline] pub fn get_agc_control(&self) -> Arc { Arc::clone(&self.is_enabled) @@ -187,7 +194,7 @@ where /// /// This method adjusts the peak level using a variable attack coefficient. /// It responds faster to sudden increases in signal level by using a - /// minimum attack coefficient of min_attack_coeff when the sample value exceeds the + /// minimum attack coefficient of `min_attack_coeff` when the sample value exceeds the /// current peak level. This adaptive behavior helps capture transients /// more accurately while maintaining smoother behavior for gradual changes. #[inline] @@ -253,23 +260,23 @@ where // Adaptive attack/release speed for AGC (Automatic Gain Control) // // This mechanism implements an asymmetric approach to gain adjustment: - // 1. Slow increase: Prevents abrupt amplification of noise during quiet periods. - // 2. Fast decrease: Rapidly attenuates sudden loud signals to avoid distortion. + // 1. **Slow increase**: Prevents abrupt amplification of noise during quiet periods. + // 2. **Fast decrease**: Rapidly attenuates sudden loud signals to avoid distortion. // // The asymmetry is crucial because: // - Gradual gain increases sound more natural and less noticeable to listeners. // - Quick gain reductions are necessary to prevent clipping and maintain audio quality. // // This approach addresses several challenges associated with high attack times: - // 1. Slow response: With a high attack time, the AGC responds very slowly to changes in input level. + // 1. **Slow response**: With a high attack time, the AGC responds very slowly to changes in input level. // This means it takes longer for the gain to adjust to new signal levels. - // 2. Initial gain calculation: When the audio starts or after a period of silence, the initial gain + // 2. **Initial gain calculation**: When the audio starts or after a period of silence, the initial gain // calculation might result in a very high gain value, especially if the input signal starts quietly. - // 3. Overshooting: As the gain slowly increases (due to the high attack time), it might overshoot + // 3. **Overshooting**: As the gain slowly increases (due to the high attack time), it might overshoot // the desired level, causing the signal to become too loud. - // 4. Overcorrection: The AGC then tries to correct this by reducing the gain, but due to the slow response, + // 4. **Overcorrection**: The AGC then tries to correct this by reducing the gain, but due to the slow response, // it might reduce the gain too much, causing the sound to drop to near-zero levels. - // 5. Slow recovery: Again, due to the high attack time, it takes a while for the gain to increase + // 5. **Slow recovery**: Again, due to the high attack time, it takes a while for the gain to increase // back to the appropriate level. // // By using a faster release time for decreasing gain, we can mitigate these issues and provide From 3ce64ef53ffd634fa82a68b5d55bd8726f64bb80 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Wed, 2 Oct 2024 04:31:16 +1300 Subject: [PATCH 54/65] Add experimental flag to enabled dynamic controls --- Cargo.toml | 4 +- src/source/agc.rs | 133 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 114 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 59811c2d..374c614b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,14 +17,16 @@ lewton = { version = "0.10", optional = true } minimp3_fixed = { version = "0.5.4", optional = true} symphonia = { version = "0.5.4", optional = true, default-features = false } crossbeam-channel = { version = "0.5.8", optional = true } -atomic_float = "1.1.0" thiserror = "1.0.49" tracing = { version = "0.1.40", optional = true } +atomic_float = { version = "1.1.0", optional = true } + [features] default = ["flac", "vorbis", "wav", "mp3"] tracing = ["dep:tracing"] +experimental = ["dep:atomic_float"] flac = ["claxon"] vorbis = ["lewton"] diff --git a/src/source/agc.rs b/src/source/agc.rs index ef451084..3cadc826 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -15,7 +15,9 @@ use super::SeekError; use crate::{Sample, Source}; +#[cfg(feature = "experimental")] use atomic_float::AtomicF32; +#[cfg(feature = "experimental")] use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -36,6 +38,7 @@ const fn power_of_two(n: usize) -> usize { /// A larger size provides more stable RMS values but increases latency. const RMS_WINDOW_SIZE: usize = power_of_two(8192); +#[cfg(feature = "experimental")] /// Automatic Gain Control filter for maintaining consistent output levels. /// /// This struct implements an AGC algorithm that dynamically adjusts audio levels @@ -54,6 +57,20 @@ pub struct AutomaticGainControl { is_enabled: Arc, } +#[cfg(not(feature = "experimental"))] +#[derive(Clone, Debug)] +pub struct AutomaticGainControl { + input: I, + target_level: f32, + absolute_max_gain: f32, + current_gain: f32, + attack_coeff: f32, + release_coeff: f32, + min_attack_coeff: f32, + peak_level: f32, + rms_window: CircularBuffer, +} + /// A circular buffer for efficient RMS calculation over a sliding window. /// /// This structure allows for constant-time updates and mean calculations, @@ -124,17 +141,35 @@ where let attack_coeff = (-1.0 / (attack_time * sample_rate as f32)).exp(); let release_coeff = (-1.0 / (release_time * sample_rate as f32)).exp(); - AutomaticGainControl { - input, - target_level: Arc::new(AtomicF32::new(target_level)), - absolute_max_gain: Arc::new(AtomicF32::new(absolute_max_gain)), - current_gain: 1.0, - attack_coeff: Arc::new(AtomicF32::new(attack_coeff)), - release_coeff: Arc::new(AtomicF32::new(release_coeff)), - min_attack_coeff: release_time, - peak_level: 0.0, - rms_window: CircularBuffer::new(), - is_enabled: Arc::new(AtomicBool::new(true)), + #[cfg(feature = "experimental")] + { + AutomaticGainControl { + input, + target_level: Arc::new(AtomicF32::new(target_level)), + absolute_max_gain: Arc::new(AtomicF32::new(absolute_max_gain)), + current_gain: 1.0, + attack_coeff: Arc::new(AtomicF32::new(attack_coeff)), + release_coeff: Arc::new(AtomicF32::new(release_coeff)), + min_attack_coeff: release_time, + peak_level: 0.0, + rms_window: CircularBuffer::new(), + is_enabled: Arc::new(AtomicBool::new(true)), + } + } + + #[cfg(not(feature = "experimental"))] + { + AutomaticGainControl { + input, + target_level, + absolute_max_gain, + current_gain: 1.0, + attack_coeff, + release_coeff, + min_attack_coeff: release_time, + peak_level: 0.0, + rms_window: CircularBuffer::new(), + } } } @@ -143,6 +178,7 @@ where I: Source, I::Item: Sample, { + #[cfg(feature = "experimental")] /// Access the target output level for real-time adjustment. /// /// Use this to dynamically modify the AGC's target level while audio is processing. @@ -152,6 +188,7 @@ where Arc::clone(&self.target_level) } + #[cfg(feature = "experimental")] /// Access the maximum gain limit for real-time adjustment. /// /// Use this to dynamically modify the AGC's maximum allowable gain during runtime. @@ -161,6 +198,7 @@ where Arc::clone(&self.absolute_max_gain) } + #[cfg(feature = "experimental")] /// Access the attack coefficient for real-time adjustment. /// /// Use this to dynamically modify how quickly the AGC responds to level increases. @@ -171,6 +209,7 @@ where Arc::clone(&self.attack_coeff) } + #[cfg(feature = "experimental")] /// Access the release coefficient for real-time adjustment. /// /// Use this to dynamically modify how quickly the AGC responds to level decreases. @@ -181,6 +220,7 @@ where Arc::clone(&self.release_coeff) } + #[cfg(feature = "experimental")] /// Access the AGC on/off control for real-time adjustment. /// /// Use this to dynamically enable or disable AGC processing during runtime. @@ -199,6 +239,7 @@ where /// more accurately while maintaining smoother behavior for gradual changes. #[inline] fn update_peak_level(&mut self, sample_value: f32) { + #[cfg(feature = "experimental")] let attack_coeff = if sample_value > self.peak_level { self.attack_coeff .load(Ordering::Relaxed) @@ -206,6 +247,14 @@ where } else { self.release_coeff.load(Ordering::Relaxed) }; + + #[cfg(not(feature = "experimental"))] + let attack_coeff = if sample_value > self.peak_level { + self.attack_coeff.min(self.min_attack_coeff) // User-defined attack time limited via release_time + } else { + self.release_coeff + }; + self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; } @@ -225,11 +274,23 @@ where /// The peak level helps prevent sudden spikes in the output signal. #[inline] fn calculate_peak_gain(&self) -> f32 { - if self.peak_level > 0.0 { - (self.target_level.load(Ordering::Relaxed) / self.peak_level) - .min(self.absolute_max_gain.load(Ordering::Relaxed)) - } else { - self.absolute_max_gain.load(Ordering::Relaxed) + #[cfg(feature = "experimental")] + { + if self.peak_level > 0.0 { + (self.target_level.load(Ordering::Relaxed) / self.peak_level) + .min(self.absolute_max_gain.load(Ordering::Relaxed)) + } else { + self.absolute_max_gain.load(Ordering::Relaxed) + } + } + + #[cfg(not(feature = "experimental"))] + { + if self.peak_level > 0.0 { + (self.target_level / self.peak_level).min(self.absolute_max_gain) + } else { + self.absolute_max_gain + } } } @@ -245,12 +306,21 @@ where let rms = self.update_rms(sample_value); // Compute the gain adjustment required to reach the target level based on RMS + #[cfg(feature = "experimental")] let rms_gain = if rms > 0.0 { self.target_level.load(Ordering::Relaxed) / rms } else { self.absolute_max_gain.load(Ordering::Relaxed) // Default to max gain if RMS is zero }; + // Compute the gain adjustment required to reach the target level based on RMS + #[cfg(not(feature = "experimental"))] + let rms_gain = if rms > 0.0 { + self.target_level / rms + } else { + self.absolute_max_gain // Default to max gain if RMS is zero + }; + // Calculate the peak limiting gain let peak_gain = self.calculate_peak_gain(); @@ -288,13 +358,25 @@ where }; // Gradually adjust the current gain towards the desired gain for smooth transitions - self.current_gain = self.current_gain * attack_speed.load(Ordering::Relaxed) - + desired_gain * (1.0 - attack_speed.load(Ordering::Relaxed)); + #[cfg(feature = "experimental")] + { + self.current_gain = self.current_gain * attack_speed.load(Ordering::Relaxed) + + desired_gain * (1.0 - attack_speed.load(Ordering::Relaxed)); + + // Ensure the calculated gain stays within the defined operational range + self.current_gain = self + .current_gain + .clamp(0.1, self.absolute_max_gain.load(Ordering::Relaxed)); + } - // Ensure the calculated gain stays within the defined operational range - self.current_gain = self - .current_gain - .clamp(0.1, self.absolute_max_gain.load(Ordering::Relaxed)); + #[cfg(not(feature = "experimental"))] + { + self.current_gain = + self.current_gain * attack_speed + desired_gain * (1.0 - attack_speed); + + // Ensure the calculated gain stays within the defined operational range + self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain); + } // Output current gain value for developers to fine tune their inputs to automatic_gain_control #[cfg(feature = "tracing")] @@ -313,6 +395,7 @@ where type Item = I::Item; #[inline] + #[cfg(feature = "experimental")] fn next(&mut self) -> Option { self.input.next().map(|sample| { if self.is_enabled.load(Ordering::Relaxed) { @@ -323,6 +406,12 @@ where }) } + #[inline] + #[cfg(not(feature = "experimental"))] + fn next(&mut self) -> Option { + self.input.next().map(|sample| self.process_sample(sample)) + } + #[inline] fn size_hint(&self) -> (usize, Option) { self.input.size_hint() From e2ee86e4b9a0e0730ba1c515ca07576ce7a05cf3 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Wed, 2 Oct 2024 04:46:21 +1300 Subject: [PATCH 55/65] Fix unused arc import --- src/source/agc.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/source/agc.rs b/src/source/agc.rs index 3cadc826..7afd414b 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -19,6 +19,7 @@ use crate::{Sample, Source}; use atomic_float::AtomicF32; #[cfg(feature = "experimental")] use std::sync::atomic::{AtomicBool, Ordering}; +#[cfg(feature = "experimental")] use std::sync::Arc; use std::time::Duration; From ef602860baf38296f54863ee3f03ec2bea09a2b8 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Wed, 2 Oct 2024 04:49:44 +1300 Subject: [PATCH 56/65] Trigger CI checks From af210a67ccc1e98508eecb25e722aaa6496673a9 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Wed, 2 Oct 2024 04:57:05 +1300 Subject: [PATCH 57/65] Fix agc_disable benchmark --- benches/effects.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/benches/effects.rs b/benches/effects.rs index 1101bd3a..871855c6 100644 --- a/benches/effects.rs +++ b/benches/effects.rs @@ -63,6 +63,8 @@ fn agc_enabled(bencher: Bencher) { }) } +#[cfg(feature = "experimental")] +// Do note you will need to pass --features experimental to run this benchmark #[divan::bench] fn agc_disabled(bencher: Bencher) { bencher From 2610a27532bb6bf7530d2a9c22b23c9d450c8227 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Wed, 2 Oct 2024 04:59:57 +1300 Subject: [PATCH 58/65] Add documentation to non experimental AutomaticGainControl --- src/source/agc.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/source/agc.rs b/src/source/agc.rs index 7afd414b..a263b4e3 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -59,6 +59,10 @@ pub struct AutomaticGainControl { } #[cfg(not(feature = "experimental"))] +/// Automatic Gain Control filter for maintaining consistent output levels. +/// +/// This struct implements an AGC algorithm that dynamically adjusts audio levels +/// based on both **peak** and **RMS** (Root Mean Square) measurements. #[derive(Clone, Debug)] pub struct AutomaticGainControl { input: I, From b7e67c10d3c2446e5698e16b0bf165595c096b61 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Tue, 1 Oct 2024 09:40:30 -0700 Subject: [PATCH 59/65] Update CHANGELOG.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forgot to change the name of the SignalGenerator struct here 😣 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1567d48..c8b4a720 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for *ALAC/AIFF* - New test signal generator sources: - - `TestSignal` source generates a sine, triangle, square wave or sawtooth + - `SignalGenerator` source generates a sine, triangle, square wave or sawtooth of a given frequency and sample rate. - `Chirp` source generates a sine wave with a linearly-increasing frequency over a given frequency range and duration. From f8cf3c555c2c270392a917c0f08dae0324fa92d3 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Thu, 3 Oct 2024 01:06:37 +1300 Subject: [PATCH 60/65] Added getters --- src/source/agc.rs | 142 ++++++++++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 68 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index a263b4e3..66fd80fb 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -183,6 +183,66 @@ where I: Source, I::Item: Sample, { + #[inline] + fn target_level(&self) -> f32 { + #[cfg(feature = "experimental")] + { + self.target_level.load(Ordering::Relaxed) + } + #[cfg(not(feature = "experimental"))] + { + self.target_level + } + } + + #[inline] + fn absolute_max_gain(&self) -> f32 { + #[cfg(feature = "experimental")] + { + self.absolute_max_gain.load(Ordering::Relaxed) + } + #[cfg(not(feature = "experimental"))] + { + self.absolute_max_gain + } + } + + #[inline] + fn attack_coeff(&self) -> f32 { + #[cfg(feature = "experimental")] + { + self.attack_coeff.load(Ordering::Relaxed) + } + #[cfg(not(feature = "experimental"))] + { + self.attack_coeff + } + } + + #[inline] + fn release_coeff(&self) -> f32 { + #[cfg(feature = "experimental")] + { + self.release_coeff.load(Ordering::Relaxed) + } + #[cfg(not(feature = "experimental"))] + { + self.release_coeff + } + } + + #[inline] + fn is_enabled(&self) -> bool { + #[cfg(feature = "experimental")] + { + self.is_enabled.load(Ordering::Relaxed) + } + #[cfg(not(feature = "experimental"))] + { + true + } + } + #[cfg(feature = "experimental")] /// Access the target output level for real-time adjustment. /// @@ -244,20 +304,10 @@ where /// more accurately while maintaining smoother behavior for gradual changes. #[inline] fn update_peak_level(&mut self, sample_value: f32) { - #[cfg(feature = "experimental")] - let attack_coeff = if sample_value > self.peak_level { - self.attack_coeff - .load(Ordering::Relaxed) - .min(self.min_attack_coeff) // User-defined attack time limited via release_time - } else { - self.release_coeff.load(Ordering::Relaxed) - }; - - #[cfg(not(feature = "experimental"))] let attack_coeff = if sample_value > self.peak_level { - self.attack_coeff.min(self.min_attack_coeff) // User-defined attack time limited via release_time + self.attack_coeff().min(self.min_attack_coeff) // User-defined attack time limited via release_time } else { - self.release_coeff + self.release_coeff() }; self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; @@ -279,23 +329,10 @@ where /// The peak level helps prevent sudden spikes in the output signal. #[inline] fn calculate_peak_gain(&self) -> f32 { - #[cfg(feature = "experimental")] - { - if self.peak_level > 0.0 { - (self.target_level.load(Ordering::Relaxed) / self.peak_level) - .min(self.absolute_max_gain.load(Ordering::Relaxed)) - } else { - self.absolute_max_gain.load(Ordering::Relaxed) - } - } - - #[cfg(not(feature = "experimental"))] - { - if self.peak_level > 0.0 { - (self.target_level / self.peak_level).min(self.absolute_max_gain) - } else { - self.absolute_max_gain - } + if self.peak_level > 0.0 { + (self.target_level() / self.peak_level).min(self.absolute_max_gain()) + } else { + self.absolute_max_gain() } } @@ -311,19 +348,10 @@ where let rms = self.update_rms(sample_value); // Compute the gain adjustment required to reach the target level based on RMS - #[cfg(feature = "experimental")] let rms_gain = if rms > 0.0 { - self.target_level.load(Ordering::Relaxed) / rms + self.target_level() / rms } else { - self.absolute_max_gain.load(Ordering::Relaxed) // Default to max gain if RMS is zero - }; - - // Compute the gain adjustment required to reach the target level based on RMS - #[cfg(not(feature = "experimental"))] - let rms_gain = if rms > 0.0 { - self.target_level / rms - } else { - self.absolute_max_gain // Default to max gain if RMS is zero + self.absolute_max_gain() // Default to max gain if RMS is zero }; // Calculate the peak limiting gain @@ -357,31 +385,16 @@ where // By using a faster release time for decreasing gain, we can mitigate these issues and provide // more responsive control over sudden level increases while maintaining smooth gain increases. let attack_speed = if desired_gain > self.current_gain { - &self.attack_coeff + self.attack_coeff() } else { - &self.release_coeff + self.release_coeff() }; // Gradually adjust the current gain towards the desired gain for smooth transitions - #[cfg(feature = "experimental")] - { - self.current_gain = self.current_gain * attack_speed.load(Ordering::Relaxed) - + desired_gain * (1.0 - attack_speed.load(Ordering::Relaxed)); - - // Ensure the calculated gain stays within the defined operational range - self.current_gain = self - .current_gain - .clamp(0.1, self.absolute_max_gain.load(Ordering::Relaxed)); - } - - #[cfg(not(feature = "experimental"))] - { - self.current_gain = - self.current_gain * attack_speed + desired_gain * (1.0 - attack_speed); + self.current_gain = self.current_gain * attack_speed + desired_gain * (1.0 - attack_speed); - // Ensure the calculated gain stays within the defined operational range - self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain); - } + // Ensure the calculated gain stays within the defined operational range + self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain()); // Output current gain value for developers to fine tune their inputs to automatic_gain_control #[cfg(feature = "tracing")] @@ -400,10 +413,9 @@ where type Item = I::Item; #[inline] - #[cfg(feature = "experimental")] fn next(&mut self) -> Option { self.input.next().map(|sample| { - if self.is_enabled.load(Ordering::Relaxed) { + if self.is_enabled() { self.process_sample(sample) } else { sample @@ -411,12 +423,6 @@ where }) } - #[inline] - #[cfg(not(feature = "experimental"))] - fn next(&mut self) -> Option { - self.input.next().map(|sample| self.process_sample(sample)) - } - #[inline] fn size_hint(&self) -> (usize, Option) { self.input.size_hint() From 5ce1fff77987f18f3ae5381329b7d572ab8d7c27 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Thu, 3 Oct 2024 01:13:20 +1300 Subject: [PATCH 61/65] Added non-atomic is_enabled() --- src/source/agc.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 66fd80fb..6dfbaf11 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -74,6 +74,7 @@ pub struct AutomaticGainControl { min_attack_coeff: f32, peak_level: f32, rms_window: CircularBuffer, + is_enabled: bool, } /// A circular buffer for efficient RMS calculation over a sliding window. @@ -174,6 +175,7 @@ where min_attack_coeff: release_time, peak_level: 0.0, rms_window: CircularBuffer::new(), + is_enabled: true, } } } @@ -239,7 +241,7 @@ where } #[cfg(not(feature = "experimental"))] { - true + self.is_enabled } } @@ -295,6 +297,16 @@ where Arc::clone(&self.is_enabled) } + #[cfg(not(feature = "experimental"))] + /// Enable or disable AGC processing. + /// + /// Use this to enable or disable AGC processing. + /// Useful for comparing processed and unprocessed audio or for disabling/enabling AGC. + #[inline] + pub fn set_enabled(&mut self, enabled: bool) { + self.is_enabled = enabled; + } + /// Updates the peak level with an adaptive attack coefficient /// /// This method adjusts the peak level using a variable attack coefficient. From bdbc159eab7e312f17f993d904317719f9771917 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Thu, 3 Oct 2024 01:19:46 +1300 Subject: [PATCH 62/65] Remove experimental bench comment --- benches/effects.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/benches/effects.rs b/benches/effects.rs index 871855c6..2849fc82 100644 --- a/benches/effects.rs +++ b/benches/effects.rs @@ -64,7 +64,6 @@ fn agc_enabled(bencher: Bencher) { } #[cfg(feature = "experimental")] -// Do note you will need to pass --features experimental to run this benchmark #[divan::bench] fn agc_disabled(bencher: Bencher) { bencher From 6f2518eb1f4ad4e542294f37c074e112f2df1502 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 02:19:44 +0200 Subject: [PATCH 63/65] adds draft for new release announcement --- ...0_announcement_and_call_for_userstories.md | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 outreach/v0.20_announcement_and_call_for_userstories.md diff --git a/outreach/v0.20_announcement_and_call_for_userstories.md b/outreach/v0.20_announcement_and_call_for_userstories.md new file mode 100644 index 00000000..d59a08d8 --- /dev/null +++ b/outreach/v0.20_announcement_and_call_for_userstories.md @@ -0,0 +1,48 @@ + + +# Announcing rodio 0.20 and call for help + + +Rodio is an audio playback library. It can decode audio files, synthesize new +sounds, apply effects to sounds & mix them. Rodio has been part of the Rust +ecosystem for 9 years now! 🎉. + +## New release +The rodio contributors have made many improvements in the last 5 months. Rodio can now: + +- Seek back and forth through sound efficiently +- Track the playback position at sample accuracy! +- Generate more signals such as chirps, white & pink noise and different + wavesforms +- Automatically adjust the gain to limit the peak volume and change in loudness + +This is ignoring the many fixes and smaller additions made by the many +contributors who helped out expand rodio. + +## Call for help + +In its 9 years of existence Rust has changed a lot. Further more Rodio is being +used for applications beyond its original scope. To improve rodio we believe its +time for larger (breaking) changes. + +### User feedback +To ensure we make the right changes we want +to know what rodio is being used for and what you all would like to use it for. + +We can use any input you have but are especially looking for users who are: +- using rodio and feel some part of the API is hard to use. +- have experienced footguns/pain point +- wanted to use rodio but could not make it fit their use-case (excluding complex + game audio (best served by [kira](https://crates.io/crates/kira)) and advanced + dsp). If you disagree and think rodio can server those excluded use-case too + let us know! + +The best way to leave your feedback is a short user story on our issue +[tracker](https://github.com/RustAudio/rodio/issues). If that is not your thing +any other form posted there works too! + +### Architecture & API +We can use input on our planned [changes](https://github.com/RustAudio/rodio/issues/614) and how to best implement them. From e33bd63bed75fc064cfd1a7aaeccba2f418e01df Mon Sep 17 00:00:00 2001 From: ugochukwu-850 Date: Fri, 4 Oct 2024 21:09:04 +0100 Subject: [PATCH 64/65] Fix for total duration for speed mutation issue Previously the total_duration function calculated the total duration by multiplying instead of deviding by the factor Causing the total duration to increase as speed factors increased instead of the other way round . I have fixed this --- src/source/speed.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/source/speed.rs b/src/source/speed.rs index d24cb8ac..722ca3d7 100644 --- a/src/source/speed.rs +++ b/src/source/speed.rs @@ -93,7 +93,7 @@ where #[inline] fn total_duration(&self) -> Option { - self.input.total_duration().map(|d| d.mul_f32(self.factor)) + self.input.total_duration().map(|d| d.div_f32(self.factor)) } #[inline] From 4f751ac80984051805a4f473fc66617afedc2e14 Mon Sep 17 00:00:00 2001 From: ugochukwu-850 Date: Sat, 5 Oct 2024 10:53:44 +0100 Subject: [PATCH 65/65] Removed redundant comment --- src/source/speed.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/source/speed.rs b/src/source/speed.rs index 722ca3d7..bde54916 100644 --- a/src/source/speed.rs +++ b/src/source/speed.rs @@ -98,11 +98,6 @@ where #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - /* TODO: This might be wrong, I do not know how speed achieves its speedup - * so I can not reason about the correctness. - * */ - - // even after 24 hours of playback f32 has enough precision let pos_accounting_for_speedup = pos.mul_f32(self.factor); self.input.try_seek(pos_accounting_for_speedup) }