Compare commits

..

1 Commits

Author SHA1 Message Date
alex 8c8f6302b7 reinstate weave / weaveWith 2025-10-13 21:57:59 +01:00
21 changed files with 114 additions and 510 deletions
+3 -36
View File
@@ -1887,52 +1887,19 @@ 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. Third option sets the waveshaping type.
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output.
* 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
* @synonyms dist
* @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
* @param {number | Pattern} distortion
* @example
* 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*4").bank("tr808").distort("3:0.5:diode")
*
*/
export const { distort, dist } = registerControl(['distort', 'distortvol', 'distorttype'], 'dist');
/**
* Postgain for waveshaping distortion.
*
* @name distortvol
* @synonyms distvol
* @param {number | Pattern} volume linear postgain of the distortion
* @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 type of distortion to apply
* @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(rand.range(1, 8))
* .distorttype("<fold chebyshev scurve diode asym sinefold>")
*/
export const { distorttype } = registerControl('distorttype', 'disttype');
export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dist');
/**
* 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)
+17 -89
View File
@@ -963,26 +963,13 @@ export const arp = register(
false,
);
/*
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
* @name weave
* @memberof Pattern
* @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd"))
* @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd"))
addToPrototype('weave', function (t, ...pats) {
return this.weaveWith(t, ...pats.map((x) => set.out(x)));
});
*/
/*
* Like 'weave', but accepts functions rather than patterns, which are applied to the pattern.
* @name weaveWith
* @memberof Pattern
*/
addToPrototype('weaveWith', function (t, ...funcs) {
const pat = this;
export const weaveWith = register('weaveWith', function (t, funcs, pat) {
const l = funcs.length;
t = Fraction(t);
if (l == 0) {
@@ -990,7 +977,21 @@ addToPrototype('weaveWith', function (t, ...funcs) {
}
return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t);
});
*/
/*
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
* @name weave
* @memberof Pattern
* @example pan(saw).weave(4, [s("bd(3,8)"), s("~ sd")])
* @example n("0 1 2 3 4 5 6 7").weave(8, [s("bd(3,8)"), s("~ sd")])
*/
export const weave = register('weave', function (t, pats, pat) {
return pat.weaveWith(
t,
pats.map((x) => (y) => y.set.out(x)),
);
});
//////////////////////////////////////////////////////////////////////
// compose matrix functions
@@ -3533,76 +3534,3 @@ export const morph = (frompat, topat, bypat) => {
bypat = reify(bypat);
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
};
/**
* Soft-clipping distortion
*
* @name soft
* @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 amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Cubic polynomial distortion
*
* @name cubic
* @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 amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Asymmetrical diode distortion
*
* @name asym
* @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 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 amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Distortion via Chebyshev polynomials
*
* @name chebyshev
* @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
Pattern.prototype[name] = function (args) {
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
return this.distort(argsPat);
};
}
+3 -6
View File
@@ -20,13 +20,10 @@ 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 tc = 'https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main';
const ts = 'https://raw.githubusercontent.com/todepond/samples/main/';
await Promise.all([
modulesLoading,
registerSynthSounds(),
@@ -39,9 +36,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`);
-1
View File
@@ -153,7 +153,6 @@ samples('github:tidalcycles/dirt-samples')
The format is `github:<user>/<repo>/<branch>`.
If `<repo>` and `<branch>` 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.
-18
View File
@@ -1,18 +0,0 @@
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 -1
View File
@@ -1,4 +1,4 @@
import { getAudioContext } from './audioContext.mjs';
import { getAudioContext } from './superdough.mjs';
let worklet;
export async function dspWorklet(ac, code) {
+1 -98
View File
@@ -1,7 +1,6 @@
import { getAudioContext } from './audioContext.mjs';
import { getAudioContext } from './superdough.mjs';
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
import { getNoiseBuffer } from './noise.mjs';
import { logger } from './logger.mjs';
export const noises = ['pink', 'white', 'brown', 'crackle'];
@@ -382,102 +381,6 @@ export function applyFM(param, value, begin) {
return { stop };
}
// 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;
const window = _mod(y + 1, 4);
return 1 - Math.abs(window - 2);
};
const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k));
const _cubic = (x, 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 + 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 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);
};
const _asym = (x, k) => _diode(x, k, true);
const _chebyshev = (x, k) => {
const kl = 10 * Math.log1p(k);
let tnm1 = 1;
let tnm2 = x;
let tn;
let y = 0;
for (let i = 1; i < 64; 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 += Math.min((1.3 * kl) / i, 2) * tn;
}
}
// Soft clip
return _soft(y, kl / 20);
};
export const distortionAlgorithms = {
scurve: _scurve,
soft: _soft,
hard: _hard,
cubic: _cubic,
diode: _diode,
asym: _asym,
fold: _fold,
sinefold: _sineFold,
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
return distortionAlgorithms[name];
};
export const getDistortion = (distort, postgain, algorithm) => {
return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } });
};
export const getFrequencyFromValue = (value, defaultNote = 36) => {
let { note, freq } = value;
note = note || defaultNote;
-1
View File
@@ -11,5 +11,4 @@ export * from './synth.mjs';
export * from './zzfx.mjs';
export * from './logger.mjs';
export * from './dspworklet.mjs';
export * from './audioContext.mjs';
export * from './wavetable.mjs';
+1 -1
View File
@@ -1,5 +1,5 @@
import { drywet } from './helpers.mjs';
import { getAudioContext } from './audioContext.mjs';
import { getAudioContext } from './superdough.mjs';
let noiseCache = {};
+23 -37
View File
@@ -1,6 +1,5 @@
import { getCommonSampleInfoFromBank } from './util.mjs';
import { registerSound, registerWaveTable } from './index.mjs';
import { getAudioContext } from './audioContext.mjs';
import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs';
import { getAudioContext, registerSound, registerWaveTable } from './index.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { logger } from './logger.mjs';
@@ -23,34 +22,35 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u];
}
export function getSampleInfo(hapValue, bank) {
const { speed = 1.0 } = hapValue;
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
return { transpose, url, index, midi, label, playbackRate };
}
export const getSampleBuffer = async (label, sampleUrl, resolveUrl) => {
// takes hapValue and returns buffer + playbackRate.
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
if (resolveUrl) {
sampleUrl = await resolveUrl(sampleUrl);
}
const ac = getAudioContext();
const buffer = await loadBuffer(sampleUrl, ac, label);
return buffer
};
function getBufferPlaybackRate(hapValue, buffer, transpose) {
const { speed = 1.0 } = hapValue;
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
if (hapValue.unit === 'c') {
playbackRate = playbackRate * buffer.duration;
}
return playbackRate;
}
return { buffer, playbackRate };
};
// creates playback ready AudioBufferSourceNode from hapValue
export function getSampleBufferSource(hapValue, buffer,transpose) {
export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
let { buffer, playbackRate } = await getSampleBuffer(hapValue, bank, resolveUrl);
if (hapValue.speed < 0) {
// should this be cached?
buffer = reverseBuffer(buffer);
}
const playbackRate = getBufferPlaybackRate(hapValue, buffer, transpose)
const ac = getAudioContext();
const bufferSource = ac.createBufferSource();
bufferSource.buffer = buffer;
@@ -120,20 +120,13 @@ function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl');
}
let path = base.slice('github:'.length);
let [_, path] = base.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
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);
if (path.split('/').length === 2) {
// assume main as default branch if none set
path += '/main';
}
other = other.join('/');
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
return `https://raw.githubusercontent.com/${path}/${subpath}`;
}
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
@@ -263,7 +256,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
const cutGroups = [];
export async function onTriggerSample(t, value, onended, bufferSrc) {
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
let {
s,
nudge = 0, // TODO: is this in seconds?
@@ -285,7 +278,7 @@ export async function onTriggerSample(t, value, onended, bufferSrc) {
// destructure adsr here, because the default should be different for synths and samples
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
const { bufferSource, sliceDuration, offset } = bufferSrc
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
// asny stuff above took too long?
if (ac.currentTime > t) {
@@ -347,21 +340,14 @@ export async function onTriggerSample(t, value, onended, bufferSrc) {
return handle;
}
async function getBufferSrcFromBank(hapValue, bank, resolveUrl = undefined) {
const { transpose, url, label } = getCommonSampleInfoFromBank(hapValue, bank)
let buffer = await getSampleBuffer(label,url, resolveUrl);
return getSampleBufferSource(hapValue, buffer, transpose)
}
function registerSample(key, bank, params) {
registerSound(key, async (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, await getBufferSrcFromBank(hapValue, bank, undefined)), {
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
type: 'sample',
samples: bank,
...params,
});
}
export function registerSampleSource(key, bank, params) {
const isWavetable = key.startsWith('wt_');
if (isWavetable) {
+22 -5
View File
@@ -9,11 +9,10 @@ import './reverb.mjs';
import './vowel.mjs';
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
import { createFilter, gainNode, getCompressor, getLfo, getWorklet, effectSend } from './helpers.mjs';
import { map } from 'nanostores';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
import { getAudioContext } from './audioContext.mjs';
import { SuperdoughAudioController } from './superdoughoutput.mjs';
export const DEFAULT_MAX_POLYPHONY = 128;
@@ -155,7 +154,6 @@ let defaultDefaultValues = {
phaserdepth: 0.75,
shapevol: 1,
distortvol: 1,
distorttype: 0,
delay: 0,
byteBeatExpression: '0',
delayfeedback: 0.5,
@@ -202,6 +200,25 @@ export function setVersionDefaults(version) {
export const resetLoadedSounds = () => soundMap.set({});
let audioContext;
export const setDefaultAudioContext = () => {
audioContext = new AudioContext({ latencyHint: 'playback' });
return audioContext;
};
export const getAudioContext = () => {
if (!audioContext) {
return setDefaultAudioContext();
}
return audioContext;
};
export function getAudioContextCurrentTime() {
return getAudioContext().currentTime;
}
let externalWorklets = [];
export function registerWorklet(url) {
externalWorklets.push(url);
@@ -456,7 +473,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
shapevol = getDefaultValue('shapevol'),
distort,
distortvol = getDefaultValue('distortvol'),
distorttype = getDefaultValue('distorttype'),
pan,
vowel,
delay = getDefaultValue('delay'),
@@ -630,7 +646,8 @@ 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 }));
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
if (tremolosync != null) {
tremolo = cps * tremolosync;
+1 -2
View File
@@ -1,6 +1,5 @@
import { clamp } from './util.mjs';
import { registerSound, soundMap } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs';
import { registerSound, getAudioContext, soundMap } from './superdough.mjs';
import {
applyFM,
destroyAudioWorkletNode,
+4 -19
View File
@@ -80,13 +80,12 @@ export function secondsToCycle(t, cps) {
// deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
export function getCommonSampleInfoFromBank(hapValue, bank) {
export function getCommonSampleInfo(hapValue, bank) {
const { s, n = 0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
let url;
let index = 0;
let {transpose, label} = getCommonSampleInfo(hapValue)
if (Array.isArray(bank)) {
index = getSoundIndex(n, bank.length);
url = bank[index];
@@ -103,20 +102,6 @@ export function getCommonSampleInfoFromBank(hapValue, bank) {
index = getSoundIndex(n, bank[closest].length);
url = bank[closest][index];
}
label = `${s}:${index}`;
return { transpose, url, label };
const label = `${s}:${index}`;
return { transpose, url, index, midi, label };
}
export function getCommonSampleInfo(hapValue) {
const { s, n = 0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
const label = `${s}:${n}`;
return { transpose, label };
}
+4 -7
View File
@@ -1,7 +1,6 @@
import { getAudioContext, registerSound } from './index.mjs';
import { getCommonSampleInfoFromBank } from './util.mjs';
import { getCommonSampleInfo } from './util.mjs';
import {
applyFM,
applyParameterModulators,
destroyAudioWorkletNode,
getADSRValues,
@@ -216,7 +215,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
}
const frequency = getFrequencyFromValue(value);
const { url, label } = getCommonSampleInfoFromBank(value, tables);
const { url, label } = getCommonSampleInfo(value, tables);
const payload = await getPayload(url, label, frameLen);
let holdEnd = t + duration;
if (clip !== undefined) {
@@ -231,12 +230,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
begin: t,
end: envEnd,
frequency,
freqspread: value.detune,
detune: value.detune,
position: value.wt,
warp: value.warp,
warpMode: warpmode,
voices: Math.max(value.unison ?? 1, 1),
panspread: value.spread,
spread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
},
{ outputChannelCount: [2] },
@@ -309,7 +308,6 @@ 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');
@@ -320,7 +318,6 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
() => {
destroyAudioWorkletNode(source);
vibratoOscillator?.stop();
fm?.stop();
node.disconnect();
wtPosModulators?.disconnect();
wtWarpModulators?.disconnect();
+20 -25
View File
@@ -4,7 +4,6 @@
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;
@@ -431,10 +430,9 @@ class DistortProcessor extends AudioWorkletProcessor {
];
}
constructor({ processorOptions }) {
constructor() {
super();
this.started = false;
this.algorithm = getDistortionAlgorithm(processorOptions.algorithm);
}
process(inputs, outputs, parameters) {
@@ -446,12 +444,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++) {
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 * this.algorithm(x, shape);
for (let i = 0; i < input.length; i++) {
output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain;
}
}
return true;
@@ -1050,15 +1049,14 @@ 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: 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: '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: 'warpMode', defaultValue: 0 },
{ name: 'voices', defaultValue: 1, min: 1 },
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
{ 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 },
];
}
@@ -1236,12 +1234,10 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
_sampleFrame(frame, phase) {
const len = frame.length;
const pos = phase * len;
let i = pos | 0;
if (i >= len) i = 0; // fast wrap
const i = pos | 0;
const frac = pos - i;
const a = frame[i];
let i1 = i + 1;
if (i1 >= len) i1 = 0;
const i1 = i + 1 < len ? i + 1 : 0; // fast wrap
const b = frame[i1];
return a + (b - a) * frac;
}
@@ -1271,7 +1267,6 @@ 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;
@@ -1280,9 +1275,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 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);
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);
let f = pv(parameters.frequency, i);
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const normalizer = 1 / Math.sqrt(voices);
@@ -1295,7 +1290,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
gainL = gain2;
gainR = gain1;
}
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
const dPhase = fVoice * this.invSR;
const level = this._chooseMip(dPhase);
const table = this.tables[level];
+1 -2
View File
@@ -1,7 +1,6 @@
//import { ZZFX } from 'zzfx';
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound } from './superdough.mjs';
import { getAudioContext } from './audioContext.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { buildSamples } from './zzfx_fork.mjs';
export const getZZFX = (value, t) => {
+1 -1
View File
@@ -1,4 +1,4 @@
import { getAudioContext } from './audioContext.mjs';
import { getAudioContext } from './superdough.mjs';
// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6
// changes: replaced this.volume with 1 + using sampleRate from getAudioContext()
-76
View File
@@ -2829,82 +2829,6 @@ 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/8 | note:D3 s:supersaw djf:0.5 ]",
-17
View File
@@ -60,23 +60,6 @@ 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.
<MiniRepl
client:idle
tune={`const effectChain = register('effectChain', (pat) => pat
.s("sawtooth")
.cutoff(500)
//.delay(0.5)
.room(0.5)
)
note("a3 c#4 e4 a4").effectChain()`}
/>
Try adding `.rev()` after `effectChain()` to hear further effects added.
# Comments
The `//` in the example above is a line comment, resulting in the `delay` function being ignored.
+1 -1
View File
@@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample
### scrub
<JsDoc client:idle name="Pattern.scrub" h={0} />
<JsDoc client:idle name="Pattern.scrub" h={0} />{' '}
### speed
+11 -67
View File
@@ -1,8 +1,6 @@
import { getSampleBufferSource, onTriggerSample, registerSampleSource } from '@strudel/webaudio';
import { registerSampleSource } from '@strudel/webaudio';
import { isAudioFile } from './files.mjs';
import { getSoundIndex, logger } from '@strudel/core';
import { registerSound } from '@strudel/webaudio';
import { getCommonSampleInfo } from '../../../packages/superdough/util.mjs';
import { logger } from '@strudel/core';
//utilites for writing and reading to the indexdb
@@ -27,15 +25,6 @@ function clearAllIDB() {
export function clearIDB(dbName) {
return window.indexedDB.deleteDatabase(dbName);
}
function registerSampleFromIdb(key, bank, params) {
}
// queries the DB, and registers the sounds so they can be played
@@ -63,76 +52,31 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
if (!isAudioFile(title)) {
return;
}
const splitRelativePath = soundFile.id.split('/');
let parentDirectory =
//fallback to file name before period and seperator if no parent directory
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
// const blob = soundFile.blob;
const blob = soundFile.blob;
return blobToDataUrl(blob).then((soundPath) => {
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
titlePathMap.set(title, soundFile.id);
titlePathMap.set(title, soundPath);
sounds.set(parentDirectory, titlePathMap);
// return blobToDataUrl(blob).then((soundPath) => {
// const titlePathMap = sounds.get(parentDirectory) ?? new Map();
// titlePathMap.set(title, soundPath);
// sounds.set(parentDirectory, titlePathMap);
// return;
// });
return;
});
}),
)
.then(() => {
sounds.forEach((titlePathMap, key) => {
const bank = Array.from(titlePathMap.keys())
const value = Array.from(titlePathMap.keys())
.sort((a, b) => {
return a.localeCompare(b);
})
.map((title) => titlePathMap.get(title));
registerSound(key, async (t, hapValue, onended) => {
const { s, n = 0 } = hapValue;
const index = getSoundIndex(n, bank.length);
let {transpose, label} = getCommonSampleInfo(hapValue)
const storeKey = bank[index];
openDB(config, (objectStore) => {
const getRequest = objectStore.get(storeKey);
getRequest.onsuccess = async (event) => {
const result = event.target.result;
let buffer = result?.blob.arrayBuffer ? await result.blob.arrayBuffer() : null;
console.info(buffer)
if (buffer) {
const bufferSource = getSampleBufferSource(hapValue,buffer,transpose)
onTriggerSample(t, hapValue, onended, bufferSource)
} else {
logger(`Could not load sample for ${storeKey}`, 'error');
}
};
})
// const buffer = objectStore.getKey(key)
// const bufferSource = getSampleBufferSource(hapValue,buffer,transpose)
// onTriggerSample(t, hapValue, onended, await getBufferSrcFromBank(hapValue, bank, undefined))
}, {
type: 'sample',
samples: bank,
});
// registerSampleSource(key, value, { prebake: false });
registerSampleSource(key, value, { prebake: false });
});
logger('imported sounds registered!', 'success');