mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 14:26:58 -04:00
Merge pull request 'Feature: LFOs and Envelopes' (#1507) from glossing/strudel:glossing/lfo-anything into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1507
This commit is contained in:
+264
-4
@@ -4,7 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Pattern, register, reify } from './pattern.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { Pattern, pure, register, reify } from './pattern.mjs';
|
||||
|
||||
export function createParam(names) {
|
||||
let isMulti = Array.isArray(names);
|
||||
@@ -2044,6 +2045,28 @@ export const { octave, oct } = registerControl('octave', 'oct');
|
||||
* )
|
||||
*/
|
||||
export const { orbit } = registerControl('orbit', 'o');
|
||||
|
||||
/**
|
||||
* A `bus` is a send which can be used for mixing patterns. It combines with..
|
||||
* s("bus") to play that bus through another pattern (for, say, applying non-linear
|
||||
* effects like distortion to multiple signals)
|
||||
*
|
||||
* otherPat.bmod(..) (to modulate another pattern with the bus)
|
||||
*
|
||||
* @name bus
|
||||
* @param {number | Pattern} number
|
||||
*/
|
||||
export const { bus } = registerControl('bus');
|
||||
|
||||
/**
|
||||
* Postgain multiplier prior to sending the signal to the audio bus.
|
||||
*
|
||||
* @name busgain
|
||||
* @synonyms bgain
|
||||
* @param {number | Pattern} number
|
||||
*/
|
||||
export const { busgain, bgain } = registerControl('busgain', 'bgain');
|
||||
|
||||
// TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that
|
||||
export const { overgain } = registerControl('overgain');
|
||||
// TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
|
||||
@@ -2088,8 +2111,7 @@ export const { panorient } = registerControl('panorient');
|
||||
// ['pitch2'],
|
||||
// ['pitch3'],
|
||||
// ['portamento'],
|
||||
// TODO: LFO rate see https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
||||
export const { rate } = registerControl('rate');
|
||||
|
||||
// TODO: slide param for certain synths
|
||||
export const { slide } = registerControl('slide');
|
||||
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
||||
@@ -2515,7 +2537,6 @@ export const { curve } = registerControl('curve');
|
||||
export const { deltaSlide } = registerControl('deltaSlide');
|
||||
export const { pitchJump } = registerControl('pitchJump');
|
||||
export const { pitchJumpTime } = registerControl('pitchJumpTime');
|
||||
export const { lfo, repeatTime } = registerControl('lfo', 'repeatTime');
|
||||
// noise on the frequency or as bubo calls it "frequency fog" :)
|
||||
export const { znoise } = registerControl('znoise');
|
||||
export const { zmod } = registerControl('zmod');
|
||||
@@ -2792,6 +2813,245 @@ export const scrub = register(
|
||||
false,
|
||||
);
|
||||
|
||||
const subControlAliases = new Map();
|
||||
const registerSubControl = (control, subControl, ...aliases) => {
|
||||
const aliasMap = subControlAliases.get(control) ?? new Map();
|
||||
const allKeys = new Set([subControl, ...aliases]);
|
||||
for (const alias of allKeys) {
|
||||
aliasMap.set(String(alias).toLowerCase(), subControl);
|
||||
}
|
||||
subControlAliases.set(control, aliasMap);
|
||||
};
|
||||
|
||||
const registerSubControls = (control, subControlAliases = []) => {
|
||||
for (const [subControl, ...aliases] of subControlAliases) {
|
||||
registerSubControl(control, subControl, ...aliases);
|
||||
}
|
||||
};
|
||||
|
||||
const getMainSubcontrolName = (control, subKey) => {
|
||||
const aliasMap = subControlAliases.get(control);
|
||||
if (!aliasMap) return subKey;
|
||||
return aliasMap.get(String(subKey).toLowerCase()) ?? subKey;
|
||||
};
|
||||
|
||||
registerSubControls('lfo', [
|
||||
['control', 'c'],
|
||||
['subControl', 'sc'],
|
||||
['rate', 'r'],
|
||||
['depth', 'dep', 'dr'],
|
||||
['depthabs', 'da'],
|
||||
['dcoffset', 'dc'],
|
||||
['shape', 'sh'],
|
||||
['skew', 'sk'],
|
||||
['curve'],
|
||||
['sync', 's'],
|
||||
]);
|
||||
registerSubControls('env', [
|
||||
['control', 'c'],
|
||||
['subControl', 'sc'],
|
||||
['attack', 'att', 'a'],
|
||||
['decay', 'dec', 'd'],
|
||||
['sustain', 'sus', 's'],
|
||||
['release', 'rel', 'r'],
|
||||
['depth', 'dep', 'dr'],
|
||||
['depthabs', 'da'],
|
||||
['acurve', 'ac'],
|
||||
['dcurve', 'dc'],
|
||||
['rcurve', 'rc'],
|
||||
]);
|
||||
registerSubControls('bmod', [
|
||||
['bus', 'b'],
|
||||
['control', 'c'],
|
||||
['subControl', 'sc'],
|
||||
['depth', 'dep', 'dr'],
|
||||
['depthabs', 'da'],
|
||||
['dc'],
|
||||
]);
|
||||
|
||||
Pattern.prototype.modulate = function (type, config, id) {
|
||||
config = { control: undefined, ...config };
|
||||
const modulatorKeys = ['lfo', 'env', 'bmod'];
|
||||
if (!modulatorKeys.includes(type)) {
|
||||
logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'bmod'`);
|
||||
return this;
|
||||
}
|
||||
let output = this;
|
||||
let defaultValue = undefined;
|
||||
for (const [rawKey, value] of Object.entries(config)) {
|
||||
const key = getMainSubcontrolName(type, rawKey);
|
||||
const valuePat = reify(value);
|
||||
output = output
|
||||
.fmap((v) => (c) => {
|
||||
if (defaultValue === undefined) {
|
||||
// default control to the control set just before this in the chain
|
||||
// e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO
|
||||
let control = getControlName(Object.keys(v).at(-1));
|
||||
if (modulatorKeys.includes(control)) {
|
||||
control = `${control}_${[...v[control].__ids].at(-1)}`;
|
||||
}
|
||||
defaultValue = control;
|
||||
}
|
||||
v[type] ??= { __ids: new Set() };
|
||||
const t = v[type];
|
||||
id ??= t.__ids.size;
|
||||
t[id] ??= { control: defaultValue };
|
||||
t.__ids.add(id); // keeps track of insertion order
|
||||
if (c === undefined) return v;
|
||||
if (key === 'control' || key === 'subControl') {
|
||||
t[id][key] = getControlName(c);
|
||||
} else {
|
||||
t[id][key] = c;
|
||||
}
|
||||
return v;
|
||||
})
|
||||
.appLeft(valuePat);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configures an LFO. Can be called in sequence like pat.lfo(...).lfo(...) to set up multiple LFOs.
|
||||
* There are two ways to declare which control will be modulated:
|
||||
* 1. Explicitly put `control` in the config (e.g. `lfo({ c: "lpf" })`)
|
||||
* 2. If the control parameter is absent, the control _immediately before_ the `lfo` call will be used
|
||||
* (e.g. `s("saw").lpf(500).lfo()` to modulate `lpf`)
|
||||
*
|
||||
* Modulators can be referred to by `id` so that they can be updated later e.g. inside
|
||||
* a `sometimes`. See example below.
|
||||
*
|
||||
* @name lfo
|
||||
* @param {Object} config LFO configuration.
|
||||
* @param {string | Pattern} [config.control] Node to modulate. Aliases: t
|
||||
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p
|
||||
* @param {number | Pattern} [config.rate] Modulation rate. Aliases: rate, r
|
||||
* @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.dcoffset] DC offset / bias for the waveform. Aliases: dc
|
||||
* @param {number | Pattern} [config.shape] Shape index. Aliases: sh
|
||||
* @param {number | Pattern} [config.skew] Skew amount. Aliases: sk
|
||||
* @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c
|
||||
* @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s
|
||||
* @param {string | Pattern} id ID to use for this modulator
|
||||
* @returns Pattern
|
||||
*
|
||||
* @example
|
||||
* s("saw").note("F1").lpf(500).lfo()
|
||||
*
|
||||
* @example
|
||||
* s("saw").lfo().lpf(500).lfo({ s: 0.3 })
|
||||
*
|
||||
* @example
|
||||
* s("saw").lpf(500).diode(0.3)
|
||||
* .lfo({ c: "lpf" })
|
||||
*
|
||||
* @example
|
||||
* s("pulse").lpf(500).lfo()
|
||||
* .lfo({ c: "s" })
|
||||
* .diode(0.3)
|
||||
* .sometimes(x => x.lfo({ s: "8" }, 1)) // lfo #1 (0-indexed)
|
||||
*
|
||||
* @example
|
||||
* s("pulse").lpf(500).lfo({ depth: 4 }, 'lpf_mod')
|
||||
* .lfo({ c: "s" })
|
||||
* .diode(0.3)
|
||||
* .sometimes(x => x.lfo({ s: "8" }, 'lpf_mod'))
|
||||
*/
|
||||
Pattern.prototype.lfo = function (config, id) {
|
||||
return this.modulate('lfo', config, id);
|
||||
};
|
||||
export const lfo = (config) => pure({}).lfo(config);
|
||||
|
||||
/**
|
||||
* Configures an envelope. Can be called in sequence like pat.env(...).env(...) to set up multiple envelopes
|
||||
* There are two ways to declare which control will be modulated:
|
||||
* 1. Explicitly put `control` in the config (e.g. `env({ c: "lpf" })`)
|
||||
* 2. If the control parameter is absent, the control _immediately before_ the `env` call will be used
|
||||
* (e.g. `s("saw").lpf(500).env({ a: 1 })` to modulate `lpf`)
|
||||
*
|
||||
* Modulators can be referred to by `id` so that they can be updated later e.g. inside
|
||||
* a `sometimes`. See example below.
|
||||
*
|
||||
* @name env
|
||||
* @param {Object} config Envelope configuration.
|
||||
* @param {string | Pattern} [config.control] Node to modulate. Aliases: t
|
||||
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p
|
||||
* @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.attack] Time to reach depth. Aliases: att, a
|
||||
* @param {number | Pattern} [config.decay] Time to reach sustain. Aliases: dec, d
|
||||
* @param {number | Pattern} [config.sustain] Sustain depth. Aliases: sus, s
|
||||
* @param {number | Pattern} [config.release] Time to return to nominal value. Aliases: rel, r
|
||||
* @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.rcurve] Snappiness of release curve (-1 = relaxed, 1 = snappy). Aliases: rc
|
||||
* @param {string | Pattern} id ID to use for this modulator
|
||||
* @returns Pattern
|
||||
*
|
||||
* @example
|
||||
* s("saw").note("F1").lpf(500).env({ a: 1 })
|
||||
*
|
||||
* @example
|
||||
* s("saw").env({ d: 1 }).note("F1")
|
||||
* .lpq(4).lpf(50)
|
||||
* .env({ a: 0.1, d: 1, ac: 0.8, dc: 0.3, depth: 50 })
|
||||
*
|
||||
* @example
|
||||
* s("saw").lpf(500).diode(0.3)
|
||||
* .env({ c: "lpf", a: 0.5, d: 0.5 })
|
||||
*
|
||||
* @example
|
||||
* s("pulse").lpf(500).env({ a: 1 })
|
||||
* .env({ c: "s", a: 1 })
|
||||
* .diode(0.3)
|
||||
* .sometimes(x => x.env({ a: "0.5" }, 1)) // envelope #1 (0-indexed)
|
||||
*
|
||||
* @example
|
||||
* s("pulse").lpf(500).env({ a: 1 }, 'lpf_mod')
|
||||
* .env({ c: "s", a: 1 })
|
||||
* .diode(0.3)
|
||||
* .sometimes(x => x.env({ a: "0.5" }, 'lpf_mod'))
|
||||
*/
|
||||
Pattern.prototype.env = function (config, id) {
|
||||
return this.modulate('env', config, id);
|
||||
};
|
||||
export const env = (config) => pure({}).env(config);
|
||||
|
||||
/**
|
||||
* Modulates with the output from a given `bus`.
|
||||
* Can be called in sequence like pat.bmod(...).bmod(...) to set up multiple modulators
|
||||
*
|
||||
* Send to an audio bus with `otherPat.bus(..)`.
|
||||
*
|
||||
* There are two ways to declare which control will be modulated:
|
||||
* 1. Explicitly put `control` in the config (e.g. `bmod({ id: 2, c: "lpf" })`)
|
||||
* 2. If the control parameter is absent, the control _immediately before_ the `bmod` call will be used
|
||||
* (e.g. `s("saw").lpf(500).bmod({ id: 2 })` to modulate `lpf`)
|
||||
*
|
||||
* Modulators can be referred to by `id` so that they can be updated later e.g. inside
|
||||
* a `sometimes`. See example below.
|
||||
*
|
||||
* @name bmod
|
||||
* @param {Object} config Bus modulation configuration.
|
||||
* @param {string | Pattern} [config.bus] Bus to get modulation signal from
|
||||
* @param {string | Pattern} [config.control] Node to modulate. Aliases: t
|
||||
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p
|
||||
* @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.dc] DC offset prior to application
|
||||
* @param {string | Pattern} id ID to use for this modulator
|
||||
* @returns Pattern
|
||||
*
|
||||
* @example
|
||||
* modulator: s("one").seg(64).gain(slider(0, 0, 1)).bus(1).dry(0)
|
||||
* carrier: s("saw").bmod({ b: 1 })
|
||||
*
|
||||
*/
|
||||
Pattern.prototype.bmod = function (config, id) {
|
||||
return this.modulate('bmod', config, id);
|
||||
};
|
||||
export const bmod = (config) => pure({}).bmod(config);
|
||||
|
||||
/**
|
||||
* Transient shaper. Gives independent control over the emphasis on transients
|
||||
* and sustains
|
||||
|
||||
@@ -166,7 +166,7 @@ export function registerSoundfonts() {
|
||||
let envEnd = holdEnd + release + 0.01;
|
||||
|
||||
// vibrato
|
||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, time);
|
||||
const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, time);
|
||||
// pitch envelope
|
||||
getPitchEnvelope(bufferSource.detune, value, time, holdEnd);
|
||||
|
||||
@@ -174,10 +174,10 @@ export function registerSoundfonts() {
|
||||
const stop = (releaseTime) => {};
|
||||
onceEnded(bufferSource, () => {
|
||||
releaseAudioNode(bufferSource);
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
vibratoHandle?.stop();
|
||||
onended();
|
||||
});
|
||||
return { node, stop };
|
||||
return { node, stop, nodes: { source: [bufferSource], ...vibratoHandle?.nodes } };
|
||||
},
|
||||
{ type: 'soundfont', prebake: true, fonts },
|
||||
);
|
||||
|
||||
@@ -104,22 +104,40 @@ function getModulationShapeInput(val) {
|
||||
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
|
||||
}
|
||||
|
||||
export function getLfo(audioContext, begin, end, properties = {}) {
|
||||
const { shape = 0, ...props } = properties;
|
||||
const { dcoffset = -0.5, depth = 1 } = properties;
|
||||
export function getEnvelope(audioContext, properties = {}) {
|
||||
return getWorklet(audioContext, 'envelope-processor', properties);
|
||||
}
|
||||
|
||||
export function getLfo(audioContext, properties = {}) {
|
||||
const {
|
||||
shape = 0,
|
||||
begin = 0,
|
||||
end = 0,
|
||||
time,
|
||||
depth = 1,
|
||||
dcoffset = -0.5,
|
||||
frequency = 1,
|
||||
skew = 0.5,
|
||||
phaseoffset = 0,
|
||||
curve = 1,
|
||||
min,
|
||||
max,
|
||||
...props
|
||||
} = properties;
|
||||
|
||||
const lfoprops = {
|
||||
frequency: 1,
|
||||
depth,
|
||||
skew: 0.5,
|
||||
phaseoffset: 0,
|
||||
time: begin,
|
||||
begin,
|
||||
end,
|
||||
shape: getModulationShapeInput(shape),
|
||||
time: time ?? begin,
|
||||
depth,
|
||||
dcoffset,
|
||||
min: dcoffset * depth,
|
||||
max: dcoffset * depth + depth,
|
||||
curve: 1,
|
||||
frequency,
|
||||
skew,
|
||||
phaseoffset,
|
||||
curve,
|
||||
shape: getModulationShapeInput(shape),
|
||||
min: min ?? dcoffset * depth,
|
||||
max: max ?? dcoffset * depth + depth,
|
||||
...props,
|
||||
};
|
||||
|
||||
@@ -161,7 +179,9 @@ export function getParamLfo(audioContext, param, start, end, lfoValues) {
|
||||
}
|
||||
let lfo;
|
||||
if (depth) {
|
||||
lfo = getLfo(audioContext, start, end, {
|
||||
lfo = getLfo(audioContext, {
|
||||
begin: start,
|
||||
end,
|
||||
depth,
|
||||
dcoffset,
|
||||
...getLfoInputs,
|
||||
@@ -331,7 +351,7 @@ export function getVibratoOscillator(param, value, t) {
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
});
|
||||
vibratoOscillator.start(t);
|
||||
return vibratoOscillator;
|
||||
return { stop: (t) => vibratoOscillator.stop(t), nodes: { vib: [vibratoOscillator], vib_gain: [gain] } };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +407,7 @@ export function applyFM(param, value, begin) {
|
||||
const ac = getAudioContext();
|
||||
const toStop = []; // fm oscillators we will expose `stop` for
|
||||
const fms = {};
|
||||
const nodes = {};
|
||||
// Matrix
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
for (let j = 0; j <= 8; j++) {
|
||||
@@ -437,11 +458,13 @@ export function applyFM(param, value, begin) {
|
||||
output = osc.connect(envGain);
|
||||
}
|
||||
fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup };
|
||||
nodes[`fm_${idx}`] = [osc];
|
||||
}
|
||||
const { input, output, freq, osc, toCleanup } = fms[idx];
|
||||
const g = gainNode(amt * freq);
|
||||
io.push(isMod ? output.connect(g) : input);
|
||||
cleanupOnEnd(osc, [...toCleanup, g]);
|
||||
nodes[`fm_${idx}_gain`] = [g];
|
||||
}
|
||||
if (!io[1]) {
|
||||
logger(
|
||||
@@ -454,6 +477,7 @@ export function applyFM(param, value, begin) {
|
||||
}
|
||||
}
|
||||
return {
|
||||
nodes,
|
||||
stop: (t) => toStop.forEach((m) => m?.stop(t)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from './helpers.mjs';
|
||||
export * from './synth.mjs';
|
||||
export * from './zzfx.mjs';
|
||||
export * from './logger.mjs';
|
||||
export * from './modulators.mjs';
|
||||
export * from './dspworklet.mjs';
|
||||
export * from './audioContext.mjs';
|
||||
export * from './wavetable.mjs';
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
modulators.mjs - Helpers for constructing modulators (envelopes, LFOs, etc.)
|
||||
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/modulators.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { gainNode, getEnvelope, getLfo, webAudioTimeout } from './helpers.mjs';
|
||||
import { errorLogger } from './logger.mjs';
|
||||
import { getSuperdoughControlTargets } from './superdoughdata.mjs';
|
||||
import { clamp } from './util.mjs';
|
||||
|
||||
const getNodeParam = (node, name) => {
|
||||
// Worklet case
|
||||
if (node?.parameters) {
|
||||
const p = node.parameters.get(name);
|
||||
if (p instanceof AudioParam) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
// Built-in node case
|
||||
let p = node?.[name];
|
||||
if (p === undefined && name === 'frequency') {
|
||||
// Fallbacks for source nodes without 'frequency' params (e.g. soundfonts)
|
||||
p = node?.['detune'] ?? node?.['playbackRate'];
|
||||
}
|
||||
if (p instanceof AudioParam) {
|
||||
return p;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const controlTargets = getSuperdoughControlTargets();
|
||||
|
||||
const getControlData = (control) => {
|
||||
return controlTargets[control.split('_')[0]];
|
||||
};
|
||||
|
||||
const getRangeForParam = (paramName, currentValue) => {
|
||||
// We clamp the frequency to a reasonable range unless the currentValue
|
||||
// is low, which indicates this may be an LFO
|
||||
if (paramName === 'frequency' && currentValue >= 30) {
|
||||
return { min: 20 - currentValue, max: 24000 - currentValue };
|
||||
}
|
||||
return { min: undefined, max: undefined };
|
||||
};
|
||||
|
||||
const clampWithWaveShaper = (modulator, min, max) => {
|
||||
const ac = getAudioContext();
|
||||
const curve = new Float32Array(256);
|
||||
for (let i = 0; i < curve.length; i++) {
|
||||
const x = (i / (curve.length - 1)) * 2 - 1;
|
||||
curve[i] = clamp(x * max, min, max);
|
||||
}
|
||||
const shaper = new WaveShaperNode(ac, { curve });
|
||||
const scaleGain = gainNode(1 / max);
|
||||
modulator.connect(scaleGain).connect(shaper);
|
||||
return { modulator, toCleanup: [shaper, scaleGain] };
|
||||
};
|
||||
|
||||
const getTargetParamsForControl = (control, nodes, subControl) => {
|
||||
const lookupKey = subControl ? `${control}_${subControl}` : control;
|
||||
const targetInfo = getControlData(lookupKey) ?? getControlData(control);
|
||||
if (!targetInfo) {
|
||||
errorLogger(new Error(`Could not find control data for target '${control}'`), 'superdough');
|
||||
return { targetParams: [], paramName: control };
|
||||
}
|
||||
const paramName = targetInfo.param;
|
||||
const nodeKey = nodes[targetInfo.node] ? targetInfo.node : control;
|
||||
const targetNodes = nodes[nodeKey];
|
||||
if (!targetNodes) {
|
||||
const keys = Object.keys(nodes);
|
||||
errorLogger(
|
||||
new Error(`Could not connect to target '${nodeKey}' — it does not exist. Available targets: ${keys.join(', ')}`),
|
||||
'superdough',
|
||||
);
|
||||
return { targetParams: [], paramName };
|
||||
}
|
||||
const audioParams = [];
|
||||
targetNodes.forEach((targetNode) => {
|
||||
const targetParam = getNodeParam(targetNode, paramName);
|
||||
audioParams.push(targetParam);
|
||||
});
|
||||
return { targetParams: audioParams, paramName };
|
||||
};
|
||||
|
||||
export const connectLFO = (id, params, nodeTracker) => {
|
||||
const { rate = 1, sync, cps, cycle, control = 'lfo', subControl, depth = 1, depthabs, ...filteredParams } = params;
|
||||
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker, subControl);
|
||||
if (!targetParams.length) return;
|
||||
let currentValue = targetParams[0].value;
|
||||
currentValue = currentValue === 0 ? 1 : currentValue;
|
||||
const { min, max } = getRangeForParam(paramName, currentValue);
|
||||
const depthValue = depthabs != null ? depthabs : depth * currentValue;
|
||||
const modParams = {
|
||||
...filteredParams,
|
||||
frequency: sync !== undefined ? sync * cps : rate,
|
||||
time: cycle / cps,
|
||||
depth: depthValue,
|
||||
min,
|
||||
max,
|
||||
};
|
||||
const lfoNode = getLfo(getAudioContext(), modParams);
|
||||
nodeTracker[`lfo_${id}`] = [lfoNode];
|
||||
targetParams.forEach((t) => lfoNode.connect(t));
|
||||
return lfoNode;
|
||||
};
|
||||
|
||||
export const connectEnvelope = (id, params, nodeTracker) => {
|
||||
const { control, subControl, acurve, dcurve, rcurve, depth = 1, depthabs, ...filteredParams } = params;
|
||||
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker, subControl);
|
||||
if (!targetParams.length) return;
|
||||
let currentValue = targetParams[0].value;
|
||||
currentValue = currentValue === 0 ? 1 : currentValue;
|
||||
const { min, max } = getRangeForParam(paramName, currentValue);
|
||||
const depthValue = depthabs != null ? depthabs : depth * currentValue;
|
||||
const envNode = getEnvelope(getAudioContext(), {
|
||||
...filteredParams,
|
||||
depth: depthValue,
|
||||
min,
|
||||
max,
|
||||
attackCurve: acurve,
|
||||
decayCurve: dcurve,
|
||||
releaseCurve: rcurve,
|
||||
});
|
||||
nodeTracker[`env_${id}`] = [envNode];
|
||||
targetParams.forEach((t) => envNode.connect(t));
|
||||
return envNode;
|
||||
};
|
||||
|
||||
export const connectBusModulator = (params, nodeTracker, controller) => {
|
||||
const ac = getAudioContext();
|
||||
const { control, subControl, depth = 1, depthabs } = params;
|
||||
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker, subControl);
|
||||
if (!targetParams.length) return { toCleanup: [] };
|
||||
const signal = controller.getBus(params.bus);
|
||||
const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 });
|
||||
dc.start(params.begin);
|
||||
const shifted = dc.connect(gainNode(1));
|
||||
signal.connect(shifted);
|
||||
let currentValue = targetParams[0].value;
|
||||
currentValue = currentValue === 0 ? 1 : currentValue;
|
||||
const { min, max } = getRangeForParam(paramName, currentValue);
|
||||
const depthValue = depthabs != null ? depthabs : depth * currentValue;
|
||||
const depthGain = gainNode((Math.sign(depthValue) * Math.abs(depthValue)) / 0.3);
|
||||
const unClamped = shifted.connect(depthGain);
|
||||
let { modulator, toCleanup } = clampWithWaveShaper(unClamped, min, max);
|
||||
webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
targetParams.forEach((t) => modulator.connect(t));
|
||||
},
|
||||
0,
|
||||
params.begin,
|
||||
);
|
||||
toCleanup.push(dc, shifted, depthGain);
|
||||
return { modulator, toCleanup };
|
||||
};
|
||||
@@ -301,7 +301,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
}
|
||||
|
||||
// vibrato
|
||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
||||
const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, t);
|
||||
|
||||
const time = t + nudge;
|
||||
bufferSource.start(time, offset);
|
||||
@@ -324,7 +324,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
node.connect(out);
|
||||
onceEnded(bufferSource, function () {
|
||||
releaseAudioNode(bufferSource);
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
vibratoHandle?.stop();
|
||||
releaseAudioNode(node);
|
||||
releaseAudioNode(out);
|
||||
onended();
|
||||
@@ -334,7 +334,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
const stop = (endTime) => {
|
||||
bufferSource.stop(endTime);
|
||||
};
|
||||
const handle = { node: out, bufferSource, stop };
|
||||
const handle = { node: out, nodes: { source: [bufferSource], ...vibratoHandle?.nodes }, stop };
|
||||
|
||||
// cut groups
|
||||
if (cut !== undefined) {
|
||||
|
||||
@@ -11,18 +11,19 @@ import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
||||
import workletsUrl from './worklets.mjs?audioworklet';
|
||||
import {
|
||||
createFilter,
|
||||
effectSend,
|
||||
gainNode,
|
||||
getCompressor,
|
||||
getDistortion,
|
||||
getLfo,
|
||||
getWorklet,
|
||||
effectSend,
|
||||
releaseAudioNode,
|
||||
} from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.mjs';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
import { getAudioContext, setAudioContext } from './audioContext.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||
import { resetSeenKeys } from './wavetable.mjs';
|
||||
|
||||
@@ -184,6 +185,7 @@ let defaultDefaultValues = {
|
||||
distortvol: 1,
|
||||
distorttype: 0,
|
||||
delay: 0,
|
||||
busgain: 1,
|
||||
byteBeatExpression: '0',
|
||||
delayfeedback: 0.5,
|
||||
delaysync: 3 / 16,
|
||||
@@ -327,9 +329,9 @@ export function connectToDestination(input, channels) {
|
||||
controller.output.connectToDestination(input, channels);
|
||||
}
|
||||
|
||||
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
|
||||
function getPhaser(begin, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
|
||||
const ac = getAudioContext();
|
||||
const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
|
||||
const lfo = getLfo(ac, { frequency, depth: sweep * 2, begin, end });
|
||||
|
||||
//filters
|
||||
const numStages = 1; //num of filters in series
|
||||
@@ -401,6 +403,7 @@ function mapChannelNumbers(channels) {
|
||||
}
|
||||
|
||||
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
|
||||
let nodes = {};
|
||||
// new: t is always expected to be the absolute target onset time
|
||||
const ac = getAudioContext();
|
||||
const audioController = getSuperdoughAudioController();
|
||||
@@ -472,6 +475,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
delaysync = getDefaultValue('delaysync'),
|
||||
delaytime,
|
||||
orbit = getDefaultValue('orbit'),
|
||||
bus,
|
||||
busgain = getDefaultValue('busgain'),
|
||||
room,
|
||||
roomfade,
|
||||
roomlp,
|
||||
@@ -512,6 +517,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
delay = applyGainCurve(delay);
|
||||
velocity = applyGainCurve(velocity);
|
||||
tremolodepth = applyGainCurve(tremolodepth);
|
||||
busgain = applyGainCurve(busgain);
|
||||
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||
|
||||
const end = t + hapDuration;
|
||||
@@ -529,7 +535,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
activeSoundSources.delete(chainID);
|
||||
}
|
||||
|
||||
let audioNodes = [];
|
||||
const audioNodes = [];
|
||||
|
||||
if (['-', '~', '_'].includes(s)) {
|
||||
return;
|
||||
@@ -543,6 +549,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
let sourceNode;
|
||||
if (source) {
|
||||
sourceNode = source(t, value, hapDuration, cps);
|
||||
nodes['source'] = [sourceNode];
|
||||
} else if (getSound(s)) {
|
||||
const { onTrigger } = getSound(s);
|
||||
const onEnded = () => {
|
||||
@@ -554,6 +561,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
if (soundHandle) {
|
||||
sourceNode = soundHandle.node;
|
||||
activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC
|
||||
nodes = { ...nodes, ...soundHandle.nodes };
|
||||
}
|
||||
} else {
|
||||
throw new Error(`sound ${s} not found! Is it loaded?`);
|
||||
@@ -572,29 +580,33 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
chain.push(sourceNode);
|
||||
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
|
||||
|
||||
transient !== undefined &&
|
||||
chain.push(
|
||||
getWorklet(
|
||||
ac,
|
||||
'transient-processor',
|
||||
{},
|
||||
{
|
||||
processorOptions: {
|
||||
attack: transient,
|
||||
sustain: transsustain,
|
||||
begin: t,
|
||||
end: endWithRelease,
|
||||
},
|
||||
if (transient !== undefined) {
|
||||
const transProcessor = getWorklet(
|
||||
ac,
|
||||
'transient-processor',
|
||||
{},
|
||||
{
|
||||
processorOptions: {
|
||||
attack: transient,
|
||||
sustain: transsustain,
|
||||
begin: t,
|
||||
end: endWithRelease,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
chain.push(transProcessor);
|
||||
nodes['transient'] = transProcessor;
|
||||
}
|
||||
|
||||
// gain stage
|
||||
chain.push(gainNode(gain));
|
||||
const initialGain = gainNode(gain);
|
||||
nodes['gain'] = [initialGain];
|
||||
chain.push(initialGain);
|
||||
|
||||
// filter
|
||||
const ftype = getFilterType(value.ftype);
|
||||
|
||||
const filt = (params) => createFilter(ac, t, end, params, cps, cycle);
|
||||
if (value.cutoff !== undefined) {
|
||||
const lpMap = {
|
||||
frequency: 'cutoff',
|
||||
@@ -617,12 +629,15 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
};
|
||||
const lpParams = pickAndRename(value, lpMap);
|
||||
lpParams.type = 'lowpass';
|
||||
const lp = () => createFilter(ac, t, end, lpParams, cps, cycle);
|
||||
const { filter: lpf1, lfo: lfo1 } = lp();
|
||||
const { filter: lpf1, lfo: lfo1 } = filt(lpParams);
|
||||
nodes['lpf'] = [lpf1];
|
||||
nodes['lpf_lfo'] = [lfo1];
|
||||
chain.push(lpf1);
|
||||
lfo1 && audioNodes.push(lfo1);
|
||||
if (ftype === '24db') {
|
||||
const { filter: lpf2, lfo: lfo2 } = lp();
|
||||
const { filter: lpf2, lfo: lfo2 } = filt(lpParams);
|
||||
nodes['lpf'].push(lpf2);
|
||||
nodes['lpf_lfo'].push(lfo2);
|
||||
chain.push(lpf2);
|
||||
lfo2 && audioNodes.push(lfo2);
|
||||
}
|
||||
@@ -650,12 +665,15 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
};
|
||||
const hpParams = pickAndRename(value, hpMap);
|
||||
hpParams.type = 'highpass';
|
||||
const hp = () => createFilter(ac, t, end, hpParams, cps, cycle);
|
||||
const { filter: hpf1, lfo: lfo1 } = hp();
|
||||
chain.push(hpf1);
|
||||
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 } = hp();
|
||||
const { filter: hpf2, lfo: lfo2 } = filt(hpParams);
|
||||
nodes['hpf'].push(hpf2);
|
||||
nodes['hpf_lfo'].push(lfo2);
|
||||
chain.push(hpf2);
|
||||
lfo2 && audioNodes.push(lfo2);
|
||||
}
|
||||
@@ -683,12 +701,15 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
};
|
||||
const bpParams = pickAndRename(value, bpMap);
|
||||
bpParams.type = 'bandpass';
|
||||
const bp = () => createFilter(ac, t, end, bpParams, cps, cycle);
|
||||
const { filter: bpf1, lfo: lfo1 } = bp();
|
||||
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 } = bp();
|
||||
const { filter: bpf2, lfo: lfo2 } = filt(bpParams);
|
||||
nodes['bpf'].push(bpf2);
|
||||
nodes['bpf_lfo'].push(lfo2);
|
||||
chain.push(bpf2);
|
||||
lfo2 && audioNodes.push(lfo2);
|
||||
}
|
||||
@@ -696,14 +717,31 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
|
||||
if (vowel !== undefined) {
|
||||
const vowelFilter = ac.createVowelFilter(vowel);
|
||||
nodes['vowel'] = [vowelFilter];
|
||||
chain.push(vowelFilter);
|
||||
}
|
||||
|
||||
// effects
|
||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
||||
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
|
||||
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;
|
||||
@@ -724,7 +762,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
const amGain = new GainNode(ac, { gain });
|
||||
|
||||
const time = cycle / cps;
|
||||
const lfo = getLfo(ac, t, endWithRelease, {
|
||||
const lfo = getLfo(ac, {
|
||||
skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1),
|
||||
frequency: tremolo,
|
||||
depth: tremolodepth,
|
||||
@@ -735,16 +773,28 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
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);
|
||||
}
|
||||
|
||||
compressorThreshold !== undefined &&
|
||||
chain.push(
|
||||
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease),
|
||||
if (compressorThreshold !== undefined) {
|
||||
const compressorNode = getCompressor(
|
||||
ac,
|
||||
compressorThreshold,
|
||||
compressorRatio,
|
||||
compressorKnee,
|
||||
compressorAttack,
|
||||
compressorRelease,
|
||||
);
|
||||
nodes['compressor'] = [compressorNode];
|
||||
chain.push(compressorNode);
|
||||
}
|
||||
|
||||
// panning
|
||||
if (pan !== undefined) {
|
||||
@@ -755,19 +805,24 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
// phaser
|
||||
if (phaserrate !== undefined && phaserdepth > 0) {
|
||||
const { filterChain, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep);
|
||||
audioNodes.push(lfo);
|
||||
nodes['phaser'] = [...filterChain];
|
||||
nodes['phaser_lfo'] = [lfo];
|
||||
chain.push(...filterChain);
|
||||
audioNodes.push(lfo);
|
||||
}
|
||||
|
||||
// last gain
|
||||
const post = new GainNode(ac, { gain: postgain });
|
||||
nodes['post'] = [post];
|
||||
chain.push(post);
|
||||
|
||||
// delay
|
||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||
orbitBus.getDelay(delaytime, delayfeedback, t);
|
||||
const send = orbitBus.sendDelay(post, delay);
|
||||
audioNodes.push(send);
|
||||
const delayNode = orbitBus.getDelay(delaytime, delayfeedback, t);
|
||||
nodes['delay'] = [delayNode];
|
||||
const delaySend = orbitBus.sendDelay(post, delay);
|
||||
nodes['delay_mix'] = [delaySend];
|
||||
audioNodes.push(delaySend);
|
||||
}
|
||||
// reverb
|
||||
if (room > 0) {
|
||||
@@ -782,13 +837,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
}
|
||||
roomIR = await loadBuffer(url, ac, ir, 0);
|
||||
}
|
||||
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
||||
const send = orbitBus.sendReverb(post, room);
|
||||
audioNodes.push(send);
|
||||
const roomNode = orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
||||
nodes['room'] = [roomNode];
|
||||
const reverbSend = orbitBus.sendReverb(post, room);
|
||||
nodes['room_mix'] = [reverbSend];
|
||||
audioNodes.push(reverbSend);
|
||||
}
|
||||
if (bus != null) {
|
||||
const busNode = audioController.getBus(bus);
|
||||
const busSend = effectSend(post, busNode, busgain);
|
||||
audioNodes.push(busSend);
|
||||
}
|
||||
|
||||
if (djf != null) {
|
||||
orbitBus.getDjf(djf, t);
|
||||
const djfNode = orbitBus.getDjf(djf, t);
|
||||
nodes['djf'] = [djfNode];
|
||||
}
|
||||
|
||||
// analyser
|
||||
@@ -808,7 +871,48 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
|
||||
// connect chain elements together
|
||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||
audioNodes = audioNodes.concat(chain);
|
||||
audioNodes.push(...chain);
|
||||
|
||||
// finally, now that `nodes` is populated, set up modulators
|
||||
if (value.lfo) {
|
||||
for (const id of value.lfo.__ids) {
|
||||
const params = value.lfo[id];
|
||||
const lfo = connectLFO(
|
||||
id,
|
||||
{
|
||||
...params,
|
||||
cps,
|
||||
cycle,
|
||||
begin: t,
|
||||
end: endWithRelease,
|
||||
},
|
||||
nodes,
|
||||
);
|
||||
lfo && audioNodes.push(lfo);
|
||||
}
|
||||
}
|
||||
if (value.env) {
|
||||
for (const id of value.env.__ids) {
|
||||
const params = value.env[id];
|
||||
const env = connectEnvelope(
|
||||
id,
|
||||
{
|
||||
...params,
|
||||
begin: t,
|
||||
end: endWithRelease,
|
||||
},
|
||||
nodes,
|
||||
);
|
||||
env && audioNodes.push(env);
|
||||
}
|
||||
}
|
||||
if (value.bmod) {
|
||||
for (const id of value.bmod.__ids) {
|
||||
const params = value.bmod[id];
|
||||
const { toCleanup } = connectBusModulator({ ...params, begin: t, end: endWithRelease }, nodes, controller);
|
||||
audioNodes.push(...toCleanup);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const superdoughTrigger = (t, hap, ct, cps) => {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
superdoughdata.mjs - Data needed for running superdough (defaults, mappings, etc.)
|
||||
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdoughdata.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Mapping from control name to webaudio node and parameter
|
||||
const CONTROL_TARGETS = {
|
||||
stretch: { node: 'stretch', param: 'pitchFactor' },
|
||||
gain: { node: 'gain', param: 'gain' },
|
||||
postgain: { node: 'post', param: 'gain' },
|
||||
pan: { node: 'pan', param: 'pan' },
|
||||
tremolo: { node: 'tremolo', param: 'frequency' },
|
||||
tremolosync: { node: 'tremolo', param: 'frequency' },
|
||||
tremolodepth: { node: 'tremolo_gain', param: 'gain' },
|
||||
tremoloskew: { node: 'tremolo', param: 'skew' },
|
||||
tremolophase: { node: 'tremolo', param: 'phase' },
|
||||
tremoloshape: { node: 'tremolo', param: 'shape' },
|
||||
|
||||
// MODULATORS
|
||||
lfo: { node: 'lfo', param: 'frequency' },
|
||||
lfo_rate: { node: 'lfo', param: 'frequency' },
|
||||
lfo_sync: { node: 'lfo', param: 'frequency' },
|
||||
lfo_depth: { node: 'lfo', param: 'depth' },
|
||||
lfo_depthabs: { node: 'lfo', param: 'depth' },
|
||||
lfo_skew: { node: 'lfo', param: 'skew' },
|
||||
lfo_curve: { node: 'lfo', param: 'curve' },
|
||||
lfo_dcoffset: { node: 'lfo', param: 'dcoffset' },
|
||||
env: { node: 'env', param: 'depth' },
|
||||
env_attack: { node: 'env', param: 'attack' },
|
||||
env_decay: { node: 'env', param: 'decay' },
|
||||
env_sustain: { node: 'env', param: 'sustain' },
|
||||
env_release: { node: 'env', param: 'release' },
|
||||
bmod: { node: 'bmod', param: 'depth' },
|
||||
bmod_depth: { node: 'bmod', param: 'depth' },
|
||||
bmod_depthabs: { node: 'bmod', param: 'depth' },
|
||||
|
||||
// LPF
|
||||
cutoff: { node: 'lpf', param: 'frequency' },
|
||||
resonance: { node: 'lpf', param: 'Q' },
|
||||
lprate: { node: 'lpf_lfo', param: 'rate' },
|
||||
lpsync: { node: 'lpf_lfo', param: 'sync' },
|
||||
lpdepth: { node: 'lpf_lfo', param: 'depth' },
|
||||
lpdepthfrequency: { node: 'lpf_lfo', param: 'depth' },
|
||||
lpshape: { node: 'lpf_lfo', param: 'shape' },
|
||||
lpdc: { node: 'lpf_lfo', param: 'dcoffset' },
|
||||
lpskew: { node: 'lpf_lfo', param: 'skew' },
|
||||
|
||||
// HPF
|
||||
hcutoff: { node: 'hpf', param: 'frequency' },
|
||||
hresonance: { node: 'hpf', param: 'Q' },
|
||||
hprate: { node: 'hpf_lfo', param: 'rate' },
|
||||
hpsync: { node: 'hpf_lfo', param: 'sync' },
|
||||
hpdepth: { node: 'hpf_lfo', param: 'depth' },
|
||||
hpdepthfrequency: { node: 'hpf_lfo', param: 'depth' },
|
||||
hpshape: { node: 'hpf_lfo', param: 'shape' },
|
||||
hpdc: { node: 'hpf_lfo', param: 'dcoffset' },
|
||||
hpskew: { node: 'hpf_lfo', param: 'skew' },
|
||||
|
||||
// BPF
|
||||
bandf: { node: 'bpf', param: 'frequency' },
|
||||
bandq: { node: 'bpf', param: 'Q' },
|
||||
bprate: { node: 'bpf_lfo', param: 'rate' },
|
||||
bpsync: { node: 'bpf_lfo', param: 'sync' },
|
||||
bpdepth: { node: 'bpf_lfo', param: 'depth' },
|
||||
bpdepthfrequency: { node: 'bpf_lfo', param: 'depth' },
|
||||
bpshape: { node: 'bpf_lfo', param: 'shape' },
|
||||
bpdc: { node: 'bpf_lfo', param: 'dcoffset' },
|
||||
bpskew: { node: 'bpf_lfo', param: 'skew' },
|
||||
|
||||
vowel: { node: 'vowel', param: 'frequency' },
|
||||
|
||||
// DISTORTION
|
||||
coarse: { node: 'coarse', param: 'coarse' },
|
||||
crush: { node: 'crush', param: 'crush' },
|
||||
shape: { node: 'shape', param: 'shape' },
|
||||
shapevol: { node: 'shape', param: 'postgain' },
|
||||
distort: { node: 'distort', param: 'distort' },
|
||||
distortvol: { node: 'distort', param: 'postgain' },
|
||||
distorttype: { node: 'distort', param: 'distort' },
|
||||
|
||||
// COMPRESSOR
|
||||
compressor: { node: 'compressor', param: 'threshold' },
|
||||
compressorRatio: { node: 'compressor', param: 'ratio' },
|
||||
compressorKnee: { node: 'compressor', param: 'knee' },
|
||||
compressorAttack: { node: 'compressor', param: 'attack' },
|
||||
compressorRelease: { node: 'compressor', param: 'release' },
|
||||
|
||||
// PHASER
|
||||
phaserrate: { node: 'phaser_lfo', param: 'rate' },
|
||||
phasersweep: { node: 'phaser_lfo', param: 'depth' },
|
||||
phasercenter: { node: 'phaser', param: 'frequency' },
|
||||
phaserdepth: { node: 'phaser', param: 'Q' },
|
||||
|
||||
// ORBIT EFFECTS
|
||||
delay: { node: 'delay_mix', param: 'gain' },
|
||||
delaytime: { node: 'delay', param: 'delayTime' },
|
||||
delayfeedback: { node: 'delay', param: 'feedback' },
|
||||
delaysync: { node: 'delay', param: 'delayTime' },
|
||||
dry: { node: 'dry', param: 'gain' },
|
||||
room: { node: 'room_mix', param: 'gain' },
|
||||
djf: { node: 'djf', param: 'value' },
|
||||
busgain: { node: 'bus', param: 'gain' },
|
||||
|
||||
// SYNTHS
|
||||
s: { node: 'source', param: 'frequency' },
|
||||
detune: { node: 'source', param: 'freqspread' },
|
||||
wt: { node: 'source', param: 'position' },
|
||||
warp: { node: 'source', param: 'warp' },
|
||||
freq: { node: 'source', param: 'frequency' },
|
||||
note: { node: 'source', param: 'frequency' },
|
||||
wtdc: { node: 'wt_lfo', param: 'dc' },
|
||||
wtskew: { node: 'wt_lfo', param: 'skew' },
|
||||
wtrate: { node: 'wt_lfo', param: 'frequency' },
|
||||
wtsync: { node: 'wt_lfo', param: 'frequency' },
|
||||
wtdepth: { node: 'wt_lfo', param: 'depth' },
|
||||
warpdc: { node: 'warp_lfo', param: 'dc' },
|
||||
warpskew: { node: 'warp_lfo', param: 'skew' },
|
||||
warprate: { node: 'warp_lfo', param: 'frequency' },
|
||||
warpsync: { node: 'warp_lfo', param: 'frequency' },
|
||||
warpdepth: { node: 'warp_lfo', param: 'depth' },
|
||||
fmi: { node: 'fm_1_gain', param: 'gain' },
|
||||
fmi2: { node: 'fm_2_gain', param: 'gain' },
|
||||
fmi3: { node: 'fm_3_gain', param: 'gain' },
|
||||
fmi4: { node: 'fm_4_gain', param: 'gain' },
|
||||
fmi5: { node: 'fm_5_gain', param: 'gain' },
|
||||
fmi6: { node: 'fm_6_gain', param: 'gain' },
|
||||
fmi7: { node: 'fm_7_gain', param: 'gain' },
|
||||
fmi8: { node: 'fm_8_gain', param: 'gain' },
|
||||
fmh: { node: 'fm_1', param: 'frequency' },
|
||||
fmh2: { node: 'fm_2', param: 'frequency' },
|
||||
fmh3: { node: 'fm_3', param: 'frequency' },
|
||||
fmh4: { node: 'fm_4', param: 'frequency' },
|
||||
fmh5: { node: 'fm_5', param: 'frequency' },
|
||||
fmh6: { node: 'fm_6', param: 'frequency' },
|
||||
fmh7: { node: 'fm_7', param: 'frequency' },
|
||||
fmh8: { node: 'fm_8', param: 'frequency' },
|
||||
pw: { node: 'source', param: 'pulsewidth' },
|
||||
pwrate: { node: 'pw_lfo', param: 'frequency' },
|
||||
pwsweep: { node: 'pw_lfo', param: 'depth' },
|
||||
vib: { node: 'vib', param: 'frequency' },
|
||||
vibmod: { node: 'vib_gain', param: 'gain' },
|
||||
byteBeatStartTime: { node: 'source', param: 'byteBeatStartTime' },
|
||||
spread: { node: 'source', param: 'panspread' },
|
||||
transient: { node: 'transient', param: 'attack' },
|
||||
};
|
||||
|
||||
export function getSuperdoughControlTargets() {
|
||||
return CONTROL_TARGETS;
|
||||
}
|
||||
@@ -2,7 +2,10 @@ import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
|
||||
import { errorLogger } from './logger.mjs';
|
||||
import { clamp } from './util.mjs';
|
||||
|
||||
let hasChanged = (now, before) => now !== undefined && now !== before;
|
||||
const hasChanged = (now, before) => now !== undefined && now !== before;
|
||||
// Node with fixed stereo channel count to prevent clicking when the input signal
|
||||
// switches from mono to stereo
|
||||
const getStereoNode = (ac) => new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||
|
||||
export class Orbit {
|
||||
reverbNode;
|
||||
@@ -11,10 +14,11 @@ export class Orbit {
|
||||
summingNode;
|
||||
djfNode;
|
||||
audioContext;
|
||||
|
||||
constructor(audioContext) {
|
||||
this.audioContext = audioContext;
|
||||
this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||
this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||
this.output = getStereoNode(audioContext);
|
||||
this.summingNode = getStereoNode(audioContext);
|
||||
this.summingNode.connect(this.output);
|
||||
}
|
||||
|
||||
@@ -34,6 +38,7 @@ export class Orbit {
|
||||
}
|
||||
const val = this.djfNode.parameters.get('value');
|
||||
val.setValueAtTime(value, t);
|
||||
return this.djfNode;
|
||||
}
|
||||
|
||||
getDelay(delaytime = 0, feedback = 0.5, t) {
|
||||
@@ -164,6 +169,7 @@ export class SuperdoughAudioController {
|
||||
audioContext;
|
||||
output;
|
||||
nodes = {};
|
||||
buses = {};
|
||||
|
||||
constructor(audioContext) {
|
||||
this.audioContext = audioContext;
|
||||
@@ -171,10 +177,14 @@ export class SuperdoughAudioController {
|
||||
}
|
||||
|
||||
reset() {
|
||||
Array.from(this.nodes).forEach((node) => {
|
||||
Object.values(this.nodes).forEach((node) => {
|
||||
node.disconnect();
|
||||
});
|
||||
Object.values(this.buses).forEach((bus) => {
|
||||
bus.disconnect();
|
||||
});
|
||||
this.nodes = {};
|
||||
this.buses = {};
|
||||
this.output.reset();
|
||||
}
|
||||
|
||||
@@ -206,4 +216,11 @@ export class SuperdoughAudioController {
|
||||
}
|
||||
return this.nodes[orbitNum];
|
||||
}
|
||||
|
||||
getBus(busNum) {
|
||||
if (this.buses[busNum] == null) {
|
||||
this.buses[busNum] = getStereoNode(this.audioContext);
|
||||
}
|
||||
return this.buses[busNum];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { clamp } from './util.mjs';
|
||||
import { registerSound, soundMap } from './superdough.mjs';
|
||||
import { getSuperdoughAudioController, registerSound, soundMap } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import {
|
||||
applyFM,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { logger } from './logger.mjs';
|
||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||
|
||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
|
||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user', 'one'];
|
||||
const waveformAliases = [
|
||||
['tri', 'triangle'],
|
||||
['sqr', 'square'],
|
||||
@@ -52,17 +52,17 @@ export function registerSynthSounds() {
|
||||
// turn down
|
||||
const g = gainNode(0.3);
|
||||
|
||||
let sound = getOscillator(s, t, value, () => {
|
||||
const sound = getOscillator(s, t, value, () => {
|
||||
releaseAudioNode(g);
|
||||
onended();
|
||||
});
|
||||
|
||||
let { node: o, stop, triggerRelease } = sound;
|
||||
const { node: o, nodes, stop, triggerRelease } = sound;
|
||||
|
||||
const { duration } = value;
|
||||
|
||||
const envGain = gainNode(1);
|
||||
let node = o.connect(g).connect(envGain);
|
||||
const node = o.connect(g).connect(envGain);
|
||||
const holdEnd = t + duration;
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
|
||||
const envEnd = holdEnd + release + 0.01;
|
||||
@@ -70,6 +70,7 @@ export function registerSynthSounds() {
|
||||
stop(envEnd);
|
||||
return {
|
||||
node,
|
||||
nodes,
|
||||
stop: (endTime) => {
|
||||
stop(endTime);
|
||||
},
|
||||
@@ -139,6 +140,7 @@ export function registerSynthSounds() {
|
||||
|
||||
return {
|
||||
node,
|
||||
nodes: { source: [o] },
|
||||
stop: (endTime) => {
|
||||
o.stop(endTime);
|
||||
},
|
||||
@@ -183,8 +185,8 @@ export function registerSynthSounds() {
|
||||
|
||||
const gainAdjustment = 1 / Math.sqrt(voices);
|
||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
||||
const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
const fmHandle = applyFM(o.parameters.get('frequency'), value, begin);
|
||||
let envGain = gainNode(1);
|
||||
envGain = o.connect(envGain);
|
||||
|
||||
@@ -195,8 +197,8 @@ export function registerSynthSounds() {
|
||||
() => {
|
||||
releaseAudioNode(o);
|
||||
onended();
|
||||
fm?.stop();
|
||||
vibratoOscillator?.stop();
|
||||
fmHandle?.stop();
|
||||
vibratoHandle?.stop();
|
||||
},
|
||||
begin,
|
||||
end,
|
||||
@@ -204,6 +206,7 @@ export function registerSynthSounds() {
|
||||
|
||||
return {
|
||||
node: envGain,
|
||||
nodes: { source: [o], ...fmHandle?.nodes, ...vibratoHandle?.nodes },
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
@@ -279,6 +282,7 @@ export function registerSynthSounds() {
|
||||
|
||||
return {
|
||||
node: envGain,
|
||||
source: o,
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
@@ -329,25 +333,25 @@ export function registerSynthSounds() {
|
||||
);
|
||||
|
||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
||||
const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
const fmHandle = applyFM(o.parameters.get('frequency'), value, begin);
|
||||
let envGain = gainNode(1);
|
||||
envGain = o.connect(envGain);
|
||||
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||
let lfo;
|
||||
let pw_lfo;
|
||||
if (pwsweep != 0) {
|
||||
lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep });
|
||||
lfo.connect(o.parameters.get('pulsewidth'));
|
||||
pw_lfo = getLfo(ac, { frequency: pwrate, depth: pwsweep, begin, end });
|
||||
pw_lfo.connect(o.parameters.get('pulsewidth'));
|
||||
}
|
||||
let timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
releaseAudioNode(o);
|
||||
releaseAudioNode(lfo);
|
||||
releaseAudioNode(pw_lfo);
|
||||
onended();
|
||||
fm?.stop();
|
||||
vibratoOscillator?.stop();
|
||||
fmHandle?.stop();
|
||||
vibratoHandle?.stop();
|
||||
},
|
||||
begin,
|
||||
end,
|
||||
@@ -355,6 +359,7 @@ export function registerSynthSounds() {
|
||||
|
||||
return {
|
||||
node: envGain,
|
||||
nodes: { source: [o], pw_lfo: [pw_lfo], ...fmHandle?.nodes, ...vibratoHandle?.nodes },
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
@@ -363,6 +368,41 @@ export function registerSynthSounds() {
|
||||
{ prebake: true, type: 'synth' },
|
||||
);
|
||||
|
||||
registerSound(
|
||||
'bus',
|
||||
(begin, value, onended) => {
|
||||
const ac = getAudioContext();
|
||||
const [attack, decay, sustain, release] = getADSRValues(
|
||||
[value.attack, value.decay, value.sustain, value.release],
|
||||
'linear',
|
||||
[0.001, 0.05, 1, 0.01],
|
||||
);
|
||||
const holdend = begin + value.duration;
|
||||
const end = holdend + release + 0.01;
|
||||
const bus = getSuperdoughAudioController().getBus(value.n ?? 0);
|
||||
const envGain = bus.connect(gainNode(0));
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||
const timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
bus.disconnect(envGain);
|
||||
onended();
|
||||
},
|
||||
begin,
|
||||
end,
|
||||
);
|
||||
|
||||
return {
|
||||
node: envGain,
|
||||
nodes: { source: [bus] },
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
};
|
||||
},
|
||||
{ prebake: true, type: 'input' },
|
||||
);
|
||||
|
||||
[...noises].forEach((s) => {
|
||||
registerSound(
|
||||
s,
|
||||
@@ -400,6 +440,7 @@ export function registerSynthSounds() {
|
||||
stop(envEnd);
|
||||
return {
|
||||
node,
|
||||
nodes: { source: [o] },
|
||||
stop: (endTime) => {
|
||||
stop(endTime);
|
||||
},
|
||||
@@ -467,8 +508,17 @@ export function getOscillator(s, t, value, onended) {
|
||||
s = 'triangle';
|
||||
}
|
||||
s = s === 'user' && !partials ? 'triangle' : s;
|
||||
// If no partials are given, use stock waveforms
|
||||
if (!partials || partials?.length === 0 || s === 'sine') {
|
||||
if (s === 'one') {
|
||||
// Constant 1 oscillator (used for modulation)
|
||||
o = new ConstantSourceNode(getAudioContext(), { offset: 1 });
|
||||
o.start(t);
|
||||
return {
|
||||
node: o,
|
||||
nodes: { source: o },
|
||||
stop: (time) => o?.stop(time),
|
||||
};
|
||||
} else if (!partials || partials?.length === 0 || s === 'sine') {
|
||||
// If no partials are given, use stock waveforms
|
||||
o = getAudioContext().createOscillator();
|
||||
o.type = s || 'triangle';
|
||||
}
|
||||
@@ -479,11 +529,11 @@ export function getOscillator(s, t, value, onended) {
|
||||
// set frequency
|
||||
o.frequency.value = getFrequencyFromValue(value);
|
||||
|
||||
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
|
||||
const vibratoHandle = getVibratoOscillator(o.detune, value, t);
|
||||
|
||||
// pitch envelope
|
||||
getPitchEnvelope(o.detune, value, t, t + duration);
|
||||
const fmModulator = applyFM(o.frequency, value, t);
|
||||
const fmHandle = applyFM(o.frequency, value, t);
|
||||
|
||||
let noiseMix;
|
||||
if (noise) {
|
||||
@@ -500,9 +550,10 @@ export function getOscillator(s, t, value, onended) {
|
||||
|
||||
return {
|
||||
node: noiseMix?.node || o,
|
||||
nodes: { source: [o], ...vibratoHandle?.nodes, ...fmHandle?.nodes },
|
||||
stop: (time) => {
|
||||
fmModulator.stop(time);
|
||||
vibratoOscillator?.stop(time);
|
||||
fmHandle.stop(time);
|
||||
vibratoHandle?.stop(time);
|
||||
noiseMix?.stop(time);
|
||||
o.stop(time);
|
||||
},
|
||||
|
||||
@@ -314,19 +314,28 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
dcoffset: value.warpdc ?? 0,
|
||||
},
|
||||
);
|
||||
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
||||
const fm = applyFM(source.parameters.get('frequency'), value, t);
|
||||
const vibratoHandle = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
||||
const fmHandle = 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');
|
||||
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
|
||||
const handle = { node, source };
|
||||
const handle = {
|
||||
node,
|
||||
nodes: {
|
||||
source: [source],
|
||||
wt_lfo: [wtPosModulators],
|
||||
warp_lfo: [wtWarpModulators],
|
||||
...fmHandle?.nodes,
|
||||
...vibratoHandle?.nodes,
|
||||
},
|
||||
};
|
||||
const timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
releaseAudioNode(source);
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
fm?.stop();
|
||||
vibratoHandle?.stop();
|
||||
fmHandle?.stop();
|
||||
releaseAudioNode(wtPosModulators);
|
||||
releaseAudioNode(wtWarpModulators);
|
||||
onended();
|
||||
|
||||
@@ -124,8 +124,8 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
{ name: 'shape', defaultValue: 0 },
|
||||
{ name: 'curve', defaultValue: 1 },
|
||||
{ name: 'dcoffset', defaultValue: 0 },
|
||||
{ name: 'min', defaultValue: 0 },
|
||||
{ name: 'max', defaultValue: 1 },
|
||||
{ name: 'min', defaultValue: -1e9 },
|
||||
{ name: 'max', defaultValue: 1e9 },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -143,7 +143,8 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
|
||||
process(_inputs, outputs, parameters) {
|
||||
const begin = parameters['begin'][0];
|
||||
if (currentTime >= parameters.end[0]) {
|
||||
const end = parameters['end'][0];
|
||||
if (currentTime >= end) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= begin) {
|
||||
@@ -161,6 +162,7 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
const curve = parameters['curve'][0];
|
||||
|
||||
const dcoffset = parameters['dcoffset'][0];
|
||||
|
||||
const min = parameters['min'][0];
|
||||
const max = parameters['max'][0];
|
||||
const shape = waveShapeNames[parameters['shape'][0]];
|
||||
@@ -967,7 +969,9 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
||||
{ name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
||||
{ name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
||||
{ name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
||||
{ name: 'peak', defaultValue: 1 },
|
||||
{ name: 'depth', defaultValue: 1 },
|
||||
{ name: 'min', defaultValue: -1e9 },
|
||||
{ name: 'max', defaultValue: 1e9 },
|
||||
{ name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 },
|
||||
];
|
||||
}
|
||||
@@ -1009,9 +1013,15 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
process(_inputs, outputs, params) {
|
||||
const begin = params['begin'][0];
|
||||
const end = params['end'][0];
|
||||
if (currentTime >= end) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= begin) {
|
||||
return true;
|
||||
}
|
||||
const out = outputs[0][0];
|
||||
if (!out) return true;
|
||||
const begin = pv(params.begin, 0);
|
||||
const retrigger = pv(params.retrigger, 0) >= 0.5; // convert to bool
|
||||
if (begin !== this.beginTime && (this.state === 0 || retrigger)) {
|
||||
// triggered
|
||||
@@ -1029,7 +1039,9 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
||||
const aCurve = pv(params.attackCurve, i);
|
||||
const dCurve = pv(params.decayCurve, i);
|
||||
const rCurve = pv(params.releaseCurve, i);
|
||||
const peak = pv(params.peak, i);
|
||||
const depth = pv(params.depth, i);
|
||||
const min = pv(params.min, i);
|
||||
const max = pv(params.max, i);
|
||||
const states = [
|
||||
{ time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle
|
||||
{ time: attack, start: this.attackStart, target: 1, curve: aCurve },
|
||||
@@ -1043,7 +1055,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
||||
this.state = (this.state + 1) % states.length;
|
||||
time = states[this.state].time;
|
||||
}
|
||||
out[i] = this.val * peak;
|
||||
out[i] = clamp(this.val * depth, min, max);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ export function registerZZFXSounds() {
|
||||
});
|
||||
return {
|
||||
node: o,
|
||||
nodes: { source: [o] },
|
||||
stop: () => {},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -3803,6 +3803,51 @@ exports[`runs examples > example "end" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "env" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]",
|
||||
"[ 1/1 → 2/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]",
|
||||
"[ 2/1 → 3/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]",
|
||||
"[ 3/1 → 4/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "env" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]",
|
||||
"[ 1/1 → 2/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]",
|
||||
"[ 2/1 → 3/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]",
|
||||
"[ 3/1 → 4/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "env" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]",
|
||||
"[ 1/1 → 2/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]",
|
||||
"[ 2/1 → 3/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]",
|
||||
"[ 3/1 → 4/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "env" example index 3 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:0.5} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 1/1 → 2/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:1} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 2/1 → 3/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:1} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 3/1 → 4/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:0.5} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "env" example index 4 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:0.5}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 1/1 → 2/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:1}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 2/1 → 3/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:1}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 3/1 → 4/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:0.5}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "euclid" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:c3 ]",
|
||||
@@ -6113,6 +6158,51 @@ exports[`runs examples > example "leslie" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lfo" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
"[ 1/1 → 2/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
"[ 2/1 → 3/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
"[ 3/1 → 4/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lfo" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]",
|
||||
"[ 1/1 → 2/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]",
|
||||
"[ 2/1 → 3/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]",
|
||||
"[ 3/1 → 4/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lfo" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
"[ 1/1 → 2/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
"[ 2/1 → 3/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
"[ 3/1 → 4/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lfo" example index 3 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s sync:8} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 1/1 → 2/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 2/1 → 3/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 3/1 → 4/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s sync:8} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lfo" example index 4 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4 sync:8}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 1/1 → 2/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 2/1 → 3/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 3/1 → 4/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4 sync:8}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "linger" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:lt ]",
|
||||
|
||||
@@ -20,6 +20,7 @@ const skippedExamples = [
|
||||
'accelerationX',
|
||||
'defaultmidimap',
|
||||
'midimaps',
|
||||
'bmod',
|
||||
];
|
||||
|
||||
describe('runs examples', () => {
|
||||
|
||||
Reference in New Issue
Block a user