From 71dbb875608570f4e955a3a5d11954de559def50 Mon Sep 17 00:00:00 2001 From: Petr Gladkikh Date: Fri, 6 Dec 2024 03:12:48 +0400 Subject: [PATCH 01/10] Resampler low pass prototype --- src/conversions/sample_rate.rs | 27 ++++++++ src/source/low_pass.rs | 115 +++++++++++++++++++++++++++++++++ src/source/mod.rs | 2 + 3 files changed, 144 insertions(+) create mode 100644 src/source/low_pass.rs diff --git a/src/conversions/sample_rate.rs b/src/conversions/sample_rate.rs index b76c23d0..bdcf2bff 100644 --- a/src/conversions/sample_rate.rs +++ b/src/conversions/sample_rate.rs @@ -2,6 +2,8 @@ use crate::conversions::Sample; use num_rational::Ratio; use std::mem; +use std::time::Duration; +use crate::Source; /// Iterator that converts from a certain sample rate to another. #[derive(Clone, Debug)] @@ -15,6 +17,7 @@ where from: u32, /// We convert chunks of `from` samples into chunks of `to` samples. to: u32, + to_full: u32, /// Number of channels in the stream channels: cpal::ChannelCount, /// One sample per channel, extracted from `input`. @@ -61,6 +64,7 @@ where assert!(num_channels >= 1); assert!(from >= 1); assert!(to >= 1); + let to_full = to; let (first_samples, next_samples) = if from == to { // if `from` == `to` == 1, then we just pass through @@ -84,6 +88,7 @@ where input, from, to, + to_full, channels: num_channels, current_frame_pos_in_chunk: 0, next_output_frame_pos_in_chunk: 0, @@ -120,6 +125,28 @@ where } } +impl Source for SampleRateConverter +where + I: Source, + I::Item: Sample, +{ + fn current_frame_len(&self) -> Option { + None + } + + fn channels(&self) -> u16 { + self.input.channels() + } + + fn sample_rate(&self) -> u32 { + self.to_full + } + + fn total_duration(&self) -> Option { + self.input.total_duration() + } +} + impl Iterator for SampleRateConverter where I: Iterator, diff --git a/src/source/low_pass.rs b/src/source/low_pass.rs new file mode 100644 index 00000000..550e9873 --- /dev/null +++ b/src/source/low_pass.rs @@ -0,0 +1,115 @@ +use std::time::Duration; +use num_rational::Ratio; +use crate::conversions::SampleRateConverter; +use crate::{Sample, Source}; + +pub struct LowPass +where + I: Iterator, +{ + input: I, + prev: Option +} + +impl LowPass +where + I: Iterator, + I::Item: Sample, +{ + #[inline] + pub fn new( + mut input: I, + ) -> LowPass { + LowPass { + input, + prev: None + } + } +} + +impl Source for LowPass +where + I: Source, + I::Item: Sample, +{ + fn current_frame_len(&self) -> Option { + None + } + + fn channels(&self) -> u16 { + self.input.channels() + } + + fn sample_rate(&self) -> u32 { + self.input.sample_rate() + } + + fn total_duration(&self) -> Option { + self.input.total_duration() + } +} + +impl Iterator for LowPass +where + I: Iterator, + I::Item: Sample + Clone, +{ + type Item = I::Item; + + fn next(&mut self) -> Option { + self.input.next().and_then(|s| { + let x = self.prev.map(|p| (p.saturating_add(s)).amplify(0.5)); + self.prev.replace(s); + x + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::conversions::SampleRateConverter; + use crate::source::SineWave; + use crate::Source; + use crate::{OutputStreamBuilder}; + use std::thread; + use std::time::Duration; + use symphonia::core::meta::StandardTagKey::Encoder; + + #[test] + fn test_low_pass() { + let stream_handle = OutputStreamBuilder::open_default_stream().unwrap(); + let mixer = stream_handle.mixer(); + { + // Generate sine wave. + let wave = SineWave::new(740.0) + .amplify(0.1) + .take_duration(Duration::from_secs(1)); + + let rate_in = wave.sample_rate(); + let channels_in = wave.channels(); + let out_freq = 44_100; + let output1 = SampleRateConverter::new( + wave, + cpal::SampleRate(rate_in), + cpal::SampleRate(out_freq * 2), + channels_in, + ); + + let lo_pass = LowPass::new(output1); + + let rate_in = lo_pass.sample_rate(); + let output2 = SampleRateConverter::new( + lo_pass, + cpal::SampleRate(rate_in), + cpal::SampleRate(out_freq), + channels_in, + ); + + mixer.add(output2); + } + WavFile + + thread::sleep(Duration::from_millis(1000)); + } +} diff --git a/src/source/mod.rs b/src/source/mod.rs index e730e802..6e52a186 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -75,6 +75,8 @@ mod zero; #[cfg(feature = "noise")] mod noise; +mod low_pass; + #[cfg(feature = "noise")] pub use self::noise::{pink, white, PinkNoise, WhiteNoise}; From f8eac5703ee128af8b241867b849adccde04ff51 Mon Sep 17 00:00:00 2001 From: Petr Gladkikh Date: Sun, 8 Dec 2024 01:41:43 +0400 Subject: [PATCH 02/10] Build fix --- src/source/low_pass.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/source/low_pass.rs b/src/source/low_pass.rs index 550e9873..0f7b2120 100644 --- a/src/source/low_pass.rs +++ b/src/source/low_pass.rs @@ -108,7 +108,6 @@ mod test { mixer.add(output2); } - WavFile thread::sleep(Duration::from_millis(1000)); } From 4a322a2fa0e474189abe5b0f6b6552d822d229e8 Mon Sep 17 00:00:00 2001 From: Petr Gladkikh Date: Sat, 14 Dec 2024 04:09:31 +0400 Subject: [PATCH 03/10] Cleanups --- src/source/blt.rs | 12 ++++++------ src/source/low_pass.rs | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/source/blt.rs b/src/source/blt.rs index c7be7310..8778b0d6 100644 --- a/src/source/blt.rs +++ b/src/source/blt.rs @@ -5,8 +5,6 @@ use crate::Source; use super::SeekError; -// Implemented following http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt - /// Internal function that builds a `BltFilter` object. pub fn low_pass(input: I, freq: u32) -> BltFilter where @@ -22,7 +20,7 @@ where high_pass_with_q(input, freq, 0.5) } -/// Same as low_pass but allows the q value (bandwidth) to be changed +/// Same as low_pass but allows the q value (bandwidth) to be changed. pub fn low_pass_with_q(input: I, freq: u32, q: f32) -> BltFilter where I: Source, @@ -38,7 +36,7 @@ where } } -/// Same as high_pass but allows the q value (bandwidth) to be changed +/// Same as high_pass but allows the q value (bandwidth) to be changed. pub fn high_pass_with_q(input: I, freq: u32, q: f32) -> BltFilter where I: Source, @@ -55,6 +53,8 @@ where } /// This applies an audio filter, it can be a high or low pass filter. +/// Implements following +/// https://github.com/WebAudio/Audio-EQ-Cookbook/blob/main/Audio-EQ-Cookbook.txt #[derive(Clone, Debug)] pub struct BltFilter { input: I, @@ -77,13 +77,13 @@ impl BltFilter { self.to_high_pass_with_q(freq, 0.5); } - /// Same as to_low_pass but allows the q value (bandwidth) to be changed + /// Same as to_low_pass but allows the q value (bandwidth) to be changed. pub fn to_low_pass_with_q(&mut self, freq: u32, q: f32) { self.formula = BltFormula::LowPass { freq, q }; self.applier = None; } - /// Same as to_high_pass but allows the q value (bandwidth) to be changed + /// Same as to_high_pass but allows the q value (bandwidth) to be changed. pub fn to_high_pass_with_q(&mut self, freq: u32, q: f32) { self.formula = BltFormula::HighPass { freq, q }; self.applier = None; diff --git a/src/source/low_pass.rs b/src/source/low_pass.rs index 0f7b2120..ea95483b 100644 --- a/src/source/low_pass.rs +++ b/src/source/low_pass.rs @@ -77,7 +77,7 @@ mod test { use symphonia::core::meta::StandardTagKey::Encoder; #[test] - fn test_low_pass() { + fn test_resample_low_pass() { let stream_handle = OutputStreamBuilder::open_default_stream().unwrap(); let mixer = stream_handle.mixer(); { @@ -111,4 +111,41 @@ mod test { thread::sleep(Duration::from_millis(1000)); } + + #[test] + fn test_resample_biquad_low_pass() { + + let stream_handle = OutputStreamBuilder::open_default_stream().unwrap(); + let mixer = stream_handle.mixer(); + { + // Generate sine wave. + let wave = SineWave::new(740.0) + .amplify(0.1) + .take_duration(Duration::from_secs(1)); + + let rate_in = wave.sample_rate(); + let channels_in = wave.channels(); + let out_freq = 44_100; + let output1 = SampleRateConverter::new( + wave, + cpal::SampleRate(rate_in), + cpal::SampleRate(out_freq * 2), + channels_in, + ); + + let lo_pass = LowPass::new(output1); + + let rate_in = lo_pass.sample_rate(); + let output2 = SampleRateConverter::new( + lo_pass, + cpal::SampleRate(rate_in), + cpal::SampleRate(out_freq), + channels_in, + ); + + mixer.add(output2); + } + + thread::sleep(Duration::from_millis(1000)); + } } From 8154e444b49cd6a0d9aec32f43bce36ec7c3fa67 Mon Sep 17 00:00:00 2001 From: Petr Gladkikh Date: Wed, 18 Dec 2024 22:11:57 +0400 Subject: [PATCH 04/10] Cargo update --- Cargo.lock | 101 ++++++++++++++++++++++++----------------------------- 1 file changed, 46 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 636ea8cc..90a18275 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,9 +122,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.2.2" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" +checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" dependencies = [ "jobserver", "libc", @@ -165,18 +165,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstyle", "clap_lex", @@ -185,9 +185,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "claxon" @@ -262,18 +262,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "dasp_sample" @@ -283,9 +283,9 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] name = "divan" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc40f214f0d9e897cfc72e2edfa5c225d3252f758c537f11ac0a80371c073a6" +checksum = "e0583193020b29b03682d8d33bb53a5b0f50df6daacece12ca99b904cfdcb8c4" dependencies = [ "cfg-if", "clap", @@ -297,9 +297,9 @@ dependencies = [ [[package]] name = "divan-macros" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bdb5411188f7f878a17964798c1264b6b0a9f915bd39b20bf99193c923e1b4e" +checksum = "8dc51d98e636f5e3b0759a39257458b22619cac7e96d932da6eeb052891bb67c" dependencies = [ "proc-macro2", "quote", @@ -479,9 +479,9 @@ checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", @@ -529,10 +529,11 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.73" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb15147158e79fd8b8afd0252522769c4f48725460b37338544d8379d94fc8f9" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -555,9 +556,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.167" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libloading" @@ -987,15 +988,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.41" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1009,9 +1010,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "shlex" @@ -1197,9 +1198,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.89" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -1208,9 +1209,9 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ "rustix", "windows-sys 0.59.0", @@ -1323,9 +1324,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.96" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21d3b25c3ea1126a2ad5f4f9068483c2af1e64168f847abe863a526b8dbfe00b" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -1334,13 +1335,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.96" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52857d4c32e496dc6537646b5b117081e71fd2ff06de792e3577a150627db283" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -1349,9 +1349,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.46" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951fe82312ed48443ac78b66fa43eded9999f738f6022e67aead7b708659e49a" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", @@ -1362,9 +1362,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.96" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "920b0ffe069571ebbfc9ddc0b36ba305ef65577c94b06262ed793716a1afd981" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1372,9 +1372,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.96" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf59002391099644be3524e23b781fa43d2be0c5aa0719a18c0731b9d195cab6" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", @@ -1385,15 +1385,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.96" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5047c5392700766601942795a436d7d2599af60dcc3cc1248c9120bfb0827b0" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "web-sys" -version = "0.3.73" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "476364ff87d0ae6bfb661053a9104ab312542658c3d8f963b7ace80b6f9b26b9" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", @@ -1468,15 +1468,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.59.0" From b5e245d0f9a2855eeaa7a26999e4d0e94992c3fe Mon Sep 17 00:00:00 2001 From: Petr Gladkikh Date: Wed, 18 Dec 2024 22:13:24 +0400 Subject: [PATCH 05/10] Build fix: add rustdoc --- src/source/noise.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/source/noise.rs b/src/source/noise.rs index 42ae0e61..1cb9a301 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -98,6 +98,7 @@ pub struct PinkNoise { } impl PinkNoise { + /// Create new pink noise source for given sample rate. pub fn new(sample_rate: cpal::SampleRate) -> Self { Self { white_noise: WhiteNoise::new(sample_rate), From 55f15b9a72e6fd11ba3489cfd31a35158191d669 Mon Sep 17 00:00:00 2001 From: Petr Gladkikh Date: Thu, 19 Dec 2024 00:24:01 +0400 Subject: [PATCH 06/10] Write test output to WAv file for offline analysis. --- src/source/low_pass.rs | 103 ++++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 48 deletions(-) diff --git a/src/source/low_pass.rs b/src/source/low_pass.rs index ea95483b..96c44d1f 100644 --- a/src/source/low_pass.rs +++ b/src/source/low_pass.rs @@ -1,14 +1,13 @@ -use std::time::Duration; -use num_rational::Ratio; -use crate::conversions::SampleRateConverter; use crate::{Sample, Source}; +use hound::{SampleFormat, WavSpec}; +use std::time::Duration; pub struct LowPass where I: Iterator, { input: I, - prev: Option + prev: Option, } impl LowPass @@ -17,13 +16,8 @@ where I::Item: Sample, { #[inline] - pub fn new( - mut input: I, - ) -> LowPass { - LowPass { - input, - prev: None - } + pub fn new(input: I) -> LowPass { + LowPass { input, prev: None } } } @@ -57,64 +51,77 @@ where type Item = I::Item; fn next(&mut self) -> Option { - self.input.next().and_then(|s| { - let x = self.prev.map(|p| (p.saturating_add(s)).amplify(0.5)); + self.input.next().map(|s| { + let x = self.prev.map_or(s, |p| (p.saturating_add(s)).amplify(0.5)); self.prev.replace(s); x }) } } +pub fn output_to_wav( + source: Box>, + wav_file: &str, +) -> Result<(), Box> { + let format = WavSpec { + channels: source.channels(), + sample_rate: source.sample_rate(), + bits_per_sample: 32, + sample_format: SampleFormat::Float, + }; + let mut writer = hound::WavWriter::create(wav_file, format)?; + for sample in source { + writer.write_sample(sample.to_f32())?; + } + writer.finalize()?; + Ok(()) +} + #[cfg(test)] mod test { use super::*; use crate::conversions::SampleRateConverter; use crate::source::SineWave; + use crate::OutputStreamBuilder; use crate::Source; - use crate::{OutputStreamBuilder}; use std::thread; use std::time::Duration; - use symphonia::core::meta::StandardTagKey::Encoder; #[test] fn test_resample_low_pass() { - let stream_handle = OutputStreamBuilder::open_default_stream().unwrap(); - let mixer = stream_handle.mixer(); - { - // Generate sine wave. - let wave = SineWave::new(740.0) - .amplify(0.1) - .take_duration(Duration::from_secs(1)); - - let rate_in = wave.sample_rate(); - let channels_in = wave.channels(); - let out_freq = 44_100; - let output1 = SampleRateConverter::new( - wave, - cpal::SampleRate(rate_in), - cpal::SampleRate(out_freq * 2), - channels_in, - ); - - let lo_pass = LowPass::new(output1); - - let rate_in = lo_pass.sample_rate(); - let output2 = SampleRateConverter::new( - lo_pass, - cpal::SampleRate(rate_in), - cpal::SampleRate(out_freq), - channels_in, - ); - - mixer.add(output2); - } - - thread::sleep(Duration::from_millis(1000)); + // Generate sine wave. + let wave = SineWave::new(740.0) + .amplify(0.1) + .take_duration(Duration::from_secs(1)); + + let rate_in = wave.sample_rate(); + let channels_in = wave.channels(); + let out_freq = 44_100 / 3; + let output1 = SampleRateConverter::new( + wave, + cpal::SampleRate(rate_in), + cpal::SampleRate(out_freq * 2), + channels_in, + ); + + // output_to_wav(Box::new(output1), "resample-orig.wav") + // .expect("write wav file"); + + let lo_pass = LowPass::new(output1); + + let rate_in = lo_pass.sample_rate(); + let output2 = SampleRateConverter::new( + lo_pass, + cpal::SampleRate(rate_in), + cpal::SampleRate(out_freq), + channels_in, + ); + output_to_wav(Box::new(output2), "resample-primitive-low-pass.wav") + .expect("write wav file"); } #[test] fn test_resample_biquad_low_pass() { - let stream_handle = OutputStreamBuilder::open_default_stream().unwrap(); let mixer = stream_handle.mixer(); { From 58d650fee9e2a1b40cef98a51fcde3610b8edbb1 Mon Sep 17 00:00:00 2001 From: Petr Gladkikh Date: Thu, 19 Dec 2024 22:49:06 +0400 Subject: [PATCH 07/10] Low pass filter as resampler postprocessing prototype --- src/conversions/sample_rate.rs | 167 ++++++++++++++++++++++++++++++++- src/source/blt.rs | 8 +- src/source/low_pass.rs | 158 ------------------------------- src/source/mod.rs | 3 +- src/source/simple_low_pass.rs | 63 +++++++++++++ src/source/sine.rs | 15 ++- 6 files changed, 247 insertions(+), 167 deletions(-) delete mode 100644 src/source/low_pass.rs create mode 100644 src/source/simple_low_pass.rs diff --git a/src/conversions/sample_rate.rs b/src/conversions/sample_rate.rs index bdcf2bff..98eaa8d9 100644 --- a/src/conversions/sample_rate.rs +++ b/src/conversions/sample_rate.rs @@ -1,11 +1,11 @@ use crate::conversions::Sample; +use crate::Source; use num_rational::Ratio; use std::mem; use std::time::Duration; -use crate::Source; -/// Iterator that converts from a certain sample rate to another. +/// Iterator that converts from one sample rate to another. #[derive(Clone, Debug)] pub struct SampleRateConverter where @@ -279,9 +279,14 @@ where #[cfg(test)] mod test { use super::SampleRateConverter; + use crate::{Sample, Source}; use core::time::Duration; use cpal::{ChannelCount, SampleRate}; + use dasp_sample::FromSample; + use hound::{SampleFormat, WavSpec}; use quickcheck::{quickcheck, TestResult}; + use std::io::BufReader; + use std::path; quickcheck! { /// Check that resampling an empty input produces no output. @@ -424,4 +429,162 @@ mod test { assert_eq!(output, [0, 5, 10, 15]); assert!((size_estimation as f32 / output.len() as f32).abs() < 2.0); } + + pub fn output_to_wav>( + source: Box>, + wav_file: &P, + ) -> Result<(), Box> { + let format = WavSpec { + channels: source.channels(), + sample_rate: source.sample_rate(), + bits_per_sample: 32, + sample_format: SampleFormat::Float, + }; + let mut writer = hound::WavWriter::create(wav_file, format)?; + for sample in source { + writer.write_sample(sample.to_f32())?; + } + writer.finalize()?; + Ok(()) + } + + use crate::source::{SimpleLowPass, SineWave}; + use crate::Sink; + + #[test] + fn resampler_tweaks() { + // let new_source = || Box::new(SineWave::new_with_sample_rate(frequency, rate_in) + // .amplify(0.1) + // .take_duration(Duration::from_secs(1))); + // assert!(rate_in as f32 > frequency * 2.0); + let new_source = move || { + let sink = Sink::new(); + let file = std::fs::File::open("tmp-584/rodio-crackling-issue/crackling.mp3") + .expect("open mp3 file"); + crate::Decoder::new(BufReader::new(file)).expect("can decode mp3") + }; + // resampler_tweaks_generate(740.0, 44_123, 14_707); + resampler_tweaks_generate("584mp3", &new_source, 44_100); // Bug #584 + } + + fn resampler_tweaks_generate + 'static>( + source_tag: &str, + new_source: &dyn Fn() -> Src, + rate_out: u32, + ) where + S: cpal::FromSample, + f32: cpal::FromSample, + { + let format_file_name = |variant: &str| { + let rate_in = new_source().sample_rate(); + format!("resample_{variant}_{source_tag}_in{rate_in}_out{rate_out}.wav") + }; + + output_to_wav( + resample_reference(Box::new(new_source()), rate_out), + &format_file_name("reference"), + ) + .expect("write wav file"); + + output_to_wav( + resample_with_simple_low_pass_direct(Box::new(new_source()), rate_out), + &format_file_name("simple_low_pass_direct"), + ) + .expect("write wav file"); + + output_to_wav( + resample_with_simple_low_pass_upsample(Box::new(new_source()), rate_out), + &format_file_name("simple_low_pass_upsampled"), + ) + .expect("write wav file"); + + output_to_wav( + resample_with_biquad_low_pass(Box::new(new_source()), rate_out), + &format_file_name("biquad_low_pass"), + ) + .expect("write wav file"); + } + + fn resample_reference( + source: Box>, + rate_out: u32, + ) -> Box> { + // Make an output recording to compare various tweaks from the tests below. + let channels_in = source.channels(); + let rate_in = source.sample_rate(); + let output1 = SampleRateConverter::new( + source, + SampleRate(rate_in), + SampleRate(rate_out), + channels_in, + ); + + Box::new(output1) + } + + fn resample_with_simple_low_pass_direct( + source: Box>, + rate_out: u32, + ) -> Box> { + let channels_in = source.channels(); + let rate_in = source.sample_rate(); + let output1 = SampleRateConverter::new( + source, + SampleRate(rate_in), + SampleRate(rate_out), + channels_in, + ); + + Box::new(output1) + } + + fn resample_with_simple_low_pass_upsample( + source: Box>, + rate_out: u32, + ) -> Box> { + let channels_in = source.channels(); + let rate_in = source.sample_rate(); + let output1 = SampleRateConverter::new( + source, + SampleRate(rate_in), + SampleRate(rate_out * 2), + channels_in, + ); + + let lo_pass = SimpleLowPass::new(output1); + let rate_in = lo_pass.sample_rate(); + let output2 = SampleRateConverter::new( + lo_pass, + SampleRate(rate_in), + SampleRate(rate_out), + channels_in, + ); + + Box::new(output2) + } + + fn resample_with_biquad_low_pass( + source: Box>, + rate_out: u32, + ) -> Box> + where + f32: FromSample, + S: FromSample, + { + let channels_in = source.channels(); + let rate_in = source.sample_rate(); + let output1 = SampleRateConverter::new( + source, + SampleRate(rate_in), + SampleRate(rate_out), + channels_in, + ); + + let lo_pass = output1 + .convert_samples::() + .low_pass(rate_in / 2) + .convert_samples::(); + + Box::new(lo_pass) + } } diff --git a/src/source/blt.rs b/src/source/blt.rs index 8778b0d6..e6fc245c 100644 --- a/src/source/blt.rs +++ b/src/source/blt.rs @@ -8,7 +8,7 @@ use super::SeekError; /// Internal function that builds a `BltFilter` object. pub fn low_pass(input: I, freq: u32) -> BltFilter where - I: Source, + I: Iterator, { low_pass_with_q(input, freq, 0.5) } @@ -23,7 +23,7 @@ where /// Same as low_pass but allows the q value (bandwidth) to be changed. pub fn low_pass_with_q(input: I, freq: u32, q: f32) -> BltFilter where - I: Source, + I: Iterator, { BltFilter { input, @@ -53,7 +53,7 @@ where } /// This applies an audio filter, it can be a high or low pass filter. -/// Implements following +/// Implements following /// https://github.com/WebAudio/Audio-EQ-Cookbook/blob/main/Audio-EQ-Cookbook.txt #[derive(Clone, Debug)] pub struct BltFilter { @@ -112,7 +112,7 @@ impl Iterator for BltFilter where I: Source, { - type Item = f32; + type Item = I::Item; #[inline] fn next(&mut self) -> Option { diff --git a/src/source/low_pass.rs b/src/source/low_pass.rs deleted file mode 100644 index 96c44d1f..00000000 --- a/src/source/low_pass.rs +++ /dev/null @@ -1,158 +0,0 @@ -use crate::{Sample, Source}; -use hound::{SampleFormat, WavSpec}; -use std::time::Duration; - -pub struct LowPass -where - I: Iterator, -{ - input: I, - prev: Option, -} - -impl LowPass -where - I: Iterator, - I::Item: Sample, -{ - #[inline] - pub fn new(input: I) -> LowPass { - LowPass { input, prev: None } - } -} - -impl Source for LowPass -where - I: Source, - I::Item: Sample, -{ - fn current_frame_len(&self) -> Option { - None - } - - fn channels(&self) -> u16 { - self.input.channels() - } - - fn sample_rate(&self) -> u32 { - self.input.sample_rate() - } - - fn total_duration(&self) -> Option { - self.input.total_duration() - } -} - -impl Iterator for LowPass -where - I: Iterator, - I::Item: Sample + Clone, -{ - type Item = I::Item; - - fn next(&mut self) -> Option { - self.input.next().map(|s| { - let x = self.prev.map_or(s, |p| (p.saturating_add(s)).amplify(0.5)); - self.prev.replace(s); - x - }) - } -} - -pub fn output_to_wav( - source: Box>, - wav_file: &str, -) -> Result<(), Box> { - let format = WavSpec { - channels: source.channels(), - sample_rate: source.sample_rate(), - bits_per_sample: 32, - sample_format: SampleFormat::Float, - }; - let mut writer = hound::WavWriter::create(wav_file, format)?; - for sample in source { - writer.write_sample(sample.to_f32())?; - } - writer.finalize()?; - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::conversions::SampleRateConverter; - use crate::source::SineWave; - use crate::OutputStreamBuilder; - use crate::Source; - use std::thread; - use std::time::Duration; - - #[test] - fn test_resample_low_pass() { - // Generate sine wave. - let wave = SineWave::new(740.0) - .amplify(0.1) - .take_duration(Duration::from_secs(1)); - - let rate_in = wave.sample_rate(); - let channels_in = wave.channels(); - let out_freq = 44_100 / 3; - let output1 = SampleRateConverter::new( - wave, - cpal::SampleRate(rate_in), - cpal::SampleRate(out_freq * 2), - channels_in, - ); - - // output_to_wav(Box::new(output1), "resample-orig.wav") - // .expect("write wav file"); - - let lo_pass = LowPass::new(output1); - - let rate_in = lo_pass.sample_rate(); - let output2 = SampleRateConverter::new( - lo_pass, - cpal::SampleRate(rate_in), - cpal::SampleRate(out_freq), - channels_in, - ); - output_to_wav(Box::new(output2), "resample-primitive-low-pass.wav") - .expect("write wav file"); - } - - #[test] - fn test_resample_biquad_low_pass() { - let stream_handle = OutputStreamBuilder::open_default_stream().unwrap(); - let mixer = stream_handle.mixer(); - { - // Generate sine wave. - let wave = SineWave::new(740.0) - .amplify(0.1) - .take_duration(Duration::from_secs(1)); - - let rate_in = wave.sample_rate(); - let channels_in = wave.channels(); - let out_freq = 44_100; - let output1 = SampleRateConverter::new( - wave, - cpal::SampleRate(rate_in), - cpal::SampleRate(out_freq * 2), - channels_in, - ); - - let lo_pass = LowPass::new(output1); - - let rate_in = lo_pass.sample_rate(); - let output2 = SampleRateConverter::new( - lo_pass, - cpal::SampleRate(rate_in), - cpal::SampleRate(out_freq), - channels_in, - ); - - mixer.add(output2); - } - - thread::sleep(Duration::from_millis(1000)); - } -} diff --git a/src/source/mod.rs b/src/source/mod.rs index 31a7231c..b919f334 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -31,6 +31,7 @@ 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::simple_low_pass::SimpleLowPass; pub use self::sine::SineWave; pub use self::skip::SkipDuration; pub use self::skippable::Skippable; @@ -81,7 +82,7 @@ mod zero; #[cfg(feature = "noise")] mod noise; -mod low_pass; +mod simple_low_pass; #[cfg(feature = "noise")] pub use self::noise::{pink, white, PinkNoise, WhiteNoise}; diff --git a/src/source/simple_low_pass.rs b/src/source/simple_low_pass.rs new file mode 100644 index 00000000..bef22ab0 --- /dev/null +++ b/src/source/simple_low_pass.rs @@ -0,0 +1,63 @@ +use crate::{Sample, Source}; +use std::time::Duration; + +/// Simplest low pass filter with approximately sample_rate/2 cutoff. +/// See also tunable low pass filter implementations [crate::source::blt::low_pass] +/// and [crate::source::blt::low_pass_with_q]. +pub struct SimpleLowPass +where + I: Iterator, +{ + input: I, + prev: Option, +} + +impl SimpleLowPass +where + I: Iterator, + I::Item: Sample, +{ + /// Create new simple low pass filter. + #[inline] + pub fn new(input: I) -> SimpleLowPass { + SimpleLowPass { input, prev: None } + } +} + +impl Source for SimpleLowPass +where + I: Source, + I::Item: Sample, +{ + fn current_frame_len(&self) -> Option { + None + } + + fn channels(&self) -> u16 { + self.input.channels() + } + + fn sample_rate(&self) -> u32 { + self.input.sample_rate() + } + + fn total_duration(&self) -> Option { + self.input.total_duration() + } +} + +impl Iterator for SimpleLowPass +where + I: Iterator, + I::Item: Sample + Clone, +{ + type Item = I::Item; + + fn next(&mut self) -> Option { + self.input.next().map(|s| { + let x = self.prev.unwrap_or(s).saturating_add(s).amplify(0.5); + self.prev.replace(s); + x + }) + } +} diff --git a/src/source/sine.rs b/src/source/sine.rs index f8b054c7..7412335e 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -1,7 +1,6 @@ -use std::time::Duration; - use crate::source::{Function, SignalGenerator}; use crate::Source; +use std::time::Duration; use super::SeekError; @@ -27,6 +26,18 @@ impl SineWave { test_sine: SignalGenerator::new(sr, freq, Function::Sine), } } + + /// Create a new generator with given frequency and sample rate. + pub fn new_with_sample_rate(freq: f32, sample_rate: u32) -> SineWave { + assert!( + sample_rate > (freq * 2.0) as u32, + "frequency is too high for the sample rate" + ); + let sr = cpal::SampleRate(sample_rate); + SineWave { + test_sine: SignalGenerator::new(sr, freq, Function::Sine), + } + } } impl Iterator for SineWave { From 604b96a8da3b4f73d982d3091901c2b32191185b Mon Sep 17 00:00:00 2001 From: Petr Gladkikh Date: Thu, 19 Dec 2024 22:59:43 +0400 Subject: [PATCH 08/10] Cleanup --- src/conversions/sample_rate.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/conversions/sample_rate.rs b/src/conversions/sample_rate.rs index 98eaa8d9..b1ed107d 100644 --- a/src/conversions/sample_rate.rs +++ b/src/conversions/sample_rate.rs @@ -448,17 +448,14 @@ mod test { Ok(()) } - use crate::source::{SimpleLowPass, SineWave}; - use crate::Sink; + use crate::source::SimpleLowPass; #[test] fn resampler_tweaks() { - // let new_source = || Box::new(SineWave::new_with_sample_rate(frequency, rate_in) + // let new_source = || Box::new(crate::source::SineWave::new_with_sample_rate(2345.0, 43210) // .amplify(0.1) // .take_duration(Duration::from_secs(1))); - // assert!(rate_in as f32 > frequency * 2.0); let new_source = move || { - let sink = Sink::new(); let file = std::fs::File::open("tmp-584/rodio-crackling-issue/crackling.mp3") .expect("open mp3 file"); crate::Decoder::new(BufReader::new(file)).expect("can decode mp3") From e13d3c4429a4802f3c2594c806a56d7e13240c62 Mon Sep 17 00:00:00 2001 From: Petr Gladkikh Date: Thu, 19 Dec 2024 23:21:01 +0400 Subject: [PATCH 09/10] Use existing file for tests So the test is runnable out of box. --- src/conversions/sample_rate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversions/sample_rate.rs b/src/conversions/sample_rate.rs index b1ed107d..41d33ec1 100644 --- a/src/conversions/sample_rate.rs +++ b/src/conversions/sample_rate.rs @@ -456,7 +456,7 @@ mod test { // .amplify(0.1) // .take_duration(Duration::from_secs(1))); let new_source = move || { - let file = std::fs::File::open("tmp-584/rodio-crackling-issue/crackling.mp3") + let file = std::fs::File::open("assets/music.mp3") .expect("open mp3 file"); crate::Decoder::new(BufReader::new(file)).expect("can decode mp3") }; From 9bbdaefc6a62dd144d9a60a2327d4b4c601733de Mon Sep 17 00:00:00 2001 From: Petr Gladkikh Date: Fri, 20 Dec 2024 21:08:20 +0400 Subject: [PATCH 10/10] cargo fmt --- src/conversions/sample_rate.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/conversions/sample_rate.rs b/src/conversions/sample_rate.rs index 41d33ec1..436bc3ff 100644 --- a/src/conversions/sample_rate.rs +++ b/src/conversions/sample_rate.rs @@ -456,8 +456,7 @@ mod test { // .amplify(0.1) // .take_duration(Duration::from_secs(1))); let new_source = move || { - let file = std::fs::File::open("assets/music.mp3") - .expect("open mp3 file"); + let file = std::fs::File::open("assets/music.mp3").expect("open mp3 file"); crate::Decoder::new(BufReader::new(file)).expect("can decode mp3") }; // resampler_tweaks_generate(740.0, 44_123, 14_707);