Compare commits

...

4 Commits

9 changed files with 554 additions and 358 deletions
+8
View File
@@ -2846,6 +2846,7 @@ registerSubControls('lfo', [
['skew', 'sk'], ['skew', 'sk'],
['curve'], ['curve'],
['sync', 's'], ['sync', 's'],
['fxi'],
]); ]);
registerSubControls('env', [ registerSubControls('env', [
['control', 'c'], ['control', 'c'],
@@ -2859,6 +2860,7 @@ registerSubControls('env', [
['acurve', 'ac'], ['acurve', 'ac'],
['dcurve', 'dc'], ['dcurve', 'dc'],
['rcurve', 'rc'], ['rcurve', 'rc'],
['fxi'],
]); ]);
registerSubControls('bmod', [ registerSubControls('bmod', [
['bus', 'b'], ['bus', 'b'],
@@ -2867,6 +2869,7 @@ registerSubControls('bmod', [
['depth', 'dep', 'dr'], ['depth', 'dep', 'dr'],
['depthabs', 'da'], ['depthabs', 'da'],
['dc'], ['dc'],
['fxi'],
]); ]);
Pattern.prototype.modulate = function (type, config, id) { Pattern.prototype.modulate = function (type, config, id) {
@@ -2932,6 +2935,7 @@ Pattern.prototype.modulate = function (type, config, id) {
* @param {number | Pattern} [config.skew] Skew amount. Aliases: sk * @param {number | Pattern} [config.skew] Skew amount. Aliases: sk
* @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c * @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c
* @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s * @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s
* @param {number | Pattern} [config.fxi] FX index to target
* @param {string | Pattern} id ID to use for this modulator * @param {string | Pattern} id ID to use for this modulator
* @returns Pattern * @returns Pattern
* *
@@ -2985,6 +2989,7 @@ export const lfo = (config) => pure({}).lfo(config);
* @param {number | Pattern} [config.acurve] Snappiness of attack curve (-1 = relaxed, 1 = snappy). Aliases: ac * @param {number | Pattern} [config.acurve] Snappiness of attack curve (-1 = relaxed, 1 = snappy). Aliases: ac
* @param {number | Pattern} [config.dcurve] Snappiness of decay curve (-1 = relaxed, 1 = snappy). Aliases: dc * @param {number | Pattern} [config.dcurve] Snappiness of decay curve (-1 = relaxed, 1 = snappy). Aliases: dc
* @param {number | Pattern} [config.rcurve] Snappiness of release curve (-1 = relaxed, 1 = snappy). Aliases: rc * @param {number | Pattern} [config.rcurve] Snappiness of release curve (-1 = relaxed, 1 = snappy). Aliases: rc
* @param {number | Pattern} [config.fxi] FX index to target
* @param {string | Pattern} id ID to use for this modulator * @param {string | Pattern} id ID to use for this modulator
* @returns Pattern * @returns Pattern
* *
@@ -3039,6 +3044,7 @@ export const env = (config) => pure({}).env(config);
* @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
* @param {number | Pattern} [config.dc] DC offset prior to application * @param {number | Pattern} [config.dc] DC offset prior to application
* @param {number | Pattern} [config.fxi] FX index to target
* @param {string | Pattern} id ID to use for this modulator * @param {string | Pattern} id ID to use for this modulator
* @returns Pattern * @returns Pattern
* *
@@ -3065,3 +3071,5 @@ export const bmod = (config) => pure({}).bmod(config);
* s("hh*16").bank("tr909").transient("<-1:1 1:-1>") * s("hh*16").bank("tr909").transient("<-1:1 1:-1>")
*/ */
export const { transient } = registerControl(['transient', 'transsustain']); export const { transient } = registerControl(['transient', 'transsustain']);
export const { FXrelease, FXrel, FXr, fxr } = registerControl('FXrelease', 'FXrel', 'FXr', 'fxr');
+32
View File
@@ -3721,3 +3721,35 @@ Pattern.prototype.phases = function (list) {
export const phases = (list) => { export const phases = (list) => {
return _ensureListPattern(list).as('phases'); return _ensureListPattern(list).as('phases');
}; };
/**
* Establishes an FX chain. Can be called by chaining .FX(<fx1>).FX(<fx2>)..
* calls and/or in a single .FX(<fx1>, <fx2>, ..) call. The <fx1>, .. are _patterns_ which
* establish the controls of the given effect. See examples.
* @name FX
* @memberof Pattern
* @returns Pattern
* @example
* $: s("[sbd <hh [bd | lt | oh]>]*4").dec(.4)
* .FX(
* phaser(0.5).gain(2),
* bpf(800),
* distort(1.3),
* room(0.2),
* delay(0.5).gain(1.25),
* distort(0.3),
* ).fxr(1.7) // sets release time of effects (like delay)
* @example
* $: s("saw").fm(0.5)
* .delay(0.3) // outer effects are applied *last*
* .FX(coarse(4)) // first coarse
* .FX(lpf(500).lpe(4).lpa(1).lpd(2)) // then lpf
* .FX(distort(1)) // then distort
*/
Pattern.prototype.FX = function (...effects) {
effects = effects.map(reify);
return this.withValue((v) => (vEff) => {
const currFX = v.FX ?? [];
return { ...v, FX: currFX.concat(vEff) };
}).appLeft(parray(effects));
};
+9 -10
View File
@@ -5,19 +5,18 @@ if (typeof DelayNode !== 'undefined') {
wet = Math.abs(wet); wet = Math.abs(wet);
this.delayTime.value = time; this.delayTime.value = time;
const feedbackGain = ac.createGain(); this.feedbackGain = ac.createGain();
feedbackGain.gain.value = Math.min(Math.abs(feedback), 0.995); this.feedbackGain.gain.value = Math.min(Math.abs(feedback), 0.995);
this.feedback = feedbackGain.gain; this.feedback = this.feedbackGain.gain;
const delayGain = ac.createGain(); this.delayGain = ac.createGain();
delayGain.gain.value = wet; this.delayGain.gain.value = wet;
this.delayGain = delayGain;
this.connect(feedbackGain); this.connect(this.feedbackGain);
this.connect(delayGain); this.connect(this.delayGain);
feedbackGain.connect(this); this.feedbackGain.connect(this);
this.connect = (target) => delayGain.connect(target); this.connect = (target) => this.delayGain.connect(target);
return this; return this;
} }
start(t) { start(t) {
+23 -9
View File
@@ -62,7 +62,10 @@ const getTargetParamsForControl = (control, nodes, subControl) => {
const lookupKey = subControl ? `${control}_${subControl}` : control; const lookupKey = subControl ? `${control}_${subControl}` : control;
const targetInfo = getControlData(lookupKey) ?? getControlData(control); const targetInfo = getControlData(lookupKey) ?? getControlData(control);
if (!targetInfo) { if (!targetInfo) {
errorLogger(new Error(`Could not find control data for target '${control}'`), 'superdough'); errorLogger(
new Error(`Could not find control data for target '${control}'. It may not be modulatable.`),
'superdough',
);
return { targetParams: [], paramName: control }; return { targetParams: [], paramName: control };
} }
const paramName = targetInfo.param; const paramName = targetInfo.param;
@@ -85,8 +88,19 @@ const getTargetParamsForControl = (control, nodes, subControl) => {
}; };
export const connectLFO = (id, params, nodeTracker) => { export const connectLFO = (id, params, nodeTracker) => {
const { rate = 1, sync, cps, cycle, control = 'lfo', subControl, depth = 1, depthabs, ...filteredParams } = params; const {
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker, subControl); rate = 1,
sync,
cps,
cycle,
control = 'lfo',
subControl,
fxi = 'main',
depth = 1,
depthabs,
...filteredParams
} = params;
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
if (!targetParams.length) return; if (!targetParams.length) return;
let currentValue = targetParams[0].value; let currentValue = targetParams[0].value;
currentValue = currentValue === 0 ? 1 : currentValue; currentValue = currentValue === 0 ? 1 : currentValue;
@@ -101,14 +115,14 @@ export const connectLFO = (id, params, nodeTracker) => {
max, max,
}; };
const lfoNode = getLfo(getAudioContext(), modParams); const lfoNode = getLfo(getAudioContext(), modParams);
nodeTracker[`lfo_${id}`] = [lfoNode]; nodeTracker.main[`lfo_${id}`] = [lfoNode];
targetParams.forEach((t) => lfoNode.connect(t)); targetParams.forEach((t) => lfoNode.connect(t));
return lfoNode; return lfoNode;
}; };
export const connectEnvelope = (id, params, nodeTracker) => { export const connectEnvelope = (id, params, nodeTracker) => {
const { control, subControl, acurve, dcurve, rcurve, depth = 1, depthabs, ...filteredParams } = params; const { control, subControl, acurve, dcurve, rcurve, depth = 1, depthabs, fxi = 'main', ...filteredParams } = params;
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker, subControl); const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
if (!targetParams.length) return; if (!targetParams.length) return;
let currentValue = targetParams[0].value; let currentValue = targetParams[0].value;
currentValue = currentValue === 0 ? 1 : currentValue; currentValue = currentValue === 0 ? 1 : currentValue;
@@ -123,15 +137,15 @@ export const connectEnvelope = (id, params, nodeTracker) => {
decayCurve: dcurve, decayCurve: dcurve,
releaseCurve: rcurve, releaseCurve: rcurve,
}); });
nodeTracker[`env_${id}`] = [envNode]; nodeTracker.main[`env_${id}`] = [envNode];
targetParams.forEach((t) => envNode.connect(t)); targetParams.forEach((t) => envNode.connect(t));
return envNode; return envNode;
}; };
export const connectBusModulator = (params, nodeTracker, controller) => { export const connectBusModulator = (params, nodeTracker, controller) => {
const ac = getAudioContext(); const ac = getAudioContext();
const { control, subControl, depth = 1, depthabs } = params; const { control, subControl, depth = 1, depthabs, fxi = 'main' } = params;
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker, subControl); const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
if (!targetParams.length) return { toCleanup: [] }; if (!targetParams.length) return { toCleanup: [] };
const signal = controller.getBus(params.bus); const signal = controller.getBus(params.bus);
const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 }); const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 });
+4 -2
View File
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { releaseAudioNode } from './helpers.mjs';
var reverbGen = {}; var reverbGen = {};
/** Generates a reverb impulse response. /** Generates a reverb impulse response.
@@ -104,8 +106,8 @@ var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt,
player.start(); player.start();
context.oncomplete = function (event) { context.oncomplete = function (event) {
callback(event.renderedBuffer); callback(event.renderedBuffer);
filter.disconnect(); releaseAudioNode(filter);
player.disconnect(); releaseAudioNode(player);
}; };
context.startRendering(); context.startRendering();
+420 -330
View File
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs'; import './feedbackdelay.mjs';
import './reverb.mjs'; import './reverb.mjs';
import './vowel.mjs'; import './vowel.mjs';
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs'; import { clamp, nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet'; import workletsUrl from './worklets.mjs?audioworklet';
import { import {
createFilter, createFilter,
@@ -18,6 +18,7 @@ import {
getLfo, getLfo,
getWorklet, getWorklet,
releaseAudioNode, releaseAudioNode,
webAudioTimeout,
} from './helpers.mjs'; } from './helpers.mjs';
import { map } from 'nanostores'; import { map } from 'nanostores';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
@@ -193,6 +194,9 @@ let defaultDefaultValues = {
i: 1, i: 1,
velocity: 1, velocity: 1,
fft: 8, fft: 8,
tremolodepth: 1,
tremolophase: 0,
release: 0.01,
}; };
const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues }); const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues });
@@ -402,8 +406,37 @@ function mapChannelNumbers(channels) {
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
} }
class Chain {
constructor(head) {
this.audioNodes = [head];
this.tails = [head];
}
connect(...nodes) {
nodes.forEach((node) => {
this.tails.forEach((tail) => {
tail.connect(node);
});
});
this.tails = nodes;
this.audioNodes.push(...nodes);
return this;
}
connectOne(idx, node) {
this.tails[idx].connect(node);
this.tails[idx] = node;
this.audioNodes.push(node);
return this;
}
releaseNodes() {
this.audioNodes.forEach((n) => releaseAudioNode(n));
this.audioNodes = [];
this.tails = [];
}
}
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
let nodes = {}; // mapping from main FX and numbered FX chains to nodes
const nodes = { main: {} };
// new: t is always expected to be the absolute target onset time // new: t is always expected to be the absolute target onset time
const ac = getAudioContext(); const ac = getAudioContext();
const audioController = getSuperdoughAudioController(); const audioController = getSuperdoughAudioController();
@@ -432,44 +465,17 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
} }
// destructure // destructure
let { let {
tremolo,
tremolosync,
tremolodepth = 1,
tremoloskew,
tremolophase = 0,
tremoloshape,
s = getDefaultValue('s'), s = getDefaultValue('s'),
bank, bank,
source, source,
gain = getDefaultValue('gain'),
postgain = getDefaultValue('postgain'), postgain = getDefaultValue('postgain'),
density = getDefaultValue('density'),
duckorbit, duckorbit,
duckonset, duckonset,
duckattack, duckattack,
duckdepth, duckdepth,
djf, djf,
// filters release = getDefaultValue('release'),
fanchor = getDefaultValue('fanchor'),
release = 0,
//phaser
phaserrate,
phaserdepth = getDefaultValue('phaserdepth'),
phasersweep,
phasercenter,
//
coarse,
crush,
dry, dry,
shape,
shapevol = getDefaultValue('shapevol'),
distort,
distortvol = getDefaultValue('distortvol'),
distorttype = getDefaultValue('distorttype'),
pan,
vowel,
delay = getDefaultValue('delay'), delay = getDefaultValue('delay'),
delayfeedback = getDefaultValue('delayfeedback'), delayfeedback = getDefaultValue('delayfeedback'),
delaysync = getDefaultValue('delaysync'), delaysync = getDefaultValue('delaysync'),
@@ -486,16 +492,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
irspeed, irspeed,
irbegin, irbegin,
i = getDefaultValue('i'), i = getDefaultValue('i'),
velocity = getDefaultValue('velocity'),
analyze, // analyser wet analyze, // analyser wet
fft = getDefaultValue('fft'), // fftSize 0 - 10 fft = getDefaultValue('fft'), // fftSize 0 - 10
compressor: compressorThreshold, FX = [],
compressorRatio, FXrelease,
compressorKnee,
compressorAttack,
compressorRelease,
transient,
transsustain,
} = value; } = value;
delaytime = delaytime ?? cycleToSeconds(delaysync, cps); delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
@@ -510,18 +510,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth); audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth);
} }
gain = applyGainCurve(nanFallback(gain, 1));
postgain = applyGainCurve(postgain); postgain = applyGainCurve(postgain);
shapevol = applyGainCurve(shapevol);
distortvol = applyGainCurve(distortvol);
delay = applyGainCurve(delay); delay = applyGainCurve(delay);
velocity = applyGainCurve(velocity);
tremolodepth = applyGainCurve(tremolodepth);
busgain = applyGainCurve(busgain); busgain = applyGainCurve(busgain);
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
const end = t + hapDuration; const end = t + hapDuration;
const endWithRelease = end + release; const fullRelease = Math.max(release, FXrelease ?? 0);
const endWithRelease = end + fullRelease;
const chainID = Math.round(Math.random() * 1000000); const chainID = Math.round(Math.random() * 1000000);
// oldest audio nodes will be destroyed if maximum polyphony is exceeded // oldest audio nodes will be destroyed if maximum polyphony is exceeded
@@ -535,8 +530,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
activeSoundSources.delete(chainID); activeSoundSources.delete(chainID);
} }
const audioNodes = [];
if (['-', '~', '_'].includes(s)) { if (['-', '~', '_'].includes(s)) {
return; return;
} }
@@ -549,19 +542,27 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
let sourceNode; let sourceNode;
if (source) { if (source) {
sourceNode = source(t, value, hapDuration, cps); sourceNode = source(t, value, hapDuration, cps);
nodes['source'] = [sourceNode]; nodes.main['source'] = [sourceNode];
} else if (getSound(s)) { } else if (getSound(s)) {
const { onTrigger } = getSound(s); const { onTrigger } = getSound(s);
const onEnded = () => { // We have to use onEnded because some sources (e.g. `sampler`) have
audioNodes.forEach((n) => releaseAudioNode(n)); // an internal duration which is longer than `value.duration`
activeSoundSources.delete(chainID); const onEnded = () =>
}; webAudioTimeout(
ac,
() => {
chain.releaseNodes();
activeSoundSources.delete(chainID);
},
0,
endWithRelease,
);
const soundHandle = await onTrigger(t, value, onEnded, cps); const soundHandle = await onTrigger(t, value, onEnded, cps);
if (soundHandle) { if (soundHandle) {
sourceNode = soundHandle.node; sourceNode = soundHandle.node;
activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC
nodes = { ...nodes, ...soundHandle.nodes }; nodes.main = { ...nodes.main, ...soundHandle.nodes };
} }
} else { } else {
throw new Error(`sound ${s} not found! Is it loaded?`); throw new Error(`sound ${s} not found! Is it loaded?`);
@@ -576,253 +577,340 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
logger('[webaudio] skip hap: still loading', ac.currentTime - t); logger('[webaudio] skip hap: still loading', ac.currentTime - t);
return; return;
} }
const chain = []; // audio nodes that will be connected to each other sequentially
chain.push(sourceNode);
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
if (transient !== undefined) { const chain = new Chain(sourceNode); // connection manager which tracks audio nodes for releasing
const transProcessor = getWorklet( FX = [...FX, value]; // run through the FX chain and then run through all FX outside of it as well
ac, for (let [idx, fx] of Object.entries(FX)) {
'transient-processor', const key = idx == FX.length - 1 ? 'main' : idx;
{}, nodes[key] ??= {};
{ const fxNodes = nodes[key];
processorOptions: { let {
attack: transient, gain = getDefaultValue('gain'),
sustain: transsustain, velocity = getDefaultValue('velocity'),
begin: t, shapevol = getDefaultValue('shapevol'),
end: endWithRelease, distorttype = getDefaultValue('distorttype'),
distortvol = getDefaultValue('distortvol'),
tremolodepth = getDefaultValue('tremolodepth'),
phaserdepth = getDefaultValue('phaserdepth'),
delay = getDefaultValue('delay'),
delayfeedback = getDefaultValue('delayfeedback'),
delaysync = getDefaultValue('delaysync'),
delaytime,
stretch = getDefaultValue('stretch'),
i = getDefaultValue('i'),
} = fx;
gain = applyGainCurve(nanFallback(gain, 1));
shapevol = applyGainCurve(shapevol);
distortvol = applyGainCurve(distortvol);
velocity = applyGainCurve(velocity);
tremolodepth = applyGainCurve(tremolodepth);
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
stretch !== undefined && chain.connect(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
if (fx.transient !== undefined) {
const transProcessor = getWorklet(
ac,
'transient-processor',
{},
{
processorOptions: {
attack: fx.transient,
sustain: fx.transsustain,
begin: t,
end: endWithRelease,
},
}, },
}, );
); chain.connect(transProcessor);
chain.push(transProcessor); fxNodes['transient'] = transProcessor;
nodes['transient'] = transProcessor; }
}
// gain stage // gain stage
const initialGain = gainNode(gain); const initialGain = gainNode(gain);
nodes['gain'] = [initialGain]; fxNodes['gain'] = [initialGain];
chain.push(initialGain); chain.connect(initialGain);
// filter // filter
const ftype = getFilterType(value.ftype); const ftype = getFilterType(value.ftype);
const filt = (params) => createFilter(ac, t, end, params, cps, cycle); const filt = (params) => createFilter(ac, t, end, params, cps, cycle);
if (value.cutoff !== undefined) { if (fx.cutoff !== undefined) {
const lpMap = { const lpMap = {
frequency: 'cutoff', frequency: 'cutoff',
q: 'resonance', q: 'resonance',
attack: 'lpattack', attack: 'lpattack',
decay: 'lpdecay', decay: 'lpdecay',
sustain: 'lpsustain', sustain: 'lpsustain',
release: 'lprelease', release: 'lprelease',
env: 'lpenv', env: 'lpenv',
anchor: 'fanchor', anchor: 'fanchor',
model: 'ftype', model: 'ftype',
drive: 'drive', drive: 'drive',
rate: 'lprate', rate: 'lprate',
sync: 'lpsync', sync: 'lpsync',
depth: 'lpdepth', depth: 'lpdepth',
depthfrequency: 'lpdepthfrequency', depthfrequency: 'lpdepthfrequency',
shape: 'lpshape', shape: 'lpshape',
dcoffset: 'lpdc', dcoffset: 'lpdc',
skew: 'lpskew', skew: 'lpskew',
}; };
const lpParams = pickAndRename(value, lpMap); const lpParams = pickAndRename(fx, lpMap);
lpParams.type = 'lowpass'; lpParams.type = 'lowpass';
const { filter: lpf1, lfo: lfo1 } = filt(lpParams); const { filter: lpf1, lfo: lfo1 } = filt(lpParams);
nodes['lpf'] = [lpf1]; fxNodes['lpf'] = [lpf1];
nodes['lpf_lfo'] = [lfo1]; fxNodes['lpf_lfo'] = [lfo1];
chain.push(lpf1); chain.connect(lpf1);
lfo1 && audioNodes.push(lfo1); lfo1 && chain.audioNodes.push(lfo1);
if (ftype === '24db') { if (ftype === '24db') {
const { filter: lpf2, lfo: lfo2 } = filt(lpParams); const { filter: lpf2, lfo: lfo2 } = filt(lpParams);
nodes['lpf'].push(lpf2); fxNodes['lpf'].push(lpf2);
nodes['lpf_lfo'].push(lfo2); fxNodes['lpf_lfo'].push(lfo2);
chain.push(lpf2); chain.connect(lpf2);
lfo2 && audioNodes.push(lfo2); lfo2 && chain.audioNodes.push(lfo2);
}
}
if (fx.hcutoff !== undefined) {
const hpMap = {
frequency: 'hcutoff',
q: 'hresonance',
attack: 'hpattack',
decay: 'hpdecay',
sustain: 'hpsustain',
release: 'hprelease',
env: 'hpenv',
anchor: 'fanchor',
model: 'ftype',
drive: 'drive',
rate: 'hprate',
sync: 'hpsync',
depth: 'hpdepth',
depthfrequency: 'hpdepthfrequency',
shape: 'hpshape',
dcoffset: 'hpdc',
skew: 'hpskew',
};
const hpParams = pickAndRename(fx, hpMap);
hpParams.type = 'highpass';
const { filter: hpf1, lfo: lfo1 } = filt(hpParams);
fxNodes['hpf'] = [hpf1];
fxNodes['hpf_lfo'] = [lfo1];
lfo1 && chain.audioNodes.push(lfo1);
chain.connect(hpf1);
if (ftype === '24db') {
const { filter: hpf2, lfo: lfo2 } = filt(hpParams);
fxNodes['hpf'].push(hpf2);
fxNodes['hpf_lfo'].push(lfo2);
chain.connect(hpf2);
lfo2 && chain.audioNodes.push(lfo2);
}
}
if (fx.bandf !== undefined) {
const bpMap = {
frequency: 'bandf',
q: 'bandq',
attack: 'bpattack',
decay: 'bpdecay',
sustain: 'bpsustain',
release: 'bprelease',
env: 'bpenv',
anchor: 'fanchor',
model: 'ftype',
drive: 'drive',
rate: 'bprate',
sync: 'bpsync',
depth: 'bpdepth',
depthfrequency: 'bpdepthfrequency',
shape: 'bpshape',
dcoffset: 'bpdc',
skew: 'bpskew',
};
const bpParams = pickAndRename(fx, bpMap);
bpParams.type = 'bandpass';
const { filter: bpf1, lfo: lfo1 } = filt(bpParams);
fxNodes['bpf'] = [bpf1];
fxNodes['bpf_lfo'] = [lfo1];
chain.connect(bpf1);
lfo1 && chain.audioNodes.push(lfo1);
if (ftype === '24db') {
const { filter: bpf2, lfo: lfo2 } = filt(bpParams);
fxNodes['bpf'].push(bpf2);
fxNodes['bpf_lfo'].push(lfo2);
chain.connect(bpf2);
lfo2 && chain.audioNodes.push(lfo2);
}
}
if (fx.vowel !== undefined) {
const vowelNode = ac.createVowelFilter(fx.vowel);
fxNodes['vowel'] = vowelNode.filters;
chain.connect(vowelNode);
}
// effects
if (fx.coarse !== undefined) {
const coarseNode = getWorklet(ac, 'coarse-processor', { coarse: fx.coarse });
fxNodes['coarse'] = [coarseNode];
chain.connect(coarseNode);
}
if (fx.crush !== undefined) {
const crushNode = getWorklet(ac, 'crush-processor', { crush: fx.crush });
fxNodes['crush'] = [crushNode];
chain.connect(crushNode);
}
if (fx.shape !== undefined) {
const shapeNode = getWorklet(ac, 'shape-processor', { shape: fx.shape, postgain: shapevol });
fxNodes['shape'] = [shapeNode];
chain.connect(shapeNode);
}
if (fx.distort !== undefined) {
const distortNode = getDistortion(fx.distort, distortvol, distorttype);
fxNodes['distort'] = [distortNode];
chain.connect(distortNode);
}
let tremolo = fx.tremolo;
if (fx.tremolosync != null) {
tremolo = cps * fx.tremolosync;
}
if (tremolo !== undefined) {
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
const gain = Math.max(1 - tremolodepth, 0);
const amGain = new GainNode(ac, { gain });
const time = cycle / cps;
const lfo = getLfo(ac, {
skew: fx.tremoloskew ?? (fx.tremoloshape != null ? 0.5 : 1),
frequency: tremolo,
depth: tremolodepth,
time,
dcoffset: 0,
shape: fx.tremoloshape,
phaseoffset: fx.tremolophase,
min: 0,
max: 1,
curve: 1.5,
begin: t,
end: endWithRelease,
});
fxNodes['tremolo'] = [lfo];
fxNodes['tremolo_gain'] = [amGain];
lfo.connect(amGain.gain);
chain.audioNodes.push(lfo);
chain.connect(amGain);
}
if (fx.compressor !== undefined) {
const compressorNode = getCompressor(
ac,
fx.compressor,
fx.compressorRatio,
fx.compressorKnee,
fx.compressorAttack,
fx.compressorRelease,
);
fxNodes['compressor'] = [compressorNode];
chain.connect(compressorNode);
}
// panning
if (fx.pan !== undefined) {
const panner = ac.createStereoPanner();
panner.pan.value = 2 * fx.pan - 1;
chain.connect(panner);
}
// phaser
if (fx.phaserrate !== undefined && phaserdepth > 0) {
const { filterChain, lfo } = getPhaser(
t,
endWithRelease,
fx.phaserrate,
phaserdepth,
fx.phasercenter,
fx.phasersweep,
);
fxNodes['phaser'] = [...filterChain];
fxNodes['phaser_lfo'] = [lfo];
filterChain.forEach((f) => chain.connect(f));
chain.audioNodes.push(lfo);
}
// delay
if (key !== 'main' && delay > 0 && delaytime > 0 && delayfeedback > 0) {
const dry = gainNode(1);
delayfeedback = clamp(delayfeedback, 0, 0.98);
const delayNode = ac.createFeedbackDelay(1, delaytime, delayfeedback);
const wetDelay = gainNode(delay);
const dryDelay = gainNode(fx.dry ?? 1);
const sum = new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
chain
.connect(dry)
.connect(dryDelay, delayNode)
.connectOne(1, wetDelay) // connect delayNode -> wetDelay
.connect(sum);
chain.audioNodes.push(delayNode.feedbackGain, delayNode.delayGain);
fxNodes['delay'] = [delayNode];
fxNodes['delay_mix'] = [wetDelay];
}
// reverb
if (key !== 'main' && fx.room > 0) {
let roomIR;
if (fx.ir !== undefined) {
let url;
let sample = getSound(fx.ir);
if (Array.isArray(sample)) {
url = sample.data.samples[fx.i % sample.data.samples.length];
} else if (typeof sample === 'object') {
url = Object.values(sample.data.samples).flat()[i % Object.values(sample.data.samples).length];
}
roomIR = await loadBuffer(url, ac, fx.ir, 0);
}
const dry = gainNode(1);
const reverbNode = ac.createReverb(
fx.roomsize,
fx.roomfade,
fx.roomlp,
fx.roomdim,
roomIR,
fx.irspeed,
fx.irbegin,
);
const wetReverb = gainNode(fx.room);
const dryReverb = gainNode(fx.dry ?? 1);
const sum = new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
chain
.connect(dry)
.connect(dryReverb, reverbNode)
.connectOne(1, wetReverb) // connect reverbNode -> wetReverb
.connect(sum);
fxNodes['room'] = [reverbNode];
fxNodes['room_mix'] = [wetReverb];
} }
} }
if (value.hcutoff !== undefined) { if (FXrelease !== undefined && FXrelease > release) {
const hpMap = { const releaseNode = gainNode(1);
frequency: 'hcutoff', releaseNode.gain.setValueAtTime(1, end + release);
q: 'hresonance', releaseNode.gain.linearRampToValueAtTime(0, endWithRelease);
attack: 'hpattack', chain.connect(releaseNode);
decay: 'hpdecay',
sustain: 'hpsustain',
release: 'hprelease',
env: 'hpenv',
anchor: 'fanchor',
model: 'ftype',
drive: 'drive',
rate: 'hprate',
sync: 'hpsync',
depth: 'hpdepth',
depthfrequency: 'hpdepthfrequency',
shape: 'hpshape',
dcoffset: 'hpdc',
skew: 'hpskew',
};
const hpParams = pickAndRename(value, hpMap);
hpParams.type = 'highpass';
const { filter: hpf1, lfo: lfo1 } = filt(hpParams);
nodes['hpf'] = [hpf1];
nodes['hpf_lfo'] = [lfo1];
lfo1 && audioNodes.push(lfo1);
chain.push(hpf1);
if (ftype === '24db') {
const { filter: hpf2, lfo: lfo2 } = filt(hpParams);
nodes['hpf'].push(hpf2);
nodes['hpf_lfo'].push(lfo2);
chain.push(hpf2);
lfo2 && audioNodes.push(lfo2);
}
}
if (value.bandf !== undefined) {
const bpMap = {
frequency: 'bandf',
q: 'bandq',
attack: 'bpattack',
decay: 'bpdecay',
sustain: 'bpsustain',
release: 'bprelease',
env: 'bpenv',
anchor: 'fanchor',
model: 'ftype',
drive: 'drive',
rate: 'bprate',
sync: 'bpsync',
depth: 'bpdepth',
depthfrequency: 'bpdepthfrequency',
shape: 'bpshape',
dcoffset: 'bpdc',
skew: 'bpskew',
};
const bpParams = pickAndRename(value, bpMap);
bpParams.type = 'bandpass';
const { filter: bpf1, lfo: lfo1 } = filt(bpParams);
nodes['bpf'] = [bpf1];
nodes['bpf_lfo'] = [lfo1];
chain.push(bpf1);
lfo1 && audioNodes.push(lfo1);
if (ftype === '24db') {
const { filter: bpf2, lfo: lfo2 } = filt(bpParams);
nodes['bpf'].push(bpf2);
nodes['bpf_lfo'].push(lfo2);
chain.push(bpf2);
lfo2 && audioNodes.push(lfo2);
}
}
if (vowel !== undefined) {
const vowelFilter = ac.createVowelFilter(vowel);
nodes['vowel'] = [vowelFilter];
chain.push(vowelFilter);
}
// effects
if (coarse !== undefined) {
const coarseNode = getWorklet(ac, 'coarse-processor', { coarse });
nodes['coarse'] = [coarseNode];
chain.push(coarseNode);
}
if (crush !== undefined) {
const crushNode = getWorklet(ac, 'crush-processor', { crush });
nodes['crush'] = [crushNode];
chain.push(crushNode);
}
if (shape !== undefined) {
const shapeNode = getWorklet(ac, 'shape-processor', { shape, postgain: shapevol });
nodes['shape'] = [shapeNode];
chain.push(shapeNode);
}
if (distort !== undefined) {
const distortNode = getDistortion(distort, distortvol, distorttype);
nodes['distort'] = [distortNode];
chain.push(distortNode);
}
if (tremolosync != null) {
tremolo = cps * tremolosync;
}
if (value.wtPosSynced != null) {
value.wtPosRate /= cps;
}
if (value.wtWarpSynced != null) {
value.wtWarpRate /= cps;
}
if (tremolo !== undefined) {
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
const gain = Math.max(1 - tremolodepth, 0);
const amGain = new GainNode(ac, { gain });
const time = cycle / cps;
const lfo = getLfo(ac, {
skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1),
frequency: tremolo,
depth: tremolodepth,
time,
dcoffset: 0,
shape: tremoloshape,
phaseoffset: tremolophase,
min: 0,
max: 1,
curve: 1.5,
begin: t,
end: endWithRelease,
});
nodes['tremolo'] = [lfo];
nodes['tremolo_gain'] = [amGain];
lfo.connect(amGain.gain);
audioNodes.push(lfo);
chain.push(amGain);
}
if (compressorThreshold !== undefined) {
const compressorNode = getCompressor(
ac,
compressorThreshold,
compressorRatio,
compressorKnee,
compressorAttack,
compressorRelease,
);
nodes['compressor'] = [compressorNode];
chain.push(compressorNode);
}
// panning
if (pan !== undefined) {
const panner = ac.createStereoPanner();
panner.pan.value = 2 * pan - 1;
chain.push(panner);
}
// phaser
if (phaserrate !== undefined && phaserdepth > 0) {
const { filterChain, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep);
nodes['phaser'] = [...filterChain];
nodes['phaser_lfo'] = [lfo];
chain.push(...filterChain);
audioNodes.push(lfo);
} }
// last gain // last gain
const post = new GainNode(ac, { gain: postgain }); const post = new GainNode(ac, { gain: postgain });
nodes['post'] = [post]; nodes.main['post'] = [post];
chain.push(post); chain.connect(post);
// delay // delay
if (delay > 0 && delaytime > 0 && delayfeedback > 0) { if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delayNode = orbitBus.getDelay(delaytime, delayfeedback, t); const delayNode = orbitBus.getDelay(delaytime, delayfeedback, t);
nodes['delay'] = [delayNode]; nodes.main['delay'] = [delayNode];
const delaySend = orbitBus.sendDelay(post, delay); const delaySend = orbitBus.sendDelay(post, delay);
nodes['delay_mix'] = [delaySend]; nodes.main['delay_mix'] = [delaySend];
audioNodes.push(delaySend); chain.audioNodes.push(delaySend);
} }
// reverb // reverb
if (room > 0) { if (room > 0) {
@@ -838,81 +926,83 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
roomIR = await loadBuffer(url, ac, ir, 0); roomIR = await loadBuffer(url, ac, ir, 0);
} }
const roomNode = orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); const roomNode = orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
nodes['room'] = [roomNode]; nodes.main['room'] = [roomNode];
const reverbSend = orbitBus.sendReverb(post, room); const reverbSend = orbitBus.sendReverb(post, room);
nodes['room_mix'] = [reverbSend]; nodes.main['room_mix'] = [reverbSend];
audioNodes.push(reverbSend); chain.audioNodes.push(reverbSend);
} }
if (bus != null) { if (bus != null) {
const busNode = audioController.getBus(bus); const busNode = audioController.getBus(bus);
const busSend = effectSend(post, busNode, busgain); const busSend = effectSend(post, busNode, busgain);
audioNodes.push(busSend); chain.audioNodes.push(busSend);
} }
if (djf != null) { if (djf != null) {
const djfNode = orbitBus.getDjf(djf, t); const djfNode = orbitBus.getDjf(djf, t);
nodes['djf'] = [djfNode]; nodes.main['djf'] = [djfNode];
} }
// analyser // analyser
if (analyze && !(ac instanceof OfflineAudioContext)) { if (analyze && !(ac instanceof OfflineAudioContext)) {
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
const analyserSend = effectSend(post, analyserNode, 1); const analyserSend = effectSend(post, analyserNode, 1);
audioNodes.push(analyserSend); chain.audioNodes.push(analyserSend);
} }
if (dry != null) { if (dry != null) {
dry = applyGainCurve(dry); dry = applyGainCurve(dry);
const dryGain = new GainNode(ac, { gain: dry }); const dryGain = new GainNode(ac, { gain: dry });
chain.push(dryGain); chain.connect(dryGain);
orbitBus.connectToOutput(dryGain); orbitBus.connectToOutput(dryGain);
} else { } else {
orbitBus.connectToOutput(post); orbitBus.connectToOutput(post);
} }
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
audioNodes.push(...chain);
// finally, now that `nodes` is populated, set up modulators // finally, now that `nodes` is populated, set up modulators
if (value.lfo) { FX.forEach((fx, idx) => {
for (const id of value.lfo.__ids) { const key = idx === FX.length - 1 ? 'main' : idx;
const params = value.lfo[id]; if (fx.lfo) {
const lfo = connectLFO( for (const id of fx.lfo.__ids) {
id, const params = fx.lfo[id];
{ params.fxi ??= key;
...params, const lfo = connectLFO(
cps, id,
cycle, {
begin: t, ...params,
end: endWithRelease, cps,
}, cycle,
nodes, begin: t,
); end: endWithRelease,
lfo && audioNodes.push(lfo); },
nodes,
);
lfo && chain.audioNodes.push(lfo);
}
} }
} if (fx.env) {
if (value.env) { for (const id of fx.env.__ids) {
for (const id of value.env.__ids) { const params = fx.env[id];
const params = value.env[id]; params.fxi ??= key;
const env = connectEnvelope( const env = connectEnvelope(
id, id,
{ {
...params, ...params,
begin: t, begin: t,
end: endWithRelease, end: endWithRelease,
}, },
nodes, nodes,
); );
env && audioNodes.push(env); env && chain.audioNodes.push(env);
}
} }
} if (fx.bmod) {
if (value.bmod) { for (const id of fx.bmod.__ids) {
for (const id of value.bmod.__ids) { const params = fx.bmod[id];
const params = value.bmod[id]; params.fxi ??= key;
const { toCleanup } = connectBusModulator({ ...params, begin: t, end: endWithRelease }, nodes, controller); const { toCleanup } = connectBusModulator({ ...params, begin: t, end: endWithRelease }, nodes, controller);
audioNodes.push(...toCleanup); chain.audioNodes.push(...toCleanup);
}
} }
} });
}; };
export const superdoughTrigger = (t, hap, ct, cps) => { export const superdoughTrigger = (t, hap, ct, cps) => {
+1 -1
View File
@@ -87,7 +87,7 @@ const CONTROL_TARGETS = {
compressorRelease: { node: 'compressor', param: 'release' }, compressorRelease: { node: 'compressor', param: 'release' },
// PHASER // PHASER
phaserrate: { node: 'phaser_lfo', param: 'rate' }, phaserrate: { node: 'phaser_lfo', param: 'frequency' },
phasersweep: { node: 'phaser_lfo', param: 'depth' }, phasersweep: { node: 'phaser_lfo', param: 'depth' },
phasercenter: { node: 'phaser', param: 'frequency' }, phasercenter: { node: 'phaser', param: 'frequency' },
phaserdepth: { node: 'phaser', param: 'Q' }, phaserdepth: { node: 'phaser', param: 'Q' },
+11 -6
View File
@@ -1,3 +1,5 @@
import { releaseAudioNode } from './helpers.mjs';
// credits to webdirt: https://github.com/dktr0/WebDirt/blob/41342e81d6ad694a2310d491fef7b7e8b0929efe/js-src/Graph.js#L597 // credits to webdirt: https://github.com/dktr0/WebDirt/blob/41342e81d6ad694a2310d491fef7b7e8b0929efe/js-src/Graph.js#L597
export var vowelFormant = { export var vowelFormant = {
a: { freqs: [660, 1120, 2750, 3000, 3350], gains: [1, 0.5012, 0.0708, 0.0631, 0.0126], qs: [80, 90, 120, 130, 140] }, a: { freqs: [660, 1120, 2750, 3000, 3350], gains: [1, 0.5012, 0.0708, 0.0631, 0.0126], qs: [80, 90, 120, 130, 140] },
@@ -46,7 +48,8 @@ if (typeof GainNode !== 'undefined') {
} }
const { gains, qs, freqs } = vowelFormant[letter]; const { gains, qs, freqs } = vowelFormant[letter];
this.makeupGain = ac.createGain(); this.makeupGain = ac.createGain();
this.audioNodes = []; this.filters = [];
this.gains = [];
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
const gain = ac.createGain(); const gain = ac.createGain();
gain.gain.value = gains[i]; gain.gain.value = gains[i];
@@ -56,9 +59,9 @@ if (typeof GainNode !== 'undefined') {
filter.frequency.value = freqs[i]; filter.frequency.value = freqs[i];
super.connect(filter); super.connect(filter);
filter.connect(gain); filter.connect(gain);
this.audioNodes.push(filter); this.filters.push(filter);
gain.connect(this.makeupGain); gain.connect(this.makeupGain);
this.audioNodes.push(gain); this.gains.push(gain);
} }
this.makeupGain.gain.value = 8; // how much makeup gain to add? this.makeupGain.gain.value = 8; // how much makeup gain to add?
return this; return this;
@@ -67,11 +70,13 @@ if (typeof GainNode !== 'undefined') {
this.makeupGain.connect(target); this.makeupGain.connect(target);
} }
disconnect() { disconnect() {
this.makeupGain.disconnect(); releaseAudioNode(this.makeupGain);
this.audioNodes.forEach((n) => n.disconnect()); this.filters.forEach(releaseAudioNode);
this.gains.forEach(releaseAudioNode);
super.disconnect(); super.disconnect();
this.makeupGain = null; this.makeupGain = null;
this.audioNodes = null; this.filters = null;
this.gains = null;
} }
} }
+46
View File
@@ -565,6 +565,52 @@ exports[`runs examples > example "_euclidRot" example index 20 1`] = `
] ]
`; `;
exports[`runs examples > example "FX" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 1/8 → 1/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 1/4 → 3/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 3/8 → 1/2 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 1/2 → 5/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 5/8 → 3/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 3/4 → 7/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 7/8 → 1/1 | s:lt decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 1/1 → 9/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 9/8 → 5/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 5/4 → 11/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 11/8 → 3/2 | s:oh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 3/2 → 13/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 13/8 → 7/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 7/4 → 15/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 15/8 → 2/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 2/1 → 17/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 17/8 → 9/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 9/4 → 19/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 19/8 → 5/2 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 5/2 → 21/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 21/8 → 11/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 11/4 → 23/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 23/8 → 3/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 3/1 → 25/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 25/8 → 13/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 13/4 → 27/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 27/8 → 7/2 | s:lt decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 7/2 → 29/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 29/8 → 15/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 15/4 → 31/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 31/8 → 4/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
]
`;
exports[`runs examples > example "FX" example index 1 1`] = `
[
"[ 0/1 → 1/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
"[ 1/1 → 2/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
"[ 2/1 → 3/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
"[ 3/1 → 4/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
]
`;
exports[`runs examples > example "accelerate" example index 0 1`] = ` exports[`runs examples > example "accelerate" example index 0 1`] = `
[ [
"[ 0/1 → 2/1 | s:sax accelerate:0 ]", "[ 0/1 → 2/1 | s:sax accelerate:0 ]",