diff --git a/CHANGELOG.md b/CHANGELOG.md index ee9544e1..107ba47e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Output audio stream buffer size can now be adjusted. +- Sources for directly generating square waves, traingle waves, square waves and + sawtooths have been added. +- An interface for defining `SignalGenerator` patterns with an `fn`, see + `GeneratorFunction`. ### Changed - Breaking: `OutputStreamBuilder` should now be used to initialize audio output stream. @@ -20,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Symphonia decoder `total_duration` incorrect value caused by conversion from `Time` to `Duration`. +- An issue with `SignalGenerator` that caused it to create increasingly distorted waveforms + over long run times has been corrected. # Version 0.20.1 (2024-11-08) diff --git a/src/source/mod.rs b/src/source/mod.rs index ceac73bd..3c58c509 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -29,14 +29,17 @@ pub use self::periodic::PeriodicAccess; pub use self::position::TrackPosition; pub use self::repeat::Repeat; pub use self::samples_converter::SamplesConverter; +pub use self::sawtooth::SawtoothWave; pub use self::signal_generator::{Function, SignalGenerator}; pub use self::sine::SineWave; pub use self::skip::SkipDuration; pub use self::skippable::Skippable; pub use self::spatial::Spatial; pub use self::speed::Speed; +pub use self::square::SquareWave; pub use self::stoppable::Stoppable; pub use self::take::TakeDuration; +pub use self::triangle::TriangleWave; pub use self::uniform::UniformSourceIterator; pub use self::zero::Zero; @@ -62,14 +65,17 @@ mod periodic; mod position; mod repeat; mod samples_converter; +mod sawtooth; mod signal_generator; mod sine; mod skip; mod skippable; mod spatial; mod speed; +mod square; mod stoppable; mod take; +mod triangle; mod uniform; mod zero; diff --git a/src/source/sawtooth.rs b/src/source/sawtooth.rs new file mode 100644 index 00000000..d34970af --- /dev/null +++ b/src/source/sawtooth.rs @@ -0,0 +1,66 @@ +use std::time::Duration; + +use crate::source::{Function, SignalGenerator}; +use crate::Source; + +use super::SeekError; + +/// An infinite source that produces a sawtooth wave. +/// +/// Always has a sample rate of 48kHz and one channel. +/// +/// This source is a thin interface on top of `SignalGenerator` provided for +/// your convenience. +#[derive(Clone, Debug)] +pub struct SawtoothWave { + test_saw: SignalGenerator, +} + +impl SawtoothWave { + const SAMPLE_RATE: u32 = 48000; + + /// The frequency of the sine. + #[inline] + pub fn new(freq: f32) -> SawtoothWave { + let sr = cpal::SampleRate(Self::SAMPLE_RATE); + SawtoothWave { + test_saw: SignalGenerator::new(sr, freq, Function::Sawtooth), + } + } +} + +impl Iterator for SawtoothWave { + type Item = f32; + + #[inline] + fn next(&mut self) -> Option { + self.test_saw.next() + } +} + +impl Source for SawtoothWave { + #[inline] + fn current_frame_len(&self) -> Option { + None + } + + #[inline] + fn channels(&self) -> u16 { + 1 + } + + #[inline] + fn sample_rate(&self) -> u32 { + Self::SAMPLE_RATE + } + + #[inline] + fn total_duration(&self) -> Option { + None + } + + #[inline] + fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { + self.test_saw.try_seek(duration) + } +} diff --git a/src/source/signal_generator.rs b/src/source/signal_generator.rs index 54969ed5..3997d167 100644 --- a/src/source/signal_generator.rs +++ b/src/source/signal_generator.rs @@ -17,6 +17,24 @@ use std::time::Duration; use super::SeekError; use crate::Source; +/// Generator function. +/// +/// A generator function is the core of a signal generator, the `SignalGenerator` type uses these +/// function to create periodic waveforms. +/// +/// # Arguments +/// * An `f32` representing a time in the signal to generate. The scale of this variable is +/// normalized to the period of the signal, such that "0.0" is time zero, "1.0" is one period of +/// the signal, "2.0" is two periods and so on. This function should be written to accept any +/// float in the range (`f32::MIN`, `f32::MAX`) but `SignalGenerator` will only pass values in +/// (0.0, 1.0) to mitigate floating point error. +/// +/// # Returns +/// +/// An `f32` representing the signal level at the passed time. This value should be normalized +/// in the range [-1.0,1.0]. +pub type GeneratorFunction = fn(f32) -> f32; + /// Waveform functions. #[derive(Clone, Debug)] pub enum Function { @@ -30,52 +48,77 @@ pub enum Function { 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()), - } +fn sine_signal(phase: f32) -> f32 { + (TAU * phase).sin() +} + +fn triangle_signal(phase: f32) -> f32 { + 4.0f32 * (phase - (phase + 0.5f32).floor()).abs() - 1f32 +} + +fn square_signal(phase: f32) -> f32 { + if phase % 1.0f32 < 0.5f32 { + 1.0f32 + } else { + -1.0f32 } } +fn sawtooth_signal(phase: f32) -> f32 { + 2.0f32 * (phase - (phase + 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, + function: GeneratorFunction, + phase_step: f32, + phase: f32, period: f32, - function: Function, - i: u64, } impl SignalGenerator { - /// Create a new `TestWaveform` object that generates an endless waveform + /// Create a new `SignalGenerator` 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 { + pub fn new(sample_rate: cpal::SampleRate, frequency: f32, f: Function) -> Self { + let function: GeneratorFunction = match f { + Function::Sine => sine_signal, + Function::Triangle => triangle_signal, + Function::Square => square_signal, + Function::Sawtooth => sawtooth_signal, + }; + + Self::with_function(sample_rate, frequency, function) + } + + /// Create a new `SignalGenerator` object that generates an endless waveform + /// from the [generator function](GeneratorFunction) `generator_function`. + /// + /// # Panics + /// + /// Will panic if `frequency` is equal to zero. + #[inline] + pub fn with_function( + sample_rate: cpal::SampleRate, + frequency: f32, + generator_function: GeneratorFunction, + ) -> Self { assert!(frequency != 0.0, "frequency must be greater than zero"); let period = sample_rate.0 as f32 / frequency; + let phase_step = 1.0f32 / period; + SignalGenerator { sample_rate, + function: generator_function, + phase_step, + phase: 0.0f32, period, - function: f, - i: 0, } } } @@ -85,8 +128,9 @@ impl Iterator for SignalGenerator { #[inline] fn next(&mut self) -> Option { - let val = Some(self.function.render(self.i, self.period)); - self.i += 1; + let f = self.function; + let val = Some(f(self.phase)); + self.phase = (self.phase + self.phase_step).rem_euclid(1.0f32); val } } @@ -114,7 +158,8 @@ impl Source for SignalGenerator { #[inline] fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { - self.i = (self.sample_rate.0 as f32 * duration.as_secs_f32()) as u64; + let seek = duration.as_secs_f32() * (self.sample_rate.0 as f32) / self.period; + self.phase = seek.rem_euclid(1.0f32); Ok(()) } } diff --git a/src/source/sine.rs b/src/source/sine.rs index da4f8b2c..f8b054c7 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -7,7 +7,10 @@ use super::SeekError; /// An infinite source that produces a sine. /// -/// Always has a rate of 48kHz and one channel. +/// Always has a sample rate of 48kHz and one channel. +/// +/// This source is a thin interface on top of `SignalGenerator` provided for +/// your convenience. #[derive(Clone, Debug)] pub struct SineWave { test_sine: SignalGenerator, @@ -56,11 +59,8 @@ 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, _: Duration) -> Result<(), SeekError> { - Ok(()) + fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { + self.test_sine.try_seek(duration) } } diff --git a/src/source/square.rs b/src/source/square.rs new file mode 100644 index 00000000..ea529653 --- /dev/null +++ b/src/source/square.rs @@ -0,0 +1,66 @@ +use std::time::Duration; + +use crate::source::{Function, SignalGenerator}; +use crate::Source; + +use super::SeekError; + +/// An infinite source that produces a square wave. +/// +/// Always has a sample rate of 48kHz and one channel. +/// +/// This source is a thin interface on top of `SignalGenerator` provided for +/// your convenience. +#[derive(Clone, Debug)] +pub struct SquareWave { + test_square: SignalGenerator, +} + +impl SquareWave { + const SAMPLE_RATE: u32 = 48000; + + /// The frequency of the sine. + #[inline] + pub fn new(freq: f32) -> SquareWave { + let sr = cpal::SampleRate(Self::SAMPLE_RATE); + SquareWave { + test_square: SignalGenerator::new(sr, freq, Function::Square), + } + } +} + +impl Iterator for SquareWave { + type Item = f32; + + #[inline] + fn next(&mut self) -> Option { + self.test_square.next() + } +} + +impl Source for SquareWave { + #[inline] + fn current_frame_len(&self) -> Option { + None + } + + #[inline] + fn channels(&self) -> u16 { + 1 + } + + #[inline] + fn sample_rate(&self) -> u32 { + Self::SAMPLE_RATE + } + + #[inline] + fn total_duration(&self) -> Option { + None + } + + #[inline] + fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { + self.test_square.try_seek(duration) + } +} diff --git a/src/source/triangle.rs b/src/source/triangle.rs new file mode 100644 index 00000000..d77a8ccd --- /dev/null +++ b/src/source/triangle.rs @@ -0,0 +1,66 @@ +use std::time::Duration; + +use crate::source::{Function, SignalGenerator}; +use crate::Source; + +use super::SeekError; + +/// An infinite source that produces a triangle wave. +/// +/// Always has a sample rate of 48kHz and one channel. +/// +/// This source is a thin interface on top of `SignalGenerator` provided for +/// your convenience. +#[derive(Clone, Debug)] +pub struct TriangleWave { + test_tri: SignalGenerator, +} + +impl TriangleWave { + const SAMPLE_RATE: u32 = 48000; + + /// The frequency of the sine. + #[inline] + pub fn new(freq: f32) -> TriangleWave { + let sr = cpal::SampleRate(Self::SAMPLE_RATE); + TriangleWave { + test_tri: SignalGenerator::new(sr, freq, Function::Triangle), + } + } +} + +impl Iterator for TriangleWave { + type Item = f32; + + #[inline] + fn next(&mut self) -> Option { + self.test_tri.next() + } +} + +impl Source for TriangleWave { + #[inline] + fn current_frame_len(&self) -> Option { + None + } + + #[inline] + fn channels(&self) -> u16 { + 1 + } + + #[inline] + fn sample_rate(&self) -> u32 { + Self::SAMPLE_RATE + } + + #[inline] + fn total_duration(&self) -> Option { + None + } + + #[inline] + fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { + self.test_tri.try_seek(duration) + } +}