From bb982e6b9b9a0a80a334fcdf11248e4d9d12a0c9 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 3 Sep 2025 21:55:48 -0500 Subject: [PATCH 01/14] 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 58a30652c..01679529c 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 81bec9399..def49c4d1 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 a518f49b6..98306115c 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 2406d56dc..0eb2bc72f 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 8f02ca8b8..26c9ae287 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/14] 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 000000000..9a3c71167 --- /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 5b02ad298..aac08061e 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 def49c4d1..b5d83e7bc 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 fd49fe338..184fc078c 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 5411f2f2f..816dd252b 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 18d1b7797..7d69aef02 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 98306115c..10d8ed9f4 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 71138ae41..5f303753a 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 f4d59024e..c3d511fd4 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 0eb2bc72f..ae03e9603 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 a6af82609..32db0395a 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 7235d1bf8..f3ee6a051 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/14] 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 b5d83e7bc..28c14f79d 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/14] 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 28c14f79d..b56065856 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/14] 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 01679529c..47626fda9 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 9a3c71167..71e01d57d 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 b56065856..9ee660ae2 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 c3d511fd4..2477e3413 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 ae03e9603..e593db17e 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 41207620f..e70a5d494 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/14] 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 32e2c0b43..b8b27ac7f 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/14] 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 9ee660ae2..8b92bd4e4 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 e593db17e..f1d95e449 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/14] 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 8b92bd4e4..5ef132d00 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 f1d95e449..5ab326226 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/14] 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 d0be3f599..dcee7f174 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 a471da9dd..6db3b04ab 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 325b2e2a6..f229cd338 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 48b7e661c..c340efa8c 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/14] 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 dcee7f174..96f7d687e 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 6db3b04ab..a471da9dd 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 8240b45b9..a37504b1d 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/14] 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 96f7d687e..e2e563e4f 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/14] 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 5148e47e7..3475fc2f4 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 e2e563e4f..a823e7bca 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 f369de13d7116ce24f952251e6e955e193a14126 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 2 Oct 2025 22:45:48 -0500 Subject: [PATCH 13/14] 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 44367f786..220b95896 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 14/14] Import --- packages/superdough/wavetable.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 220b95896..da6abcf76 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,