From f6fc55b43e861eaad7530993d70418e7182fb7ba Mon Sep 17 00:00:00 2001 From: Vasilii Milovidov Date: Wed, 4 Oct 2023 01:30:20 +0400 Subject: [PATCH] tests: reverb --- src-tauri/Cargo.lock | 20 ++++++ src-tauri/Cargo.toml | 5 +- src-tauri/src/reverbgen.rs | 117 +++++++++++++++++++++++++++----- src-tauri/src/superdough.rs | 1 - src-tauri/src/webaudiobridge.rs | 30 +++++++- 5 files changed, 151 insertions(+), 22 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 81390e5f0..dfdf3ceb5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -109,9 +109,12 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" name = "app" version = "0.1.0" dependencies = [ + "hound", "midir", "mini-moka", "quick_cache", + "rand 0.8.5", + "rand_distr", "reqwest", "rosc", "serde", @@ -1848,6 +1851,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + [[package]] name = "line-wrap" version = "0.1.1" @@ -2192,6 +2201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2724,6 +2734,16 @@ dependencies = [ "getrandom 0.2.10", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "rand_hc" version = "0.2.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 78fe567d9..5acba49c9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,6 +25,9 @@ web-audio-api = { git = "https://github.com/orottier/web-audio-api-rs.git", bran reqwest = "0.11.20" quick_cache = "0.4.0" mini-moka = "0.10.2" +hound = "3.4.0" +rand = "0.8.5" +rand_distr = "0.4.3" [features] # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. @@ -35,5 +38,5 @@ custom-protocol = ["tauri/custom-protocol"] [profile.release] panic = "abort" codegen-units = 1 -lto = true +lto = "thin" opt-level = "s" diff --git a/src-tauri/src/reverbgen.rs b/src-tauri/src/reverbgen.rs index 557463ed8..ea0a2503b 100644 --- a/src-tauri/src/reverbgen.rs +++ b/src-tauri/src/reverbgen.rs @@ -1,22 +1,105 @@ -use web_audio_api::AudioBuffer; -use web_audio_api::context::{AudioContext, BaseAudioContext}; +use rand_distr::{Distribution, Normal}; -pub fn generate_reverb(context: &AudioContext) { - let sample_rate = 44100.0; - let num_channels = 2.0; - let total_time = 2.0 * 1.5; - let decay_samples = (2.0_f64 * sample_rate).round() as usize; - let num_samples = (total_time * sample_rate).round() as usize; - let fade_samples = 0.0; - let decay_base = (1.0_f64 / 1000.0_f64).powf(1.0 / decay_samples as f64) as f32; - let mut reverb_ir = context.create_buffer(2, num_samples, 44100.0); +fn apply_decay(input: &mut [f32], decay_factor: f32) { + let len = input.len(); - for i in 0..num_channels as usize { - let mut chan = reverb_ir.get_channel_data_mut(i); - for j in 0..num_samples { - chan[j] = -0.3 * decay_base.powf(j as f32); - } + for i in 0..len { + // Multiply each sample by an exponentially decreasing factor + let decay_amt = (-(i as f32) / (len as f32) * decay_factor).exp(); + input[i] *= decay_amt; } - println!("{:?}", reverb_ir); } +fn apply_fir_filter(input: &mut [f32], coefficients: &[f32]) { + let num_coeffs = coefficients.len(); + let len = input.len(); + + let input_copy = input.to_vec(); + + for i in num_coeffs..len { + let mut acc = 0.0; + for j in 0..num_coeffs { + acc += input_copy[i - j] * coefficients[j]; + } + input[i] = acc; + } +} + +fn sinc(x: f32, fc: f32) -> f32 { + let pi = std::f32::consts::PI; + if x == 0.0 { + 2.0 * fc + } else { + let arg = 2.0 * pi * fc * x; + arg.sin() / arg + } +} + +fn hamming(n: usize, size: usize) -> f32 { + 0.54 - 0.46 * ((2. * std::f32::consts::PI * n as f32) / ((size - 1) as f32)).cos() +} + +pub fn generate_coefficients_lp(size: usize, fc: f32) -> Vec { + let mut h = vec![0.0; size]; + let m = (size - 1) / 2; + + for n in 0..size { + h[n] = sinc((n as f32 - m as f32), fc); + h[n] *= hamming(n, size); + } + + let sum: f32 = h.iter().sum(); + for n in 0..size { + h[n] /= sum; + } + + h +} + +pub fn create_impulse_response( + len: usize, + decay_factor: f32, + fir_coeffs: &[f32], + sample_rate: f32, + fade: f32, +) -> Vec { + let mut rng = rand::thread_rng(); + let mut normal = Normal::new(0.0, 1.0).unwrap(); + let fade_in_samples = (fade * sample_rate) as usize; // Compute the number of samples for the fade-in duration + + let mut impulse_response: Vec = (0..len) + .map(|_| normal.sample(&mut rng) as f32) + .map(|v| v.max(-1.0).min(1.0)) // Clamp values to the -1.0 to 1.0 range + .collect(); + + // Apply fade-in to the starting samples + for i in 0..fade_in_samples.min(len) { + impulse_response[i] *= i as f32 / fade_in_samples as f32; + } + + apply_decay(&mut impulse_response, decay_factor); + apply_fir_filter(&mut impulse_response, fir_coeffs); + + impulse_response +} + +pub fn write_to_wav(filename: &str, data: &[f32]) { + let spec = hound::WavSpec { + channels: 1, + sample_rate: 44100, + bits_per_sample: 16, + sample_format: hound::SampleFormat::Int, + }; + + let mut writer = hound::WavWriter::create(filename, spec).unwrap(); + for &sample_float in data { + // Assuming 16-bit audio + let amplitude_scale = i16::MAX as f32; + let sample = (sample_float * amplitude_scale).round() as i16; + writer.write_sample(sample).unwrap(); + } + writer.finalize().unwrap(); +} + + + diff --git a/src-tauri/src/superdough.rs b/src-tauri/src/superdough.rs index 2022975e0..b78511f62 100644 --- a/src-tauri/src/superdough.rs +++ b/src-tauri/src/superdough.rs @@ -7,7 +7,6 @@ use web_audio_api::{ node::BiquadFilterType::{Bandpass, Highpass, Lowpass}, }; use web_audio_api::node::{ConvolverNode, DelayNode, DynamicsCompressorNode}; -use crate::reverbgen::generate_reverb; use crate::webaudiobridge::WebAudioMessage; #[derive(Clone, Copy, Debug)] diff --git a/src-tauri/src/webaudiobridge.rs b/src-tauri/src/webaudiobridge.rs index 47432797c..068781ca4 100644 --- a/src-tauri/src/webaudiobridge.rs +++ b/src-tauri/src/webaudiobridge.rs @@ -22,6 +22,7 @@ use web_audio_api::{ }; use web_audio_api::context::OfflineAudioContext; use web_audio_api::node::{ConvolverNode, ConvolverOptions, DelayNode}; +use crate::reverbgen::{create_impulse_response, generate_coefficients_lp, write_to_wav}; use crate::superdough::{ADSRMessage, apply_filter_adsr, BPFMessage, DelayMessage, Delay, FilterADSRMessage, HPFMessage, LoopMessage, LPFMessage, Sampler, Synth, WebAudioInstrument, ReverbMessage, Reverb}; @@ -228,6 +229,31 @@ pub fn init( }; // REVERB + if message.reverb.room.is_some() { + let file_path = "/Users/vasiliymilovidov/samples/ir3.wav".to_string(); + if let Some(ir) = cache.get(&file_path) { + let mut reverb = audio_context.create_convolver(); + reverb.set_buffer(ir); + reverb.connect(&compressor); + sampler.envelope.connect(&reverb); + } else { + let len = (audio_context.sample_rate() * message.reverb.size.unwrap_or(0.5)) as usize; + let decay = 5.0; + let num_coeffs = 101; + let fade = 0.1; + let cutoff = 4000.0 / audio_context.sample_rate(); + let cache_cache = cache.clone(); + tokio::spawn(async move { + let fir = generate_coefficients_lp(num_coeffs, cutoff); + let ir = create_impulse_response(len, decay, &fir, 44100.0, fade); + write_to_wav("/Users/vasiliymilovidov/samples/ir3.wav", &ir); + let context = OfflineAudioContext::new(2, 400, 44100.0); + let file = File::open("/Users/vasiliymilovidov/samples/ir3.wav").expect("File booom!"); + let buf = context.decode_audio_data_sync(file).unwrap(); + cache_cache.insert("/Users/vasiliymilovidov/samples/ir3.wav".to_string(), buf); + }); + } + } if message.reverb.ir.is_some() { let (ir_url, ir_file_path, ir_file_path_clone) = create_ir_filepath(&message); @@ -248,8 +274,6 @@ pub fn init( let requested_size = message.reverb.size.unwrap_or(2.0).clone(); if let Some(ir_buffer) = ir_cache.get(&ir_file_path_clone) { - let t = audio_context.current_time(); - let ir_buffer_duration = ir_buffer.duration(); let mut reverb = audio_context.create_convolver(); reverb.set_buffer(ir_buffer); reverb.connect(&compressor); @@ -278,9 +302,9 @@ pub fn init( } } + // CONNECT SAMPLER TO OUTPUT AND PLAY sampler.envelope.connect(&compressor); sampler.play(t, &message, message.adsr.release.unwrap_or(0.001)); - // CONNECT SAMPLER TO OUTPUT AND PLAY // IF FILE EXIST - DECODE IT } else if let Ok(file) = File::open(&file_path_clone) { let cache_clone = cache.clone();