Skip to content
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)

Expand Down
6 changes: 6 additions & 0 deletions src/source/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

Expand Down
66 changes: 66 additions & 0 deletions src/source/sawtooth.rs
Original file line number Diff line number Diff line change
@@ -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<f32> {
self.test_saw.next()
}
}

impl Source for SawtoothWave {
#[inline]
fn current_frame_len(&self) -> Option<usize> {
None
}

#[inline]
fn channels(&self) -> u16 {
1
}

#[inline]
fn sample_rate(&self) -> u32 {
Self::SAMPLE_RATE
}

#[inline]
fn total_duration(&self) -> Option<Duration> {
None
}

#[inline]
fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> {
self.test_saw.try_seek(duration)
}
}
99 changes: 72 additions & 27 deletions src/source/signal_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
}
}
}
Expand All @@ -85,8 +128,9 @@ impl Iterator for SignalGenerator {

#[inline]
fn next(&mut self) -> Option<f32> {
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
}
}
Expand Down Expand Up @@ -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(())
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/source/sine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
}
66 changes: 66 additions & 0 deletions src/source/square.rs
Original file line number Diff line number Diff line change
@@ -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<f32> {
self.test_square.next()
}
}

impl Source for SquareWave {
#[inline]
fn current_frame_len(&self) -> Option<usize> {
None
}

#[inline]
fn channels(&self) -> u16 {
1
}

#[inline]
fn sample_rate(&self) -> u32 {
Self::SAMPLE_RATE
}

#[inline]
fn total_duration(&self) -> Option<Duration> {
None
}

#[inline]
fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> {
self.test_square.try_seek(duration)
}
}
Loading
Loading