diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index e317af35..79155833 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1288,6 +1288,151 @@ export const { fanchor } = registerControl('fanchor'); */ // currently an alias of 'hcutoff' https://codeberg.org/uzu/strudel/issues/496 // ['hpf'], + +/** + * Rate of the LFO for the lowpass filter + * + * @name lprate + * @param {number | Pattern} rate rate in hertz + */ +export const { lprate } = registerControl('lprate'); + +/** + * Cycle-synced rate of the LFO for the lowpass filter + * + * @name lpsync + * @param {number | Pattern} rate rate in cycles + */ +export const { lpsync } = registerControl('lpsync'); + +/** + * Depth of the LFO for the lowpass filter + * + * @name lpdepth + * @param {number | Pattern} depth depth of modulation + */ +export const { lpdepth } = registerControl('lpdepth'); + +/** + * Shape of the LFO for the lowpass filter + * + * @name lpshape + * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) + */ +export const { lpshape } = registerControl('lpshape'); + +/** + * DC offset of the LFO for the lowpass filter + * + * @name lpdc + * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar + */ +export const { lpdc } = registerControl('lpdc'); + +/** + * Skew of the LFO for the lowpass filter + * + * @name lpskew + * @param {number | Pattern} skew How much to bend the LFO shape + */ +export const { lpskew } = registerControl('lpskew'); + +/** + * Rate of the LFO for the bandpass filter + * + * @name bprate + * @param {number | Pattern} rate rate in hertz + */ +export const { bprate } = registerControl('bprate'); + +/** + * Cycle-synced rate of the LFO for the bandpass filter + * + * @name bpsync + * @param {number | Pattern} rate rate in cycles + */ +export const { bpsync } = registerControl('bpsync'); + +/** + * Depth of the LFO for the bandpass filter + * + * @name bpdepth + * @param {number | Pattern} depth depth of modulation + */ +export const { bpdepth } = registerControl('bpdepth'); + +/** + * Shape of the LFO for the bandpass filter + * + * @name bpshape + * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) + */ +export const { bpshape } = registerControl('bpshape'); + +/** + * DC offset of the LFO for the bandpass filter + * + * @name bpdc + * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar + */ +export const { bpdc } = registerControl('bpdc'); + +/** + * Skew of the LFO for the bandpass filter + * + * @name bpskew + * @param {number | Pattern} skew How much to bend the LFO shape + */ +export const { bpskew } = registerControl('bpskew'); + +/** + * Rate of the LFO for the highpass filter + * + * @name hprate + * @param {number | Pattern} rate rate in hertz + */ +export const { hprate } = registerControl('hprate'); + +/** + * Cycle-synced rate of the LFO for the highpass filter + * + * @name hpsync + * @param {number | Pattern} rate rate in cycles + */ +export const { hpsync } = registerControl('hpsync'); + +/** + * Depth of the LFO for the highpass filter + * + * @name hpdepth + * @param {number | Pattern} depth depth of modulation + */ +export const { hpdepth } = registerControl('hpdepth'); + +/** + * Shape of the LFO for the highpass filter + * + * @name hpshape + * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) + */ +export const { hpshape } = registerControl('hpshape'); + +/** + * DC offset of the LFO for the highpass filter + * + * @name hpdc + * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar + */ +export const { hpdc } = registerControl('hpdc'); + +/** + * Skew of the LFO for the highpass filter + * + * @name hpskew + * @param {number | Pattern} skew How much to bend the LFO shape + */ +export const { hpskew } = registerControl('hpskew'); + /** * Applies a vibrato to the frequency of the oscillator. * diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index b06f584a..47161ed6 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -154,6 +154,24 @@ 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 getParamLfo(audioContext, param, start, end, lfoValues) { + let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues; + if (depth == null) { + const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null); + depth = hasLFOParams ? defaultDepth : 0; + } + let lfo; + if (depth) { + lfo = getLfo(audioContext, start, end, { + depth, + dcoffset, + ...getLfoInputs, + }); + lfo.connect(param); + } + return lfo; +} + // helper utility for applying standard modulators to a parameter export function applyParameterModulators(audioContext, param, start, end, envelopeValues, lfoValues) { let { amount, offset, defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues; @@ -170,55 +188,58 @@ export function applyParameterModulators(audioContext, param, start, end, envelo const [attack, decay, sustain, release] = getADSRValues(values, curve, defaultValues); getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve); } - let lfo; - let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues; - - if (depth == null) { - const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null); - depth = hasLFOParams ? defaultDepth : 0; - } - if (depth) { - lfo = getLfo(audioContext, start, end, { - depth, - dcoffset, - ...getLfoInputs, - }); - lfo.connect(param); - } - + const lfo = getParamLfo(audioContext, param, start, end, lfoValues); return { lfo, disconnect: () => lfo?.disconnect() }; } -export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) { - const curve = 'exponential'; - const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]); - let filter; - let frequencyParam; +export function createFilter(context, start, end, params, cps) { + let { + frequency, + anchor, + env, + type, + model, + q = 1, + drive = 0.69, + depth, + dcoffset = -0.5, + skew, + shape, + rate, + sync, + } = params; + let frequencyParam, filter; if (model === 'ladder') { - filter = getWorklet(context, 'ladder-processor', { frequency, q: Q, drive }); + filter = getWorklet(context, 'ladder-processor', { frequency, q, drive }); frequencyParam = filter.parameters.get('frequency'); } else { filter = context.createBiquadFilter(); filter.type = type; - filter.Q.value = Q; + filter.Q.value = q; filter.frequency.value = frequency; frequencyParam = filter.frequency; } - + const envelopeValues = [params.attack, params.decay, params.sustain, params.release]; + const [attack, decay, sustain, release] = getADSRValues(envelopeValues, 'exponential', [0.005, 0.14, 0, 0.1]); // envelope is active when any of these values is set - const hasEnvelope = att ?? dec ?? sus ?? rel ?? fenv; + const hasEnvelope = [...envelopeValues, env].some((v) => v !== undefined); // Apply ADSR to filter frequency - if (hasEnvelope !== undefined) { - fenv = nanFallback(fenv, 1, true); - fanchor = nanFallback(fanchor, 0, true); - const fenvAbs = Math.abs(fenv); - const offset = fenvAbs * fanchor; + if (hasEnvelope) { + env = nanFallback(env, 1, true); + anchor = nanFallback(anchor, 0, true); + const envAbs = Math.abs(env); + const offset = envAbs * anchor; 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(frequencyParam, attack, decay, sustain, release, min, max, start, end, curve); - return filter; + let max = clamp(2 ** (envAbs - offset) * frequency, 0, 20000); + if (env < 0) [min, max] = [max, min]; + getParamADSR(frequencyParam, attack, decay, sustain, release, min, max, start, end, 'exponential'); } + + if (sync != null) { + rate = cps * sync; + } + const lfoValues = { depth, dcoffset, skew, shape, frequency: rate }; + getParamLfo(context, frequencyParam, start, end, lfoValues); return filter; } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 36bd6896..b59e3ae0 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th import './feedbackdelay.mjs'; import './reverb.mjs'; import './vowel.mjs'; -import { nanFallback, _mod, cycleToSeconds } from './util.mjs'; +import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs'; import { map } from 'nanostores'; @@ -146,11 +146,6 @@ let defaultDefaultValues = { gain: 0.8, postgain: 1, density: '.03', - ftype: '12db', - fanchor: 0, - resonance: 1, - hresonance: 1, - bandq: 1, channels: [1, 2], phaserdepth: 0.75, shapevol: 1, @@ -415,32 +410,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) djf, // filters fanchor = getDefaultValue('fanchor'), - drive = 0.69, release = 0, - // low pass - cutoff, - lpenv, - lpattack, - lpdecay, - lpsustain, - lprelease, - resonance = getDefaultValue('resonance'), - // high pass - hpenv, - hcutoff, - hpattack, - hpdecay, - hpsustain, - hprelease, - hresonance = getDefaultValue('hresonance'), - // band pass - bpenv, - bandf, - bpattack, - bpdecay, - bpsustain, - bprelease, - bandq = getDefaultValue('bandq'), //phaser phaserrate: phaser, @@ -565,57 +535,87 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // gain stage chain.push(gainNode(gain)); - //filter + // filter const ftype = getFilterType(value.ftype); - if (cutoff !== undefined) { - let lp = () => - createFilter( - ac, - 'lowpass', - cutoff, - resonance, - lpattack, - lpdecay, - lpsustain, - lprelease, - lpenv, - t, - end, - fanchor, - ftype, - drive, - ); + + if (value.cutoff !== undefined) { + const lpMap = { + frequency: 'cutoff', + q: 'resonance', + attack: 'lpattack', + decay: 'lpdecay', + sustain: 'lpsustain', + release: 'lprelease', + env: 'lpenv', + anchor: 'fanchor', + model: 'ftype', + drive: 'drive', + rate: 'lprate', + sync: 'lpsync', + depth: 'lpdepth', + shape: 'lpshape', + dcoffset: 'lpdc', + skew: 'lpskew', + }; + const lpParams = pickAndRename(value, lpMap); + lpParams.type = 'lowpass'; + let lp = () => createFilter(ac, t, end, lpParams, cps); chain.push(lp()); if (ftype === '24db') { chain.push(lp()); } } - if (hcutoff !== undefined) { - let hp = () => - createFilter( - ac, - 'highpass', - hcutoff, - hresonance, - hpattack, - hpdecay, - hpsustain, - hprelease, - hpenv, - t, - end, - fanchor, - ); + if (value.hcutoff !== undefined) { + const hpMap = { + frequency: 'hcutoff', + q: 'hresonance', + attack: 'hpattack', + decay: 'hpdecay', + sustain: 'hpsustain', + release: 'hprelease', + env: 'hpenv', + anchor: 'fanchor', + model: 'ftype', + drive: 'drive', + rate: 'hprate', + sync: 'hpsync', + depth: 'hpdepth', + shape: 'hpshape', + dcoffset: 'hpdc', + skew: 'hpskew', + }; + const hpParams = pickAndRename(value, hpMap); + hpParams.type = 'highpass'; + let hp = () => createFilter(ac, t, end, hpParams, cps); chain.push(hp()); if (ftype === '24db') { chain.push(hp()); } } - if (bandf !== undefined) { - let bp = () => - createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor); + if (value.bandf !== undefined) { + const bpMap = { + frequency: 'bandf', + q: 'bandq', + attack: 'bpattack', + decay: 'bpdecay', + sustain: 'bpsustain', + release: 'bprelease', + env: 'bpenv', + anchor: 'fanchor', + model: 'ftype', + drive: 'drive', + rate: 'bprate', + sync: 'bpsync', + depth: 'bpdepth', + shape: 'bpshape', + dcoffset: 'bpdc', + skew: 'bpskew', + }; + const bpParams = pickAndRename(value, bpMap); + bpParams.type = 'bandpass'; + let bp = () => createFilter(ac, t, end, bpParams, cps); chain.push(bp()); if (ftype === '24db') { chain.push(bp()); diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 418b7af0..44ec79f7 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -109,3 +109,8 @@ export function getCommonSampleInfo(hapValue, bank) { const label = `${s}:${index}`; return { transpose, url, index, midi, label }; } + +/** Selects entries from `source` and renames them via `map` */ +export const pickAndRename = (source, map) => { + return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]])); +};