From c456186f0fc654ea1a91497e4c8af6b56885eb7e Mon Sep 17 00:00:00 2001 From: Vasilii Milovidov Date: Wed, 20 Sep 2023 16:36:03 +0400 Subject: [PATCH] make it closer to the js implementation --- packages/desktopbridge/webaudiobridge.mjs | 61 +++--- src-tauri/src/superdough.rs | 256 ++++++++++++---------- 2 files changed, 170 insertions(+), 147 deletions(-) diff --git a/packages/desktopbridge/webaudiobridge.mjs b/packages/desktopbridge/webaudiobridge.mjs index a1da21034..256f8607d 100644 --- a/packages/desktopbridge/webaudiobridge.mjs +++ b/packages/desktopbridge/webaudiobridge.mjs @@ -1,6 +1,6 @@ import {Invoke} from './utils.mjs'; import {duration, isNote, midiToFreq, noteToMidi, Pattern} from '@strudel.cycles/core'; -import {getAudioContext} from '@strudel.cycles/webaudio'; +import {getAudioContext, getEnvelope} from '@strudel.cycles/webaudio'; export const desktopAudio = async (value, deadline, hapDuration) => { const ac = getAudioContext(); @@ -49,10 +49,10 @@ export const desktopAudio = async (value, deadline, hapDuration) => { loop = 0, loopBegin = 0, loopEnd = 1, - attack, - decay, - sustain, - release, + attack = 0.001, + decay = 0.05, + sustain = 1, + release = 0.001, lpattack = 0.0001, lpdecay = 0.2, lpsustain = 0.6, @@ -71,8 +71,6 @@ export const desktopAudio = async (value, deadline, hapDuration) => { } = value; value.duration = hapDuration; - console.log(value.duration); - console.log(t); if (isNote(note)) { note = noteToMidi(note); } @@ -83,49 +81,44 @@ export const desktopAudio = async (value, deadline, hapDuration) => { delaytime = Math.abs(delaytime); } - let adsr_on = 1; + let adsr_on = attack !== 0.001 || decay !== 0.05 || sustain !== 1 || release !== 0.001 ? 1 : 0; - if (attack === undefined || decay === undefined || sustain === undefined || release === undefined) { - adsr_on = 0; - attack = 0.001; - decay = 0.001; - sustain = 1; - release = 2; - } - - const loop_packaged = [loop, loopBegin, loopEnd]; - const delay_packaged = [delay, delayfeedback, delaytime]; - const lpf_packaged = [cutoff, resonance]; - const hpf_packaged = [hcutoff, hresonance]; - const band_packaged = [bandf, bandq]; - const adsr_packaged = [attack, decay, sustain, release, adsr_on]; - const lpenv_packaged = [lpattack, lpdecay, lpsustain, lprelease, lpenv]; - const hpenv_packaged = [hpattack, hpdecay, hpsustain, hprelease, hpenv]; - const bpenv_packaged = [bpattack, bpdecay, bpsustain, bprelease, bpenv]; + const packages = { + loop: [loop, loopBegin, loopEnd], + delay: [delay, delayfeedback, delaytime], + lpf: [cutoff, resonance], + hpf: [hcutoff, hresonance], + bpf: [bandf, bandq], + adsr: [attack, decay, sustain, release, adsr_on], + lpenv: [lpattack, lpdecay, lpsustain, lprelease, lpenv], + 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: lpf_packaged, - hpf: hpf_packaged, - bpf: band_packaged, + lpf: packages.lpf, + hpf: packages.hpf, + bpf: packages.bpf, duration: hapDuration, velocity: velocity, - delay: delay_packaged, + delay: packages.delay, speed: speed, begin: begin, end: end, - looper: loop_packaged, - adsr: adsr_packaged, - lpenv: lpenv_packaged, - hpenv: hpenv_packaged, - bpenv: bpenv_packaged, + looper: packages.loop, + adsr: packages.adsr, + lpenv: packages.lpenv, + hpenv: packages.hpenv, + bpenv: packages.bpenv, }); if (messagesfromjs.length) { diff --git a/src-tauri/src/superdough.rs b/src-tauri/src/superdough.rs index fa37c10e4..56d6d7659 100644 --- a/src-tauri/src/superdough.rs +++ b/src-tauri/src/superdough.rs @@ -1,6 +1,6 @@ use std::fs::File; use web_audio_api::context::{AudioContext, BaseAudioContext}; -use web_audio_api::node::{AudioNode, AudioScheduledSourceNode, BiquadFilterNode, BiquadFilterType, GainNode, OscillatorType}; +use web_audio_api::node::{AudioBufferSourceNode, AudioNode, AudioScheduledSourceNode, BiquadFilterNode, BiquadFilterType, GainNode, OscillatorNode, OscillatorType}; use web_audio_api::node::BiquadFilterType::{Bandpass, Highpass, Lowpass}; use crate::webaudiobridge::WebAudioMessage; @@ -56,88 +56,47 @@ pub struct FilterADSR { pub fn superdough(message: &WebAudioMessage, context: &mut AudioContext) { let now = context.current_time(); - let env = context.create_gain(); - env.gain().set_value(0.); - env.connect(&context.destination()); - let filters = create_filters(context, message); - - let connect_filter_nodes = |node: &dyn AudioNode, filters: &Vec, env: &GainNode| { - if let Some(first_filter) = filters.first() { - if let Some(last_filter) = filters.last() { - node.connect(first_filter); - last_filter.connect(env); - } - } else { - node.connect(env); - } - }; + let mut chain: Vec<&dyn AudioNode> = Vec::new(); + // Play synth or sample match message.waveform.as_str() { "sine" | "square" | "triangle" | "saw" => { + + // Create nodes for synth playback let osc = context.create_oscillator(); - osc.set_type(create_osc_type(&message)); - osc.frequency().set_value(message.note); - connect_filter_nodes(&osc, &filters, &env); - osc.start_at(now); - if message.adsr.adsr_on == 1 { - apply_synth_adsr(&env, message, now); - } else { - apply_default_synth_adsr(&env, message, now); + chain.push(&osc); + let filters = create_filters(context, message); + for f in &filters { + chain.push(f); } - // apply_synth_adsr(&env, message, now); - for f in filters { - apply_filter_adsr(&f, message, &f.type_(), now); - } - osc.stop_at(now + message.duration + message.adsr.release); + let env = context.create_gain(); + chain.push(&env); + + // Connect nodes and play synth + connect_nodes(chain, context); + play_synth(&message, now, osc, filters, &env); } _ => { match File::open(format!("samples/{}/{}.wav", message.bank, message.waveform)) { Ok(file) => { + + // Create nodes for sample playback let audio_buffer = context.decode_audio_data_sync(file).unwrap(); let audio_buffer_duration = audio_buffer.duration(); let src = context.create_buffer_source(); src.set_buffer(audio_buffer); - connect_filter_nodes(&src, &filters, &env); - src.playback_rate().set_value(message.speed); - - 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(), - ); - if message.adsr.adsr_on == 1 { - apply_drum_adsr(&env, message, now); - } else { - apply_default_drum_adsr(&env, message, now); - } - for f in filters { - apply_filter_adsr(&f, message, &f.type_(), now); - } - src.stop_at(now + message.duration + message.adsr.release); - } else { - src.start_at_with_offset( - now, - start_at, - ); - if message.adsr.adsr_on == 1 { - apply_drum_adsr(&env, message, now); - } else { - apply_default_drum_adsr(&env, message, now); - } - for f in filters { - apply_filter_adsr(&f, message, &f.type_(), now); - }; - src.stop_at(stop_at); + chain.push(&src); + let filters = create_filters(context, message); + for f in &filters { + chain.push(f); } + let env = context.create_gain(); + chain.push(&env); + + // Connect nodes and play sample + connect_nodes(chain, context); + src.playback_rate().set_value(message.speed); + play_sample(message, now, audio_buffer_duration, src, filters, &env); } Err(e) => eprintln!("Failed to open file: {:?}", e), } @@ -145,6 +104,20 @@ pub fn superdough(message: &WebAudioMessage, context: &mut AudioContext) { } } +fn play_synth(message: &&WebAudioMessage, now: f64, osc: OscillatorNode, filters: Vec ,env: &GainNode) { + 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); + } + osc.stop_at(now + message.duration + message.adsr.release); +} + fn create_osc_type(message: &WebAudioMessage) -> OscillatorType { match message.waveform.as_str() { "sine" => OscillatorType::Sine, @@ -155,6 +128,44 @@ fn create_osc_type(message: &WebAudioMessage) -> OscillatorType { } } +fn play_sample(message: &WebAudioMessage, now: f64, audio_buffer_duration: f64, src: AudioBufferSourceNode, filters: Vec, env: &GainNode) { + 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), + } + for f in filters { + apply_filter_adsr(&f, message, &f.type_(), now); + } + 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); + }; + src.stop_at(stop_at); + } +} + fn create_filters(context: &mut AudioContext, message: &WebAudioMessage) -> Vec { let mut filters = Vec::new(); @@ -193,50 +204,6 @@ fn create_filters(context: &mut AudioContext, message: &WebAudioMessage) -> Vec< filters } -fn apply_synth_adsr(envelope: &GainNode, message: &WebAudioMessage, now: f64) { - envelope.gain() - .set_value_at_time(0., now) - .linear_ramp_to_value_at_time(message.velocity, now + message.adsr.attack) - .linear_ramp_to_value_at_time( - message.adsr.sustain * message.velocity, - now + message.adsr.attack + message.adsr.decay, - ) - .linear_ramp_to_value_at_time(0.0, now + message.duration + message.adsr.release); -} - -fn apply_default_synth_adsr(envelope: &GainNode, message: &WebAudioMessage, now: f64) { - envelope.gain() - .set_value_at_time(0., now) - .linear_ramp_to_value_at_time(message.velocity, now + 0.001) - .linear_ramp_to_value_at_time( - message.velocity, - now + 0.001 + 0.05, - ) - .linear_ramp_to_value_at_time(0.0, now + message.duration + 0.001); -} - -fn apply_drum_adsr(envelope: &GainNode, message: &WebAudioMessage, now: f64) { - envelope.gain() - .set_value_at_time(0., now) - .linear_ramp_to_value_at_time(message.velocity, now + message.adsr.attack) - .linear_ramp_to_value_at_time( - message.adsr.sustain * message.velocity, - now + message.adsr.attack + message.adsr.decay, - ) - .linear_ramp_to_value_at_time(0.0, now + message.duration + message.adsr.release); -} - -fn apply_default_drum_adsr(envelope: &GainNode, message: &WebAudioMessage, now: f64) { - envelope.gain() - .set_value_at_time(0., now) - .linear_ramp_to_value_at_time(message.velocity, now + 0.001) - .linear_ramp_to_value_at_time( - 1.0 * message.velocity, - now + 0.001 + 0.001, - ) - .linear_ramp_to_value_at_time(0.0, now + message.duration + 0.001); -} - fn apply_filter_adsr(filter_node: &BiquadFilterNode, message: &WebAudioMessage, filter: &BiquadFilterType, now: f64) { let env = match filter { Lowpass => message.lpenv, @@ -266,3 +233,66 @@ fn apply_filter_adsr(filter_node: &BiquadFilterNode, message: &WebAudioMessage, 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., now) + .linear_ramp_to_value_at_time(message.velocity, now + attack) + .linear_ramp_to_value_at_time( + sustain * message.velocity, + now + attack + decay, + ) + .set_value_at_time(sustain * message.velocity, now + message.duration) + .linear_ramp_to_value_at_time(0.0, 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.001); +} + +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.05, 0.001); +} + +fn connect_nodes<'a>(nodes: Vec<&'a (dyn AudioNode + 'a)>, context: &'a mut AudioContext) -> 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(&context.destination()); + } + nodes +} \ No newline at end of file