From 592b9829906345879b28ce7341f4f4313a4afa8b Mon Sep 17 00:00:00 2001 From: Vasilii Milovidov Date: Tue, 26 Sep 2023 15:54:39 +0400 Subject: [PATCH] wip --- packages/desktopbridge/webaudiobridge.mjs | 69 +++++++++++--------- src-tauri/src/superdough.rs | 79 ++++++++++++++++++++++- src-tauri/src/webaudiobridge.rs | 76 +++++++++++++--------- 3 files changed, 158 insertions(+), 66 deletions(-) diff --git a/packages/desktopbridge/webaudiobridge.mjs b/packages/desktopbridge/webaudiobridge.mjs index a226cdd9e..6710524c9 100644 --- a/packages/desktopbridge/webaudiobridge.mjs +++ b/packages/desktopbridge/webaudiobridge.mjs @@ -15,7 +15,7 @@ export const desktopAudio = async (value, deadline, hapDuration) => { let { note = 'C3', - s = 'triangle', + s, bank = '', source, gain = 0.8, @@ -70,44 +70,52 @@ export const desktopAudio = async (value, deadline, hapDuration) => { bpenv = 1, n = 0, } = value; - bank = getSound(s).data.samples; - // SAMPLES - value.duration = hapDuration; if (isNote(note)) { note = noteToMidi(note); } - const baseUrl = getSound(s).data.baseUrl; - let path; - if (baseUrl === './piano/') { - path = 'https://strudel.tidalcycles.org/'; - } else if (baseUrl === './EmuSP12/') { - path = 'https://strudel.tidalcycles.org/'; - } - + value.duration = hapDuration; let transpose = 0; transpose = note - 36; let sampleUrl; - let key; - if (Array.isArray(bank)) { - sampleUrl = path !== undefined ? path + bank[n % bank.length].replace('./', '') : bank[n % bank.length].replace('./', ''); + if (s === 'sine' || s === 'square' || s === 'saw' || s === 'triangle') { + sampleUrl = 'none'; } else { - const midiDiff = (noteA) => noteToMidi(noteA) - note; - // object format will expect keys as notes - const closest = Object.keys(bank) - .filter((k) => !k.startsWith('_')) - .reduce( - (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), - null, - ); - transpose = -midiDiff(closest); // semitones to repitch - sampleUrl = path !== undefined ? path + bank[closest][n % bank[closest].length].replace('./', '') : bank[closest][n % bank[closest].length].replace('./', ''); + bank = getSound(s).data.samples; + console.log('bank', bank) + + // if (bank && s) { + // s = `${bank}_${s}`; + // } + + let path = ''; + if (getSound(s).data.baseUrl !== undefined) { + const baseUrl = getSound(s).data.baseUrl; + if (baseUrl === './piano/') { + path = 'https://strudel.tidalcycles.org/'; + } else if (baseUrl === './EmuSP12/') { + path = 'https://strudel.tidalcycles.org/'; + } + } + + if (Array.isArray(bank)) { + sampleUrl = path !== undefined ? path + bank[n % bank.length].replace('./', '') : bank[n % bank.length].replace('./', ''); + } else { + const midiDiff = (noteA) => noteToMidi(noteA) - note; + // object format will expect keys as notes + const closest = Object.keys(bank) + .filter((k) => !k.startsWith('_')) + .reduce( + (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), + null, + ); + transpose = -midiDiff(closest); // semitones to repitch + sampleUrl = path !== undefined ? path + bank[closest][n % bank[closest].length].replace('./', '') : bank[closest][n % bank[closest].length].replace('./', ''); + } } - + console.log('sampleUrl', sampleUrl) const playbackRate = 1.0 * Math.pow(2, transpose / 12); - // const sound = getSoundData(s); - // SYNTHS if (delay !== 0) { delay = Math.abs(delay); delayfeedback = Math.abs(delayfeedback); @@ -127,16 +135,13 @@ export const desktopAudio = async (value, deadline, hapDuration) => { hpenv: [hpattack, hpdecay, hpsustain, hprelease, hpenv], bpenv: [bpattack, bpdecay, bpsustain, bprelease, bpenv], }; - const offset = (t - getAudioContext().currentTime) * 1000; const roundedOffset = Math.round(offset); const messagesfromjs = []; - messagesfromjs.push({ note: midiToFreq(note), offset: roundedOffset, waveform: s, - bank: bank, lpf: packages.lpf, hpf: packages.hpf, bpf: packages.bpf, @@ -144,7 +149,7 @@ export const desktopAudio = async (value, deadline, hapDuration) => { velocity: velocity, delay: packages.delay, orbit: orbit, - speed: speed, + speed: speed * playbackRate, begin: begin, end: end, looper: packages.loop, diff --git a/src-tauri/src/superdough.rs b/src-tauri/src/superdough.rs index daa870589..ff22c5ea5 100644 --- a/src-tauri/src/superdough.rs +++ b/src-tauri/src/superdough.rs @@ -1,4 +1,5 @@ use std::fs::File; +use web_audio_api::AudioBuffer; use web_audio_api::context::{AudioContext, BaseAudioContext}; use web_audio_api::node::{AudioBufferSourceNode, AudioNode, AudioScheduledSourceNode, BiquadFilterNode, BiquadFilterType, DelayNode, DynamicsCompressorNode, GainNode, OscillatorNode, OscillatorType}; use web_audio_api::node::BiquadFilterType::{Bandpass, Highpass, Lowpass}; @@ -54,6 +55,80 @@ pub struct FilterADSR { pub env: f64, } + +pub fn sample(message: &WebAudioMessage, context: &mut AudioContext, audio_buffer: AudioBuffer) { + let now = context.current_time(); + let mut chain: Vec<&dyn AudioNode> = Vec::new(); + let compressor = context.create_dynamics_compressor(); + compressor.connect(&context.destination()); + compressor.threshold().set_value(-50.0); + let env = context.create_gain(); + let delay = context.create_delay(1.); + // Play synth or sample + + + // Create nodes for sample playback + // let audio_buffer = context.decode_audio_data_sync(file).unwrap(); + let audio_buffer_duration = audio_buffer.duration(); + let mut src = context.create_buffer_source(); + src.set_buffer(audio_buffer); + chain.push(&src); + let filters = create_filters(context, message); + for f in &filters { + chain.push(f); + } + let env = context.create_gain(); + chain.push(&env); + + + if message.delay.wet > 0.0 { + create_delay(message, context, &env, &delay, now, &compressor); + } + // Connect nodes and play sample + connect_nodes(chain, context, &compressor); + src.playback_rate().set_value(message.speed); + let src = play_sample(message, now, audio_buffer_duration, src, filters, &env); + src.set_onended(move |_| { + delay.disconnect(); + }); +} + + +pub fn superdough_synth(message: &WebAudioMessage, context: &mut AudioContext) { + let now = context.current_time(); + let mut chain: Vec<&dyn AudioNode> = Vec::new(); + let compressor = context.create_dynamics_compressor(); + compressor.connect(&context.destination()); + compressor.threshold().set_value(-50.0); + let env = context.create_gain(); + let delay = context.create_delay(1.); + // Play synth or sample + + let osc = context.create_oscillator(); + // Create nodes for synth playback + chain.push(&osc); + let filters = create_filters(context, message); + for f in &filters { + chain.push(f); + } + chain.push(&env); + + + if message.delay.wet > 0.0 { + create_delay(message, context, &env, &delay, now, &compressor); + }; + + // Connect nodes and play synth + connect_nodes(chain, context, &compressor); + let osc = play_synth(&message, now, osc, filters, &env); + + + osc.set_onended(move |_| { + delay.disconnect(); + }); +} + + pub fn superdough(message: &WebAudioMessage, context: &mut AudioContext) { let now = context.current_time(); let mut chain: Vec<&dyn AudioNode> = Vec::new(); @@ -85,11 +160,11 @@ pub fn superdough(message: &WebAudioMessage, context: &mut AudioContext) { osc.set_onended(move |_| { - delay.disconnect(); + delay.disconnect(); }); } _ => { - match File::open(format!("samples/{}/{}.wav", message.bank, message.waveform)) { + match File::open(format!("samples/{}.wav", message.waveform)) { Ok(file) => { // Create nodes for sample playback diff --git a/src-tauri/src/webaudiobridge.rs b/src-tauri/src/webaudiobridge.rs index 23f18c156..168b004af 100644 --- a/src-tauri/src/webaudiobridge.rs +++ b/src-tauri/src/webaudiobridge.rs @@ -3,6 +3,7 @@ use std::{ time::Duration, thread::sleep, }; +use std::fs::File; use mini_moka::sync::Cache; use reqwest::Url; @@ -11,14 +12,12 @@ use tokio::{ time::Instant, }; use serde::Deserialize; +// use tokio::fs::File; use tokio::io::AsyncWriteExt; -use web_audio_api::{ - context::{AudioContext, AudioContextLatencyCategory, AudioContextOptions}, - node::AudioNode, -}; -use crate::superdough::{ADSR, BPF, Delay, FilterADSR, HPF, Loop, LPF, superdough}; +use web_audio_api::{AudioBuffer, context::{AudioContext, AudioContextLatencyCategory, AudioContextOptions}, node::AudioNode}; +use web_audio_api::context::BaseAudioContext; +use crate::superdough::{ADSR, BPF, Delay, FilterADSR, HPF, Loop, LPF, sample, superdough, superdough_synth}; -const BLOCK_SIZE: usize = 128; #[derive(Debug)] pub struct WebAudioMessage { @@ -26,7 +25,6 @@ pub struct WebAudioMessage { pub instant: Instant, pub offset: f64, pub waveform: String, - pub bank: String, pub lpf: LPF, pub hpf: HPF, pub bpf: BPF, @@ -90,8 +88,7 @@ pub fn init( /* ........................................................... Prepare audio context ............................................................*/ -// let cache = Cache::new(10_000); -// let cache_clone_1 = cache.clone(); + let cache:Cache = Cache::new(10_000); /* ........................................................... Process queued messages ............................................................*/ @@ -104,30 +101,47 @@ pub fn init( return true; }; - let url = Url::parse(&*message.sampleurl).unwrap(); - let fname = 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 = "/Users/vasiliymilovidov/samples/".to_owned() + fname; - let url_copy = url.clone(); - - // superdough(message.clone(), &mut audio_context); - - - tokio::spawn(async move { - match tokio::fs::metadata(&file_path).await { - Ok(_) => { - println!("File exists"); - } - Err(_) => { - println!("File does not exist, downloading"); - let resp = reqwest::get(url_copy).await.unwrap().text().await.unwrap(); - let bytes = resp.bytes().await.unwrap(); - let mut out = tokio::fs::File::create(file_path).await.unwrap(); - out.write_all(&bytes).await.unwrap(); - } + match message.waveform.as_str() { + "sine" | "square" | "triangle" | "saw" => { + // superdough(message.clone(), &mut audio_context); + superdough_synth(message.clone(), &mut audio_context); } - }); + _ => { + let url = Url::parse(&*message.sampleurl).unwrap(); + let fname = 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 = "/Users/vasiliymilovidov/samples/".to_owned() + fname; + let url_copy = url.clone(); + let file_path_copy = file_path.clone(); + match cache.get(&file_path_copy) { + Some(buff) => { + sample(message.clone(), &mut audio_context, buff.clone()); + return false; + } + None => { + match File::open(file_path_copy.clone()) { + Ok(file) => { + let buff = audio_context.decode_audio_data_sync(file).unwrap(); + cache.insert(file_path_copy.clone(), buff.clone()); + } + Err(_) => {} + } + } + } + tokio::spawn(async move { + match tokio::fs::metadata(&file_path).await { + Ok(_) => {} + Err(_) => { + let resp = reqwest::get(url_copy).await.unwrap(); + let bytes = resp.bytes().await.unwrap(); + let mut out = tokio::fs::File::create(file_path).await.unwrap(); + out.write_all(&bytes).await.unwrap(); + } + } + }); + } + } return false; }); tokio::time::sleep(Duration::from_millis(1)).await; @@ -151,7 +165,6 @@ pub struct MessageFromJS { note: f32, offset: f64, waveform: String, - bank: String, lpf: (f32, f32), hpf: (f32, f32), bpf: (f32, f32), @@ -186,7 +199,6 @@ pub async fn sendwebaudio( instant: Instant::now(), offset: m.offset, waveform: m.waveform, - bank: m.bank, lpf: LPF { frequency: m.lpf.0, resonance: m.lpf.1,