Compare commits

..

6 Commits

Author SHA1 Message Date
Alex McLean 7bcd5897da format 2025-10-13 15:17:46 +01:00
Aria a7adebe036 Let pos or neg run until sign switches 2025-10-11 23:25:56 -05:00
Aria 62379269f0 Polarity approach 2025-10-11 15:12:10 -05:00
Aria 1162a42218 One more example 2025-10-11 01:28:31 -05:00
Aria 32d11d0332 Move timeline back to pattern file 2025-10-11 01:11:05 -05:00
Aria e5eb3b8d7b First pass at timelines 2025-10-10 18:47:33 -05:00
21 changed files with 144 additions and 444 deletions
+3 -53
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)
@@ -2320,23 +2287,6 @@ export const { miditouch } = registerControl('miditouch');
// TODO: what is this?
export const { polyTouch } = registerControl('polyTouch');
/**
* Checks if a control name exists in the controlAlias map.
* @name hasControlName
* @param {string} alias The control name to check
* @returns {boolean} True if the control name exists, false otherwise
*/
export const hasControlName = (alias) => {
// Check if the name exists as a key (alias) or value (main control name)
return controlAlias.has(alias) || Array.from(controlAlias.values()).includes(alias);
};
/**
* Gets the control name from the controlAlias map.
* @name getControlName
* @param {string} alias The control name to get
* @returns {string} The control name
*/
export const getControlName = (alias) => {
if (controlAlias.has(alias)) {
return controlAlias.get(alias);
+51 -68
View File
@@ -8,6 +8,7 @@ import TimeSpan from './timespan.mjs';
import Fraction, { isFraction, lcm } from './fraction.mjs';
import Hap from './hap.mjs';
import State from './state.mjs';
import { getTime } from './time.mjs';
import { unionWithObj } from './value.mjs';
import {
@@ -3534,75 +3535,57 @@ export const morph = (frompat, topat, bypat) => {
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
};
export const TIMELINES = {
state: {},
polarities: {},
};
/**
* Soft-clipping distortion
* Aligns the pattern with the specified `timeline` by ID.
*
* @name soft
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
* More specifically, if a timeline ID is encountered for the first time, that moment
* in time is marked as the "start" of that pattern. Thereafter, any other patterns aligned
* with that same id will begin at that same moment in time.
*
* If the sign of the id changes (positive to negative or vice versa), we reset the timeline.
*
* @param {number | Pattern} id Timeline id. Must be a non-zero number. Switch signs to reset.
* @returns Pattern
* @example
* s("tri").seg(8).n(irand(12)).scale("G#:minor").lpf(400).room(2)
* .timeline("<1 -1 1 2>")
* @example
* $: n("[0 .. 6]/4").scale("F:minor")
* // Execute the following line at any point in the session
* // and it will always start on note 0
* // $: n("[0 .. 6]/4").scale("F:minor").timeline(2)
*/
/**
* 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);
};
}
export const timeline = register('timeline', (id, pat) => {
if (typeof id !== 'number' || id === 0) {
logger(
`[query] ${id} is not a valid timeline id. Please ensure it is a non-zero number. Defaulting to timeline 1.`,
);
id = 1;
}
const { state, polarities } = TIMELINES;
const key = Math.abs(id);
// We let the pattern run until the sign switches
const p = id > 0 ? 1 : -1;
let t = polarities[key] === p ? state[key] : Math.ceil(getTime());
return (
pat
.late(t)
.onTrigger((hap) => {
const T = Number(hap.part.begin);
// Set state on the first trigger
state[key] ??= T;
polarities[key] ??= p;
if (polarities[key] !== p) {
state[key] = T;
}
polarities[key] = p;
}, false)
// Add labels
.withValue((v) => ({ ...v, timeline: id, offset: t }))
);
});
+6 -1
View File
@@ -4,7 +4,7 @@ import { evaluate as _evaluate } from './evaluate.mjs';
import { errorLogger, logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
import { register, Pattern, isPattern, silence, stack, TIMELINES } from './pattern.mjs';
export function repl({
defaultOutput,
@@ -52,6 +52,11 @@ export function repl({
onToggle: (started) => {
updateState({ started });
onToggle?.(started);
if (!started) {
// Reset timeline state
TIMELINES.state = {};
TIMELINES.polarities = {};
}
},
setInterval,
clearInterval,
-8
View File
@@ -8,14 +8,6 @@ This package adds midi functionality to strudel Patterns.
npm i @strudel/midi --save
```
## Enabling MIDI for Local Development in Chrome
1. Open Chrome and navigate to `chrome://flags`
2. Search for "Insecure origins treated as secure"
3. In the text field that appears, add your development origin (e.g., http://localhost:3000)
4. Enable the flag
5. Restart Chrome
## Available Controls
The following MIDI controls are available:
+7 -40
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import * as _WebMidi from 'webmidi';
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi, hasControlName, getControlName, registerControl } from '@strudel/core';
import { noteToMidi, getControlName } from '@strudel/core';
import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance:
@@ -100,34 +100,10 @@ export const midicontrolMap = new Map();
function unifyMapping(mapping) {
return Object.fromEntries(
Object.entries(mapping).map(([key, mapping]) => {
// Convert number to object with ccn property
if (typeof mapping === 'number') {
mapping = { ccn: mapping };
}
// Get the non-aliased control name from the key
const controlName = getControlName(key);
// Check if the key or controlName already exists in the controlAlias map
if (hasControlName(key) || hasControlName(controlName)) {
// Show warning and carry on.
logger(`[midimap] '[${key}, ${controlName}]' overwrites a Strudel API.`);
// Throw error to stop the music
//throw new Error(`[midimap] '${key}' overwrites a Strudel API.`);
}
// Register the control in the midicontrolMap if it doesn't exist
if (!midicontrolMap.has(controlName)) {
try {
registerControl(controlName);
} catch (err) {
throw new Error(`[midimap] Failed to register midimap control '${controlName}': ${err.message}`);
}
} else {
logger(`[midimap] '${controlName}' already registered as a midimap control. Skipping registration.`);
}
return [controlName, mapping];
return [getControlName(key), mapping];
}),
);
}
@@ -186,14 +162,7 @@ export async function midimaps(map) {
map = await loadCache[map];
}
if (typeof map === 'object') {
Object.entries(map).forEach(([name, mapping]) => {
try {
midicontrolMap.set(name, unifyMapping(mapping));
} catch (err) {
logger(`[midi] Error setting midimap '${name}': ${err.message}`);
throw err;
}
});
Object.entries(map).forEach(([name, mapping]) => midicontrolMap.set(name, unifyMapping(mapping)));
}
}
@@ -355,18 +324,18 @@ Pattern.prototype.midi = function (midiport, options = {}) {
const device = getDevice(midiConfig.midiport, outputs);
const otherOutputs = outputs.filter((o) => o.name !== device.name);
logger(
`[midi] Midi enabled! Using "${device.name}". ${
`Midi enabled! Using "${device.name}". ${
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
}`,
);
},
onDisconnected: ({ outputs }) =>
logger(`[midi] Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});
return this.onTrigger((hap, currentTime, cps, targetTime) => {
if (!WebMidi.enabled) {
logger('[midi] Midi not enabled');
logger('Midi not enabled');
return;
}
hap.ensureObjectValue();
@@ -414,9 +383,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
} else if (midimap !== 'default') {
// Add warning when a non-existent midimap is specified
throw new Error(
`[midimap] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`,
);
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
}
// Handle note
+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 = {};
+7 -15
View File
@@ -1,6 +1,5 @@
import { getCommonSampleInfo } 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';
@@ -121,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 || '') => {
+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,
+2 -5
View File
@@ -1,7 +1,6 @@
import { getAudioContext, registerSound } from './index.mjs';
import { getCommonSampleInfo } from './util.mjs';
import {
applyFM,
applyParameterModulators,
destroyAudioWorkletNode,
getADSRValues,
@@ -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()
+17 -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 ]",
@@ -10920,6 +10844,23 @@ exports[`runs examples > example "take" example index 2 1`] = `
]
`;
exports[`runs examples > example "timeline" example index 0 1`] = `[]`;
exports[`runs examples > example "timeline" example index 1 1`] = `
[
"[ 0/1 → 4/7 | note:F3 ]",
"[ (4/7 → 1/1) ⇝ 8/7 | note:G3 ]",
"[ 4/7 ⇜ (1/1 → 8/7) | note:G3 ]",
"[ 8/7 → 12/7 | note:Ab3 ]",
"[ (12/7 → 2/1) ⇝ 16/7 | note:Bb3 ]",
"[ 12/7 ⇜ (2/1 → 16/7) | note:Bb3 ]",
"[ 16/7 → 20/7 | note:C4 ]",
"[ (20/7 → 3/1) ⇝ 24/7 | note:Db4 ]",
"[ 20/7 ⇜ (3/1 → 24/7) | note:Db4 ]",
"[ 24/7 → 4/1 | note:Eb4 ]",
]
`;
exports[`runs examples > example "tour" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:e s:folkharp ]",
-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.