From 18a198df9bd1be01edc324bd5dbf9c7300e1c60c Mon Sep 17 00:00:00 2001 From: Vasilii Milovidov Date: Mon, 2 Oct 2023 21:53:08 +0400 Subject: [PATCH] tests: reverb --- packages/core/controls.mjs | 10 +++ packages/desktopbridge/webaudiobridge.mjs | 60 ++++++++++++++---- src-tauri/src/superdough.rs | 8 +++ src-tauri/src/webaudiobridge.rs | 77 ++++++++++++++++++++--- 4 files changed, 132 insertions(+), 23 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 6cac6e540..7cd1a006e 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -970,6 +970,16 @@ const generic_params = [ * */ [['room', 'size']], + /** + * Sets the sample to use as an impulse response for the reverb. + * + * @name iresponse + * @param {string | Pattern} sample sample to set as the impulse response + * @example + * s("bd sd").room(.8).ir("") + * + */ + [['ir', 'i'], 'iresponse'], /** * Sets the room size of the reverb, see {@link room}. * diff --git a/packages/desktopbridge/webaudiobridge.mjs b/packages/desktopbridge/webaudiobridge.mjs index 6f8bbf56c..cc00e5488 100644 --- a/packages/desktopbridge/webaudiobridge.mjs +++ b/packages/desktopbridge/webaudiobridge.mjs @@ -39,7 +39,9 @@ export const desktopAudio = async (value, deadline, hapDuration) => { delaytime, orbit = 1, room, - size = 2, + size, + ir, + i = 0, velocity = 1, analyze, // analyser wet fft = 8, // fftSize 0 - 10 @@ -133,19 +135,37 @@ export const desktopAudio = async (value, deadline, hapDuration) => { delaytime = Math.abs(delaytime); } - const packages = { - loop: [loop, loopBegin, loopEnd], - delay: [delay, delaytime, delayfeedback], - lpf: [cutoff, resonance], - hpf: [hcutoff, hresonance], - bpf: [bandf, bandq], - adsr: [attack, decay, sustain, release], - lpenv: [lpattack, lpdecay, lpsustain, lprelease, lpenv], - hpenv: [hpattack, hpdecay, hpsustain, hprelease, hpenv], - bpenv: [bpattack, bpdecay, bpsustain, bprelease, bpenv], - }; + let ir_path; + if (getSound(ir).data.baseUrl !== undefined) { + baseUrl = getSound(ir).data.baseUrl; + if (baseUrl === './piano/') { + ir_path = 'https://strudel.tidalcycles.org/'; + } else if (baseUrl === './EmuSP12/') { + ir_path = 'https://strudel.tidalcycles.org/'; + } else { + ir_path = ''; + } + } + + let url; + if (ir !== undefined) { + let ir_samples = getSound(ir); + if (Array.isArray(ir_samples)) { + url = + ir_path !== undefined + ? ir_path + ir_samples[n % ir_samples.length].replace('./', '') + : ir_samples[n % ir_samples.length].replace('./', ''); + // url = ir_samples.data.samples[i % ir_samples.data.samples.length]; + } else if (typeof ir_samples === 'object') { + url = + ir_path !== undefined + ? ir_path + + Object.values(ir_samples.data.samples)[i & Object.values(ir_samples.data.samples).length].replace('./', '') + : Object.values(ir_samples.data.samples)[i & Object.values(ir_samples.data.samples).length].replace('./', ''); + } + } + console.log('url', url); - console.log('unit', unit); let folder; if (baseUrl !== undefined) { folder = baseUrl.replace('./', '') + '/' + s + '/'; @@ -156,6 +176,19 @@ export const desktopAudio = async (value, deadline, hapDuration) => { const offset = (t - getAudioContext().currentTime) * 1000; const roundedOffset = Math.round(offset); const messagesfromjs = []; + const packages = { + loop: [loop, loopBegin, loopEnd], + delay: [delay, delaytime, delayfeedback], + reverb: [room, size, ir, url], + lpf: [cutoff, resonance], + hpf: [hcutoff, hresonance], + bpf: [bandf, bandq], + adsr: [attack, decay, sustain, release], + lpenv: [lpattack, lpdecay, lpsustain, lprelease, lpenv], + hpenv: [hpattack, hpdecay, hpsustain, hprelease, hpenv], + bpenv: [bpattack, bpdecay, bpsustain, bprelease, bpenv], + }; + messagesfromjs.push({ note: midiToFreq(note), offset: roundedOffset, @@ -166,6 +199,7 @@ export const desktopAudio = async (value, deadline, hapDuration) => { duration: hapDuration, velocity: velocity, delay: packages.delay, + reverb: packages.reverb, orbit: orbit, speed: speed, begin: begin, diff --git a/src-tauri/src/superdough.rs b/src-tauri/src/superdough.rs index 0a4830b33..48f2ce3d4 100644 --- a/src-tauri/src/superdough.rs +++ b/src-tauri/src/superdough.rs @@ -16,6 +16,14 @@ pub struct DelayMessage { pub feedback: Option, } +#[derive(Clone, Debug)] +pub struct ReverbMessage { + pub room: Option, + pub size: Option, + pub ir: Option, + pub url: Option, +} + #[derive(Clone, Copy, Debug)] pub struct LoopMessage { pub is_loop: u8, diff --git a/src-tauri/src/webaudiobridge.rs b/src-tauri/src/webaudiobridge.rs index bb1b7fb80..ae313b5c7 100644 --- a/src-tauri/src/webaudiobridge.rs +++ b/src-tauri/src/webaudiobridge.rs @@ -20,8 +20,8 @@ use web_audio_api::{ context::{AudioContext, AudioContextLatencyCategory, AudioContextOptions}, node::AudioNode, }; -use web_audio_api::node::DelayNode; -use crate::superdough::{ADSRMessage, apply_filter_adsr, BPFMessage, DelayMessage, Delay, EffectsBus, FilterADSRMessage, HPFMessage, LoopMessage, LPFMessage, make_delay, Sampler, Synth, WebAudioInstrument}; +use web_audio_api::node::{ConvolverNode, DelayNode}; +use crate::superdough::{ADSRMessage, apply_filter_adsr, BPFMessage, DelayMessage, Delay, FilterADSRMessage, HPFMessage, LoopMessage, LPFMessage, Sampler, Synth, WebAudioInstrument, ReverbMessage}; #[derive(Debug, Clone)] @@ -36,6 +36,7 @@ pub struct WebAudioMessage { pub duration: f64, pub velocity: f32, pub delay: DelayMessage, + pub reverb: ReverbMessage, pub orbit: usize, pub speed: f32, pub begin: f64, @@ -119,7 +120,8 @@ pub fn init( ..AudioContextOptions::default() }); let _ = audio_context.create_buffer_source(); - let mut delays:HashMap = HashMap::new(); + let mut delays: HashMap = HashMap::new(); + let mut reverbs: HashMap = HashMap::new(); let compressor = audio_context.create_dynamics_compressor(); compressor.threshold().set_value(-50.0); compressor.connect(&audio_context.destination()); @@ -158,13 +160,7 @@ pub fn init( } } _ => { - let url = Url::parse(&*message.sampleurl).expect("failed to parse url"); - let filename = url.path_segments() - .and_then(Iterator::last) - .and_then(|name| if name.is_empty() { None } else { Some(name) }) - .unwrap_or("tmp.ben"); - let file_path = format!("samples/{}{}", message.dirname, filename); - let file_path_clone = file_path.clone(); + let (url, file_path, file_path_clone) = create_filepath(&message); tokio::spawn(async move { if tokio::fs::metadata(&file_path.clone()).await.is_err() { @@ -210,6 +206,38 @@ pub fn init( delay.feedback.gain().set_value(message.delay.feedback.unwrap_or(0.5)); delay.pre_gain.gain().set_value_at_time(message.delay.wet.unwrap_or(0.25), t + message.delay.delay_time.unwrap_or(0.5) as f64); }; + + if message.reverb.ir.is_some() { + let (ir_url, ir_file_path, ir_file_path_clone) = create_ir_filepath(&message); + tokio::spawn(async move { + if tokio::fs::metadata(&ir_file_path.clone()).await.is_err() { + let response = reqwest::get(ir_url) + .await + .unwrap_or_else(|_| panic!("Failed to send GET request")); + + let bytes = response.bytes().await.unwrap(); + let path = Path::new(&ir_file_path); + let mut file = create_file_and_dirs(path).await; + file.write_all(&bytes) + .await + .unwrap_or_else(|_| panic!("Failed to write to file")); + } + }); + + if let Some(ir_buffer) = cache.get(&ir_file_path_clone) { + reverbs.entry(message.orbit).or_insert({ + let mut reverb = audio_context.create_convolver(); + reverb.set_buffer(ir_buffer.clone()); + reverb + }); + let reverb = reverbs.get(&message.orbit).unwrap(); + sampler.envelope.connect(reverb); + } else if let Ok(file) = File::open(&ir_file_path_clone) { + let ir_buffer = audio_context.decode_audio_data_sync(file).unwrap_or_else(|_| panic!("Failed to decode audio data")); + cache.insert(ir_file_path_clone.clone(), ir_buffer); + } + } + sampler.envelope.connect(&compressor); sampler.play(t, &message, audio_buffer_duration); } else if let Ok(file) = File::open(&file_path_clone) { @@ -226,6 +254,28 @@ pub fn init( }); } +fn create_filepath(message: &WebAudioMessage) -> (Url, String, String) { + let url = Url::parse(&*message.sampleurl).expect("failed to parse url"); + let filename = url.path_segments() + .and_then(Iterator::last) + .and_then(|name| if name.is_empty() { None } else { Some(name) }) + .unwrap_or("tmp.ben"); + let file_path = format!("/Users/vasiliymilovidov/samples/{}{}", message.dirname, filename); + let file_path_clone = file_path.clone(); + (url, file_path, file_path_clone) +} + +fn create_ir_filepath(message: &WebAudioMessage) -> (Url, String, String) { + let url = Url::parse(&message.reverb.url.clone().unwrap()).expect("failed to parse url"); + let filename = url.path_segments() + .and_then(Iterator::last) + .and_then(|name| if name.is_empty() { None } else { Some(name) }) + .unwrap_or("tmp.ben"); + let file_path = format!("/Users/vasiliymilovidov/samples/{}", filename); + let file_path_clone = file_path.clone(); + (url, file_path, file_path_clone) +} + pub async fn async_process_model( mut input_receiver: mpsc::Receiver>, output_transmitter: mpsc::Sender>, @@ -248,6 +298,7 @@ pub struct MessageFromJS { duration: f64, velocity: f32, delay: (Option, Option, Option), + reverb: (Option, Option, Option, Option), orbit: usize, speed: f32, begin: f64, @@ -298,6 +349,12 @@ pub async fn sendwebaudio( delay_time: m.delay.1, feedback: m.delay.2, }, + reverb: ReverbMessage { + room: m.reverb.0, + size: m.reverb.1, + ir: m.reverb.2, + url: m.reverb.3, + }, orbit: m.orbit, speed: m.speed, begin: m.begin,