Move things around to allow selection by name and avoid circular imports; fixed chebyshev
This commit is contained in:
parent
bb982e6b9b
commit
42ab65abdb
12 changed files with 142 additions and 106 deletions
18
packages/superdough/audioContext.mjs
Normal file
18
packages/superdough/audioContext.mjs
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { getAudioContext } from './superdough.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
|
||||||
let worklet;
|
let worklet;
|
||||||
export async function dspWorklet(ac, code) {
|
export async function dspWorklet(ac, code) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { getAudioContext } from './superdough.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { clamp, nanFallback } from './util.mjs';
|
import { clamp, ffloor, nanFallback } from './util.mjs';
|
||||||
import { getNoiseBuffer } from './noise.mjs';
|
import { getNoiseBuffer } from './noise.mjs';
|
||||||
|
|
||||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||||
|
|
@ -308,6 +308,110 @@ export function applyFM(param, value, begin) {
|
||||||
return { stop };
|
return { stop };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDistortion = (distort, postgain, algorithm) => {
|
// Saturation curves
|
||||||
return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain, algorithm });
|
|
||||||
|
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 });
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@ export * from './synth.mjs';
|
||||||
export * from './zzfx.mjs';
|
export * from './zzfx.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './dspworklet.mjs';
|
export * from './dspworklet.mjs';
|
||||||
|
export * from './audioContext.mjs';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { drywet } from './helpers.mjs';
|
import { drywet } from './helpers.mjs';
|
||||||
import { getAudioContext } from './superdough.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
|
||||||
let noiseCache = {};
|
let noiseCache = {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs';
|
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 { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { createFilter, gainNode, getCompressor, getDistortion, getWorklet, webAu
|
||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
import { logger, errorLogger } from './logger.mjs';
|
import { logger, errorLogger } from './logger.mjs';
|
||||||
import { loadBuffer } from './sampler.mjs';
|
import { loadBuffer } from './sampler.mjs';
|
||||||
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
|
||||||
export const DEFAULT_MAX_POLYPHONY = 128;
|
export const DEFAULT_MAX_POLYPHONY = 128;
|
||||||
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
|
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
|
||||||
|
|
@ -193,25 +194,6 @@ export function setVersionDefaults(version) {
|
||||||
|
|
||||||
export const resetLoadedSounds = () => soundMap.set({});
|
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;
|
let workletsLoading;
|
||||||
function loadWorklets() {
|
function loadWorklets() {
|
||||||
if (!workletsLoading) {
|
if (!workletsLoading) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
|
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 {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
gainNode,
|
gainNode,
|
||||||
|
|
|
||||||
|
|
@ -76,3 +76,5 @@ export function cycleToSeconds(cycle, cps) {
|
||||||
export function secondsToCycle(t, cps) {
|
export function secondsToCycle(t, cps) {
|
||||||
return t * cps;
|
return t * cps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ffloor = (x) => x | 0; // fast floor for positive numbers
|
||||||
|
|
@ -4,12 +4,11 @@
|
||||||
|
|
||||||
import OLAProcessor from './ola-processor';
|
import OLAProcessor from './ola-processor';
|
||||||
import FFT from './fft.js';
|
import FFT from './fft.js';
|
||||||
|
import { getDistortionAlgorithm } from './helpers.mjs';
|
||||||
|
|
||||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||||
const _mod = (n, m) => ((n % m) + m) % m;
|
const _mod = (n, m) => ((n % m) + m) % m;
|
||||||
const ffloor = (x) => x | 0;
|
|
||||||
const pv = (arr, n) => arr[n] ?? arr[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
|
// Restrict phase to the range [0, maxPhase) via wrapping
|
||||||
function wrapPhase(phase, maxPhase = 1) {
|
function wrapPhase(phase, maxPhase = 1) {
|
||||||
|
|
@ -344,85 +343,12 @@ class LadderProcessor extends AudioWorkletProcessor {
|
||||||
}
|
}
|
||||||
registerProcessor('ladder-processor', LadderProcessor);
|
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 {
|
class DistortProcessor extends AudioWorkletProcessor {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [
|
||||||
{ name: 'distort', defaultValue: 0 },
|
{ name: 'distort', defaultValue: 0 },
|
||||||
{ name: 'postgain', defaultValue: 1 },
|
{ 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;
|
return false;
|
||||||
}
|
}
|
||||||
this.started = hasInput;
|
this.started = hasInput;
|
||||||
|
const { algorithm } = getDistortionAlgorithm(pv(parameters.algorithm, 0)); // do not allow audio rate algo changes
|
||||||
for (let n = 0; n < blockSize; n++) {
|
for (let n = 0; n < blockSize; n++) {
|
||||||
const postgain = clamp(pv(parameters.postgain, n), 0.001, 1);
|
const postgain = clamp(pv(parameters.postgain, n), 0.001, 1);
|
||||||
const shape = Math.expm1(pv(parameters.distort, n));
|
const shape = Math.expm1(pv(parameters.distort, n));
|
||||||
const algorithm = _getAlgorithm(pv(parameters.algorithm, n));
|
|
||||||
for (let ch = 0; ch < input.length; ch++) {
|
for (let ch = 0; ch < input.length; ch++) {
|
||||||
const x = input[ch][n];
|
const x = input[ch][n];
|
||||||
output[ch][n] = postgain * algorithm(x, shape);
|
output[ch][n] = postgain * algorithm(x, shape);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
//import { ZZFX } from 'zzfx';
|
//import { ZZFX } from 'zzfx';
|
||||||
import { midiToFreq, noteToMidi } from './util.mjs';
|
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';
|
import { buildSamples } from './zzfx_fork.mjs';
|
||||||
|
|
||||||
export const getZZFX = (value, t) => {
|
export const getZZFX = (value, t) => {
|
||||||
|
|
|
||||||
|
|
@ -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
|
// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6
|
||||||
// changes: replaced this.volume with 1 + using sampleRate from getAudioContext()
|
// changes: replaced this.volume with 1 + using sampleRate from getAudioContext()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue