From 38af4af4ff321f0cebf545633c55890abdc9b9d5 Mon Sep 17 00:00:00 2001 From: Vasilii Milovidov Date: Fri, 29 Sep 2023 13:09:27 +0400 Subject: [PATCH] Adding structs and traits --- packages/desktopbridge/webaudiobridge.mjs | 38 +-- src-tauri/Cargo.lock | 122 +++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/superdough.rs | 396 ++++++++-------------- src-tauri/src/webaudiobridge.rs | 187 ++++++---- 5 files changed, 419 insertions(+), 325 deletions(-) diff --git a/packages/desktopbridge/webaudiobridge.mjs b/packages/desktopbridge/webaudiobridge.mjs index 62d4f1667..895b15460 100644 --- a/packages/desktopbridge/webaudiobridge.mjs +++ b/packages/desktopbridge/webaudiobridge.mjs @@ -6,8 +6,8 @@ export const desktopAudio = async (value, deadline, hapDuration) => { const ac = getAudioContext(); if (typeof value !== 'object') { throw new Error( - `expected hap.value to be an object, but got "${value}". Hint: append .note() or .s() to the end`, - 'error', + `expected hap.value to be an object, but got "${value}". Hint: append .note() or .s() to the end`, + 'error', ); } @@ -15,7 +15,7 @@ export const desktopAudio = async (value, deadline, hapDuration) => { let { note = 'C2', - s, + s = 'triangle', bank, source, gain = 0.8, @@ -50,24 +50,24 @@ export const desktopAudio = async (value, deadline, hapDuration) => { loopBegin = 0, loopEnd = 1, attack = 0.001, - decay = 0.05, + decay = 0.005, sustain = 1, - release = 0.1, + release = 0.001, lpattack = 0.0001, lpdecay = 0.2, lpsustain = 0.6, lprelease = 0.2, - lpenv = 1, + lpenv = 0, hpattack = 0.0001, hpdecay = 0.2, hpsustain = 0.6, hprelease = 0.2, - hpenv = 1, + hpenv = 0, bpattack = 0.0001, bpdecay = 0.2, bpsustain = 0.6, bprelease = 0.2, - bpenv = 1, + bpenv = 0, n = 0, freq, } = value; @@ -101,21 +101,21 @@ export const desktopAudio = async (value, deadline, hapDuration) => { let map = getSound(s).data.samples; if (Array.isArray(map)) { sampleUrl = - path !== undefined ? path + map[n % map.length].replace('./', '') : map[n % map.length].replace('./', ''); + path !== undefined ? path + map[n % map.length].replace('./', '') : map[n % map.length].replace('./', ''); } else { const midiDiff = (noteA) => noteToMidi(noteA) - midi; // object format will expect keys as notes const closest = Object.keys(map) - .filter((k) => !k.startsWith('_')) - .reduce( - (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), - null, - ); + .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 + map[closest][n % map[closest].length].replace('./', '') - : map[closest][n % map[closest].length].replace('./', ''); + path !== undefined + ? path + map[closest][n % map[closest].length].replace('./', '') + : map[closest][n % map[closest].length].replace('./', ''); } } if (isNote(note)) { @@ -183,9 +183,9 @@ const hap2value = (hap) => { return { ...hap.value, velocity: hap.context.velocity }; }; export const webaudioDesktopOutputTrigger = (t, hap, ct, cps) => - desktopAudio(hap2value(hap), t - ct, hap.duration / cps, cps); + desktopAudio(hap2value(hap), t - ct, hap.duration / cps, cps); export const webaudioDesktopOutput = (hap, deadline, hapDuration) => - desktopAudio(hap2value(hap), deadline, hapDuration); + desktopAudio(hap2value(hap), deadline, hapDuration); Pattern.prototype.webaudio = function () { return this.onTrigger(webaudioDesktopOutputTrigger); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4eab45d70..81390e5f0 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -110,6 +110,7 @@ name = "app" version = "0.1.0" dependencies = [ "midir", + "mini-moka", "quick_cache", "reqwest", "rosc", @@ -280,6 +281,12 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" +[[package]] +name = "bytecount" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" + [[package]] name = "bytemuck" version = "1.13.1" @@ -328,6 +335,37 @@ dependencies = [ "system-deps 6.1.1", ] +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cfa25e60aea747ec7e1124f238816749faa93759c6ff5b31f1ccdda137f4479" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + [[package]] name = "cargo_toml" version = "0.15.3" @@ -746,6 +784,19 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.0", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "dasp_sample" version = "0.11.0" @@ -878,6 +929,15 @@ dependencies = [ "libc", ] +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -1930,6 +1990,21 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mini-moka" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e0b72e7c9042467008b10279fc732326bd605459ae03bda88825909dd19b56" +dependencies = [ + "crossbeam-channel", + "crossbeam-utils", + "dashmap", + "skeptic", + "smallvec", + "tagptr", + "triomphe", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2545,6 +2620,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998" +dependencies = [ + "bitflags 1.3.2", + "memchr", + "unicase", +] + [[package]] name = "quick-xml" version = "0.28.2" @@ -3142,6 +3228,21 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +[[package]] +name = "skeptic" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +dependencies = [ + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + [[package]] name = "slab" version = "0.4.8" @@ -3489,6 +3590,12 @@ dependencies = [ "version-compare 0.1.1", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tao" version = "0.16.2" @@ -4032,6 +4139,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "triomphe" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee8098afad3fb0c54a9007aab6804558410503ad676d4633f9c2559a00ac0f" + [[package]] name = "try-lock" version = "0.2.4" @@ -4044,6 +4157,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 23bd84d21..78fe567d9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,6 +24,7 @@ rosc = "0.10.1" web-audio-api = { git = "https://github.com/orottier/web-audio-api-rs.git", branch = "main" } reqwest = "0.11.20" quick_cache = "0.4.0" +mini-moka = "0.10.2" [features] # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. diff --git a/src-tauri/src/superdough.rs b/src-tauri/src/superdough.rs index fece84d79..09f11a127 100644 --- a/src-tauri/src/superdough.rs +++ b/src-tauri/src/superdough.rs @@ -54,206 +54,170 @@ pub struct FilterADSR { pub env: f64, } -pub fn superdough_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 delay = context.create_delay(1.); - 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, &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 trait WebAudioPlayer { + fn set_adsr(&mut self, t: f64, adsr: &ADSR, velocity: f32, duration: f64); + fn play(&mut self, t: f64, message: &WebAudioMessage, duration: f64,); + fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec; } +pub struct Synth { + pub oscillator: OscillatorNode, + pub envelope: GainNode, +} -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.); - - 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); +impl Synth { + pub fn new(context: &mut AudioContext, message: &WebAudioMessage) -> Self { + let oscillator = context.create_oscillator(); + let envelope = context.create_gain(); + Self { oscillator, envelope } } - 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, &compressor); - let osc = play_synth(&message, now, osc, filters, &env); - - - osc.set_onended(move |_| { - delay.disconnect(); - }); -} - -fn create_delay(message: &WebAudioMessage, context: &mut AudioContext, env: &GainNode, delay: &DelayNode, now: f64, compressor: &DynamicsCompressorNode) { - let output = context.create_gain(); - output.connect(compressor); - - delay.delay_time().set_value(message.delay.delay_time); - delay.connect(&output); - - let feedback = context.create_gain(); - feedback.gain().set_value(message.delay.feedback); - feedback.connect(delay); - delay.connect(&feedback); - - let pre_gain = context.create_gain(); - pre_gain.gain().set_value_at_time(message.delay.wet, now + message.delay.delay_time as f64); - pre_gain.connect(&feedback); - - let input = context.create_gain(); - input.connect(&pre_gain); - env.connect(&input); -} - -fn play_synth(message: &&WebAudioMessage, now: f64, mut osc: OscillatorNode, filters: Vec, env: &GainNode) -> OscillatorNode { - osc.set_type(create_osc_type(&message)); - osc.frequency().set_value(message.note); - osc.start(); - match message.adsr.adsr_on { - 1 => apply_synth_adsr(&env, message, now), - _ => apply_default_synth_adsr(&env, message, now), - }; - for f in filters { - apply_filter_adsr(&f, message, &f.type_(), now); + pub fn set_frequency(&mut self, frequency: &f32) { + self.oscillator.frequency().set_value(*frequency); } - osc.stop_at(now + message.duration + message.adsr.release + 2.); - osc -} - -fn create_osc_type(message: &WebAudioMessage) -> OscillatorType { - match message.waveform.as_str() { - "sine" => OscillatorType::Sine, - "square" => OscillatorType::Square, - "triangle" => OscillatorType::Triangle, - "saw" => OscillatorType::Sawtooth, - "sawtooth" => OscillatorType::Sawtooth, - _ => panic!("Invalid oscillator type"), - } -} - -fn play_sample(message: &WebAudioMessage, now: f64, audio_buffer_duration: f64, mut src: AudioBufferSourceNode, filters: Vec, env: &GainNode) -> AudioBufferSourceNode { - let (start_at, stop_at) = if message.speed < 0.0 { - (audio_buffer_duration, now + message.duration + 0.2) - } else { - (message.begin * audio_buffer_duration, now + message.duration + message.adsr.release) - }; - if message.looper.is_loop > 0 { - src.set_loop(true); - src.set_loop_start(message.looper.loop_start); - src.set_loop_end(message.looper.loop_end); - src.start_at_with_offset( - now, - src.loop_start(), - ); - match message.adsr.adsr_on { - 1 => apply_drum_adsr(&env, message, now), - _ => apply_default_drum_adsr(&env, message, now), + pub fn set_waveform(&mut self, waveform: &str) { + match waveform { + "sine" => self.oscillator.set_type(OscillatorType::Sine), + "square" => self.oscillator.set_type(OscillatorType::Square), + "triangle" => self.oscillator.set_type(OscillatorType::Triangle), + "saw" | "sawtooth" => self.oscillator.set_type(OscillatorType::Sawtooth), + _ => {} } - for f in filters { - apply_filter_adsr(&f, message, &f.type_(), now); + } +} + +impl WebAudioPlayer for Synth { + fn set_adsr(&mut self, t: f64, adsr: &ADSR, velocity: f32, duration: f64) { + self.envelope.gain() + .set_value_at_time(0.0, t) + .linear_ramp_to_value_at_time(velocity, t + adsr.attack) + .linear_ramp_to_value_at_time((adsr.sustain + 0.001) * velocity, t + adsr.attack + adsr.decay) + .set_value_at_time(adsr.sustain * velocity, t + duration) + .linear_ramp_to_value_at_time(0.0, t + duration + adsr.release); + } + + fn play(&mut self, t: f64, message: &WebAudioMessage, release: f64) { + self.oscillator.start(); + self.oscillator.stop_at(t + message.duration + release); + } + + fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec { + let mut filters = Vec::new(); + if message.bpf.frequency > 0.0 { + let mut bpf = context.create_biquad_filter(); + bpf.set_type(Bandpass); + bpf.frequency().set_value(message.bpf.frequency); + bpf.q().set_value(message.bpf.resonance); + filters.push(bpf); + } else if message.lpf.frequency > 0.0 { + let mut lpf = context.create_biquad_filter(); + lpf.set_type(Lowpass); + lpf.frequency().set_value(message.lpf.frequency); + lpf.q().set_value(message.lpf.resonance); + filters.push(lpf); + } else if message.hpf.frequency > 0.0 { + let mut hpf = context.create_biquad_filter(); + hpf.set_type(Highpass); + hpf.frequency().set_value(message.hpf.frequency); + hpf.q().set_value(message.hpf.resonance); + filters.push(hpf); } - src.stop_at(now + message.duration + message.adsr.release); - } else { - src.start_at_with_offset( - now, - start_at, - ); - match message.adsr.adsr_on { - 1 => apply_drum_adsr(&env, message, now), - _ => apply_default_drum_adsr(&env, message, now), - } - for f in filters { - apply_filter_adsr(&f, message, &f.type_(), now); + + if !filters.is_empty() { + self.oscillator.connect(filters.first().unwrap()); + filters.last().unwrap().connect(&self.envelope); + } else { + self.oscillator.connect(&self.envelope); }; - src.stop_at(stop_at); + + filters } - src } -fn create_filters(context: &mut AudioContext, message: &WebAudioMessage) -> Vec { - let mut filters = Vec::new(); - - if message.bpf.frequency > 0.0 { - let mut bpf = context.create_biquad_filter(); - bpf.set_type(Bandpass); - bpf.frequency().set_value(message.bpf.frequency); - bpf.q().set_value(message.bpf.resonance); - filters.push(bpf); - } else if message.lpf.frequency > 0.0 && message.hpf.frequency > 0.0 { - let mut lpf = context.create_biquad_filter(); - let mut hpf = context.create_biquad_filter(); - lpf.set_type(Lowpass); - lpf.frequency().set_value(message.lpf.frequency); - lpf.q().set_value(message.lpf.resonance); - hpf.set_type(Highpass); - hpf.frequency().set_value(message.hpf.frequency); - hpf.q().set_value(message.hpf.resonance); - lpf.connect(&hpf); - filters.push(lpf); - filters.push(hpf); - } else if message.lpf.frequency > 0.0 { - let mut lpf = context.create_biquad_filter(); - lpf.set_type(Lowpass); - lpf.frequency().set_value(message.lpf.frequency); - lpf.q().set_value(message.lpf.resonance); - filters.push(lpf); - } else if message.hpf.frequency > 0.0 { - let mut hpf = context.create_biquad_filter(); - hpf.set_type(Highpass); - hpf.frequency().set_value(message.hpf.frequency); - hpf.q().set_value(message.hpf.resonance); - filters.push(hpf); - } - - filters +pub struct Sampler { + pub sample: AudioBufferSourceNode, + pub envelope: GainNode, } -fn apply_filter_adsr(filter_node: &BiquadFilterNode, message: &WebAudioMessage, filter: &BiquadFilterType, now: f64) { +impl Sampler { + pub fn new(context: &mut AudioContext, message: &WebAudioMessage, audio_buffer: AudioBuffer) -> Self { + let mut sample = context.create_buffer_source(); + sample.set_buffer(audio_buffer); + let envelope = context.create_gain(); + Self { sample, envelope } + } +} + +impl WebAudioPlayer for Sampler { + fn set_adsr(&mut self, t: f64, adsr: &ADSR, velocity: f32, duration: f64) { + self.envelope.gain() + .set_value_at_time(0.0, t) + .linear_ramp_to_value_at_time(velocity, t + adsr.attack) + .linear_ramp_to_value_at_time((adsr.sustain + 0.00001) * velocity, t + adsr.attack + adsr.decay) + .set_value_at_time(adsr.sustain * velocity, t + duration) + .linear_ramp_to_value_at_time(0.0, t + duration + adsr.release); + } + + fn play(&mut self, t: f64, message: &WebAudioMessage, release: f64) { + let buffer_duration = release; + let (start_at, stop_at) = if message.speed < 0.0 { + (buffer_duration, t + message.duration + 0.2) + } else { + (message.begin * buffer_duration, t + message.duration + message.adsr.release) + }; + if message.looper.is_loop > 0 { + self.sample.set_loop(true); + self.sample.set_loop_start(message.looper.loop_start); + self.sample.set_loop_end(message.looper.loop_end); + self.sample.start_at_with_offset( + t, + self.sample.loop_start(), + ); + self.sample.stop_at(t + message.duration + message.adsr.release); + } else { + self.sample.start_at_with_offset( + t, + start_at, + ); + self.sample.stop_at(stop_at); + } + } + + fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec { + let mut filters = Vec::new(); + if message.bpf.frequency > 0.0 { + let mut bpf = context.create_biquad_filter(); + bpf.set_type(Bandpass); + bpf.frequency().set_value(message.bpf.frequency); + bpf.q().set_value(message.bpf.resonance); + filters.push(bpf); + } else if message.lpf.frequency > 0.0 { + let mut lpf = context.create_biquad_filter(); + lpf.set_type(Lowpass); + lpf.frequency().set_value(message.lpf.frequency); + lpf.q().set_value(message.lpf.resonance); + filters.push(lpf); + } else if message.hpf.frequency > 0.0 { + let mut hpf = context.create_biquad_filter(); + hpf.set_type(Highpass); + hpf.frequency().set_value(message.hpf.frequency); + hpf.q().set_value(message.hpf.resonance); + filters.push(hpf); + } + + if !filters.is_empty() { + self.sample.connect(filters.first().unwrap()); + filters.last().unwrap().connect(&self.envelope); + } else { + self.sample.connect(&self.envelope); + }; + filters + } +} + +pub fn apply_filter_adsr(filter_node: &BiquadFilterNode, message: &WebAudioMessage, filter: &BiquadFilterType, now: f64) { let env = match filter { Lowpass => message.lpenv, Highpass => message.hpenv, @@ -281,67 +245,3 @@ fn apply_filter_adsr(filter_node: &BiquadFilterNode, message: &WebAudioMessage, filter_node.frequency().set_value_at_time(sustain_level, now + message.duration); filter_node.frequency().linear_ramp_to_value_at_time(min, now + message.duration + env.release.max(0.1)); } - -//TODO fix envelopes -fn apply_adsr( - envelope: &GainNode, - message: &WebAudioMessage, - now: f64, - attack: f64, - sustain: f32, - decay: f64, - release: f64, -) { - envelope - .gain() - .set_value_at_time(0.0, now) - .linear_ramp_to_value_at_time(message.velocity, now + attack) - .exponential_ramp_to_value_at_time( - (sustain + 0.00001) * message.velocity, - now + attack + decay, - ) - .set_value_at_time(sustain * message.velocity, now + message.duration) - .linear_ramp_to_value_at_time(0.00001, now + message.duration + release); -} - -fn apply_synth_adsr(envelope: &GainNode, message: &WebAudioMessage, now: f64) { - apply_adsr( - envelope, - message, - now, - message.adsr.attack, - message.adsr.sustain, - message.adsr.decay, - message.adsr.release, - ); -} - -fn apply_default_synth_adsr(envelope: &GainNode, message: &WebAudioMessage, now: f64) { - apply_adsr(envelope, message, now, 0.001, message.velocity, 0.05, 0.07); -} - -fn apply_drum_adsr(envelope: &GainNode, message: &WebAudioMessage, now: f64) { - apply_adsr( - envelope, - message, - now, - message.adsr.attack, - message.adsr.sustain, - message.adsr.decay, - message.adsr.release, - ); -} - -fn apply_default_drum_adsr(envelope: &GainNode, message: &WebAudioMessage, now: f64) { - apply_adsr(envelope, message, now, 0.001, message.velocity, 0.5, 0.5); -} - -fn connect_nodes<'a>(nodes: Vec<&'a (dyn AudioNode + 'a)>, compressor: &DynamicsCompressorNode) -> Vec<&'a (dyn AudioNode)> { - for i in 0..nodes.len() - 1 { - nodes[i].connect(nodes[i + 1]); - } - if let Some(last_node) = nodes.last() { - last_node.connect(compressor); - } - nodes -} \ No newline at end of file diff --git a/src-tauri/src/webaudiobridge.rs b/src-tauri/src/webaudiobridge.rs index 5995ee896..340625df4 100644 --- a/src-tauri/src/webaudiobridge.rs +++ b/src-tauri/src/webaudiobridge.rs @@ -4,19 +4,22 @@ use std::{ }; use std::fs::File; use std::path::Path; -use quick_cache::sync::Cache; -// use mini_moka::sync::Cache; +use std::sync::mpsc::{RecvError, SendError}; +use std::time::{Instant, SystemTime}; +use mini_moka::sync::Cache; use reqwest::Url; -use tokio::{fs, sync::{mpsc, Mutex}, time::Instant}; +use tokio::{fs, sync::{mpsc, Mutex}}; use serde::Deserialize; use tokio::io::AsyncWriteExt; use web_audio_api::{AudioBuffer, context::{AudioContext, AudioContextLatencyCategory, AudioContextOptions}}; use web_audio_api::context::BaseAudioContext; -use crate::superdough::{ADSR, BPF, Delay, FilterADSR, HPF, Loop, LPF, superdough_sample, superdough_synth}; +use web_audio_api::node::{AudioBufferSourceNode, AudioNode, AudioScheduledSourceNode, BiquadFilterNode, DelayNode, DynamicsCompressorNode, GainNode, OscillatorNode, OscillatorType}; +use web_audio_api::node::BiquadFilterType::{Bandpass, Highpass, Lowpass}; +use crate::superdough::{ADSR, apply_filter_adsr, BPF, Delay, FilterADSR, HPF, Loop, LPF, Sampler, Synth, WebAudioPlayer}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct WebAudioMessage { pub note: f32, pub instant: Instant, @@ -52,6 +55,8 @@ pub fn init( async_output_transmitter: mpsc::Sender>, ) { tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await }); + let (sender, receiver) = std::sync::mpsc::channel(); + let message_queue: Arc>> = Arc::new(Mutex::new(Vec::new())); /* ........................................................... Listen For incoming messages and add to queue @@ -70,20 +75,7 @@ pub fn init( }); let message_queue_clone = Arc::clone(&message_queue); - - /* ........................................................... - Prepare audio context - ............................................................*/ - let latency_hint = match std::env::var("WEB_AUDIO_LATENCY").as_deref() { - Ok("playback") => AudioContextLatencyCategory::Playback, - _ => AudioContextLatencyCategory::default(), - }; - let mut audio_context = AudioContext::new(AudioContextOptions { - latency_hint, - ..AudioContextOptions::default() - }); tauri::async_runtime::spawn(async move { - let cache: Cache = Cache::new(10_000); /* ........................................................... Process queued messages ............................................................*/ @@ -96,51 +88,130 @@ pub fn init( return true; }; - match message.waveform.as_str() { - "sine" | "square" | "triangle" | "saw" | "sawtooth" => { - superdough_synth(message.clone(), &mut audio_context); - } - _ => { - 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); + match sender.send(message.clone()) { + Ok(_) => {} + Err(_) => {} + }; - if let Some(audio_buffer) = cache.get(&file_path) { - superdough_sample( - message.clone(), - &mut audio_context, - audio_buffer.clone(), - ); - } else if let Ok(file) = File::open(&file_path) { - let audio_buffer = audio_context.decode_audio_data_sync(file) - .unwrap_or_else(|_| panic!("Failed to decode audio data")); - cache.insert(file_path.clone(), audio_buffer.clone()); - } - - tokio::spawn(async move { - if tokio::fs::metadata(&file_path).await.is_err() { - let response = reqwest::get(url) - .await - .unwrap_or_else(|_| panic!("Failed to send GET request")); - - let bytes = response.bytes().await.unwrap(); - let path = Path::new(&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")); - } - }); - } - } return false; }); tokio::time::sleep(Duration::from_millis(1)).await; } }); + + /* ........................................................... + Create and setup WebAudio + ............................................................*/ + tauri::async_runtime::spawn(async move { + let latency_hint = match std::env::var("WEB_AUDIO_LATENCY").as_deref() { + Ok("playback") => AudioContextLatencyCategory::Playback, + _ => AudioContextLatencyCategory::default(), + }; + let mut audio_context = AudioContext::new(AudioContextOptions { + latency_hint, + ..AudioContextOptions::default() + }); + let mut temp_src = audio_context.create_buffer_source(); + let compressor = audio_context.create_dynamics_compressor(); + compressor.threshold().set_value(-50.0); + compressor.connect(&audio_context.destination()); + let delay = audio_context.create_delay(1.); + let output = audio_context.create_gain(); + output.connect(&compressor); + delay.connect(&output); + let feedback = audio_context.create_gain(); + feedback.connect(&delay); + delay.connect(&feedback); + let pre_gain = audio_context.create_gain(); + pre_gain.connect(&feedback); + let input = audio_context.create_gain(); + input.connect(&pre_gain); + let cache: Cache = Cache::builder() + .max_capacity(31 * 1024 * 1024) + .time_to_idle(Duration::from_secs(40)) + .build(); + loop { + match receiver.recv() { + Ok(message) => { + match message.waveform.as_str() { + "sine" | "square" | "triangle" | "saw" | "sawtooth" => { + let t = audio_context.current_time(); + delay.delay_time().set_value(message.delay.delay_time); + feedback.gain().set_value(message.delay.feedback); + pre_gain.gain().set_value_at_time(message.delay.wet, t + message.delay.delay_time as f64); + + let mut synth = Synth::new(&mut audio_context, &message); + synth.set_frequency(&message.note); + synth.set_waveform(&message.waveform); + let filters = synth.set_filters(&mut audio_context, &message); + if message.delay.wet > 0.0 { + synth.envelope.connect(&input); + } + synth.envelope.connect(&compressor); + synth.play(t, &message, message.adsr.release); + synth.set_adsr(t, &message.adsr, message.velocity, message.duration); + if message.lpenv.env > 0.0 || message.hpenv.env > 0.0 || message.bpenv.env > 0.0 { + for f in filters { + apply_filter_adsr(&f, &message, &f.type_(), t); + } + } + } + _ => { + 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(); + + tokio::spawn(async move { + if tokio::fs::metadata(&file_path.clone()).await.is_err() { + let response = reqwest::get(url) + .await + .unwrap_or_else(|_| panic!("Failed to send GET request")); + + let bytes = response.bytes().await.unwrap(); + let path = Path::new(&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(audio_buffer) = cache.get(&file_path_clone) { + let t = audio_context.current_time(); + delay.delay_time().set_value(message.delay.delay_time); + feedback.gain().set_value(message.delay.feedback); + pre_gain.gain().set_value_at_time(message.delay.wet, t + message.delay.delay_time as f64); + let audio_buffer_duration = audio_buffer.duration(); + let mut sampler = Sampler::new(&mut audio_context, &message, audio_buffer); + sampler.sample.playback_rate().set_value(message.speed); + sampler.set_adsr(t, &message.adsr, message.velocity, message.duration); + let filters = sampler.set_filters(&mut audio_context, &message); + if message.lpenv.env > 0.0 || message.hpenv.env > 0.0 || message.bpenv.env > 0.0 { + for f in filters { + apply_filter_adsr(&f, &message, &f.type_(), t); + } + } + if message.delay.wet > 0.0 { + sampler.envelope.connect(&input); + }; + sampler.envelope.connect(&compressor); + sampler.play(t, &message, audio_buffer_duration); + } else if let Ok(file) = File::open(&file_path_clone) { + let audio_buffer = audio_context.decode_audio_data_sync(file) + .unwrap_or_else(|_| panic!("Failed to decode audio data")); + cache.insert(file_path_clone.clone(), audio_buffer); + } + } + } + } + Err(_) => {} + } + } + }); } pub async fn async_process_model( @@ -266,4 +337,4 @@ async fn create_file_and_dirs(path: &Path) -> fs::File { } let file = fs::File::create(path).await.unwrap(); file -} \ No newline at end of file +}