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 }); } }); }}