Merge pull request #602 from iluvcapra/synth-waveforms

Synthesizer Waveforms
This commit is contained in:
David Kleingeld 2024-10-01 10:37:10 +02:00 committed by GitHub
commit 1d95a5ca45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 586 additions and 17 deletions

View file

@ -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 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"
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

View file

@ -19,6 +19,7 @@ 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"], optional = true }
tracing = { version = "0.1.40", optional = true }
[features]
@ -30,6 +31,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"]
@ -61,3 +63,7 @@ harness = false
[[example]]
name = "music_m4a"
required-features = ["symphonia-isomp4", "symphonia-aac"]
[[example]]
name = "noise_generator"
required-features = ["noise"]

View file

@ -0,0 +1,41 @@
//! 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);
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);
}
#[cfg(not(feature = "noise"))]
fn main() {
println!("rodio has not been compiled with noise sources, use `--features noise` to enable this feature.");
println!("Exiting...");
}

View file

@ -0,0 +1,83 @@
//! Test signal generator example.
fn main() {
use rodio::source::{chirp, Function, SignalGenerator, Source};
use std::thread;
use std::time::Duration;
let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap();
let test_signal_duration = Duration::from_millis(1000);
let interval_duration = Duration::from_millis(1500);
println!("Playing 1000 Hz tone");
stream_handle
.play_raw(
SignalGenerator::new(cpal::SampleRate(48000), 1000.0, Function::Sine)
.amplify(0.1)
.take_duration(test_signal_duration),
)
.unwrap();
thread::sleep(interval_duration);
println!("Playing 10,000 Hz tone");
stream_handle
.play_raw(
SignalGenerator::new(cpal::SampleRate(48000), 10000.0, Function::Sine)
.amplify(0.1)
.take_duration(test_signal_duration),
)
.unwrap();
thread::sleep(interval_duration);
println!("Playing 440 Hz Triangle Wave");
stream_handle
.play_raw(
SignalGenerator::new(cpal::SampleRate(48000), 440.0, Function::Triangle)
.amplify(0.1)
.take_duration(test_signal_duration),
)
.unwrap();
thread::sleep(interval_duration);
println!("Playing 440 Hz Sawtooth Wave");
stream_handle
.play_raw(
SignalGenerator::new(cpal::SampleRate(48000), 440.0, Function::Sawtooth)
.amplify(0.1)
.take_duration(test_signal_duration),
)
.unwrap();
thread::sleep(interval_duration);
println!("Playing 440 Hz Square Wave");
stream_handle
.play_raw(
SignalGenerator::new(cpal::SampleRate(48000), 440.0, Function::Square)
.amplify(0.1)
.take_duration(test_signal_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(test_signal_duration),
)
.unwrap();
thread::sleep(interval_duration);
}

View file

@ -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
@ -120,7 +125,7 @@
//! 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))]
pub use cpal::{
self, traits::DeviceTrait, Device, Devices, DevicesError, InputDevices, OutputDevices,

76
src/source/chirp.rs Normal file
View file

@ -0,0 +1,76 @@
//! Chirp/sweep source.
use std::{f32::consts::TAU, 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<Self::Item> {
let i = self.elapsed_samples;
let ratio = self.elapsed_samples as f32 / self.total_samples as f32;
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())
}
}
impl Source for Chirp {
fn current_frame_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> u16 {
1
}
fn sample_rate(&self) -> u32 {
self.sample_rate.0
}
fn total_duration(&self) -> Option<Duration> {
let secs: f64 = self.total_samples as f64 / self.sample_rate.0 as f64;
Some(Duration::new(1, 0).mul_f64(secs))
}
}

View file

@ -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;
@ -26,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;
@ -40,6 +42,7 @@ mod amplify;
mod blt;
mod buffered;
mod channel_volume;
mod chirp;
mod crossfade;
mod delay;
mod done;
@ -56,6 +59,7 @@ mod periodic;
mod position;
mod repeat;
mod samples_converter;
mod signal_generator;
mod sine;
mod skip;
mod skippable;
@ -66,6 +70,11 @@ mod take;
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

158
src/source/noise.rs Normal file
View file

@ -0,0 +1,158 @@
//! Noise sources.
//!
//!
use crate::Source;
use super::SeekError;
use rand::{rngs::SmallRng, RngCore, SeedableRng};
/// Convenience function to create a new `WhiteNoise` noise source.
#[inline]
pub fn white(sample_rate: cpal::SampleRate) -> WhiteNoise {
WhiteNoise::new(sample_rate)
}
/// Convenience function to 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]. This source generates random
/// samples as provided by the `rand::rngs::SmallRng` randomness source.
#[derive(Clone, Debug)]
pub struct WhiteNoise {
sample_rate: cpal::SampleRate,
rng: SmallRng,
}
impl WhiteNoise {
/// 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,
rng: SmallRng::from_entropy(),
}
}
}
impl Iterator for WhiteNoise {
type Item = f32;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let rand = self.rng.next_u32() as f32 / u32::MAX as f32;
let scaled = rand * 2.0 - 1.0;
Some(scaled)
}
}
impl Source for WhiteNoise {
#[inline]
fn current_frame_len(&self) -> Option<usize> {
None
}
#[inline]
fn channels(&self) -> u16 {
1
}
#[inline]
fn sample_rate(&self) -> u32 {
self.sample_rate.0
}
#[inline]
fn total_duration(&self) -> Option<std::time::Duration> {
None
}
#[inline]
fn try_seek(&mut self, _: std::time::Duration) -> Result<(), SeekError> {
// Does nothing, should do nothing
Ok(())
}
}
/// Generates an infinite stream of pink noise samples in [-1.0, 1.0].
///
/// 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 {
white_noise: WhiteNoise,
b: [f32; 7],
}
impl PinkNoise {
pub fn new(sample_rate: cpal::SampleRate) -> Self {
Self {
white_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<Self::Item> {
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;
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;
Some(pink)
}
}
impl Source for PinkNoise {
fn current_frame_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> u16 {
1
}
fn sample_rate(&self) -> u32 {
self.white_noise.sample_rate()
}
fn total_duration(&self) -> Option<std::time::Duration> {
None
}
#[inline]
fn try_seek(&mut self, _: std::time::Duration) -> Result<(), SeekError> {
// Does nothing, should do nothing
Ok(())
}
}

View file

@ -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<f32> {
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<usize> {
None
}
#[inline]
fn channels(&self) -> u16 {
1
}
#[inline]
fn sample_rate(&self) -> u32 {
self.sample_rate.0
}
#[inline]
fn total_duration(&self) -> Option<Duration> {
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);
}
}

View file

@ -1,6 +1,6 @@
use std::f32::consts::PI;
use std::time::Duration;
use crate::source::{Function, SignalGenerator};
use crate::Source;
use super::SeekError;
@ -10,17 +10,18 @@ use super::SeekError;
/// Always has a rate of 48kHz and one channel.
#[derive(Clone, Debug)]
pub struct SineWave {
freq: f32,
num_sample: usize,
test_sine: SignalGenerator,
}
impl SineWave {
const SAMPLE_RATE: u32 = 48000;
/// The frequency of the sine.
#[inline]
pub fn new(freq: f32) -> SineWave {
let sr = cpal::SampleRate(Self::SAMPLE_RATE);
SineWave {
freq,
num_sample: 0,
test_sine: SignalGenerator::new(sr, freq, Function::Sine),
}
}
}
@ -30,10 +31,7 @@ impl Iterator for SineWave {
#[inline]
fn next(&mut self) -> Option<f32> {
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.test_sine.next()
}
}
@ -50,7 +48,7 @@ impl Source for SineWave {
#[inline]
fn sample_rate(&self) -> u32 {
48000
Self::SAMPLE_RATE
}
#[inline]
@ -58,12 +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, _: 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(())
}
}