From bb982e6b9b9a0a80a334fcdf11248e4d9d12a0c9 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 3 Sep 2025 21:55:48 -0500 Subject: [PATCH 01/30] First pass at modes --- packages/core/controls.mjs | 35 +++++++- packages/superdough/helpers.mjs | 4 + packages/superdough/superdough.mjs | 6 +- packages/superdough/worklets.mjs | 89 +++++++++++++++++-- .../src/repl/components/panel/SettingsTab.jsx | 2 +- 5 files changed, 124 insertions(+), 12 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 58a30652..01679529 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1575,7 +1575,7 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size', export const { shape } = registerControl(['shape', 'shapevol']); /** * Wave shaping distortion. CAUTION: it can get loud. - * Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. + * Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. Third option sets the waveshaping type. * Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;) * * @name distort @@ -1585,9 +1585,40 @@ export const { shape } = registerControl(['shape', 'shapevol']); * s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>") * @example * note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4") + * @example + * s("bd*4").bank("tr909").distort("4:0.5:fold") * */ -export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dist'); +export const { distort, dist } = registerControl(['distort', 'distortvol', 'distorttype'], 'dist'); + +/** + * Postgain for waveshaping distortion. + * + * @name distortvol + * @synonyms distvol + * @param {number | Pattern} type + * @example + * s("bd*4").bank("tr909").distort(2).distortvol(0.8) + */ +export const { distortvol } = registerControl('distortvol', 'distvol'); + +/** + * Type of waveshaping distortion to apply. + * + * @name distorttype + * @synonyms disttype + * @param {number | string | Pattern} type + * @example + * s("bd*4").bank("tr909").distort(2).distorttype("<0 1 2>") + * + * @example + * s("sine").note("F1*2").release(1) + * .penv(24).pdecay(0.05) + * .distort(70) + * .distorttype("") + */ +export const { distorttype } = registerControl('distorttype', 'disttype'); + /** * Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")` * More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 81bec939..def49c4d 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -307,3 +307,7 @@ export function applyFM(param, value, begin) { } return { stop }; } + +export const getDistortion = (distort, postgain, algorithm) => { + return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain, algorithm }); +}; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a518f49b..98306115 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -9,7 +9,7 @@ import './reverb.mjs'; import './vowel.mjs'; import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; -import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs'; +import { createFilter, gainNode, getCompressor, getDistortion, getWorklet, webAudioTimeout } from './helpers.mjs'; import { map } from 'nanostores'; import { logger, errorLogger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; @@ -608,6 +608,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) shapevol = getDefaultValue('shapevol'), distort, distortvol = getDefaultValue('distortvol'), + distorttype, pan, vowel, delay = getDefaultValue('delay'), @@ -782,8 +783,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // effects coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse })); crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); - shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); - distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); + distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype)); if (tremolosync != null) { tremolo = cps * tremolosync; diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 2406d56d..0eb2bc72 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -7,6 +7,9 @@ import FFT from './fft.js'; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const _mod = (n, m) => ((n % m) + m) % m; +const ffloor = (x) => x | 0; +const pv = (arr, n) => arr[n] ?? arr[0]; +const mix = (a, b, t) => (1 - t) * a + t * b; // Restrict phase to the range [0, maxPhase) via wrapping function wrapPhase(phase, maxPhase = 1) { @@ -341,11 +344,85 @@ class LadderProcessor extends AudioWorkletProcessor { } registerProcessor('ladder-processor', LadderProcessor); +// Saturation curves + +const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1) + +const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x)); +const _soft = (x, k) => Math.tanh(x * (1 + k)); +const _hard = (x, k) => clamp((1 + k) * x, -1, 1); +const _sine = (x, k) => Math.sin(x * (1 + k)); + +const _fold = (x, k) => { + let y = (1 + k) * x; + while (y > 1 || y < -1) { + y = y > 1 ? 2 - y : -2 - y; + } + return y; +}; + +const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k)); + +const _pow = (x, k) => { + const t = __squash(k); + const p = 1 / (1 + 0.5 * t); // tame k + return _soft(Math.sign(x) * Math.pow(Math.abs(x), p), 0.5 * k); +}; + +const _diode = (x, k, asym = false) => { + const g = 1 + k; // gain + const t = __squash(k); + const bias = 0.15 * t; + const pos = _soft(x + bias, k); + const neg = _soft(asym ? bias : -x + bias, k); + const y = pos - neg; + // We divide by the derivative at 0 so that the distortion is roughly + // the identity map near 0 => small values are preserved and undistorted + const sech = 1 / Math.cosh(g * bias); + const sech2 = sech * sech; // derivative of tanh is sech^2 + const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x + return _soft(y / denom, k); +}; + +const _asym = (x, k) => _diode(x, k, true); + +const _cubic = (x, k) => { + const t = __squash(k); + const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1) + return _soft(cubic, k); +}; + +export const saturationAlgos = { + scurve: _scurve, + soft: _soft, + hard: _hard, + sine: _sine, + fold: _fold, + sinefold: _sineFold, + pow: _pow, + cubic: _cubic, + diode: _diode, + asym: _asym, +}; +const _algoNames = Object.freeze(Object.keys(saturationAlgos)); + +const _getAlgorithm = (algo) => { + let algoName = typeof algo === 'string' ? algo : _algoNames[algo % _algoNames.length]; + if (!_algoNames.includes(algoName)) { + algoName = _algoNames[0]; + logger(`[superdough] Could not find waveshaping algorithm ${algo}. + Available options are ${_algoNames.join(', ')}. + Defaulting to ${algoName}.`); + } + return saturationAlgos[algoName]; +}; + class DistortProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ { name: 'distort', defaultValue: 0 }, { name: 'postgain', defaultValue: 1 }, + { name: 'algorithm', defaultValue: 0, min: 0, max: _algoNames.length - 1 }, ]; } @@ -363,13 +440,13 @@ class DistortProcessor extends AudioWorkletProcessor { return false; } this.started = hasInput; - - const shape = Math.expm1(parameters.distort[0]); - const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0])); - for (let n = 0; n < blockSize; n++) { - for (let i = 0; i < input.length; i++) { - output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain; + const postgain = clamp(pv(parameters.postgain, n), 0.001, 1); + const shape = Math.expm1(pv(parameters.distort, n)); + const algorithm = _getAlgorithm(pv(parameters.algorithm, n)); + for (let ch = 0; ch < input.length; ch++) { + const x = input[ch][n]; + output[ch][n] = postgain * algorithm(x, shape); } } return true; diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 8f02ca8b..26c9ae28 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -312,7 +312,7 @@ export function SettingsTab({ started }) { confirmDialog('Sure?').then((r) => { if (r) { const { userPatterns } = settingsMap.get(); // keep current patterns - settingsMap.set({...defaultSettings, userPatterns}); + settingsMap.set({ ...defaultSettings, userPatterns }); } }); }} From 42ab65abdb6d55e8227f39c6318652519efa3ef8 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 4 Sep 2025 11:47:01 -0500 Subject: [PATCH 02/30] Move things around to allow selection by name and avoid circular imports; fixed chebyshev --- packages/superdough/audioContext.mjs | 18 +++++ packages/superdough/dspworklet.mjs | 2 +- packages/superdough/helpers.mjs | 112 ++++++++++++++++++++++++++- packages/superdough/index.mjs | 1 + packages/superdough/noise.mjs | 2 +- packages/superdough/sampler.mjs | 3 +- packages/superdough/superdough.mjs | 20 +---- packages/superdough/synth.mjs | 3 +- packages/superdough/util.mjs | 2 + packages/superdough/worklets.mjs | 80 +------------------ packages/superdough/zzfx.mjs | 3 +- packages/superdough/zzfx_fork.mjs | 2 +- 12 files changed, 142 insertions(+), 106 deletions(-) create mode 100644 packages/superdough/audioContext.mjs diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs new file mode 100644 index 00000000..9a3c7116 --- /dev/null +++ b/packages/superdough/audioContext.mjs @@ -0,0 +1,18 @@ +let audioContext; + +export const setDefaultAudioContext = () => { + audioContext = new AudioContext(); + return audioContext; +}; + +export const getAudioContext = () => { + if (!audioContext) { + return setDefaultAudioContext(); + } + + return audioContext; +}; + +export function getAudioContextCurrentTime() { + return getAudioContext().currentTime; +} \ No newline at end of file diff --git a/packages/superdough/dspworklet.mjs b/packages/superdough/dspworklet.mjs index 5b02ad29..aac08061 100644 --- a/packages/superdough/dspworklet.mjs +++ b/packages/superdough/dspworklet.mjs @@ -1,4 +1,4 @@ -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; let worklet; export async function dspWorklet(ac, code) { diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index def49c4d..b5d83e7b 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,5 +1,5 @@ -import { getAudioContext } from './superdough.mjs'; -import { clamp, nanFallback } from './util.mjs'; +import { getAudioContext } from './audioContext.mjs'; +import { clamp, ffloor, nanFallback } from './util.mjs'; import { getNoiseBuffer } from './noise.mjs'; export const noises = ['pink', 'white', 'brown', 'crackle']; @@ -308,6 +308,110 @@ export function applyFM(param, value, begin) { return { stop }; } -export const getDistortion = (distort, postgain, algorithm) => { - return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain, algorithm }); +// Saturation curves + +const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1) + +const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x)); +const _soft = (x, k) => Math.tanh(x * (1 + k)); +const _hard = (x, k) => clamp((1 + k) * x, -1, 1); +const _sine = (x, k) => Math.sin(x * (1 + k)); + +const _fold = (x, k) => { + let y = (1 + k) * x; + while (y > 1 || y < -1) { + y = y > 1 ? 2 - y : -2 - y; + } + return y; +}; + +const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k)); + +const _pow = (x, k) => { + const t = __squash(k); + const p = 1 / (1 + 0.5 * t); // tame k + return _soft(Math.sign(x) * Math.pow(Math.abs(x), p), 0.5 * k); +}; + +const _cubic = (x, k) => { + const t = __squash(k); + const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1) + return _soft(cubic, k); +}; + +const _diode = (x, k, asym = false) => { + const g = 1 + k; // gain + const t = __squash(k); + const bias = 0.15 * t; + const pos = _soft(x + bias, k); + const neg = _soft(asym ? bias : -x + bias, k); + const y = pos - neg; + // We divide by the derivative at 0 so that the distortion is roughly + // the identity map near 0 => small values are preserved and undistorted + const sech = 1 / Math.cosh(g * bias); + const sech2 = sech * sech; // derivative of tanh is sech^2 + const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x + return _soft(y / denom, k); +}; + +const _asym = (x, k) => _diode(x, k, true); + +const _chebyshev = (x, k) => { + const kl = Math.log1p(k); + let tnm1 = 1; + let tnm2 = x; + let tn; + let y = 0; + const iterations = 1 + ffloor(Math.min(16 * kl, 255)); + for (let i = 1; i < iterations; i++) { + if (i < 2) { + // Already set inital conditions + y += i == 0 ? tnm1 : tnm2; + continue; + } + tn = 2 * x * tnm1 - tnm2; // https://en.wikipedia.org/wiki/Chebyshev_polynomials#Recurrence_definition + tnm2 = tnm1; + tnm1 = tn; + if (i % 2 === 0) { + y += tn; + } + } + // Soft clip + return _soft(y, 0); +}; + +export const distortionAlgorithms = { + scurve: _scurve, + soft: _soft, + hard: _hard, + sine: _sine, + fold: _fold, + sinefold: _sineFold, + pow: _pow, + cubic: _cubic, + diode: _diode, + asym: _asym, + chebyshev: _chebyshev, +}; +const _algoNames = Object.freeze(Object.keys(distortionAlgorithms)); + +export const getDistortionAlgorithm = (algo) => { + let index = algo; + if (typeof algo === 'string') { + index = _algoNames.indexOf(algo); + if (index === -1) { + logger(`[superdough] Could not find waveshaping algorithm ${algo}. + Available options are ${_algoNames.join(', ')}. + Defaulting to ${_algoNames[0]}.`); + index = 0; + } + } + const name = _algoNames[index % _algoNames.length]; // allow for wrapping if algo was a number + const algorithm = distortionAlgorithms[name]; + return { algorithm, index }; +}; + +export const getDistortion = (distort, postgain, algorithm) => { + const { _algorithm, index } = getDistortionAlgorithm(algorithm); + return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain, algorithm: index }); }; diff --git a/packages/superdough/index.mjs b/packages/superdough/index.mjs index fd49fe33..184fc078 100644 --- a/packages/superdough/index.mjs +++ b/packages/superdough/index.mjs @@ -11,3 +11,4 @@ export * from './synth.mjs'; export * from './zzfx.mjs'; export * from './logger.mjs'; export * from './dspworklet.mjs'; +export * from './audioContext.mjs'; diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 5411f2f2..816dd252 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -1,5 +1,5 @@ import { drywet } from './helpers.mjs'; -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; let noiseCache = {}; diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 18d1b779..7d69aef0 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,5 +1,6 @@ import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs'; -import { getAudioContext, registerSound } from './index.mjs'; +import { registerSound } from './index.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { logger } from './logger.mjs'; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 98306115..10d8ed9f 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -13,6 +13,7 @@ import { createFilter, gainNode, getCompressor, getDistortion, getWorklet, webAu import { map } from 'nanostores'; import { logger, errorLogger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; +import { getAudioContext } from './audioContext.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; @@ -193,25 +194,6 @@ export function setVersionDefaults(version) { export const resetLoadedSounds = () => soundMap.set({}); -let audioContext; - -export const setDefaultAudioContext = () => { - audioContext = new AudioContext(); - return audioContext; -}; - -export const getAudioContext = () => { - if (!audioContext) { - return setDefaultAudioContext(); - } - - return audioContext; -}; - -export function getAudioContextCurrentTime() { - return getAudioContext().currentTime; -} - let workletsLoading; function loadWorklets() { if (!workletsLoading) { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 71138ae4..5f303753 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,5 +1,6 @@ import { clamp, midiToFreq, noteToMidi } from './util.mjs'; -import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs'; +import { registerSound, soundMap, getLfo } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { applyFM, gainNode, diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index f4d59024..c3d511fd 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -76,3 +76,5 @@ export function cycleToSeconds(cycle, cps) { export function secondsToCycle(t, cps) { return t * cps; } + +export const ffloor = (x) => x | 0; // fast floor for positive numbers \ No newline at end of file diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 0eb2bc72..ae03e960 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -4,12 +4,11 @@ import OLAProcessor from './ola-processor'; import FFT from './fft.js'; +import { getDistortionAlgorithm } from './helpers.mjs'; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const _mod = (n, m) => ((n % m) + m) % m; -const ffloor = (x) => x | 0; const pv = (arr, n) => arr[n] ?? arr[0]; -const mix = (a, b, t) => (1 - t) * a + t * b; // Restrict phase to the range [0, maxPhase) via wrapping function wrapPhase(phase, maxPhase = 1) { @@ -344,85 +343,12 @@ class LadderProcessor extends AudioWorkletProcessor { } registerProcessor('ladder-processor', LadderProcessor); -// Saturation curves - -const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1) - -const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x)); -const _soft = (x, k) => Math.tanh(x * (1 + k)); -const _hard = (x, k) => clamp((1 + k) * x, -1, 1); -const _sine = (x, k) => Math.sin(x * (1 + k)); - -const _fold = (x, k) => { - let y = (1 + k) * x; - while (y > 1 || y < -1) { - y = y > 1 ? 2 - y : -2 - y; - } - return y; -}; - -const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k)); - -const _pow = (x, k) => { - const t = __squash(k); - const p = 1 / (1 + 0.5 * t); // tame k - return _soft(Math.sign(x) * Math.pow(Math.abs(x), p), 0.5 * k); -}; - -const _diode = (x, k, asym = false) => { - const g = 1 + k; // gain - const t = __squash(k); - const bias = 0.15 * t; - const pos = _soft(x + bias, k); - const neg = _soft(asym ? bias : -x + bias, k); - const y = pos - neg; - // We divide by the derivative at 0 so that the distortion is roughly - // the identity map near 0 => small values are preserved and undistorted - const sech = 1 / Math.cosh(g * bias); - const sech2 = sech * sech; // derivative of tanh is sech^2 - const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x - return _soft(y / denom, k); -}; - -const _asym = (x, k) => _diode(x, k, true); - -const _cubic = (x, k) => { - const t = __squash(k); - const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1) - return _soft(cubic, k); -}; - -export const saturationAlgos = { - scurve: _scurve, - soft: _soft, - hard: _hard, - sine: _sine, - fold: _fold, - sinefold: _sineFold, - pow: _pow, - cubic: _cubic, - diode: _diode, - asym: _asym, -}; -const _algoNames = Object.freeze(Object.keys(saturationAlgos)); - -const _getAlgorithm = (algo) => { - let algoName = typeof algo === 'string' ? algo : _algoNames[algo % _algoNames.length]; - if (!_algoNames.includes(algoName)) { - algoName = _algoNames[0]; - logger(`[superdough] Could not find waveshaping algorithm ${algo}. - Available options are ${_algoNames.join(', ')}. - Defaulting to ${algoName}.`); - } - return saturationAlgos[algoName]; -}; - class DistortProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ { name: 'distort', defaultValue: 0 }, { name: 'postgain', defaultValue: 1 }, - { name: 'algorithm', defaultValue: 0, min: 0, max: _algoNames.length - 1 }, + { name: 'algorithm', defaultValue: 0, min: 0 }, ]; } @@ -440,10 +366,10 @@ class DistortProcessor extends AudioWorkletProcessor { return false; } this.started = hasInput; + const { algorithm } = getDistortionAlgorithm(pv(parameters.algorithm, 0)); // do not allow audio rate algo changes for (let n = 0; n < blockSize; n++) { const postgain = clamp(pv(parameters.postgain, n), 0.001, 1); const shape = Math.expm1(pv(parameters.distort, n)); - const algorithm = _getAlgorithm(pv(parameters.algorithm, n)); for (let ch = 0; ch < input.length; ch++) { const x = input[ch][n]; output[ch][n] = postgain * algorithm(x, shape); diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index a6af8260..32db0395 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -1,6 +1,7 @@ //import { ZZFX } from 'zzfx'; import { midiToFreq, noteToMidi } from './util.mjs'; -import { registerSound, getAudioContext } from './superdough.mjs'; +import { registerSound } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { buildSamples } from './zzfx_fork.mjs'; export const getZZFX = (value, t) => { diff --git a/packages/superdough/zzfx_fork.mjs b/packages/superdough/zzfx_fork.mjs index 7235d1bf..f3ee6a05 100644 --- a/packages/superdough/zzfx_fork.mjs +++ b/packages/superdough/zzfx_fork.mjs @@ -1,4 +1,4 @@ -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; // https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6 // changes: replaced this.volume with 1 + using sampleRate from getAudioContext() From 9b1c23d49f6cdddc792f74a6af06991d9bb6c951 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 4 Sep 2025 20:05:28 -0500 Subject: [PATCH 03/30] Some tuning experimentally and adding back chebyshev --- packages/superdough/helpers.mjs | 38 ++++++++++++--------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index b5d83e7b..28c14f79 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -315,10 +315,9 @@ const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1) const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x)); const _soft = (x, k) => Math.tanh(x * (1 + k)); const _hard = (x, k) => clamp((1 + k) * x, -1, 1); -const _sine = (x, k) => Math.sin(x * (1 + k)); const _fold = (x, k) => { - let y = (1 + k) * x; + let y = (1 + 0.5 * k) * x; while (y > 1 || y < -1) { y = y > 1 ? 2 - y : -2 - y; } @@ -327,29 +326,23 @@ const _fold = (x, k) => { const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k)); -const _pow = (x, k) => { - const t = __squash(k); - const p = 1 / (1 + 0.5 * t); // tame k - return _soft(Math.sign(x) * Math.pow(Math.abs(x), p), 0.5 * k); -}; - const _cubic = (x, k) => { - const t = __squash(k); + const t = __squash(Math.log1p(k)); const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1) return _soft(cubic, k); }; const _diode = (x, k, asym = false) => { - const g = 1 + k; // gain - const t = __squash(k); - const bias = 0.15 * t; - const pos = _soft(x + bias, k); - const neg = _soft(asym ? bias : -x + bias, k); + const g = 1 + 2 * k; // gain + const t = __squash(Math.log1p(k)); + const bias = 0.07 * t; + const pos = _soft(x + bias, 2 * k); + const neg = _soft(asym ? bias : -x + bias, 2 * k); const y = pos - neg; // We divide by the derivative at 0 so that the distortion is roughly // the identity map near 0 => small values are preserved and undistorted const sech = 1 / Math.cosh(g * bias); - const sech2 = sech * sech; // derivative of tanh is sech^2 + const sech2 = sech * sech; // derivative of soft (i.e. tanh) is sech^2 const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x return _soft(y / denom, k); }; @@ -357,13 +350,12 @@ const _diode = (x, k, asym = false) => { const _asym = (x, k) => _diode(x, k, true); const _chebyshev = (x, k) => { - const kl = Math.log1p(k); + const kl = 10 * Math.log1p(k); let tnm1 = 1; let tnm2 = x; let tn; let y = 0; - const iterations = 1 + ffloor(Math.min(16 * kl, 255)); - for (let i = 1; i < iterations; i++) { + for (let i = 1; i < 64; i++) { if (i < 2) { // Already set inital conditions y += i == 0 ? tnm1 : tnm2; @@ -373,24 +365,22 @@ const _chebyshev = (x, k) => { tnm2 = tnm1; tnm1 = tn; if (i % 2 === 0) { - y += tn; + y += Math.min(1.3 * kl / i, 2) * tn; } } // Soft clip - return _soft(y, 0); + return _soft(y, kl / 20); }; export const distortionAlgorithms = { scurve: _scurve, soft: _soft, hard: _hard, - sine: _sine, - fold: _fold, - sinefold: _sineFold, - pow: _pow, cubic: _cubic, diode: _diode, asym: _asym, + fold: _fold, + sinefold: _sineFold, chebyshev: _chebyshev, }; const _algoNames = Object.freeze(Object.keys(distortionAlgorithms)); From 51b150b8298ba2839ef787d7bb58b17d03e995c1 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 10 Sep 2025 21:27:56 -0500 Subject: [PATCH 04/30] Optimize folding for audio rate --- packages/superdough/helpers.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 28c14f79..b5606585 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -311,17 +311,17 @@ export function applyFM(param, value, begin) { // Saturation curves const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1) +const _mod = (n, m) => ((n % m) + m) % m; const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x)); const _soft = (x, k) => Math.tanh(x * (1 + k)); const _hard = (x, k) => clamp((1 + k) * x, -1, 1); const _fold = (x, k) => { + // Closed form folding for audio rate let y = (1 + 0.5 * k) * x; - while (y > 1 || y < -1) { - y = y > 1 ? 2 - y : -2 - y; - } - return y; + const window = _mod(y + 1, 4); + return 1 - Math.abs(window - 2); }; const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k)); From 9848740e180c920384ef6cba01d70ac481b66df3 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 10 Sep 2025 21:39:02 -0500 Subject: [PATCH 05/30] Formatting and tests --- packages/core/controls.mjs | 6 +- packages/superdough/audioContext.mjs | 2 +- packages/superdough/helpers.mjs | 5 +- packages/superdough/util.mjs | 2 +- packages/superdough/worklets.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 76 +++++++++++++++++++++++ 6 files changed, 85 insertions(+), 8 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 01679529..47626fda 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1586,7 +1586,7 @@ export const { shape } = registerControl(['shape', 'shapevol']); * @example * note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4") * @example - * s("bd*4").bank("tr909").distort("4:0.5:fold") + * s("bd:4*4").bank("tr808").distort("3:0.5:diode") * */ export const { distort, dist } = registerControl(['distort', 'distortvol', 'distorttype'], 'dist'); @@ -1614,8 +1614,8 @@ export const { distortvol } = registerControl('distortvol', 'distvol'); * @example * s("sine").note("F1*2").release(1) * .penv(24).pdecay(0.05) - * .distort(70) - * .distorttype("") + * .distort(rand.range(1, 8)) + * .distorttype("") */ export const { distorttype } = registerControl('distorttype', 'disttype'); diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs index 9a3c7116..71e01d57 100644 --- a/packages/superdough/audioContext.mjs +++ b/packages/superdough/audioContext.mjs @@ -15,4 +15,4 @@ export const getAudioContext = () => { export function getAudioContextCurrentTime() { return getAudioContext().currentTime; -} \ No newline at end of file +} diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index b5606585..9ee660ae 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,6 +1,7 @@ import { getAudioContext } from './audioContext.mjs'; -import { clamp, ffloor, nanFallback } from './util.mjs'; +import { clamp, nanFallback } from './util.mjs'; import { getNoiseBuffer } from './noise.mjs'; +import { logger } from './logger.mjs'; export const noises = ['pink', 'white', 'brown', 'crackle']; @@ -365,7 +366,7 @@ const _chebyshev = (x, k) => { tnm2 = tnm1; tnm1 = tn; if (i % 2 === 0) { - y += Math.min(1.3 * kl / i, 2) * tn; + y += Math.min((1.3 * kl) / i, 2) * tn; } } // Soft clip diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index c3d511fd..2477e341 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -77,4 +77,4 @@ export function secondsToCycle(t, cps) { return t * cps; } -export const ffloor = (x) => x | 0; // fast floor for positive numbers \ No newline at end of file +export const ffloor = (x) => x | 0; // fast floor for positive numbers diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index ae03e960..e593db17 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -4,7 +4,7 @@ import OLAProcessor from './ola-processor'; import FFT from './fft.js'; -import { getDistortionAlgorithm } from './helpers.mjs'; +import { getDistortionAlgorithm } from './helpers.mjs'; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const _mod = (n, m) => ((n % m) + m) % m; diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 41207620..e70a5d49 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2775,6 +2775,82 @@ exports[`runs examples > example "distort" example index 1 1`] = ` ] `; +exports[`runs examples > example "distort" example index 2 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 1/4 → 1/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 1/2 → 3/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 3/4 → 1/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 1/1 → 5/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 5/4 → 3/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 3/2 → 7/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 7/4 → 2/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 2/1 → 9/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 9/4 → 5/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 5/2 → 11/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 11/4 → 3/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 3/1 → 13/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 13/4 → 7/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 7/2 → 15/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 15/4 → 4/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", +] +`; + +exports[`runs examples > example "distorttype" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 1/4 → 1/2 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 1/2 → 3/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 3/4 → 1/1 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 1/1 → 5/4 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 5/4 → 3/2 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 3/2 → 7/4 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 7/4 → 2/1 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 2/1 → 9/4 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 9/4 → 5/2 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 5/2 → 11/4 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 11/4 → 3/1 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 3/1 → 13/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 13/4 → 7/2 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 7/2 → 15/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 15/4 → 4/1 | s:bd bank:tr909 distort:2 distorttype:0 ]", +] +`; + +exports[`runs examples > example "distorttype" example index 1 1`] = ` +[ + "[ (0/1 → 1/2) ⇝ 1/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]", + "[ 0/1 ⇜ (1/2 → 1/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]", + "[ (1/1 → 3/2) ⇝ 2/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]", + "[ 1/1 ⇜ (3/2 → 2/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]", + "[ (2/1 → 5/2) ⇝ 3/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]", + "[ 2/1 ⇜ (5/2 → 3/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]", + "[ (3/1 → 7/2) ⇝ 4/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]", + "[ 3/1 ⇜ (7/2 → 4/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]", +] +`; + +exports[`runs examples > example "distortvol" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 1/4 → 1/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 1/2 → 3/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 3/4 → 1/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 1/1 → 5/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 5/4 → 3/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 3/2 → 7/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 7/4 → 2/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 2/1 → 9/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 9/4 → 5/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 5/2 → 11/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 11/4 → 3/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 3/1 → 13/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 13/4 → 7/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 7/2 → 15/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 15/4 → 4/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", +] +`; + exports[`runs examples > example "djf" example index 0 1`] = ` [ "[ 0/1 → 1/4 | n:0 s:superzow octave:3 djf:0.5 ]", From 69b034349a02a9db8c0406f6a530e00403a0037f Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Sep 2025 14:30:05 -0500 Subject: [PATCH 06/30] Codeformat --- website/src/pages/learn/xen.mdx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index 32e2c0b4..b8b27ac7 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -16,10 +16,7 @@ These functions allow the use of scales other than your typical chromatic 12 bas Here's an example of how to configure a basic hexany scale: - + Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3` @@ -67,6 +64,7 @@ Another helpful trick when exploring new tunings is to strum them. Many have a much more enchanting sound that was chosen over many generations of musicians for being strummed. Take the `sanza` tuning: + + This quality is often due to how the tunings were formed with instruments that were played differently than a piano. As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical: From f0669ce3bad299bf52de86b49f0eee1a146e96a1 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 24 Sep 2025 17:32:03 -0700 Subject: [PATCH 07/30] Move algorithm into constructor --- packages/superdough/helpers.mjs | 9 +++++++-- packages/superdough/worklets.mjs | 7 +++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 9ee660ae..8b92bd4e 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -403,6 +403,11 @@ export const getDistortionAlgorithm = (algo) => { }; export const getDistortion = (distort, postgain, algorithm) => { - const { _algorithm, index } = getDistortionAlgorithm(algorithm); - return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain, algorithm: index }); + const { index } = getDistortionAlgorithm(algorithm); + return getWorklet( + getAudioContext(), + 'distort-processor', + { distort, postgain }, + { processorOptions: { algorithm: index } }, + ); }; diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index e593db17..f1d95e44 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -348,13 +348,13 @@ class DistortProcessor extends AudioWorkletProcessor { return [ { name: 'distort', defaultValue: 0 }, { name: 'postgain', defaultValue: 1 }, - { name: 'algorithm', defaultValue: 0, min: 0 }, ]; } - constructor() { + constructor({ processorOptions }) { super(); this.started = false; + this.algorithm = getDistortionAlgorithm(processorOptions.algorithm).algorithm; } process(inputs, outputs, parameters) { @@ -366,13 +366,12 @@ class DistortProcessor extends AudioWorkletProcessor { return false; } this.started = hasInput; - const { algorithm } = getDistortionAlgorithm(pv(parameters.algorithm, 0)); // do not allow audio rate algo changes for (let n = 0; n < blockSize; n++) { const postgain = clamp(pv(parameters.postgain, n), 0.001, 1); const shape = Math.expm1(pv(parameters.distort, n)); for (let ch = 0; ch < input.length; ch++) { const x = input[ch][n]; - output[ch][n] = postgain * algorithm(x, shape); + output[ch][n] = postgain * this.algorithm(x, shape); } } return true; From b3537a9acb6cbf3480de7afbf93c7921969dfd25 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 24 Sep 2025 22:17:30 -0700 Subject: [PATCH 08/30] Some cleanup --- packages/superdough/helpers.mjs | 11 ++--------- packages/superdough/worklets.mjs | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 8b92bd4e..5ef132d0 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -398,16 +398,9 @@ export const getDistortionAlgorithm = (algo) => { } } const name = _algoNames[index % _algoNames.length]; // allow for wrapping if algo was a number - const algorithm = distortionAlgorithms[name]; - return { algorithm, index }; + return distortionAlgorithms[name]; }; export const getDistortion = (distort, postgain, algorithm) => { - const { index } = getDistortionAlgorithm(algorithm); - return getWorklet( - getAudioContext(), - 'distort-processor', - { distort, postgain }, - { processorOptions: { algorithm: index } }, - ); + return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } }); }; diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index f1d95e44..5ab32622 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -354,7 +354,7 @@ class DistortProcessor extends AudioWorkletProcessor { constructor({ processorOptions }) { super(); this.started = false; - this.algorithm = getDistortionAlgorithm(processorOptions.algorithm).algorithm; + this.algorithm = getDistortionAlgorithm(processorOptions.algorithm); } process(inputs, outputs, parameters) { From c5f1085bd9e8a0a061247eb2e165fde574809ea7 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 30 Sep 2025 23:51:59 -0500 Subject: [PATCH 09/30] WIP --- packages/core/pattern.mjs | 18 ++++++++++++++++++ packages/superdough/helpers.mjs | 1 + packages/superdough/sampler.mjs | 4 ++-- packages/superdough/wavetable.mjs | 1 - 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d0be3f59..dcee7f17 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3536,3 +3536,21 @@ export const morph = (frompat, topat, bypat) => { bypat = reify(bypat); return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by)))); }; + +/** + * Wavefolding distortion + * + * @name fold + * @param {number | Pattern} distortion + * + */ +export const fold = register('fold', function (amt, vol = 1, pat) { + return pat.withvValue((v) => { + return { + ...v, + distortion: amt, + distortvol: vol, + distorttype: 'fold', + }; + }).innerJoin(); +}); diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index a471da9d..6db3b04a 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -475,6 +475,7 @@ export const getDistortionAlgorithm = (algo) => { }; export const getDistortion = (distort, postgain, algorithm) => { + debugger; return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } }); }; diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 325b2e2a..f229cd33 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,5 +1,5 @@ -import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs'; -import { registerSound, registerWavetable } from './index.mjs'; +import { getCommonSampleInfo } from './util.mjs'; +import { registerSound, registerWaveTable } from './index.mjs'; import { getAudioContext } from './audioContext.mjs'; import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { logger } from './logger.mjs'; diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 48b7e661..c340efa8 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -5,7 +5,6 @@ import { destroyAudioWorkletNode, getADSRValues, getFrequencyFromValue, - getLfo, getParamADSR, getPitchEnvelope, getVibratoOscillator, From a14bf4d97bc26a70a637464a0543d6861c5d9b69 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 1 Oct 2025 00:36:59 -0500 Subject: [PATCH 10/30] Add aliases for distort types --- packages/core/pattern.mjs | 67 +++++++++++++++++++++++++----- packages/superdough/helpers.mjs | 1 - packages/superdough/superdough.mjs | 3 +- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index dcee7f17..96f7d687 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3537,6 +3537,41 @@ export const morph = (frompat, topat, bypat) => { return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by)))); }; +/** + * Soft-clipping distortion + * + * @name soft + * @param {number | Pattern} distortion + * + */ +/** + * Hard-clipping distortion + * + * @name hard + * @param {number | Pattern} distortion + * + */ +/** + * Cubic polynomial distortion + * + * @name cubic + * @param {number | Pattern} distortion + * + */ +/** + * Diode-emulating distortion + * + * @name diode + * @param {number | Pattern} distortion + * + */ +/** + * Asymmetrical diode distortion + * + * @name asym + * @param {number | Pattern} distortion + * + */ /** * Wavefolding distortion * @@ -3544,13 +3579,25 @@ export const morph = (frompat, topat, bypat) => { * @param {number | Pattern} distortion * */ -export const fold = register('fold', function (amt, vol = 1, pat) { - return pat.withvValue((v) => { - return { - ...v, - distortion: amt, - distortvol: vol, - distorttype: 'fold', - }; - }).innerJoin(); -}); +/** + * Wavefolding distortion composed with sinusoid + * + * @name sinefold + * @param {number | Pattern} distortion + * + */ +/** + * Distortion via Chebyshev polynomials + * + * @name chebyshev + * @param {number | Pattern} distortion + * + */ + +const algoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev']; +for (const name of algoNames) { + Pattern.prototype[name] = function (args) { + const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name])); + return this.distort(argsPat); + }; +} diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 6db3b04a..a471da9d 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -475,7 +475,6 @@ export const getDistortionAlgorithm = (algo) => { }; export const getDistortion = (distort, postgain, algorithm) => { - debugger; return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } }); }; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 8240b45b..a37504b1 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -155,6 +155,7 @@ let defaultDefaultValues = { phaserdepth: 0.75, shapevol: 1, distortvol: 1, + distorttype: 0, delay: 0, byteBeatExpression: '0', delayfeedback: 0.5, @@ -455,7 +456,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) shapevol = getDefaultValue('shapevol'), distort, distortvol = getDefaultValue('distortvol'), - distorttype, + distorttype = getDefaultValue('distorttype'), pan, vowel, delay = getDefaultValue('delay'), From c885d84785f73a4624d26bd75f608e58baa0206a Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 1 Oct 2025 00:39:44 -0500 Subject: [PATCH 11/30] Add comment --- packages/core/pattern.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 96f7d687..e2e563e4 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3594,8 +3594,9 @@ export const morph = (frompat, topat, bypat) => { * */ -const algoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev']; -for (const name of algoNames) { +const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev']; +for (const name of distAlgoNames) { + // Add aliases for distortion algorithms Pattern.prototype[name] = function (args) { const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name])); return this.distort(argsPat); From 72298c5f037f43470d9be6929b1a099a2b3b4228 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 1 Oct 2025 00:45:26 -0500 Subject: [PATCH 12/30] Docstring cleanup --- packages/core/controls.mjs | 8 +++++--- packages/core/pattern.mjs | 25 ++++++++++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 5148e47e..3475fc2f 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1892,7 +1892,9 @@ export const { shape } = registerControl(['shape', 'shapevol']); * * @name distort * @synonyms dist - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * @param {number | string | Pattern} type type of distortion to apply * @example * s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>") * @example @@ -1908,7 +1910,7 @@ export const { distort, dist } = registerControl(['distort', 'distortvol', 'dist * * @name distortvol * @synonyms distvol - * @param {number | Pattern} type + * @param {number | Pattern} volume linear postgain of the distortion * @example * s("bd*4").bank("tr909").distort(2).distortvol(0.8) */ @@ -1919,7 +1921,7 @@ export const { distortvol } = registerControl('distortvol', 'distvol'); * * @name distorttype * @synonyms disttype - * @param {number | string | Pattern} type + * @param {number | string | Pattern} type type of distortion to apply * @example * s("bd*4").bank("tr909").distort(2).distorttype("<0 1 2>") * diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index e2e563e4..a823e7bc 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3541,59 +3541,66 @@ export const morph = (frompat, topat, bypat) => { * Soft-clipping distortion * * @name soft - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ /** * Hard-clipping distortion * * @name hard - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ /** * Cubic polynomial distortion * * @name cubic - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ /** * Diode-emulating distortion * * @name diode - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ /** * Asymmetrical diode distortion * * @name asym - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ /** * Wavefolding distortion * * @name fold - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ /** * Wavefolding distortion composed with sinusoid * * @name sinefold - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ /** * Distortion via Chebyshev polynomials * * @name chebyshev - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion * */ - const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev']; for (const name of distAlgoNames) { // Add aliases for distortion algorithms From ac582b4d40791276f3eda4f2012ab31491981861 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 2 Oct 2025 01:06:29 -0500 Subject: [PATCH 13/30] Wrap properly when phase === 1 --- packages/superdough/worklets.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index ccfaf510..05855988 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1234,7 +1234,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { _sampleFrame(frame, phase) { const len = frame.length; const pos = phase * len; - const i = pos | 0; + let i = pos | 0; + if (i >= len) i = 0; const frac = pos - i; const a = frame[i]; const i1 = i + 1 < len ? i + 1 : 0; // fast wrap From 99e14dae5ca88443c098abc4fcc56b7e1859832e Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 2 Oct 2025 01:12:42 -0500 Subject: [PATCH 14/30] Consistency --- packages/superdough/worklets.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 05855988..7858871c 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1235,10 +1235,11 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const len = frame.length; const pos = phase * len; let i = pos | 0; - if (i >= len) i = 0; + if (i >= len) i = 0; // fast wrap const frac = pos - i; const a = frame[i]; - const i1 = i + 1 < len ? i + 1 : 0; // fast wrap + let i1 = i + 1; + if (i1 >= len) i1 = 0; const b = frame[i1]; return a + (b - a) * frac; } From f369de13d7116ce24f952251e6e955e193a14126 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 2 Oct 2025 22:45:48 -0500 Subject: [PATCH 15/30] Hook up FM to wavetables --- packages/superdough/wavetable.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 44367f78..220b9589 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -308,6 +308,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { }, ); const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t); + const fm = applyFM(source.parameters.get('frequency'), value, t); const envGain = ac.createGain(); const node = source.connect(envGain); getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); @@ -318,6 +319,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { () => { destroyAudioWorkletNode(source); vibratoOscillator?.stop(); + fm?.stop(); node.disconnect(); wtPosModulators?.disconnect(); wtWarpModulators?.disconnect(); From d496919fe507caa6323ce5d33b553e2b2a15d974 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 2 Oct 2025 22:48:41 -0500 Subject: [PATCH 16/30] Import --- packages/superdough/wavetable.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 220b9589..da6abcf7 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -1,6 +1,7 @@ import { getAudioContext, registerSound } from './index.mjs'; import { getCommonSampleInfo } from './util.mjs'; import { + applyFM, applyParameterModulators, destroyAudioWorkletNode, getADSRValues, From 120f89d57fc38a70f082d1bd1b977e2d4bb8d943 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 4 Oct 2025 01:37:57 -0500 Subject: [PATCH 17/30] Handle detune in the presence of pitch envelope --- packages/core/test/pattern.test.mjs | 4 ++-- packages/superdough/wavetable.mjs | 4 ++-- packages/superdough/worklets.mjs | 24 +++++++++++++----------- website/src/components/Header/Search.css | 4 ++-- website/src/pages/learn/samples.mdx | 2 +- 5 files changed, 20 insertions(+), 18 deletions(-) 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/wavetable.mjs b/packages/superdough/wavetable.mjs index 44367f78..3329dd96 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -230,12 +230,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { begin: t, end: envEnd, frequency, - detune: value.detune, + freqspread: value.detune, position: value.wt, warp: value.warp, warpMode: warpmode, voices: Math.max(value.unison ?? 1, 1), - spread: value.spread, + panspread: value.spread, phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0, }, { outputChannelCount: [2] }, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 7858871c..dc9d93b2 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1049,14 +1049,15 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { return [ { name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, { name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, - { name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 }, - { name: 'detune', defaultValue: 0.18 }, - { name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 }, - { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 }, + { name: 'frequency', defaultValue: 440, min: Number.EPSILON }, + { name: 'detune', defaultValue: 0 }, + { name: 'freqspread', defaultValue: 0.18, min: 0 }, + { name: 'position', defaultValue: 0, min: 0, max: 1 }, + { name: 'warp', defaultValue: 0, min: 0, max: 1 }, { name: 'warpMode', defaultValue: 0 }, - { name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 }, - { name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 }, - { name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 }, + { name: 'voices', defaultValue: 1, min: 1 }, + { name: 'panspread', defaultValue: 0.7, min: 0, max: 1 }, + { name: 'phaserand', defaultValue: 0, min: 0, max: 1 }, ]; } @@ -1269,6 +1270,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } for (let i = 0; i < outL.length; i++) { const detune = pv(parameters.detune, i); + const freqspread = pv(parameters.freqspread, i); const tablePos = clamp(pv(parameters.position, i), 0, 1); const idx = tablePos * (this.numFrames - 1); const fIdx = idx | 0; @@ -1277,9 +1279,9 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const warpMode = pv(parameters.warpMode, i); const voices = pv(parameters.voices, i); const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1); - const spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 0; - const gain1 = Math.sqrt(0.5 - 0.5 * spread); - const gain2 = Math.sqrt(0.5 + 0.5 * spread); + const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0; + const gain1 = Math.sqrt(0.5 - 0.5 * panspread); + const gain2 = Math.sqrt(0.5 + 0.5 * panspread); let f = pv(parameters.frequency, i); f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune const normalizer = 1 / Math.sqrt(voices); @@ -1292,7 +1294,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { gainL = gain2; gainR = gain1; } - const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune + const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune const dPhase = fVoice * this.invSR; const level = this._chooseMip(dPhase); const table = this.tables[level]; 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 From 0d0822598351fcb7b0050a2f452f0fada4375fee Mon Sep 17 00:00:00 2001 From: Jieren Chen Date: Sun, 5 Oct 2025 13:53:00 -0400 Subject: [PATCH 18/30] change the trigger handler to match new hap --- packages/mqtt/mqtt.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mqtt/mqtt.mjs b/packages/mqtt/mqtt.mjs index aef01bd9..d74de342 100644 --- a/packages/mqtt/mqtt.mjs +++ b/packages/mqtt/mqtt.mjs @@ -82,7 +82,7 @@ Pattern.prototype.mqtt = function ( cx.connect(props); } return this.withHap((hap) => { - const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => { + const onTrigger = (hap, currentTime, cps, targetTime) => { let msg_topic = topic; if (!cx || !cx.isConnected()) { return; From 694162a7b1d0c916ffa76461bcb5fb77bf046459 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 14:13:33 +0100 Subject: [PATCH 19/30] fix purity of 'withState' so it returns a new pattern --- packages/core/pattern.mjs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d0be3f59..72f6371b 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -98,10 +98,7 @@ export class Pattern { // runs func on query state withState(func) { - return this.withHaps((haps, state) => { - func(state); - return haps; - }); + return new Pattern((state) => this.query(func(state))); } /** From fdde05e75114e8aedb38d89648d83d1c58b3b303 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 14:51:21 +0100 Subject: [PATCH 20/30] add pattern id to query state controls --- packages/core/repl.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 53562f21..8dbfbec7 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -214,7 +214,10 @@ export function repl({ } let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { - let patterns = Object.values(pPatterns); + let patterns = []; + for (const [key, value] of Object.entries(pPatterns)) { + patterns.push(value.withState(state => state.setControls({id: key}))); + } if (eachTransform) { // Explicit lambda so only element (not index and array) are passed patterns = patterns.map((x) => eachTransform(x)); @@ -228,6 +231,7 @@ export function repl({ pattern = allTransforms[i](pattern); } } + if (!isPattern(pattern)) { const message = `got "${typeof evaluated}" instead of pattern`; throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.')); From 086596c47ca564743510301a963815572d6b48b0 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 14:52:00 +0100 Subject: [PATCH 21/30] fix setControls to do union with existing controls --- packages/core/state.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/state.mjs b/packages/core/state.mjs index 162dc7da..92871081 100644 --- a/packages/core/state.mjs +++ b/packages/core/state.mjs @@ -19,9 +19,9 @@ export class State { return this.setSpan(func(this.span)); } - // Returns new State with different controls + // Returns new State with added controls. setControls(controls) { - return new State(this.span, controls); + return new State(this.span, {...this.controls, ...controls}); } } From ffbbc43c8973059a95cfd69d0594e5cac71905ef Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 15:34:51 +0100 Subject: [PATCH 22/30] add cyclist version to the query metadata --- packages/core/cyclist.mjs | 2 +- packages/core/neocyclist.mjs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index ad096103..59e410c5 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -57,7 +57,7 @@ export class Cyclist { } // query the pattern for events - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' }); haps.forEach((hap) => { if (hap.hasOnset()) { diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 261f08ac..3e412074 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -38,8 +38,7 @@ export class NeoCyclist { if (this.started === false) { return; } - - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' }); haps.forEach((hap) => { if (hap.hasOnset()) { const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps); From 64f16c4f337c63692b5f89e8226e37a0db82c923 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 15:35:34 +0100 Subject: [PATCH 23/30] placate format gods --- packages/core/repl.mjs | 2 +- packages/core/state.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 8dbfbec7..171697eb 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -216,7 +216,7 @@ export function repl({ if (Object.keys(pPatterns).length) { let patterns = []; for (const [key, value] of Object.entries(pPatterns)) { - patterns.push(value.withState(state => state.setControls({id: key}))); + patterns.push(value.withState((state) => state.setControls({ id: key }))); } if (eachTransform) { // Explicit lambda so only element (not index and array) are passed diff --git a/packages/core/state.mjs b/packages/core/state.mjs index 92871081..8aa58195 100644 --- a/packages/core/state.mjs +++ b/packages/core/state.mjs @@ -21,7 +21,7 @@ export class State { // Returns new State with added controls. setControls(controls) { - return new State(this.span, {...this.controls, ...controls}); + return new State(this.span, { ...this.controls, ...controls }); } } From 0065db8569d351954b83469c1e644087f5e7da80 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sat, 11 Oct 2025 17:40:55 -0700 Subject: [PATCH 24/30] Docs: add example of custom chained function This adds a subsection to the "Understand/Coding Syntax" documentation that shows a simple example of converting the previous chain of effects into a custom, reusable chained function. There's also a prompt for the reader to experiment. --- website/src/pages/learn/code.mdx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/website/src/pages/learn/code.mdx b/website/src/pages/learn/code.mdx index f9c6b3a2..11b8b0da 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -60,6 +60,25 @@ Strudel makes heavy use of chained functions. Here is a more sophisticated examp .room(0.5)`} /> +## Write your own chained function + +You can write your own chained function using `register`. Here's the above chain but registered as a reusable, chained function. + + pat + .s("sawtooth") + .cutoff(500) + //.delay(0.5) + .room(0.5) + ); +note("a3 c#4 e4 a4").effectChain() +`} +/> + +Try adding `.rev()` after `effectChain()` to see further effects added. + # Comments The `//` in the example above is a line comment, resulting in the `delay` function being ignored. From af53bab2596750b8dc8818a54797e9ccf7d0e5d7 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sat, 11 Oct 2025 17:50:58 -0700 Subject: [PATCH 25/30] Fixing some syntax and a typo --- website/src/pages/learn/code.mdx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/website/src/pages/learn/code.mdx b/website/src/pages/learn/code.mdx index 11b8b0da..6ff9e4a9 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -66,18 +66,16 @@ You can write your own chained function using `register`. Here's the above chain pat + tune={`const effectChain = register('effectChain', (pat) => pat .s("sawtooth") .cutoff(500) //.delay(0.5) .room(0.5) ); -note("a3 c#4 e4 a4").effectChain() -`} +note("a3 c#4 e4 a4").effectChain()`} /> -Try adding `.rev()` after `effectChain()` to see further effects added. +Try adding `.rev()` after `effectChain()` to hear further effects added. # Comments From 9c5c71c31a08321348e9eeaf4139c1f4e8a4c853 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Sat, 11 Oct 2025 17:52:26 -0700 Subject: [PATCH 26/30] Removing semicolon to be more idiomatic;;; --- website/src/pages/learn/code.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/code.mdx b/website/src/pages/learn/code.mdx index 6ff9e4a9..c454dcce 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -71,7 +71,7 @@ You can write your own chained function using `register`. Here's the above chain .cutoff(500) //.delay(0.5) .room(0.5) - ); + ) note("a3 c#4 e4 a4").effectChain()`} /> From 72eaf96b4fb922fc4aa36ef92d90843f8395b62c Mon Sep 17 00:00:00 2001 From: Erik Fox Date: Sun, 12 Oct 2025 12:30:36 -0400 Subject: [PATCH 27/30] fix: repair REPL sample sources and URL concat (#1640) --- packages/repl/prebake.mjs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index 3f421f98..26d875c2 100644 --- a/packages/repl/prebake.mjs +++ b/packages/repl/prebake.mjs @@ -20,10 +20,13 @@ export async function prebake() { // import('@strudel/osc'), ); // load samples - const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/'; + const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main'; // TODO: move this onto the strudel repo - const ts = 'https://raw.githubusercontent.com/todepond/samples/main/'; + const ts = 'https://raw.githubusercontent.com/todepond/samples/main'; + + const tc = 'https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main'; + await Promise.all([ modulesLoading, registerSynthSounds(), @@ -36,9 +39,9 @@ export async function prebake() { samples(`${ds}/tidal-drum-machines.json`), samples(`${ds}/piano.json`), samples(`${ds}/Dirt-Samples.json`), - samples(`${ds}/uzu-drumkit.json`), samples(`${ds}/vcsl.json`), samples(`${ds}/mridangam.json`), + samples(`${tc}/strudel.json`), ]); aliasBank(`${ts}/tidal-drum-machines-alias.json`); From 3c1a8c8bdb60ec9e0a89d74fe5b4e79722d36d13 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 14 Oct 2025 11:41:11 -0500 Subject: [PATCH 28/30] Format --- packages/core/test/pattern.test.mjs | 4 ++-- website/src/components/Header/Search.css | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 625f4075..1df5c877 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/website/src/components/Header/Search.css b/website/src/components/Header/Search.css index b14146cb..456ef9f6 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%); From 909e0154fe9aea8c33d20779e1b083404447501b Mon Sep 17 00:00:00 2001 From: prezmop Date: Sun, 12 Oct 2025 08:01:30 +0200 Subject: [PATCH 29/30] default to samples if repo not found --- packages/superdough/sampler.mjs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index b195ba79..02271a68 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -120,13 +120,20 @@ function githubPath(base, subpath = '') { if (!base.startsWith('github:')) { throw new Error('expected "github:" at the start of pseudoUrl'); } - let [_, path] = base.split('github:'); + let path = base.slice('github:'.length); path = path.endsWith('/') ? path.slice(0, -1) : path; - if (path.split('/').length === 2) { - // assume main as default branch if none set - path += '/main'; + + let components = path.split('/'); + let user = components[0]; + let repo = components.length >= 2 ? components[1] : 'samples'; + let branch = components.length >= 3 ? components[2] : 'main'; + let other = components.slice(3); + if (subpath) { + other.push(subpath); } - return `https://raw.githubusercontent.com/${path}/${subpath}`; + other = other.join('/'); + + return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`; } export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => { From 83c7e63432fec16ea59968863b6ad3f80ad20063 Mon Sep 17 00:00:00 2001 From: prezmop Date: Sun, 12 Oct 2025 08:05:04 +0200 Subject: [PATCH 30/30] update docs --- packages/superdough/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/superdough/README.md b/packages/superdough/README.md index 0c6e4f14..f5947d83 100644 --- a/packages/superdough/README.md +++ b/packages/superdough/README.md @@ -153,6 +153,7 @@ samples('github:tidalcycles/dirt-samples') The format is `github://`. +If `` and `` are not specified, they will default to `samples` and `main` respectively. It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo. The format is also expected to be the same as explained above.