diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index 7e3c2a8e..8a332ac2 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/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs
index 1df5c877..625f4075 100644
--- a/packages/core/test/pattern.test.mjs
+++ b/packages/core/test/pattern.test.mjs
@@ -796,7 +796,7 @@ describe('Pattern', () => {
});
});
describe('apply', () => {
- it('Can apply a function', () => {
+ (it('Can apply a function', () => {
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
}),
it('Can apply a pattern of functions', () => {
@@ -804,7 +804,7 @@ describe('Pattern', () => {
expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual(
sequence('a', 'b').fast(2, 3).firstCycle(),
);
- });
+ }));
});
describe('layer', () => {
it('Can layer up multiple functions', () => {
diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs
index 47bca330..147291ad 100644
--- a/packages/superdough/helpers.mjs
+++ b/packages/superdough/helpers.mjs
@@ -153,6 +153,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;
@@ -169,41 +187,39 @@ 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;
+export function createFilter(context, start, end, params, cps) {
+ let {
+ frequency,
+ fanchor,
+ fenv,
+ type,
+ model,
+ q = 1,
+ drive = 0.69,
+ depth,
+ dcoffset = -0.5,
+ skew,
+ shape,
+ rate,
+ sync,
+ } = params;
let frequencyParam;
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;
// Apply ADSR to filter frequency
@@ -216,8 +232,15 @@ export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fe
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;
}
+
+ if (sync != null) {
+ rate = cps * sync;
+ }
+ const lfoValues = { depth, dcoffset, skew, shape, frequency: rate };
+ getParamLfo(context, frequencyParam, start, end, lfoValues);
+ // TODO: remember to disconnect LFO
+ // lfo?.disconnect()
return filter;
}
diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs
index 52ed9bff..bae42235 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, getLfo, getWorklet, effectSend } from './helpers.mjs';
import { map } from 'nanostores';
@@ -145,11 +145,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,
@@ -432,32 +427,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,
@@ -581,57 +551,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 475c05f6..f296cca0 100644
--- a/packages/superdough/util.mjs
+++ b/packages/superdough/util.mjs
@@ -105,3 +105,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]]));
+};
diff --git a/website/src/components/Header/Search.css b/website/src/components/Header/Search.css
index 456ef9f6..b14146cb 100644
--- a/website/src/components/Header/Search.css
+++ b/website/src/components/Header/Search.css
@@ -18,8 +18,8 @@
--docsearch-modal-background: var(--background);
--docsearch-muted-color: color-mix(in srgb, var(--foreground), #fff 30%);
--docsearch-key-gradient: var(--foreground);
- --docsearch-key-shadow: inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground),
- 0 1px 2px 1px var(--gutterBackground);
+ --docsearch-key-shadow:
+ inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), 0 1px 2px 1px var(--gutterBackground);
}
.dark {
--docsearch-muted-color: color-mix(in srgb, var(--foreground), #000 30%);
diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx
index eb79cccf..201d57df 100644
--- a/website/src/pages/learn/samples.mdx
+++ b/website/src/pages/learn/samples.mdx
@@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample
### scrub
-{' '}
+
### speed