diff --git a/CHANGELOG.md b/CHANGELOG.md index cdcf20b..48daa31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for *ALAC/AIFF* - Add `automatic_gain_control` source for dynamic audio level adjustment. -- New sources: +- 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 diff --git a/Cargo.toml b/Cargo.toml index 374c614..3ca0866 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } atomic_float = { version = "1.1.0", optional = true } @@ -33,6 +34,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"] @@ -64,3 +66,7 @@ harness = false [[example]] name = "music_m4a" required-features = ["symphonia-isomp4", "symphonia-aac"] + +[[example]] +name = "noise_generator" +required-features = ["noise"] diff --git a/README.md b/README.md index 614ab1f..91b0369 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 diff --git a/examples/noise_generator.rs b/examples/noise_generator.rs new file mode 100644 index 0000000..a72bcb7 --- /dev/null +++ b/examples/noise_generator.rs @@ -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..."); +} diff --git a/examples/signal_generator.rs b/examples/signal_generator.rs new file mode 100644 index 0000000..08fd476 --- /dev/null +++ b/examples/signal_generator.rs @@ -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); +} diff --git a/src/lib.rs b/src/lib.rs index bcb05fc..10ef60d 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 @@ -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, diff --git a/src/source/chirp.rs b/src/source/chirp.rs new file mode 100644 index 0000000..9942cab --- /dev/null +++ b/src/source/chirp.rs @@ -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 { + 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 { + 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)) + } +} diff --git a/src/source/mod.rs b/src/source/mod.rs index 75dd7e2..816387a 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -11,6 +11,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; @@ -27,6 +28,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; @@ -42,6 +44,7 @@ mod amplify; mod blt; mod buffered; mod channel_volume; +mod chirp; mod crossfade; mod delay; mod done; @@ -58,6 +61,7 @@ mod periodic; mod position; mod repeat; mod samples_converter; +mod signal_generator; mod sine; mod skip; mod skippable; @@ -68,6 +72,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 diff --git a/src/source/noise.rs b/src/source/noise.rs new file mode 100644 index 0000000..42ae0e6 --- /dev/null +++ b/src/source/noise.rs @@ -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 { + 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 { + 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, _: 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 { + 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 { + None + } + + fn channels(&self) -> u16 { + 1 + } + + fn sample_rate(&self) -> u32 { + self.white_noise.sample_rate() + } + + fn total_duration(&self) -> Option { + None + } + + #[inline] + fn try_seek(&mut self, _: std::time::Duration) -> Result<(), SeekError> { + // Does nothing, should do nothing + Ok(()) + } +} diff --git a/src/source/signal_generator.rs b/src/source/signal_generator.rs new file mode 100644 index 0000000..54969ed --- /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); + } +} diff --git a/src/source/sine.rs b/src/source/sine.rs index 44aa48e..da4f8b2 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -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 { - 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(()) } }