From 7da75544933d9f5c4b5a20ae7fad611171955e37 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Fri, 15 Dec 2023 11:32:23 -0700 Subject: [PATCH 01/37] it works --- packages/superdough/helpers.mjs | 16 ++++++++++++++++ packages/superdough/sampler.mjs | 5 +++-- packages/superdough/synth.mjs | 11 +++++++++-- src-tauri/src/oscbridge.rs | 4 ++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index d87ea94d..91bc17bc 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -89,6 +89,22 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) { return new DynamicsCompressorNode(ac, options); } +const adsrmin = 0.001; +export const getADSRDefaults = ( + a, + d, + s, + r, + def = { attack: adsrmin, decay: adsrmin, sustain: 1, release: adsrmin }, +) => { + console.log(a, d, s, r); + if (a == null && d == null && s == null && r == null) { + return def; + } + const sustain = s ?? ((a != null && d == null) || (a == null && d == null)) ? def.sustain : adsrmin; + return { attack: a ?? adsrmin, decay: d ?? adsrmin, sustain, release: r ?? adsrmin }; +}; + export function createFilter( context, type, diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 6df5a6b6..e4ead52e 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,6 +1,6 @@ import { noteToMidi, valueToMidi, nanFallback } from './util.mjs'; import { getAudioContext, registerSound } from './index.mjs'; -import { getEnvelope } from './helpers.mjs'; +import { getADSRDefaults, getEnvelope } from './helpers.mjs'; import { logger } from './logger.mjs'; const bufferCache = {}; // string: Promise @@ -251,7 +251,8 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { loop = s.startsWith('wt_') ? 1 : value.loop; const ac = getAudioContext(); // destructure adsr here, because the default should be different for synths and samples - const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value; + + const { attack, decay, sustain, release } = getADSRDefaults(value.attack, value.decay, value.sustain, value.release); //const soundfont = getSoundfontKey(s); const time = t + nudge; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index edc3c01e..a715947e 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,6 +1,6 @@ import { midiToFreq, noteToMidi } from './util.mjs'; import { registerSound, getAudioContext } from './superdough.mjs'; -import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs'; +import { gainNode, getADSRDefaults, getEnvelope, getExpEnvelope } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; const mod = (freq, range = 1, type = 'sine') => { @@ -30,7 +30,14 @@ export function registerSynthSounds() { s, (t, value, onended) => { // destructure adsr here, because the default should be different for synths and samples - let { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value; + const { attack, decay, sustain, release } = getADSRDefaults( + value.attack, + value.decay, + value.sustain, + value.release, + { attack: 0.001, decay: 0.05, sustain: 0.6, release: 0.01 }, + ); + console.log({ attack, decay, sustain, release }); let sound; if (waveforms.includes(s)) { diff --git a/src-tauri/src/oscbridge.rs b/src-tauri/src/oscbridge.rs index 6060cd9d..8148622e 100644 --- a/src-tauri/src/oscbridge.rs +++ b/src-tauri/src/oscbridge.rs @@ -53,8 +53,8 @@ pub fn init( /* ........................................................... Open OSC Ports ............................................................*/ - let sock = UdpSocket::bind("127.0.0.1:57122").unwrap(); - let to_addr = String::from("127.0.0.1:57120"); + let sock = UdpSocket::bind("169.254.248.82:57122").unwrap(); + let to_addr = String::from("169.254.180.239:57120"); sock.set_nonblocking(true).unwrap(); sock.connect(to_addr).expect("could not connect to OSC address"); From 9663c2ec858cc37ede5211d59297b8b849761d84 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Fri, 15 Dec 2023 12:16:28 -0700 Subject: [PATCH 02/37] fileter envelopes --- packages/superdough/helpers.mjs | 19 +++++++---------- packages/superdough/sampler.mjs | 4 ++-- packages/superdough/superdough.mjs | 33 ++++++++++++++++++------------ packages/superdough/synth.mjs | 14 +++++-------- 4 files changed, 34 insertions(+), 36 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 91bc17bc..94bdc11f 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -88,21 +88,16 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) { }; return new DynamicsCompressorNode(ac, options); } - -const adsrmin = 0.001; -export const getADSRDefaults = ( - a, - d, - s, - r, - def = { attack: adsrmin, decay: adsrmin, sustain: 1, release: adsrmin }, -) => { +const envmin = 0.001; +export const getADSRValues = (params, defaultValues = [envmin, envmin, 1, envmin]) => { + const [a, d, s, r] = params; + const [defA, defD, defS, defR] = defaultValues; console.log(a, d, s, r); if (a == null && d == null && s == null && r == null) { - return def; + return defaultValues; } - const sustain = s ?? ((a != null && d == null) || (a == null && d == null)) ? def.sustain : adsrmin; - return { attack: a ?? adsrmin, decay: d ?? adsrmin, sustain, release: r ?? adsrmin }; + const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? defS : envmin; + return [a ?? envmin, d ?? envmin, sustain, r ?? envmin]; }; export function createFilter( diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index e4ead52e..fb1dfda9 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,6 +1,6 @@ import { noteToMidi, valueToMidi, nanFallback } from './util.mjs'; import { getAudioContext, registerSound } from './index.mjs'; -import { getADSRDefaults, getEnvelope } from './helpers.mjs'; +import { getADSRValues, getEnvelope } from './helpers.mjs'; import { logger } from './logger.mjs'; const bufferCache = {}; // string: Promise @@ -252,7 +252,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const ac = getAudioContext(); // destructure adsr here, because the default should be different for synths and samples - const { attack, decay, sustain, release } = getADSRDefaults(value.attack, value.decay, value.sustain, value.release); + const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); //const soundfont = getSoundfontKey(s); const time = t + nudge; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 3be97615..036e63ba 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -9,7 +9,7 @@ import './reverb.mjs'; import './vowel.mjs'; import { clamp, nanFallback } from './util.mjs'; import workletsUrl from './worklets.mjs?url'; -import { createFilter, gainNode, getCompressor } from './helpers.mjs'; +import { createFilter, gainNode, getADSRValues, getCompressor } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; @@ -269,26 +269,16 @@ export const superdough = async (value, deadline, hapDuration) => { // low pass cutoff, lpenv, - lpattack = 0.01, - lpdecay = 0.01, - lpsustain = 1, - lprelease = 0.01, resonance = 1, // high pass hpenv, hcutoff, - hpattack = 0.01, - hpdecay = 0.01, - hpsustain = 1, - hprelease = 0.01, + hresonance = 1, // band pass bpenv, bandf, - bpattack = 0.01, - bpdecay = 0.01, - bpsustain = 1, - bprelease = 0.01, + bandq = 1, channels = [1, 2], //phaser @@ -322,6 +312,7 @@ export const superdough = async (value, deadline, hapDuration) => { compressorAttack, compressorRelease, } = value; + gain = nanFallback(gain, 1); //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior @@ -366,7 +357,15 @@ export const superdough = async (value, deadline, hapDuration) => { // gain stage chain.push(gainNode(gain)); + const filterEnvDefaults = [0.01, 0.01, 1, 0.01]; + if (cutoff !== undefined) { + const [lpattack, lpdecay, lpsustain, lprelease] = getADSRValues( + [value.lpattack, value.lpdecay, value.lpsustain, value.lprelease], + filterEnvDefaults, + ); + console.log(lpattack, 'atta'); + let lp = () => createFilter( ac, @@ -389,6 +388,10 @@ export const superdough = async (value, deadline, hapDuration) => { } if (hcutoff !== undefined) { + const [hpattack, hpdecay, hpsustain, hprelease] = getADSRValues( + [value.hpattack, value.hpdecay, value.hpsustain, value.hprelease], + filterEnvDefaults, + ); let hp = () => createFilter( ac, @@ -411,6 +414,10 @@ export const superdough = async (value, deadline, hapDuration) => { } if (bandf !== undefined) { + const [bpattack, bpdecay, bpsustain, bprelease] = getADSRValues( + [value.bpattack, value.bpdecay, value.bpsustain, value.bprelease], + filterEnvDefaults, + ); let bp = () => createFilter( ac, diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index a715947e..5977e9ac 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,6 +1,6 @@ import { midiToFreq, noteToMidi } from './util.mjs'; import { registerSound, getAudioContext } from './superdough.mjs'; -import { gainNode, getADSRDefaults, getEnvelope, getExpEnvelope } from './helpers.mjs'; +import { gainNode, getADSRValues, getEnvelope, getExpEnvelope } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; const mod = (freq, range = 1, type = 'sine') => { @@ -29,15 +29,11 @@ export function registerSynthSounds() { registerSound( s, (t, value, onended) => { - // destructure adsr here, because the default should be different for synths and samples - const { attack, decay, sustain, release } = getADSRDefaults( - value.attack, - value.decay, - value.sustain, - value.release, - { attack: 0.001, decay: 0.05, sustain: 0.6, release: 0.01 }, + const defaultADSRValues = [0.001, 0.05, 0.6, 0.01]; + const [attack, decay, sustain, release] = getADSRValues( + [value.attack, value.decay, value.sustain, value.release], + defaultADSRValues, ); - console.log({ attack, decay, sustain, release }); let sound; if (waveforms.includes(s)) { From 88f677e9100276597406793a07e271895b6ca3f5 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Fri, 15 Dec 2023 18:54:04 -0500 Subject: [PATCH 03/37] fix envelope behavior --- packages/superdough/helpers.mjs | 2 +- packages/superdough/superdough.mjs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index d75f5b14..d628d851 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -77,7 +77,7 @@ export const getParamADSR = (param, attack, decay, sustain, release, min, max, b param.setValueAtTime(min, begin); param.linearRampToValueAtTime(peak, begin + attack); param.linearRampToValueAtTime(sustainLevel, begin + attack + decay); - param.setValueAtTime(sustainLevel, end); + // param.setValueAtTime(sustainLevel, end); param.linearRampToValueAtTime(min, end + Math.max(release, 0.1)); }; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 036e63ba..9a35c277 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -364,7 +364,6 @@ export const superdough = async (value, deadline, hapDuration) => { [value.lpattack, value.lpdecay, value.lpsustain, value.lprelease], filterEnvDefaults, ); - console.log(lpattack, 'atta'); let lp = () => createFilter( From 96e0cce2d267d797e10f68d9d254d7325de0ed46 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sat, 16 Dec 2023 00:30:35 -0500 Subject: [PATCH 04/37] improve response --- packages/superdough/helpers.mjs | 34 ++++++++++++------------------ packages/superdough/superdough.mjs | 31 ++++++++++++--------------- 2 files changed, 26 insertions(+), 39 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index d628d851..1bc1a878 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -71,14 +71,17 @@ export const getADSR = (attack, decay, sustain, release, velocity, begin, end) = }; export const getParamADSR = (param, attack, decay, sustain, release, min, max, begin, end) => { + // let phase = begin; const range = max - min; const peak = min + range; const sustainLevel = min + sustain * range; param.setValueAtTime(min, begin); - param.linearRampToValueAtTime(peak, begin + attack); - param.linearRampToValueAtTime(sustainLevel, begin + attack + decay); - // param.setValueAtTime(sustainLevel, end); - param.linearRampToValueAtTime(min, end + Math.max(release, 0.1)); + param.exponentialRampToValueAtTime(peak, begin + attack); + param.exponentialRampToValueAtTime(sustainLevel, begin + attack + decay); + //stop current envelope ramp at event end, and begin release ramp + console.log(param); + param.cancelAndHoldAtTime(end); + param.exponentialRampToValueAtTime(min, end + Math.max(release, 0.1)); }; export function getCompressor(ac, threshold, ratio, knee, attack, release) { @@ -91,11 +94,14 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) { }; return new DynamicsCompressorNode(ac, options); } + +// changes the default values of the envelope based on what parameters the user has defined +// so it behaves more like you would expect/familiar as other synthesis tools +// ex: sound(val).decay(val) will behave as a decay only envelope. sound(val).attack(val).decay(val) will behave like an "ad" env, etc. const envmin = 0.001; export const getADSRValues = (params, defaultValues = [envmin, envmin, 1, envmin]) => { const [a, d, s, r] = params; const [defA, defD, defS, defR] = defaultValues; - console.log(a, d, s, r); if (a == null && d == null && s == null && r == null) { return defaultValues; } @@ -103,20 +109,8 @@ export const getADSRValues = (params, defaultValues = [envmin, envmin, 1, envmin return [a ?? envmin, d ?? envmin, sustain, r ?? envmin]; }; -export function createFilter( - context, - type, - frequency, - Q, - attack, - decay, - sustain, - release, - fenv, - start, - end, - fanchor = 0.5, -) { +export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor = 0.5) { + const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], [0.01, 0.01, 1, 0.01]); const filter = context.createBiquadFilter(); filter.type = type; filter.Q.value = Q; @@ -129,8 +123,6 @@ export function createFilter( const min = clamp(2 ** -offset * frequency, 0, 20000); const max = clamp(2 ** (fenv - offset) * frequency, 0, 20000); - // console.log('min', min, 'max', max); - getParamADSR(filter.frequency, attack, decay, sustain, release, min, max, start, end); return filter; } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 9a35c277..b4c16a53 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -9,7 +9,7 @@ import './reverb.mjs'; import './vowel.mjs'; import { clamp, nanFallback } from './util.mjs'; import workletsUrl from './worklets.mjs?url'; -import { createFilter, gainNode, getADSRValues, getCompressor } from './helpers.mjs'; +import { createFilter, gainNode, getCompressor } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; @@ -269,16 +269,26 @@ export const superdough = async (value, deadline, hapDuration) => { // low pass cutoff, lpenv, + lpattack, + lpdecay, + lpsustain, + lprelease, resonance = 1, // high pass hpenv, hcutoff, - + hpattack, + hpdecay, + hpsustain, + hprelease, hresonance = 1, // band pass bpenv, bandf, - + bpattack, + bpdecay, + bpsustain, + bprelease, bandq = 1, channels = [1, 2], //phaser @@ -357,14 +367,7 @@ export const superdough = async (value, deadline, hapDuration) => { // gain stage chain.push(gainNode(gain)); - const filterEnvDefaults = [0.01, 0.01, 1, 0.01]; - if (cutoff !== undefined) { - const [lpattack, lpdecay, lpsustain, lprelease] = getADSRValues( - [value.lpattack, value.lpdecay, value.lpsustain, value.lprelease], - filterEnvDefaults, - ); - let lp = () => createFilter( ac, @@ -387,10 +390,6 @@ export const superdough = async (value, deadline, hapDuration) => { } if (hcutoff !== undefined) { - const [hpattack, hpdecay, hpsustain, hprelease] = getADSRValues( - [value.hpattack, value.hpdecay, value.hpsustain, value.hprelease], - filterEnvDefaults, - ); let hp = () => createFilter( ac, @@ -413,10 +412,6 @@ export const superdough = async (value, deadline, hapDuration) => { } if (bandf !== undefined) { - const [bpattack, bpdecay, bpsustain, bprelease] = getADSRValues( - [value.bpattack, value.bpdecay, value.bpsustain, value.bprelease], - filterEnvDefaults, - ); let bp = () => createFilter( ac, From 93a4efcf6d0ac943298d118de56a0203928c3003 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sat, 16 Dec 2023 13:21:23 -0500 Subject: [PATCH 05/37] updated fontloader --- packages/soundfonts/fontloader.mjs | 12 +++++++-- packages/superdough/helpers.mjs | 43 +++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 017ba763..b6c8c8b4 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -1,5 +1,5 @@ import { noteToMidi, freqToMidi } from '@strudel.cycles/core'; -import { getAudioContext, registerSound, getEnvelope } from '@strudel.cycles/webaudio'; +import { getAudioContext, registerSound, getEnvelope, getADSRValues } from '@strudel.cycles/webaudio'; import gm from './gm.mjs'; let loadCache = {}; @@ -131,7 +131,15 @@ export function registerSoundfonts() { name, async (time, value, onended) => { const { n = 0 } = value; - const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value; + const [attack, decay, sustain, release] = getADSRValues([ + value.attack, + value.decay, + value.sustain, + value.release, + ]); + + console.log(attack, decay, sustain, release); + const font = fonts[n % fonts.length]; const ctx = getAudioContext(); const bufferSource = await getFontBufferSource(font, value, ctx); diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 1bc1a878..e8c695b8 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -70,18 +70,41 @@ export const getADSR = (attack, decay, sustain, release, velocity, begin, end) = return gainNode; }; -export const getParamADSR = (param, attack, decay, sustain, release, min, max, begin, end) => { - // let phase = begin; +export const getParamADSR = ( + context, + param, + attack, + decay, + sustain, + release, + min, + max, + begin, + end, + //exponential works better for frequency modulations (such as filter cutoff) due to human ear perception + curve = 'exponential', +) => { + const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime'; + let phase = begin; const range = max - min; const peak = min + range; - const sustainLevel = min + sustain * range; + param.setValueAtTime(min, begin); - param.exponentialRampToValueAtTime(peak, begin + attack); - param.exponentialRampToValueAtTime(sustainLevel, begin + attack + decay); - //stop current envelope ramp at event end, and begin release ramp - console.log(param); - param.cancelAndHoldAtTime(end); - param.exponentialRampToValueAtTime(min, end + Math.max(release, 0.1)); + phase += attack; + //attack + param[ramp](peak, phase); + phase += decay; + const sustainLevel = min + sustain * range; + //decay + param[ramp](sustainLevel, phase); + //this timeout can be replaced with cancelAndHoldAtTime once it is implemented in Firefox + setTimeout(() => { + //sustain at current value + param.cancelScheduledValues(0); + phase += Math.max(release, 0.1); + //release + param[ramp](min, phase); + }, (end - context.currentTime) * 1000); }; export function getCompressor(ac, threshold, ratio, knee, attack, release) { @@ -123,7 +146,7 @@ export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fe const min = clamp(2 ** -offset * frequency, 0, 20000); const max = clamp(2 ** (fenv - offset) * frequency, 0, 20000); - getParamADSR(filter.frequency, attack, decay, sustain, release, min, max, start, end); + getParamADSR(context, filter.frequency, attack, decay, sustain, release, min, max, start, end); return filter; } From b5dc65d52a1a2550011beb5cb84419aff38135d1 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sat, 16 Dec 2023 13:36:05 -0500 Subject: [PATCH 06/37] format --- packages/soundfonts/fontloader.mjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index b6c8c8b4..f8b10d38 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -137,9 +137,6 @@ export function registerSoundfonts() { value.sustain, value.release, ]); - - console.log(attack, decay, sustain, release); - const font = fonts[n % fonts.length]; const ctx = getAudioContext(); const bufferSource = await getFontBufferSource(font, value, ctx); From fe60f3401510fa7a51cfcd424989448d7e4bafed Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sun, 17 Dec 2023 12:13:05 -0500 Subject: [PATCH 07/37] removed accidental file commit --- src-tauri/src/oscbridge.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/oscbridge.rs b/src-tauri/src/oscbridge.rs index 8148622e..6060cd9d 100644 --- a/src-tauri/src/oscbridge.rs +++ b/src-tauri/src/oscbridge.rs @@ -53,8 +53,8 @@ pub fn init( /* ........................................................... Open OSC Ports ............................................................*/ - let sock = UdpSocket::bind("169.254.248.82:57122").unwrap(); - let to_addr = String::from("169.254.180.239:57120"); + let sock = UdpSocket::bind("127.0.0.1:57122").unwrap(); + let to_addr = String::from("127.0.0.1:57120"); sock.set_nonblocking(true).unwrap(); sock.connect(to_addr).expect("could not connect to OSC address"); From d7fae2620e2ce87b01079537bc45a195e4f0a3c5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 20 Dec 2023 00:21:14 -0500 Subject: [PATCH 08/37] create release audio param method, make volume envelopes consistant --- packages/superdough/helpers.mjs | 42 +++++++++++++++++++++------------ packages/superdough/synth.mjs | 2 +- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index e8c695b8..011feb25 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,6 +1,27 @@ import { getAudioContext } from './superdough.mjs'; import { clamp } from './util.mjs'; +AudioParam.prototype.setRelease = function (startTime, endTime, endValue, curve = 'linear') { + const ctx = getAudioContext(); + const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime'; + const param = this; + if (AudioParam.prototype.cancelAndHoldAtTime == null) { + //this replicates cancelAndHoldAtTime behavior for Firefox + setTimeout(() => { + //sustain at current value + const currValue = param.value; + param.cancelScheduledValues(0); + param.setValueAtTime(currValue, 0); + //release + param[ramp](endValue, endTime); + }, (startTime - ctx.currentTime) * 1000); + } else { + param.cancelAndHoldAtTime(startTime); + //release + param[ramp](endValue, endTime); + } +}; + export function gainNode(value) { const node = getAudioContext().createGain(); node.gain.value = value; @@ -21,13 +42,10 @@ export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => return { node: gainNode, stop: (t) => { - // to make sure the release won't begin before sustain is reached - phase = Math.max(t, phase); - // see https://github.com/tidalcycles/strudel/issues/522 - gainNode.gain.setValueAtTime(sustainLevel, phase); - phase += release; - gainNode.gain.linearRampToValueAtTime(0, phase); // release - return phase; + const endTime = t + release; + gainNode.gain.setRelease(t, endTime, 0); + // helps prevent pops from overlapping sounds + return endTime - 0.01; }, }; }; @@ -97,14 +115,8 @@ export const getParamADSR = ( const sustainLevel = min + sustain * range; //decay param[ramp](sustainLevel, phase); - //this timeout can be replaced with cancelAndHoldAtTime once it is implemented in Firefox - setTimeout(() => { - //sustain at current value - param.cancelScheduledValues(0); - phase += Math.max(release, 0.1); - //release - param[ramp](min, phase); - }, (end - context.currentTime) * 1000); + phase += Math.max(release, 0.1); + param.setRelease(end, phase, min, curve); }; export function getCompressor(ac, threshold, ratio, knee, attack, release) { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 74255dbf..3d93b8cd 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -42,7 +42,7 @@ export function registerSynthSounds() { let { density } = value; sound = getNoiseOscillator(s, t, density); } - + console.log(sound); let { node: o, stop, triggerRelease } = sound; // turn down From 34ba81a84119b557f5b8e8eeb6ece9da7ef277a4 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 20 Dec 2023 00:25:56 -0500 Subject: [PATCH 09/37] fixed test complaint --- packages/superdough/helpers.mjs | 38 +++++++++++++++++---------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 011feb25..a9d5a05a 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,26 +1,28 @@ import { getAudioContext } from './superdough.mjs'; import { clamp } from './util.mjs'; -AudioParam.prototype.setRelease = function (startTime, endTime, endValue, curve = 'linear') { - const ctx = getAudioContext(); - const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime'; - const param = this; - if (AudioParam.prototype.cancelAndHoldAtTime == null) { - //this replicates cancelAndHoldAtTime behavior for Firefox - setTimeout(() => { - //sustain at current value - const currValue = param.value; - param.cancelScheduledValues(0); - param.setValueAtTime(currValue, 0); +if (AudioParam != null) { + AudioParam.prototype.setRelease = function (startTime, endTime, endValue, curve = 'linear') { + const ctx = getAudioContext(); + const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime'; + const param = this; + if (AudioParam.prototype.cancelAndHoldAtTime == null) { + //this replicates cancelAndHoldAtTime behavior for Firefox + setTimeout(() => { + //sustain at current value + const currValue = param.value; + param.cancelScheduledValues(0); + param.setValueAtTime(currValue, 0); + //release + param[ramp](endValue, endTime); + }, (startTime - ctx.currentTime) * 1000); + } else { + param.cancelAndHoldAtTime(startTime); //release param[ramp](endValue, endTime); - }, (startTime - ctx.currentTime) * 1000); - } else { - param.cancelAndHoldAtTime(startTime); - //release - param[ramp](endValue, endTime); - } -}; + } + }; +} export function gainNode(value) { const node = getAudioContext().createGain(); From f6d9ad51c62bc478bc2947509afcc0e064e8a489 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 20 Dec 2023 01:44:34 -0500 Subject: [PATCH 10/37] trying to fix divergent firefox behavior --- packages/superdough/helpers.mjs | 56 ++++++++++++++++----------------- packages/superdough/synth.mjs | 5 +-- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index a9d5a05a..bcc92cdd 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,28 +1,25 @@ import { getAudioContext } from './superdough.mjs'; import { clamp } from './util.mjs'; -if (AudioParam != null) { - AudioParam.prototype.setRelease = function (startTime, endTime, endValue, curve = 'linear') { - const ctx = getAudioContext(); - const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime'; - const param = this; - if (AudioParam.prototype.cancelAndHoldAtTime == null) { - //this replicates cancelAndHoldAtTime behavior for Firefox - setTimeout(() => { - //sustain at current value - const currValue = param.value; - param.cancelScheduledValues(0); - param.setValueAtTime(currValue, 0); - //release - param[ramp](endValue, endTime); - }, (startTime - ctx.currentTime) * 1000); - } else { - param.cancelAndHoldAtTime(startTime); +const setRelease = (param, startTime, endTime, endValue, curve = 'linear') => { + const ctx = getAudioContext(); + const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime'; + if (param.cancelAndHoldAtTime == null) { + //this replicates cancelAndHoldAtTime behavior for Firefox + setTimeout(() => { + //sustain at current value + const currValue = param.value; + param.cancelScheduledValues(0); + param.setValueAtTime(currValue, 0); //release param[ramp](endValue, endTime); - } - }; -} + }, (startTime - ctx.currentTime) * 1000); + } else { + param.cancelAndHoldAtTime(startTime); + //release + param[ramp](endValue, endTime); + } +}; export function gainNode(value) { const node = getAudioContext().createGain(); @@ -45,9 +42,9 @@ export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => node: gainNode, stop: (t) => { const endTime = t + release; - gainNode.gain.setRelease(t, endTime, 0); + setRelease(gainNode.gain, t, endTime, 0); // helps prevent pops from overlapping sounds - return endTime - 0.01; + return endTime; }, }; }; @@ -117,8 +114,8 @@ export const getParamADSR = ( const sustainLevel = min + sustain * range; //decay param[ramp](sustainLevel, phase); - phase += Math.max(release, 0.1); - param.setRelease(end, phase, min, curve); + phase += release; + setRelease(param, end, phase, min, curve); }; export function getCompressor(ac, threshold, ratio, knee, attack, release) { @@ -135,19 +132,20 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) { // changes the default values of the envelope based on what parameters the user has defined // so it behaves more like you would expect/familiar as other synthesis tools // ex: sound(val).decay(val) will behave as a decay only envelope. sound(val).attack(val).decay(val) will behave like an "ad" env, etc. -const envmin = 0.001; -export const getADSRValues = (params, defaultValues = [envmin, envmin, 1, envmin]) => { + +export const getADSRValues = (params, defaultValues, min = 0, max = 1) => { + defaultValues = defaultValues ?? [min, min, max, min]; const [a, d, s, r] = params; const [defA, defD, defS, defR] = defaultValues; if (a == null && d == null && s == null && r == null) { return defaultValues; } - const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? defS : envmin; - return [a ?? envmin, d ?? envmin, sustain, r ?? envmin]; + const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? max : min; + return [a ?? min, d ?? min, sustain, r ?? min]; }; export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor = 0.5) { - const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], [0.01, 0.01, 1, 0.01]); + const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], [0.001, 0.1, 1, 0.01], 0.001, 1); const filter = context.createBiquadFilter(); filter.type = type; filter.Q.value = Q; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 3d93b8cd..fcf63893 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -29,10 +29,12 @@ export function registerSynthSounds() { registerSound( s, (t, value, onended) => { - const defaultADSRValues = [0.001, 0.05, 0.6, 0.01]; + const defaultADSRValues = [0.001, 0.1, 0.6, 0.01]; const [attack, decay, sustain, release] = getADSRValues( [value.attack, value.decay, value.sustain, value.release], defaultADSRValues, + 0, + 0.6, ); let sound; @@ -42,7 +44,6 @@ export function registerSynthSounds() { let { density } = value; sound = getNoiseOscillator(s, t, density); } - console.log(sound); let { node: o, stop, triggerRelease } = sound; // turn down From 30e402b9f14c47324d2ce80d09174e6be39ffb91 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 20 Dec 2023 01:52:43 -0500 Subject: [PATCH 11/37] fixed release bug --- packages/superdough/helpers.mjs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index bcc92cdd..17999529 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -114,8 +114,8 @@ export const getParamADSR = ( const sustainLevel = min + sustain * range; //decay param[ramp](sustainLevel, phase); - phase += release; - setRelease(param, end, phase, min, curve); + + setRelease(param, end, end + release, min, curve); }; export function getCompressor(ac, threshold, ratio, knee, attack, release) { @@ -132,20 +132,19 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) { // changes the default values of the envelope based on what parameters the user has defined // so it behaves more like you would expect/familiar as other synthesis tools // ex: sound(val).decay(val) will behave as a decay only envelope. sound(val).attack(val).decay(val) will behave like an "ad" env, etc. - -export const getADSRValues = (params, defaultValues, min = 0, max = 1) => { - defaultValues = defaultValues ?? [min, min, max, min]; +const envmin = 0.001; +export const getADSRValues = (params, defaultValues = [envmin, envmin, 1, envmin]) => { const [a, d, s, r] = params; const [defA, defD, defS, defR] = defaultValues; if (a == null && d == null && s == null && r == null) { return defaultValues; } - const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? max : min; - return [a ?? min, d ?? min, sustain, r ?? min]; + const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? defS : envmin; + return [a ?? envmin, d ?? envmin, sustain, r ?? envmin]; }; export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor = 0.5) { - const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], [0.001, 0.1, 1, 0.01], 0.001, 1); + const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], [0.01, 0.01, 1, 0.01]); const filter = context.createBiquadFilter(); filter.type = type; filter.Q.value = Q; From e0a7fb8290fe31000895006f464137f7ea3939ce Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 20 Dec 2023 09:28:40 -0500 Subject: [PATCH 12/37] filter should decay to set frequency --- packages/superdough/helpers.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 17999529..451412d5 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -157,7 +157,7 @@ export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fe const min = clamp(2 ** -offset * frequency, 0, 20000); const max = clamp(2 ** (fenv - offset) * frequency, 0, 20000); - getParamADSR(context, filter.frequency, attack, decay, sustain, release, min, max, start, end); + getParamADSR(context, filter.frequency, attack, decay, sustain, release, frequency, max, start, end); return filter; } From 2dea3911ba18256043db39868e87d29767905344 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 20 Dec 2023 09:29:07 -0500 Subject: [PATCH 13/37] remove unused variable --- packages/superdough/helpers.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 451412d5..a2329cda 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -154,7 +154,6 @@ export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fe if (!isNaN(fenv) && fenv !== 0) { const offset = fenv * fanchor; - const min = clamp(2 ** -offset * frequency, 0, 20000); const max = clamp(2 ** (fenv - offset) * frequency, 0, 20000); getParamADSR(context, filter.frequency, attack, decay, sustain, release, frequency, max, start, end); From deb973afa5dfd85d7d3ec935903c1bd7b5a073f2 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 20 Dec 2023 10:17:12 -0500 Subject: [PATCH 14/37] fixed hold behavior --- packages/superdough/helpers.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index a2329cda..9ef0a113 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -16,6 +16,7 @@ const setRelease = (param, startTime, endTime, endValue, curve = 'linear') => { }, (startTime - ctx.currentTime) * 1000); } else { param.cancelAndHoldAtTime(startTime); + param.setValueAtTime(param.value, startTime); //release param[ramp](endValue, endTime); } @@ -146,6 +147,7 @@ export const getADSRValues = (params, defaultValues = [envmin, envmin, 1, envmin export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor = 0.5) { const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], [0.01, 0.01, 1, 0.01]); const filter = context.createBiquadFilter(); + filter.type = type; filter.Q.value = Q; filter.frequency.value = frequency; From fb81f6f268debc3374dfead211ef3cae5c45ed95 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 20 Dec 2023 10:50:06 -0500 Subject: [PATCH 15/37] still working on popping issue with firefox --- packages/superdough/helpers.mjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 9ef0a113..d6e397c6 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,7 +1,7 @@ import { getAudioContext } from './superdough.mjs'; import { clamp } from './util.mjs'; -const setRelease = (param, startTime, endTime, endValue, curve = 'linear') => { +const setRelease = (param, phase, sustain, startTime, endTime, endValue, curve = 'linear') => { const ctx = getAudioContext(); const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime'; if (param.cancelAndHoldAtTime == null) { @@ -15,9 +15,10 @@ const setRelease = (param, startTime, endTime, endValue, curve = 'linear') => { param[ramp](endValue, endTime); }, (startTime - ctx.currentTime) * 1000); } else { + if (phase < startTime) { + param.setValueAtTime(sustain, startTime); + } param.cancelAndHoldAtTime(startTime); - param.setValueAtTime(param.value, startTime); - //release param[ramp](endValue, endTime); } }; @@ -43,7 +44,7 @@ export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => node: gainNode, stop: (t) => { const endTime = t + release; - setRelease(gainNode.gain, t, endTime, 0); + setRelease(gainNode.gain, phase, sustain, t, endTime, 0); // helps prevent pops from overlapping sounds return endTime; }, @@ -113,10 +114,11 @@ export const getParamADSR = ( param[ramp](peak, phase); phase += decay; const sustainLevel = min + sustain * range; + //decay param[ramp](sustainLevel, phase); - setRelease(param, end, end + release, min, curve); + setRelease(param, phase, sustain, end, end + release, min, curve); }; export function getCompressor(ac, threshold, ratio, knee, attack, release) { From dec039ead3dccd68f343418ac81891798d728160 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 20 Dec 2023 13:09:04 -0500 Subject: [PATCH 16/37] fixed filter envelope popping... --- packages/superdough/helpers.mjs | 44 +++++++++------------------------ packages/superdough/synth.mjs | 13 +++++----- 2 files changed, 18 insertions(+), 39 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index d6e397c6..51361e79 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -67,30 +67,7 @@ export const getExpEnvelope = (attack, decay, sustain, release, velocity, begin) }; }; -export const getADSR = (attack, decay, sustain, release, velocity, begin, end) => { - const gainNode = getAudioContext().createGain(); - gainNode.gain.setValueAtTime(0, begin); - gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack - gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start - gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end - gainNode.gain.linearRampToValueAtTime(0, end + release); // release - // for some reason, using exponential ramping creates little cracklings - /* let t = begin; - gainNode.gain.setValueAtTime(0, t); - gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack)); - const sustainGain = Math.max(sustain * velocity, 0.001); - gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay)); - if (end - begin < attack + decay) { - gainNode.gain.cancelAndHoldAtTime(end); - } else { - gainNode.gain.setValueAtTime(sustainGain, end); - } - gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */ - return gainNode; -}; - export const getParamADSR = ( - context, param, attack, decay, @@ -118,7 +95,7 @@ export const getParamADSR = ( //decay param[ramp](sustainLevel, phase); - setRelease(param, phase, sustain, end, end + release, min, curve); + setRelease(param, phase, sustainLevel, end, end + release, min, curve); }; export function getCompressor(ac, threshold, ratio, knee, attack, release) { @@ -135,19 +112,22 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) { // changes the default values of the envelope based on what parameters the user has defined // so it behaves more like you would expect/familiar as other synthesis tools // ex: sound(val).decay(val) will behave as a decay only envelope. sound(val).attack(val).decay(val) will behave like an "ad" env, etc. -const envmin = 0.001; -export const getADSRValues = (params, defaultValues = [envmin, envmin, 1, envmin]) => { + +export const getADSRValues = (params, curve = 'linear', defaultValues) => { + const envmin = curve === 'exponential' ? 0.001 : 0; + const releaseMin = 0.01; + const envmax = 1; const [a, d, s, r] = params; - const [defA, defD, defS, defR] = defaultValues; if (a == null && d == null && s == null && r == null) { - return defaultValues; + return defaultValues ?? [envmin, envmin, envmax, releaseMin]; } - const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? defS : envmin; - return [a ?? envmin, d ?? envmin, sustain, r ?? envmin]; + const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin; + return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)]; }; export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor = 0.5) { - const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], [0.01, 0.01, 1, 0.01]); + const curve = 'exponential'; + const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]); const filter = context.createBiquadFilter(); filter.type = type; @@ -160,7 +140,7 @@ export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fe const max = clamp(2 ** (fenv - offset) * frequency, 0, 20000); - getParamADSR(context, filter.frequency, attack, decay, sustain, release, frequency, max, start, end); + getParamADSR(filter.frequency, attack, decay, sustain, release, frequency, max, start, end, curve); return filter; } diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index fcf63893..09c8163b 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -29,13 +29,12 @@ export function registerSynthSounds() { registerSound( s, (t, value, onended) => { - const defaultADSRValues = [0.001, 0.1, 0.6, 0.01]; - const [attack, decay, sustain, release] = getADSRValues( - [value.attack, value.decay, value.sustain, value.release], - defaultADSRValues, - 0, - 0.6, - ); + const [attack, decay, sustain, release] = getADSRValues([ + value.attack, + value.decay, + value.sustain, + value.release, + ]); let sound; if (waveforms.includes(s)) { From 5671b40707b86f9e9be5d0eb319a2bcbe2e0e4de Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 20 Dec 2023 15:24:14 -0500 Subject: [PATCH 17/37] account for phase complete --- packages/superdough/helpers.mjs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 51361e79..b5c01b08 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -4,7 +4,11 @@ import { clamp } from './util.mjs'; const setRelease = (param, phase, sustain, startTime, endTime, endValue, curve = 'linear') => { const ctx = getAudioContext(); const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime'; - if (param.cancelAndHoldAtTime == null) { + // if the decay stage is complete before the note event is done, we don't need to do anything special + if (phase < startTime) { + param.setValueAtTime(sustain, startTime); + param[ramp](endValue, endTime); + } else if (param.cancelAndHoldAtTime == null) { //this replicates cancelAndHoldAtTime behavior for Firefox setTimeout(() => { //sustain at current value @@ -15,9 +19,7 @@ const setRelease = (param, phase, sustain, startTime, endTime, endValue, curve = param[ramp](endValue, endTime); }, (startTime - ctx.currentTime) * 1000); } else { - if (phase < startTime) { - param.setValueAtTime(sustain, startTime); - } + //stop the envelope, hold the value, and then set the release stage param.cancelAndHoldAtTime(startTime); param[ramp](endValue, endTime); } From 77fee0b8661dfbee9565ed35d6aa9002d02115be Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 20 Dec 2023 17:39:11 -0500 Subject: [PATCH 18/37] fixed popping on font envelope --- packages/superdough/helpers.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index b5c01b08..8b7d9631 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -46,7 +46,7 @@ export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => node: gainNode, stop: (t) => { const endTime = t + release; - setRelease(gainNode.gain, phase, sustain, t, endTime, 0); + setRelease(gainNode.gain, phase, sustainLevel, t, endTime, 0); // helps prevent pops from overlapping sounds return endTime; }, From 9756d63cf114742c5cf51f2817dc363c89e15cc5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 31 Dec 2023 11:58:06 -0500 Subject: [PATCH 19/37] prettier --- packages/soundfonts/fontloader.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index a986481f..683c249b 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -1,4 +1,3 @@ - import { noteToMidi, freqToMidi, getSoundIndex } from '@strudel.cycles/core'; import { getAudioContext, registerSound, getEnvelope, getADSRValues } from '@strudel.cycles/webaudio'; import gm from './gm.mjs'; From 83c820a18c92b4dbc430ed978c4876584b57d348 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 1 Jan 2024 18:50:23 -0500 Subject: [PATCH 20/37] prettier again --- packages/superdough/helpers.mjs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 8b7d9631..68e88e87 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -15,6 +15,7 @@ const setRelease = (param, phase, sustain, startTime, endTime, endValue, curve = const currValue = param.value; param.cancelScheduledValues(0); param.setValueAtTime(currValue, 0); + //release param[ramp](endValue, endTime); }, (startTime - ctx.currentTime) * 1000); @@ -89,6 +90,7 @@ export const getParamADSR = ( param.setValueAtTime(min, begin); phase += attack; + //attack param[ramp](peak, phase); phase += decay; @@ -139,13 +141,10 @@ export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fe // Apply ADSR to filter frequency if (!isNaN(fenv) && fenv !== 0) { const offset = fenv * fanchor; - const max = clamp(2 ** (fenv - offset) * frequency, 0, 20000); - getParamADSR(filter.frequency, attack, decay, sustain, release, frequency, max, start, end, curve); return filter; } - return filter; } From d582bbbb601c4b7e4fe0ecc8969be634504d64da Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 3 Jan 2024 22:17:11 +0100 Subject: [PATCH 21/37] fix: format --- packages/superdough/helpers.mjs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 68e88e87..7544ab7f 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -10,15 +10,18 @@ const setRelease = (param, phase, sustain, startTime, endTime, endValue, curve = param[ramp](endValue, endTime); } else if (param.cancelAndHoldAtTime == null) { //this replicates cancelAndHoldAtTime behavior for Firefox - setTimeout(() => { - //sustain at current value - const currValue = param.value; - param.cancelScheduledValues(0); - param.setValueAtTime(currValue, 0); + setTimeout( + () => { + //sustain at current value + const currValue = param.value; + param.cancelScheduledValues(0); + param.setValueAtTime(currValue, 0); - //release - param[ramp](endValue, endTime); - }, (startTime - ctx.currentTime) * 1000); + //release + param[ramp](endValue, endTime); + }, + (startTime - ctx.currentTime) * 1000, + ); } else { //stop the envelope, hold the value, and then set the release stage param.cancelAndHoldAtTime(startTime); From 2ecc6b30aa3829c73660f9ae4a3d0500489685fc Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 3 Jan 2024 22:37:35 +0100 Subject: [PATCH 22/37] add ad function --- packages/core/controls.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index d979ff23..5bb0a37f 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1394,6 +1394,11 @@ controls.adsr = register('adsr', (adsr, pat) => { const [attack, decay, sustain, release] = adsr; return pat.set({ attack, decay, sustain, release }); }); +controls.ad = register('ad', (t, pat) => { + t = !Array.isArray(t) ? [t] : t; + const [attack, decay = attack] = t; + return pat.attack(attack).decay(decay); +}); controls.ds = register('ds', (ds, pat) => { ds = !Array.isArray(ds) ? [ds] : ds; const [decay, sustain] = ds; From 041a809b07106765edf83cbd9e3223bc0af0b325 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 3 Jan 2024 22:41:03 +0100 Subject: [PATCH 23/37] add ar function --- packages/core/controls.mjs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 5bb0a37f..993f1c1d 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1399,10 +1399,15 @@ controls.ad = register('ad', (t, pat) => { const [attack, decay = attack] = t; return pat.attack(attack).decay(decay); }); -controls.ds = register('ds', (ds, pat) => { - ds = !Array.isArray(ds) ? [ds] : ds; - const [decay, sustain] = ds; +controls.ds = register('ds', (t, pat) => { + t = !Array.isArray(t) ? [t] : t; + const [decay, sustain = 0] = t; return pat.set({ decay, sustain }); }); +controls.ds = register('ar', (t, pat) => { + t = !Array.isArray(t) ? [t] : t; + const [attack, release = attack] = t; + return pat.set({ attack, release }); +}); export default controls; From a2974099c1932961cac391ae2341fe7ef2ed2776 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 3 Jan 2024 23:06:23 +0100 Subject: [PATCH 24/37] add dec synonym for decay --- packages/core/controls.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 993f1c1d..60372522 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -234,7 +234,7 @@ const generic_params = [ * note("c3 e3").decay("<.1 .2 .3 .4>").sustain(0) * */ - ['decay'], + ['decay', 'dec'], /** * Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset. * From 2ee392be9b28ccdc9f65d5ffd18f9e90536aba0f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 5 Jan 2024 01:00:22 -0500 Subject: [PATCH 25/37] fixed all the things --- packages/core/util.mjs | 2 +- packages/soundfonts/fontloader.mjs | 22 +++-- packages/superdough/helpers.mjs | 121 ++++++++++----------------- packages/superdough/sampler.mjs | 31 ++++--- packages/superdough/synth.mjs | 61 +++++++++----- packages/superdough/util.mjs | 2 +- website/src/repl/panel/SoundsTab.jsx | 56 +++++++------ 7 files changed, 145 insertions(+), 150 deletions(-) diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 6c0439b6..ef55de95 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -86,7 +86,7 @@ export const midi2note = (n) => { // modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does) export const _mod = (n, m) => ((n % m) + m) % m; -export function nanFallback(value, fallback) { +export function nanFallback(value, fallback = 0) { if (isNaN(Number(value))) { logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning'); return fallback; diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 683c249b..8d8fb02b 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -1,5 +1,5 @@ import { noteToMidi, freqToMidi, getSoundIndex } from '@strudel.cycles/core'; -import { getAudioContext, registerSound, getEnvelope, getADSRValues } from '@strudel.cycles/webaudio'; +import { getAudioContext, registerSound, getParamADSR, getADSRValues } from '@strudel.cycles/webaudio'; import gm from './gm.mjs'; let loadCache = {}; @@ -136,23 +136,27 @@ export function registerSoundfonts() { value.sustain, value.release, ]); + + const { duration } = value; const n = getSoundIndex(value.n, fonts.length); const font = fonts[n]; const ctx = getAudioContext(); const bufferSource = await getFontBufferSource(font, value, ctx); bufferSource.start(time); - const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 0.3, time); - bufferSource.connect(envelope); - const stop = (releaseTime) => { - const silentAt = releaseEnvelope(releaseTime); - bufferSource.stop(silentAt); - }; + const envGain = ctx.createGain(); + const node = bufferSource.connect(envGain); + const holdEnd = time + duration; + getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, time, holdEnd, 'linear'); + let envEnd = holdEnd + release + 0.01; + + bufferSource.stop(envEnd); + const stop = (releaseTime) => {}; bufferSource.onended = () => { bufferSource.disconnect(); - envelope.disconnect(); + node.disconnect(); onended(); }; - return { node: envelope, stop }; + return { node, stop }; }, { type: 'soundfont', prebake: true, fonts }, ); diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 7544ab7f..ac22fc7c 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,33 +1,5 @@ import { getAudioContext } from './superdough.mjs'; -import { clamp } from './util.mjs'; - -const setRelease = (param, phase, sustain, startTime, endTime, endValue, curve = 'linear') => { - const ctx = getAudioContext(); - const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime'; - // if the decay stage is complete before the note event is done, we don't need to do anything special - if (phase < startTime) { - param.setValueAtTime(sustain, startTime); - param[ramp](endValue, endTime); - } else if (param.cancelAndHoldAtTime == null) { - //this replicates cancelAndHoldAtTime behavior for Firefox - setTimeout( - () => { - //sustain at current value - const currValue = param.value; - param.cancelScheduledValues(0); - param.setValueAtTime(currValue, 0); - - //release - param[ramp](endValue, endTime); - }, - (startTime - ctx.currentTime) * 1000, - ); - } else { - //stop the envelope, hold the value, and then set the release stage - param.cancelAndHoldAtTime(startTime); - param[ramp](endValue, endTime); - } -}; +import { clamp, nanFallback } from './util.mjs'; export function gainNode(value) { const node = getAudioContext().createGain(); @@ -35,44 +7,13 @@ export function gainNode(value) { return node; } -// alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future -export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => { - const gainNode = getAudioContext().createGain(); - let phase = begin; - gainNode.gain.setValueAtTime(0, begin); - phase += attack; - gainNode.gain.linearRampToValueAtTime(velocity, phase); // attack - phase += decay; - let sustainLevel = sustain * velocity; - gainNode.gain.linearRampToValueAtTime(sustainLevel, phase); // decay / sustain - // sustain end - return { - node: gainNode, - stop: (t) => { - const endTime = t + release; - setRelease(gainNode.gain, phase, sustainLevel, t, endTime, 0); - // helps prevent pops from overlapping sounds - return endTime; - }, - }; +const getSlope = (y1, y2, x1, x2) => { + const denom = x2 - x1; + if (denom === 0) { + return 0; + } + return (y2 - y1) / (x2 - x1); }; - -export const getExpEnvelope = (attack, decay, sustain, release, velocity, begin) => { - sustain = Math.max(0.001, sustain); - velocity = Math.max(0.001, velocity); - const gainNode = getAudioContext().createGain(); - gainNode.gain.setValueAtTime(0.0001, begin); - gainNode.gain.exponentialRampToValueAtTime(velocity, begin + attack); - gainNode.gain.exponentialRampToValueAtTime(sustain * velocity, begin + attack + decay); - return { - node: gainNode, - stop: (t) => { - // similar to getEnvelope, this will glitch if sustain level has not been reached - gainNode.gain.exponentialRampToValueAtTime(0.0001, t + release); - }, - }; -}; - export const getParamADSR = ( param, attack, @@ -86,23 +27,47 @@ export const getParamADSR = ( //exponential works better for frequency modulations (such as filter cutoff) due to human ear perception curve = 'exponential', ) => { + attack = nanFallback(attack); + decay = nanFallback(decay); + sustain = nanFallback(sustain); + release = nanFallback(release); + const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime'; - let phase = begin; + if (curve === 'exponential') { + min = Math.max(0.0001, min); + } const range = max - min; const peak = min + range; + const sustainVal = min + sustain * range; + const duration = end - begin; + + const envValAtTime = (time) => { + if (attack > time) { + return time * getSlope(min, peak, 0, attack) + 0; + } else { + return (time - attack) * getSlope(peak, sustainVal, 0, decay) + peak; + } + }; param.setValueAtTime(min, begin); - phase += attack; - - //attack - param[ramp](peak, phase); - phase += decay; - const sustainLevel = min + sustain * range; - - //decay - param[ramp](sustainLevel, phase); - - setRelease(param, phase, sustainLevel, end, end + release, min, curve); + if (attack > duration) { + //attack + param[ramp](envValAtTime(duration), end); + } else if (attack + decay > duration) { + //attack + param[ramp](envValAtTime(attack), begin + attack); + //decay + param[ramp](envValAtTime(duration), end); + } else { + //attack + param[ramp](envValAtTime(attack), begin + attack); + //decay + param[ramp](envValAtTime(attack + decay), begin + attack + decay); + //sustain + param.setValueAtTime(sustainVal, end); + } + //release + param[ramp](min, end + release); }; export function getCompressor(ac, threshold, ratio, knee, attack, release) { diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index ac39da66..f0e46221 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,6 +1,6 @@ import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs'; import { getAudioContext, registerSound } from './index.mjs'; -import { getADSRValues, getEnvelope } from './helpers.mjs'; +import { getADSRValues, getParamADSR } from './helpers.mjs'; import { logger } from './logger.mjs'; const bufferCache = {}; // string: Promise @@ -243,6 +243,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { begin = 0, loopEnd = 1, end = 1, + duration, vib, vibmod = 0.5, } = value; @@ -255,7 +256,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const ac = getAudioContext(); // destructure adsr here, because the default should be different for synths and samples - const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); + let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); //const soundfont = getSoundfontKey(s); const time = t + nudge; @@ -299,25 +300,29 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset; } bufferSource.start(time, offset); - const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t); - bufferSource.connect(envelope); + const envGain = ac.createGain(); + const node = bufferSource.connect(envGain); + const holdEnd = t + duration; + getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); + const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... - envelope.connect(out); + node.connect(out); bufferSource.onended = function () { bufferSource.disconnect(); vibratoOscillator?.stop(); - envelope.disconnect(); + node.disconnect(); out.disconnect(); onended(); }; + let envEnd = holdEnd + release + 0.01; + bufferSource.stop(envEnd); const stop = (endTime, playWholeBuffer = clip === undefined && loop === undefined) => { - let releaseTime = endTime; - if (playWholeBuffer) { - const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; - releaseTime = t + (end - begin) * bufferDuration; - } - const silentAt = releaseEnvelope(releaseTime); - bufferSource.stop(silentAt); + // did not reimplement this behavior, because it mostly seems to confuse people... + // if (playWholeBuffer) { + // const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; + // envEnd = t + (end - begin) * bufferDuration; + // } + // bufferSource.stop(envEnd); }; const handle = { node: out, bufferSource, stop }; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 09c8163b..cfe3567c 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,6 +1,6 @@ import { midiToFreq, noteToMidi } from './util.mjs'; import { registerSound, getAudioContext } from './superdough.mjs'; -import { gainNode, getADSRValues, getEnvelope, getExpEnvelope } from './helpers.mjs'; +import { gainNode, getADSRValues, getParamADSR } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; const mod = (freq, range = 1, type = 'sine') => { @@ -43,26 +43,30 @@ export function registerSynthSounds() { let { density } = value; sound = getNoiseOscillator(s, t, density); } + let { node: o, stop, triggerRelease } = sound; // turn down const g = gainNode(0.3); - // gain envelope - const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t); + const { duration } = value; o.onended = () => { o.disconnect(); g.disconnect(); onended(); }; + + const envGain = gainNode(1); + let node = o.connect(g).connect(envGain); + const holdEnd = t + duration; + getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); + const envEnd = holdEnd + release + 0.01; + triggerRelease?.(envEnd); + stop(envEnd); return { - node: o.connect(g).connect(envelope), - stop: (releaseTime) => { - const silentAt = releaseEnvelope(releaseTime); - triggerRelease?.(releaseTime); - stop(silentAt); - }, + node, + stop: (releaseTime) => {}, }; }, { type: 'synth', prebake: true }, @@ -122,6 +126,7 @@ export function getOscillator( fmrelease: fmRelease, fmvelocity: fmVelocity, fmwave: fmWaveform = 'sine', + duration, }, ) { let ac = getAudioContext(); @@ -151,26 +156,38 @@ export function getOscillator( o.start(t); // FM - let stopFm, fmEnvelope; + let stopFm; + let envGain = ac.createGain(); if (fmModulationIndex) { const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform); if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) { // no envelope by default modulator.connect(o.frequency); } else { - fmAttack = fmAttack ?? 0.001; - fmDecay = fmDecay ?? 0.001; - fmSustain = fmSustain ?? 1; - fmRelease = fmRelease ?? 0.001; - fmVelocity = fmVelocity ?? 1; - fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t); + const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]); + + const holdEnd = t + duration; + // let envEnd = holdEnd + release + 0.01; + + getParamADSR( + envGain.gain, + attack, + decay, + sustain, + release, + 0, + 1, + t, + holdEnd, + fmEnvelopeType === 'exp' ? 'exponential' : 'linear', + ); + if (fmEnvelopeType === 'exp') { - fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t); - fmEnvelope.node.maxValue = fmModulationIndex * 2; - fmEnvelope.node.minValue = 0.00001; + envGain.maxValue = fmModulationIndex * 2; + envGain.minValue = 0.00001; } - modulator.connect(fmEnvelope.node); - fmEnvelope.node.connect(o.frequency); + modulator.connect(envGain); + envGain.connect(o.frequency); } stopFm = stop; } @@ -202,7 +219,7 @@ export function getOscillator( o.stop(time); }, triggerRelease: (time) => { - fmEnvelope?.stop(time); + // envGain?.stop(time); }, }; } diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 29eaa7bc..cebabfda 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -54,7 +54,7 @@ export const valueToMidi = (value, fallbackValue) => { return fallbackValue; }; -export function nanFallback(value, fallback) { +export function nanFallback(value, fallback = 0) { if (isNaN(Number(value))) { logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning'); return fallback; diff --git a/website/src/repl/panel/SoundsTab.jsx b/website/src/repl/panel/SoundsTab.jsx index a3639dfb..7c16b266 100644 --- a/website/src/repl/panel/SoundsTab.jsx +++ b/website/src/repl/panel/SoundsTab.jsx @@ -57,32 +57,36 @@ export function SoundsTab() { settingsMap.setKey('soundsFilter', 'user')} />
- {soundEntries.map(([name, { data, onTrigger }]) => ( - { - const ctx = getAudioContext(); - const params = { - note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined, - s: name, - clip: 1, - release: 0.5, - }; - const time = ctx.currentTime + 0.05; - const onended = () => trigRef.current?.node?.disconnect(); - trigRef.current = Promise.resolve(onTrigger(time, params, onended)); - trigRef.current.then((ref) => { - connectToDestination(ref?.node); - }); - }} - > - {' '} - {name} - {data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''} - {data?.type === 'soundfont' ? `(${data.fonts.length})` : ''} - - ))} + {soundEntries.map(([name, { data, onTrigger }]) => { + return ( + { + const ctx = getAudioContext(); + const params = { + note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined, + s: name, + clip: 1, + release: 0.5, + sustain: 1, + duration: 0.5, + }; + const time = ctx.currentTime + 0.05; + const onended = () => trigRef.current?.node?.disconnect(); + trigRef.current = Promise.resolve(onTrigger(time, params, onended)); + trigRef.current.then((ref) => { + connectToDestination(ref?.node); + }); + }} + > + {' '} + {name} + {data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''} + {data?.type === 'soundfont' ? `(${data.fonts.length})` : ''} + + ); + })} {!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''}
From b2f63897f9644a2c1e25d576b3b6a82f1b21a638 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 5 Jan 2024 01:07:54 -0500 Subject: [PATCH 26/37] change default FM env to exp because it modulates frequency and sounds way better :) --- packages/superdough/synth.mjs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index cfe3567c..c4586e92 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -119,7 +119,7 @@ export function getOscillator( // fm fmh: fmHarmonicity = 1, fmi: fmModulationIndex, - fmenv: fmEnvelopeType = 'lin', + fmenv: fmEnvelopeType = 'exp', fmattack: fmAttack, fmdecay: fmDecay, fmsustain: fmSustain, @@ -165,10 +165,7 @@ export function getOscillator( modulator.connect(o.frequency); } else { const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]); - const holdEnd = t + duration; - // let envEnd = holdEnd + release + 0.01; - getParamADSR( envGain.gain, attack, @@ -181,11 +178,6 @@ export function getOscillator( holdEnd, fmEnvelopeType === 'exp' ? 'exponential' : 'linear', ); - - if (fmEnvelopeType === 'exp') { - envGain.maxValue = fmModulationIndex * 2; - envGain.minValue = 0.00001; - } modulator.connect(envGain); envGain.connect(o.frequency); } From 150a1b8eed86f39136a638788d91595478d1fe4e Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 5 Jan 2024 17:45:09 -0500 Subject: [PATCH 27/37] restore buffer hold behavior --- packages/superdough/sampler.mjs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index f0e46221..36f71277 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -302,7 +302,12 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { bufferSource.start(time, offset); const envGain = ac.createGain(); const node = bufferSource.connect(envGain); - const holdEnd = t + duration; + let holdEnd = t + duration; + if (clip == null && loop == null && value.release == null) { + const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; + holdEnd = t + bufferDuration; + } + getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... @@ -316,14 +321,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { }; let envEnd = holdEnd + release + 0.01; bufferSource.stop(envEnd); - const stop = (endTime, playWholeBuffer = clip === undefined && loop === undefined) => { - // did not reimplement this behavior, because it mostly seems to confuse people... - // if (playWholeBuffer) { - // const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; - // envEnd = t + (end - begin) * bufferDuration; - // } - // bufferSource.stop(envEnd); - }; + const stop = (endTime, playWholeBuffer) => {}; const handle = { node: out, bufferSource, stop }; // cut groups From 62cd2263c2048ec53f8bd92350a532b1a491108f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 10 Jan 2024 00:03:18 +0100 Subject: [PATCH 28/37] add silent flag to nanFallback --- packages/superdough/util.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index cebabfda..4e3c7e41 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -54,9 +54,9 @@ export const valueToMidi = (value, fallbackValue) => { return fallbackValue; }; -export function nanFallback(value, fallback = 0) { +export function nanFallback(value, fallback = 0, silent) { if (isNaN(Number(value))) { - logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning'); + !silent && logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning'); return fallback; } return value; From f785823e0051c3951ea868816a5cbd12fc79f725 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 10 Jan 2024 00:08:13 +0100 Subject: [PATCH 29/37] add fenv to filter cutoff controls --- packages/core/controls.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 60372522..97129779 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -270,7 +270,7 @@ const generic_params = [ * s("bd sd,hh*3").bpf("<1000 2000 4000 8000>") * */ - [['bandf', 'bandq'], 'bpf', 'bp'], + [['bandf', 'bandq', 'bpenv'], 'bpf', 'bp'], // TODO: in tidal, it seems to be normalized /** * Sets the **b**and-**p**ass **q**-factor (resonance). @@ -481,7 +481,7 @@ const generic_params = [ * s("bd*8").lpf("1000:0 1000:10 1000:20 1000:30") * */ - [['cutoff', 'resonance'], 'ctf', 'lpf', 'lp'], + [['cutoff', 'resonance', 'lpenv'], 'ctf', 'lpf', 'lp'], /** * Sets the lowpass filter envelope modulation depth. @@ -758,7 +758,7 @@ const generic_params = [ * .vibmod("<.25 .5 1 2 12>:8") */ [['vibmod', 'vib'], 'vmod'], - [['hcutoff', 'hresonance'], 'hpf', 'hp'], + [['hcutoff', 'hresonance', 'hpenv'], 'hpf', 'hp'], /** * Controls the **h**igh-**p**ass **q**-value. * From f99557cc0567dba6f1c3ef74e559631f73c59b56 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 10 Jan 2024 00:08:49 +0100 Subject: [PATCH 30/37] fix: fanchor + default fanchor to 0 (breaking change) --- packages/superdough/helpers.mjs | 12 ++++++++---- packages/superdough/superdough.mjs | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index ac22fc7c..99531eff 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -97,7 +97,7 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => { return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)]; }; -export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor = 0.5) { +export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor) { const curve = 'exponential'; const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]); const filter = context.createBiquadFilter(); @@ -105,12 +105,16 @@ export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fe filter.type = type; filter.Q.value = Q; filter.frequency.value = frequency; - + // envelope is active when any of these values is set + const hasEnvelope = att ?? dec ?? sus ?? rel ?? fenv; // Apply ADSR to filter frequency - if (!isNaN(fenv) && fenv !== 0) { + if (hasEnvelope !== undefined) { + fenv = nanFallback(fenv, 1, true); + fanchor = nanFallback(fanchor, 0, true); const offset = fenv * fanchor; + const min = clamp(2 ** -offset * frequency, 0, 20000); const max = clamp(2 ** (fenv - offset) * frequency, 0, 20000); - getParamADSR(filter.frequency, attack, decay, sustain, release, frequency, max, start, end, curve); + getParamADSR(filter.frequency, attack, decay, sustain, release, min, max, start, end, curve); return filter; } return filter; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index f5674f2c..d03007f1 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -276,7 +276,7 @@ export const superdough = async (value, deadline, hapDuration) => { density = 0.03, // filters ftype = '12db', - fanchor = 0.5, + fanchor = 0, // low pass cutoff, lpenv, From c68dba8c21f429cf0a0a14cd911a660648d740ab Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 10 Jan 2024 14:28:38 +0100 Subject: [PATCH 31/37] fix: use end --- packages/superdough/sampler.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 36f71277..932be995 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -302,11 +302,11 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { bufferSource.start(time, offset); const envGain = ac.createGain(); const node = bufferSource.connect(envGain); - let holdEnd = t + duration; if (clip == null && loop == null && value.release == null) { const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; - holdEnd = t + bufferDuration; + duration = (end - begin) * bufferDuration; } + let holdEnd = t + duration; getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); From f26fd18c7a8b2537d25c40f32dcf4b317d7cec9d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 10 Jan 2024 15:02:10 +0100 Subject: [PATCH 32/37] use 0.001 as linear mintime as well (to prevent cracks) --- packages/superdough/helpers.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 99531eff..43e435b8 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -86,7 +86,7 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) { // ex: sound(val).decay(val) will behave as a decay only envelope. sound(val).attack(val).decay(val) will behave like an "ad" env, etc. export const getADSRValues = (params, curve = 'linear', defaultValues) => { - const envmin = curve === 'exponential' ? 0.001 : 0; + const envmin = curve === 'exponential' ? 0.001 : 0.001; const releaseMin = 0.01; const envmax = 1; const [a, d, s, r] = params; From 6f3b2fa66b238fda198db544626a6687e58fe9d1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 13 Jan 2024 00:26:16 +0100 Subject: [PATCH 33/37] negative fenv values now stay in the same range as positives --- packages/superdough/helpers.mjs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 43e435b8..846130e8 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -111,9 +111,11 @@ export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fe if (hasEnvelope !== undefined) { fenv = nanFallback(fenv, 1, true); fanchor = nanFallback(fanchor, 0, true); - const offset = fenv * fanchor; - const min = clamp(2 ** -offset * frequency, 0, 20000); - const max = clamp(2 ** (fenv - offset) * frequency, 0, 20000); + const fenvAbs = Math.abs(fenv); + const offset = fenvAbs * fanchor; + let min = clamp(2 ** -offset * frequency, 0, 20000); + let max = clamp(2 ** (fenvAbs - offset) * frequency, 0, 20000); + if (fenv < 0) [min, max] = [max, min]; getParamADSR(filter.frequency, attack, decay, sustain, release, min, max, start, end, curve); return filter; } From 4fb5c1828d5310cd4be9c47ca4299fb9f3b0c557 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 13 Jan 2024 01:16:26 +0100 Subject: [PATCH 34/37] i have no clue why this works --- packages/superdough/helpers.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 846130e8..bee26f38 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -37,13 +37,14 @@ export const getParamADSR = ( min = Math.max(0.0001, min); } const range = max - min; - const peak = min + range; + const peak = max; const sustainVal = min + sustain * range; const duration = end - begin; const envValAtTime = (time) => { if (attack > time) { - return time * getSlope(min, peak, 0, attack) + 0; + let slope = getSlope(min, peak, 0, attack); + return time * slope + (min > peak ? min : 0); } else { return (time - attack) * getSlope(peak, sustainVal, 0, decay) + peak; } From eefa90a2bc7416ba38bfbd9b803f36dfd0ccaaeb Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 13 Jan 2024 22:24:59 +0100 Subject: [PATCH 35/37] fix: synth default envelope --- packages/superdough/synth.mjs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index c4586e92..6a3e381a 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -29,12 +29,11 @@ export function registerSynthSounds() { registerSound( s, (t, value, onended) => { - const [attack, decay, sustain, release] = getADSRValues([ - value.attack, - value.decay, - value.sustain, - value.release, - ]); + const [attack, decay, sustain, release] = getADSRValues( + [value.attack, value.decay, value.sustain, value.release], + 'linear', + [0.001, 0.05, 0.6, 0.01], + ); let sound; if (waveforms.includes(s)) { From e6d028fd6b0771de26d1fd88c97c52ed3af847e7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 14 Jan 2024 16:27:11 +0100 Subject: [PATCH 36/37] soundfont mixing --- packages/soundfonts/fontloader.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 8d8fb02b..dc3f4b61 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -146,7 +146,7 @@ export function registerSoundfonts() { const envGain = ctx.createGain(); const node = bufferSource.connect(envGain); const holdEnd = time + duration; - getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, time, holdEnd, 'linear'); + getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, time, holdEnd, 'linear'); let envEnd = holdEnd + release + 0.01; bufferSource.stop(envEnd); From 090194856413b3dcf4d7a6a9dd67aedc7b742ab1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 14 Jan 2024 21:32:38 +0100 Subject: [PATCH 37/37] fanchor default to 0.5 for now --- packages/superdough/superdough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index d03007f1..f5674f2c 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -276,7 +276,7 @@ export const superdough = async (value, deadline, hapDuration) => { density = 0.03, // filters ftype = '12db', - fanchor = 0, + fanchor = 0.5, // low pass cutoff, lpenv,