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:
commit
367e07da0c
15 changed files with 989 additions and 111 deletions
|
|
@ -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/>.
|
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) {
|
export function createParam(names) {
|
||||||
let isMulti = Array.isArray(names);
|
let isMulti = Array.isArray(names);
|
||||||
|
|
@ -2044,6 +2045,28 @@ export const { octave, oct } = registerControl('octave', 'oct');
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
export const { orbit } = registerControl('orbit', 'o');
|
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
|
// 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');
|
export const { overgain } = registerControl('overgain');
|
||||||
// TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
|
// 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'],
|
// ['pitch2'],
|
||||||
// ['pitch3'],
|
// ['pitch3'],
|
||||||
// ['portamento'],
|
// ['portamento'],
|
||||||
// TODO: LFO rate see https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
|
||||||
export const { rate } = registerControl('rate');
|
|
||||||
// TODO: slide param for certain synths
|
// TODO: slide param for certain synths
|
||||||
export const { slide } = registerControl('slide');
|
export const { slide } = registerControl('slide');
|
||||||
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
// 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 { deltaSlide } = registerControl('deltaSlide');
|
||||||
export const { pitchJump } = registerControl('pitchJump');
|
export const { pitchJump } = registerControl('pitchJump');
|
||||||
export const { pitchJumpTime } = registerControl('pitchJumpTime');
|
export const { pitchJumpTime } = registerControl('pitchJumpTime');
|
||||||
export const { lfo, repeatTime } = registerControl('lfo', 'repeatTime');
|
|
||||||
// noise on the frequency or as bubo calls it "frequency fog" :)
|
// noise on the frequency or as bubo calls it "frequency fog" :)
|
||||||
export const { znoise } = registerControl('znoise');
|
export const { znoise } = registerControl('znoise');
|
||||||
export const { zmod } = registerControl('zmod');
|
export const { zmod } = registerControl('zmod');
|
||||||
|
|
@ -2792,6 +2813,245 @@ export const scrub = register(
|
||||||
false,
|
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
|
* Transient shaper. Gives independent control over the emphasis on transients
|
||||||
* and sustains
|
* and sustains
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@ export function registerSoundfonts() {
|
||||||
let envEnd = holdEnd + release + 0.01;
|
let envEnd = holdEnd + release + 0.01;
|
||||||
|
|
||||||
// vibrato
|
// vibrato
|
||||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, time);
|
const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, time);
|
||||||
// pitch envelope
|
// pitch envelope
|
||||||
getPitchEnvelope(bufferSource.detune, value, time, holdEnd);
|
getPitchEnvelope(bufferSource.detune, value, time, holdEnd);
|
||||||
|
|
||||||
|
|
@ -174,10 +174,10 @@ export function registerSoundfonts() {
|
||||||
const stop = (releaseTime) => {};
|
const stop = (releaseTime) => {};
|
||||||
onceEnded(bufferSource, () => {
|
onceEnded(bufferSource, () => {
|
||||||
releaseAudioNode(bufferSource);
|
releaseAudioNode(bufferSource);
|
||||||
releaseAudioNode(vibratoOscillator);
|
vibratoHandle?.stop();
|
||||||
onended();
|
onended();
|
||||||
});
|
});
|
||||||
return { node, stop };
|
return { node, stop, nodes: { source: [bufferSource], ...vibratoHandle?.nodes } };
|
||||||
},
|
},
|
||||||
{ type: 'soundfont', prebake: true, fonts },
|
{ 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;
|
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLfo(audioContext, begin, end, properties = {}) {
|
export function getEnvelope(audioContext, properties = {}) {
|
||||||
const { shape = 0, ...props } = properties;
|
return getWorklet(audioContext, 'envelope-processor', properties);
|
||||||
const { dcoffset = -0.5, depth = 1 } = 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 = {
|
const lfoprops = {
|
||||||
frequency: 1,
|
|
||||||
depth,
|
|
||||||
skew: 0.5,
|
|
||||||
phaseoffset: 0,
|
|
||||||
time: begin,
|
|
||||||
begin,
|
begin,
|
||||||
end,
|
end,
|
||||||
shape: getModulationShapeInput(shape),
|
time: time ?? begin,
|
||||||
|
depth,
|
||||||
dcoffset,
|
dcoffset,
|
||||||
min: dcoffset * depth,
|
frequency,
|
||||||
max: dcoffset * depth + depth,
|
skew,
|
||||||
curve: 1,
|
phaseoffset,
|
||||||
|
curve,
|
||||||
|
shape: getModulationShapeInput(shape),
|
||||||
|
min: min ?? dcoffset * depth,
|
||||||
|
max: max ?? dcoffset * depth + depth,
|
||||||
...props,
|
...props,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -161,7 +179,9 @@ export function getParamLfo(audioContext, param, start, end, lfoValues) {
|
||||||
}
|
}
|
||||||
let lfo;
|
let lfo;
|
||||||
if (depth) {
|
if (depth) {
|
||||||
lfo = getLfo(audioContext, start, end, {
|
lfo = getLfo(audioContext, {
|
||||||
|
begin: start,
|
||||||
|
end,
|
||||||
depth,
|
depth,
|
||||||
dcoffset,
|
dcoffset,
|
||||||
...getLfoInputs,
|
...getLfoInputs,
|
||||||
|
|
@ -331,7 +351,7 @@ export function getVibratoOscillator(param, value, t) {
|
||||||
releaseAudioNode(vibratoOscillator);
|
releaseAudioNode(vibratoOscillator);
|
||||||
});
|
});
|
||||||
vibratoOscillator.start(t);
|
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 ac = getAudioContext();
|
||||||
const toStop = []; // fm oscillators we will expose `stop` for
|
const toStop = []; // fm oscillators we will expose `stop` for
|
||||||
const fms = {};
|
const fms = {};
|
||||||
|
const nodes = {};
|
||||||
// Matrix
|
// Matrix
|
||||||
for (let i = 1; i <= 8; i++) {
|
for (let i = 1; i <= 8; i++) {
|
||||||
for (let j = 0; j <= 8; j++) {
|
for (let j = 0; j <= 8; j++) {
|
||||||
|
|
@ -437,11 +458,13 @@ export function applyFM(param, value, begin) {
|
||||||
output = osc.connect(envGain);
|
output = osc.connect(envGain);
|
||||||
}
|
}
|
||||||
fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup };
|
fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup };
|
||||||
|
nodes[`fm_${idx}`] = [osc];
|
||||||
}
|
}
|
||||||
const { input, output, freq, osc, toCleanup } = fms[idx];
|
const { input, output, freq, osc, toCleanup } = fms[idx];
|
||||||
const g = gainNode(amt * freq);
|
const g = gainNode(amt * freq);
|
||||||
io.push(isMod ? output.connect(g) : input);
|
io.push(isMod ? output.connect(g) : input);
|
||||||
cleanupOnEnd(osc, [...toCleanup, g]);
|
cleanupOnEnd(osc, [...toCleanup, g]);
|
||||||
|
nodes[`fm_${idx}_gain`] = [g];
|
||||||
}
|
}
|
||||||
if (!io[1]) {
|
if (!io[1]) {
|
||||||
logger(
|
logger(
|
||||||
|
|
@ -454,6 +477,7 @@ export function applyFM(param, value, begin) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
nodes,
|
||||||
stop: (t) => toStop.forEach((m) => m?.stop(t)),
|
stop: (t) => toStop.forEach((m) => m?.stop(t)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export * from './helpers.mjs';
|
||||||
export * from './synth.mjs';
|
export * from './synth.mjs';
|
||||||
export * from './zzfx.mjs';
|
export * from './zzfx.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
|
export * from './modulators.mjs';
|
||||||
export * from './dspworklet.mjs';
|
export * from './dspworklet.mjs';
|
||||||
export * from './audioContext.mjs';
|
export * from './audioContext.mjs';
|
||||||
export * from './wavetable.mjs';
|
export * from './wavetable.mjs';
|
||||||
|
|
|
||||||
158
packages/superdough/modulators.mjs
Normal file
158
packages/superdough/modulators.mjs
Normal file
|
|
@ -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
|
// vibrato
|
||||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, t);
|
||||||
|
|
||||||
const time = t + nudge;
|
const time = t + nudge;
|
||||||
bufferSource.start(time, offset);
|
bufferSource.start(time, offset);
|
||||||
|
|
@ -324,7 +324,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||||
node.connect(out);
|
node.connect(out);
|
||||||
onceEnded(bufferSource, function () {
|
onceEnded(bufferSource, function () {
|
||||||
releaseAudioNode(bufferSource);
|
releaseAudioNode(bufferSource);
|
||||||
releaseAudioNode(vibratoOscillator);
|
vibratoHandle?.stop();
|
||||||
releaseAudioNode(node);
|
releaseAudioNode(node);
|
||||||
releaseAudioNode(out);
|
releaseAudioNode(out);
|
||||||
onended();
|
onended();
|
||||||
|
|
@ -334,7 +334,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||||
const stop = (endTime) => {
|
const stop = (endTime) => {
|
||||||
bufferSource.stop(endTime);
|
bufferSource.stop(endTime);
|
||||||
};
|
};
|
||||||
const handle = { node: out, bufferSource, stop };
|
const handle = { node: out, nodes: { source: [bufferSource], ...vibratoHandle?.nodes }, stop };
|
||||||
|
|
||||||
// cut groups
|
// cut groups
|
||||||
if (cut !== undefined) {
|
if (cut !== undefined) {
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,19 @@ import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
||||||
import workletsUrl from './worklets.mjs?audioworklet';
|
import workletsUrl from './worklets.mjs?audioworklet';
|
||||||
import {
|
import {
|
||||||
createFilter,
|
createFilter,
|
||||||
|
effectSend,
|
||||||
gainNode,
|
gainNode,
|
||||||
getCompressor,
|
getCompressor,
|
||||||
getDistortion,
|
getDistortion,
|
||||||
getLfo,
|
getLfo,
|
||||||
getWorklet,
|
getWorklet,
|
||||||
effectSend,
|
|
||||||
releaseAudioNode,
|
releaseAudioNode,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.mjs';
|
||||||
import { loadBuffer } from './sampler.mjs';
|
import { loadBuffer } from './sampler.mjs';
|
||||||
import { getAudioContext, setAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||||
import { resetSeenKeys } from './wavetable.mjs';
|
import { resetSeenKeys } from './wavetable.mjs';
|
||||||
|
|
||||||
|
|
@ -184,6 +185,7 @@ let defaultDefaultValues = {
|
||||||
distortvol: 1,
|
distortvol: 1,
|
||||||
distorttype: 0,
|
distorttype: 0,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
|
busgain: 1,
|
||||||
byteBeatExpression: '0',
|
byteBeatExpression: '0',
|
||||||
delayfeedback: 0.5,
|
delayfeedback: 0.5,
|
||||||
delaysync: 3 / 16,
|
delaysync: 3 / 16,
|
||||||
|
|
@ -327,9 +329,9 @@ export function connectToDestination(input, channels) {
|
||||||
controller.output.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 ac = getAudioContext();
|
||||||
const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
|
const lfo = getLfo(ac, { frequency, depth: sweep * 2, begin, end });
|
||||||
|
|
||||||
//filters
|
//filters
|
||||||
const numStages = 1; //num of filters in series
|
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) => {
|
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
|
// new: t is always expected to be the absolute target onset time
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const audioController = getSuperdoughAudioController();
|
const audioController = getSuperdoughAudioController();
|
||||||
|
|
@ -472,6 +475,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
delaysync = getDefaultValue('delaysync'),
|
delaysync = getDefaultValue('delaysync'),
|
||||||
delaytime,
|
delaytime,
|
||||||
orbit = getDefaultValue('orbit'),
|
orbit = getDefaultValue('orbit'),
|
||||||
|
bus,
|
||||||
|
busgain = getDefaultValue('busgain'),
|
||||||
room,
|
room,
|
||||||
roomfade,
|
roomfade,
|
||||||
roomlp,
|
roomlp,
|
||||||
|
|
@ -512,6 +517,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
delay = applyGainCurve(delay);
|
delay = applyGainCurve(delay);
|
||||||
velocity = applyGainCurve(velocity);
|
velocity = applyGainCurve(velocity);
|
||||||
tremolodepth = applyGainCurve(tremolodepth);
|
tremolodepth = applyGainCurve(tremolodepth);
|
||||||
|
busgain = applyGainCurve(busgain);
|
||||||
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||||
|
|
||||||
const end = t + hapDuration;
|
const end = t + hapDuration;
|
||||||
|
|
@ -529,7 +535,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
activeSoundSources.delete(chainID);
|
activeSoundSources.delete(chainID);
|
||||||
}
|
}
|
||||||
|
|
||||||
let audioNodes = [];
|
const audioNodes = [];
|
||||||
|
|
||||||
if (['-', '~', '_'].includes(s)) {
|
if (['-', '~', '_'].includes(s)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -543,6 +549,7 @@ 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];
|
||||||
} else if (getSound(s)) {
|
} else if (getSound(s)) {
|
||||||
const { onTrigger } = getSound(s);
|
const { onTrigger } = getSound(s);
|
||||||
const onEnded = () => {
|
const onEnded = () => {
|
||||||
|
|
@ -554,6 +561,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
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 };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`sound ${s} not found! Is it loaded?`);
|
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);
|
chain.push(sourceNode);
|
||||||
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
|
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
|
||||||
|
|
||||||
transient !== undefined &&
|
if (transient !== undefined) {
|
||||||
chain.push(
|
const transProcessor = getWorklet(
|
||||||
getWorklet(
|
ac,
|
||||||
ac,
|
'transient-processor',
|
||||||
'transient-processor',
|
{},
|
||||||
{},
|
{
|
||||||
{
|
processorOptions: {
|
||||||
processorOptions: {
|
attack: transient,
|
||||||
attack: transient,
|
sustain: transsustain,
|
||||||
sustain: transsustain,
|
begin: t,
|
||||||
begin: t,
|
end: endWithRelease,
|
||||||
end: endWithRelease,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
|
chain.push(transProcessor);
|
||||||
|
nodes['transient'] = transProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
// gain stage
|
// gain stage
|
||||||
chain.push(gainNode(gain));
|
const initialGain = gainNode(gain);
|
||||||
|
nodes['gain'] = [initialGain];
|
||||||
|
chain.push(initialGain);
|
||||||
|
|
||||||
// filter
|
// filter
|
||||||
const ftype = getFilterType(value.ftype);
|
const ftype = getFilterType(value.ftype);
|
||||||
|
|
||||||
|
const filt = (params) => createFilter(ac, t, end, params, cps, cycle);
|
||||||
if (value.cutoff !== undefined) {
|
if (value.cutoff !== undefined) {
|
||||||
const lpMap = {
|
const lpMap = {
|
||||||
frequency: 'cutoff',
|
frequency: 'cutoff',
|
||||||
|
|
@ -617,12 +629,15 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
};
|
};
|
||||||
const lpParams = pickAndRename(value, lpMap);
|
const lpParams = pickAndRename(value, lpMap);
|
||||||
lpParams.type = 'lowpass';
|
lpParams.type = 'lowpass';
|
||||||
const lp = () => createFilter(ac, t, end, lpParams, cps, cycle);
|
const { filter: lpf1, lfo: lfo1 } = filt(lpParams);
|
||||||
const { filter: lpf1, lfo: lfo1 } = lp();
|
nodes['lpf'] = [lpf1];
|
||||||
|
nodes['lpf_lfo'] = [lfo1];
|
||||||
chain.push(lpf1);
|
chain.push(lpf1);
|
||||||
lfo1 && audioNodes.push(lfo1);
|
lfo1 && audioNodes.push(lfo1);
|
||||||
if (ftype === '24db') {
|
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);
|
chain.push(lpf2);
|
||||||
lfo2 && audioNodes.push(lfo2);
|
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);
|
const hpParams = pickAndRename(value, hpMap);
|
||||||
hpParams.type = 'highpass';
|
hpParams.type = 'highpass';
|
||||||
const hp = () => createFilter(ac, t, end, hpParams, cps, cycle);
|
const { filter: hpf1, lfo: lfo1 } = filt(hpParams);
|
||||||
const { filter: hpf1, lfo: lfo1 } = hp();
|
nodes['hpf'] = [hpf1];
|
||||||
chain.push(hpf1);
|
nodes['hpf_lfo'] = [lfo1];
|
||||||
lfo1 && audioNodes.push(lfo1);
|
lfo1 && audioNodes.push(lfo1);
|
||||||
|
chain.push(hpf1);
|
||||||
if (ftype === '24db') {
|
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);
|
chain.push(hpf2);
|
||||||
lfo2 && audioNodes.push(lfo2);
|
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);
|
const bpParams = pickAndRename(value, bpMap);
|
||||||
bpParams.type = 'bandpass';
|
bpParams.type = 'bandpass';
|
||||||
const bp = () => createFilter(ac, t, end, bpParams, cps, cycle);
|
const { filter: bpf1, lfo: lfo1 } = filt(bpParams);
|
||||||
const { filter: bpf1, lfo: lfo1 } = bp();
|
nodes['bpf'] = [bpf1];
|
||||||
|
nodes['bpf_lfo'] = [lfo1];
|
||||||
chain.push(bpf1);
|
chain.push(bpf1);
|
||||||
lfo1 && audioNodes.push(lfo1);
|
lfo1 && audioNodes.push(lfo1);
|
||||||
if (ftype === '24db') {
|
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);
|
chain.push(bpf2);
|
||||||
lfo2 && audioNodes.push(lfo2);
|
lfo2 && audioNodes.push(lfo2);
|
||||||
}
|
}
|
||||||
|
|
@ -696,14 +717,31 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
|
|
||||||
if (vowel !== undefined) {
|
if (vowel !== undefined) {
|
||||||
const vowelFilter = ac.createVowelFilter(vowel);
|
const vowelFilter = ac.createVowelFilter(vowel);
|
||||||
|
nodes['vowel'] = [vowelFilter];
|
||||||
chain.push(vowelFilter);
|
chain.push(vowelFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// effects
|
// effects
|
||||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
if (coarse !== undefined) {
|
||||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
const coarseNode = getWorklet(ac, 'coarse-processor', { coarse });
|
||||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
nodes['coarse'] = [coarseNode];
|
||||||
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
|
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) {
|
if (tremolosync != null) {
|
||||||
tremolo = cps * tremolosync;
|
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 amGain = new GainNode(ac, { gain });
|
||||||
|
|
||||||
const time = cycle / cps;
|
const time = cycle / cps;
|
||||||
const lfo = getLfo(ac, t, endWithRelease, {
|
const lfo = getLfo(ac, {
|
||||||
skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1),
|
skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1),
|
||||||
frequency: tremolo,
|
frequency: tremolo,
|
||||||
depth: tremolodepth,
|
depth: tremolodepth,
|
||||||
|
|
@ -735,16 +773,28 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 1,
|
max: 1,
|
||||||
curve: 1.5,
|
curve: 1.5,
|
||||||
|
begin: t,
|
||||||
|
end: endWithRelease,
|
||||||
});
|
});
|
||||||
|
nodes['tremolo'] = [lfo];
|
||||||
|
nodes['tremolo_gain'] = [amGain];
|
||||||
lfo.connect(amGain.gain);
|
lfo.connect(amGain.gain);
|
||||||
audioNodes.push(lfo);
|
audioNodes.push(lfo);
|
||||||
chain.push(amGain);
|
chain.push(amGain);
|
||||||
}
|
}
|
||||||
|
|
||||||
compressorThreshold !== undefined &&
|
if (compressorThreshold !== undefined) {
|
||||||
chain.push(
|
const compressorNode = getCompressor(
|
||||||
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease),
|
ac,
|
||||||
|
compressorThreshold,
|
||||||
|
compressorRatio,
|
||||||
|
compressorKnee,
|
||||||
|
compressorAttack,
|
||||||
|
compressorRelease,
|
||||||
);
|
);
|
||||||
|
nodes['compressor'] = [compressorNode];
|
||||||
|
chain.push(compressorNode);
|
||||||
|
}
|
||||||
|
|
||||||
// panning
|
// panning
|
||||||
if (pan !== undefined) {
|
if (pan !== undefined) {
|
||||||
|
|
@ -755,19 +805,24 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
// phaser
|
// phaser
|
||||||
if (phaserrate !== undefined && phaserdepth > 0) {
|
if (phaserrate !== undefined && phaserdepth > 0) {
|
||||||
const { filterChain, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep);
|
const { filterChain, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep);
|
||||||
audioNodes.push(lfo);
|
nodes['phaser'] = [...filterChain];
|
||||||
|
nodes['phaser_lfo'] = [lfo];
|
||||||
chain.push(...filterChain);
|
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];
|
||||||
chain.push(post);
|
chain.push(post);
|
||||||
|
|
||||||
// delay
|
// delay
|
||||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||||
orbitBus.getDelay(delaytime, delayfeedback, t);
|
const delayNode = orbitBus.getDelay(delaytime, delayfeedback, t);
|
||||||
const send = orbitBus.sendDelay(post, delay);
|
nodes['delay'] = [delayNode];
|
||||||
audioNodes.push(send);
|
const delaySend = orbitBus.sendDelay(post, delay);
|
||||||
|
nodes['delay_mix'] = [delaySend];
|
||||||
|
audioNodes.push(delaySend);
|
||||||
}
|
}
|
||||||
// reverb
|
// reverb
|
||||||
if (room > 0) {
|
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);
|
roomIR = await loadBuffer(url, ac, ir, 0);
|
||||||
}
|
}
|
||||||
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
const roomNode = orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
||||||
const send = orbitBus.sendReverb(post, room);
|
nodes['room'] = [roomNode];
|
||||||
audioNodes.push(send);
|
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) {
|
if (djf != null) {
|
||||||
orbitBus.getDjf(djf, t);
|
const djfNode = orbitBus.getDjf(djf, t);
|
||||||
|
nodes['djf'] = [djfNode];
|
||||||
}
|
}
|
||||||
|
|
||||||
// analyser
|
// analyser
|
||||||
|
|
@ -808,7 +871,48 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
|
|
||||||
// connect chain elements together
|
// connect chain elements together
|
||||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
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) => {
|
export const superdoughTrigger = (t, hap, ct, cps) => {
|
||||||
|
|
|
||||||
150
packages/superdough/superdoughdata.mjs
Normal file
150
packages/superdough/superdoughdata.mjs
Normal file
|
|
@ -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 { errorLogger } from './logger.mjs';
|
||||||
import { clamp } from './util.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 {
|
export class Orbit {
|
||||||
reverbNode;
|
reverbNode;
|
||||||
|
|
@ -11,10 +14,11 @@ export class Orbit {
|
||||||
summingNode;
|
summingNode;
|
||||||
djfNode;
|
djfNode;
|
||||||
audioContext;
|
audioContext;
|
||||||
|
|
||||||
constructor(audioContext) {
|
constructor(audioContext) {
|
||||||
this.audioContext = audioContext;
|
this.audioContext = audioContext;
|
||||||
this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
this.output = getStereoNode(audioContext);
|
||||||
this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
this.summingNode = getStereoNode(audioContext);
|
||||||
this.summingNode.connect(this.output);
|
this.summingNode.connect(this.output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,6 +38,7 @@ export class Orbit {
|
||||||
}
|
}
|
||||||
const val = this.djfNode.parameters.get('value');
|
const val = this.djfNode.parameters.get('value');
|
||||||
val.setValueAtTime(value, t);
|
val.setValueAtTime(value, t);
|
||||||
|
return this.djfNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDelay(delaytime = 0, feedback = 0.5, t) {
|
getDelay(delaytime = 0, feedback = 0.5, t) {
|
||||||
|
|
@ -164,6 +169,7 @@ export class SuperdoughAudioController {
|
||||||
audioContext;
|
audioContext;
|
||||||
output;
|
output;
|
||||||
nodes = {};
|
nodes = {};
|
||||||
|
buses = {};
|
||||||
|
|
||||||
constructor(audioContext) {
|
constructor(audioContext) {
|
||||||
this.audioContext = audioContext;
|
this.audioContext = audioContext;
|
||||||
|
|
@ -171,10 +177,14 @@ export class SuperdoughAudioController {
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
Array.from(this.nodes).forEach((node) => {
|
Object.values(this.nodes).forEach((node) => {
|
||||||
node.disconnect();
|
node.disconnect();
|
||||||
});
|
});
|
||||||
|
Object.values(this.buses).forEach((bus) => {
|
||||||
|
bus.disconnect();
|
||||||
|
});
|
||||||
this.nodes = {};
|
this.nodes = {};
|
||||||
|
this.buses = {};
|
||||||
this.output.reset();
|
this.output.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,4 +216,11 @@ export class SuperdoughAudioController {
|
||||||
}
|
}
|
||||||
return this.nodes[orbitNum];
|
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 { clamp } from './util.mjs';
|
||||||
import { registerSound, soundMap } from './superdough.mjs';
|
import { getSuperdoughAudioController, registerSound, soundMap } from './superdough.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
|
|
@ -19,7 +19,7 @@ import {
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||||
|
|
||||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
|
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user', 'one'];
|
||||||
const waveformAliases = [
|
const waveformAliases = [
|
||||||
['tri', 'triangle'],
|
['tri', 'triangle'],
|
||||||
['sqr', 'square'],
|
['sqr', 'square'],
|
||||||
|
|
@ -52,17 +52,17 @@ export function registerSynthSounds() {
|
||||||
// turn down
|
// turn down
|
||||||
const g = gainNode(0.3);
|
const g = gainNode(0.3);
|
||||||
|
|
||||||
let sound = getOscillator(s, t, value, () => {
|
const sound = getOscillator(s, t, value, () => {
|
||||||
releaseAudioNode(g);
|
releaseAudioNode(g);
|
||||||
onended();
|
onended();
|
||||||
});
|
});
|
||||||
|
|
||||||
let { node: o, stop, triggerRelease } = sound;
|
const { node: o, nodes, stop, triggerRelease } = sound;
|
||||||
|
|
||||||
const { duration } = value;
|
const { duration } = value;
|
||||||
|
|
||||||
const envGain = gainNode(1);
|
const envGain = gainNode(1);
|
||||||
let node = o.connect(g).connect(envGain);
|
const node = o.connect(g).connect(envGain);
|
||||||
const holdEnd = t + duration;
|
const holdEnd = t + duration;
|
||||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
|
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
|
||||||
const envEnd = holdEnd + release + 0.01;
|
const envEnd = holdEnd + release + 0.01;
|
||||||
|
|
@ -70,6 +70,7 @@ export function registerSynthSounds() {
|
||||||
stop(envEnd);
|
stop(envEnd);
|
||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
|
nodes,
|
||||||
stop: (endTime) => {
|
stop: (endTime) => {
|
||||||
stop(endTime);
|
stop(endTime);
|
||||||
},
|
},
|
||||||
|
|
@ -139,6 +140,7 @@ export function registerSynthSounds() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
|
nodes: { source: [o] },
|
||||||
stop: (endTime) => {
|
stop: (endTime) => {
|
||||||
o.stop(endTime);
|
o.stop(endTime);
|
||||||
},
|
},
|
||||||
|
|
@ -183,8 +185,8 @@ export function registerSynthSounds() {
|
||||||
|
|
||||||
const gainAdjustment = 1 / Math.sqrt(voices);
|
const gainAdjustment = 1 / Math.sqrt(voices);
|
||||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||||
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
const fmHandle = applyFM(o.parameters.get('frequency'), value, begin);
|
||||||
let envGain = gainNode(1);
|
let envGain = gainNode(1);
|
||||||
envGain = o.connect(envGain);
|
envGain = o.connect(envGain);
|
||||||
|
|
||||||
|
|
@ -195,8 +197,8 @@ export function registerSynthSounds() {
|
||||||
() => {
|
() => {
|
||||||
releaseAudioNode(o);
|
releaseAudioNode(o);
|
||||||
onended();
|
onended();
|
||||||
fm?.stop();
|
fmHandle?.stop();
|
||||||
vibratoOscillator?.stop();
|
vibratoHandle?.stop();
|
||||||
},
|
},
|
||||||
begin,
|
begin,
|
||||||
end,
|
end,
|
||||||
|
|
@ -204,6 +206,7 @@ export function registerSynthSounds() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: envGain,
|
node: envGain,
|
||||||
|
nodes: { source: [o], ...fmHandle?.nodes, ...vibratoHandle?.nodes },
|
||||||
stop: (time) => {
|
stop: (time) => {
|
||||||
timeoutNode.stop(time);
|
timeoutNode.stop(time);
|
||||||
},
|
},
|
||||||
|
|
@ -279,6 +282,7 @@ export function registerSynthSounds() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: envGain,
|
node: envGain,
|
||||||
|
source: o,
|
||||||
stop: (time) => {
|
stop: (time) => {
|
||||||
timeoutNode.stop(time);
|
timeoutNode.stop(time);
|
||||||
},
|
},
|
||||||
|
|
@ -329,25 +333,25 @@ export function registerSynthSounds() {
|
||||||
);
|
);
|
||||||
|
|
||||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||||
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
const fmHandle = applyFM(o.parameters.get('frequency'), value, begin);
|
||||||
let envGain = gainNode(1);
|
let envGain = gainNode(1);
|
||||||
envGain = o.connect(envGain);
|
envGain = o.connect(envGain);
|
||||||
|
|
||||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||||
let lfo;
|
let pw_lfo;
|
||||||
if (pwsweep != 0) {
|
if (pwsweep != 0) {
|
||||||
lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep });
|
pw_lfo = getLfo(ac, { frequency: pwrate, depth: pwsweep, begin, end });
|
||||||
lfo.connect(o.parameters.get('pulsewidth'));
|
pw_lfo.connect(o.parameters.get('pulsewidth'));
|
||||||
}
|
}
|
||||||
let timeoutNode = webAudioTimeout(
|
let timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
releaseAudioNode(o);
|
releaseAudioNode(o);
|
||||||
releaseAudioNode(lfo);
|
releaseAudioNode(pw_lfo);
|
||||||
onended();
|
onended();
|
||||||
fm?.stop();
|
fmHandle?.stop();
|
||||||
vibratoOscillator?.stop();
|
vibratoHandle?.stop();
|
||||||
},
|
},
|
||||||
begin,
|
begin,
|
||||||
end,
|
end,
|
||||||
|
|
@ -355,6 +359,7 @@ export function registerSynthSounds() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: envGain,
|
node: envGain,
|
||||||
|
nodes: { source: [o], pw_lfo: [pw_lfo], ...fmHandle?.nodes, ...vibratoHandle?.nodes },
|
||||||
stop: (time) => {
|
stop: (time) => {
|
||||||
timeoutNode.stop(time);
|
timeoutNode.stop(time);
|
||||||
},
|
},
|
||||||
|
|
@ -363,6 +368,41 @@ export function registerSynthSounds() {
|
||||||
{ prebake: true, type: 'synth' },
|
{ 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) => {
|
[...noises].forEach((s) => {
|
||||||
registerSound(
|
registerSound(
|
||||||
s,
|
s,
|
||||||
|
|
@ -400,6 +440,7 @@ export function registerSynthSounds() {
|
||||||
stop(envEnd);
|
stop(envEnd);
|
||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
|
nodes: { source: [o] },
|
||||||
stop: (endTime) => {
|
stop: (endTime) => {
|
||||||
stop(endTime);
|
stop(endTime);
|
||||||
},
|
},
|
||||||
|
|
@ -467,8 +508,17 @@ export function getOscillator(s, t, value, onended) {
|
||||||
s = 'triangle';
|
s = 'triangle';
|
||||||
}
|
}
|
||||||
s = s === 'user' && !partials ? 'triangle' : s;
|
s = s === 'user' && !partials ? 'triangle' : s;
|
||||||
// If no partials are given, use stock waveforms
|
if (s === 'one') {
|
||||||
if (!partials || partials?.length === 0 || s === 'sine') {
|
// 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 = getAudioContext().createOscillator();
|
||||||
o.type = s || 'triangle';
|
o.type = s || 'triangle';
|
||||||
}
|
}
|
||||||
|
|
@ -479,11 +529,11 @@ export function getOscillator(s, t, value, onended) {
|
||||||
// set frequency
|
// set frequency
|
||||||
o.frequency.value = getFrequencyFromValue(value);
|
o.frequency.value = getFrequencyFromValue(value);
|
||||||
|
|
||||||
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
|
const vibratoHandle = getVibratoOscillator(o.detune, value, t);
|
||||||
|
|
||||||
// pitch envelope
|
// pitch envelope
|
||||||
getPitchEnvelope(o.detune, value, t, t + duration);
|
getPitchEnvelope(o.detune, value, t, t + duration);
|
||||||
const fmModulator = applyFM(o.frequency, value, t);
|
const fmHandle = applyFM(o.frequency, value, t);
|
||||||
|
|
||||||
let noiseMix;
|
let noiseMix;
|
||||||
if (noise) {
|
if (noise) {
|
||||||
|
|
@ -500,9 +550,10 @@ export function getOscillator(s, t, value, onended) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: noiseMix?.node || o,
|
node: noiseMix?.node || o,
|
||||||
|
nodes: { source: [o], ...vibratoHandle?.nodes, ...fmHandle?.nodes },
|
||||||
stop: (time) => {
|
stop: (time) => {
|
||||||
fmModulator.stop(time);
|
fmHandle.stop(time);
|
||||||
vibratoOscillator?.stop(time);
|
vibratoHandle?.stop(time);
|
||||||
noiseMix?.stop(time);
|
noiseMix?.stop(time);
|
||||||
o.stop(time);
|
o.stop(time);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -314,19 +314,28 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||||
dcoffset: value.warpdc ?? 0,
|
dcoffset: value.warpdc ?? 0,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
const vibratoHandle = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
||||||
const fm = applyFM(source.parameters.get('frequency'), value, t);
|
const fmHandle = applyFM(source.parameters.get('frequency'), value, t);
|
||||||
const envGain = ac.createGain();
|
const envGain = ac.createGain();
|
||||||
const node = source.connect(envGain);
|
const node = source.connect(envGain);
|
||||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
|
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
|
||||||
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
|
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(
|
const timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
releaseAudioNode(source);
|
releaseAudioNode(source);
|
||||||
releaseAudioNode(vibratoOscillator);
|
vibratoHandle?.stop();
|
||||||
fm?.stop();
|
fmHandle?.stop();
|
||||||
releaseAudioNode(wtPosModulators);
|
releaseAudioNode(wtPosModulators);
|
||||||
releaseAudioNode(wtWarpModulators);
|
releaseAudioNode(wtWarpModulators);
|
||||||
onended();
|
onended();
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,8 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||||
{ name: 'shape', defaultValue: 0 },
|
{ name: 'shape', defaultValue: 0 },
|
||||||
{ name: 'curve', defaultValue: 1 },
|
{ name: 'curve', defaultValue: 1 },
|
||||||
{ name: 'dcoffset', defaultValue: 0 },
|
{ name: 'dcoffset', defaultValue: 0 },
|
||||||
{ name: 'min', defaultValue: 0 },
|
{ name: 'min', defaultValue: -1e9 },
|
||||||
{ name: 'max', defaultValue: 1 },
|
{ name: 'max', defaultValue: 1e9 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,7 +143,8 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||||
|
|
||||||
process(_inputs, outputs, parameters) {
|
process(_inputs, outputs, parameters) {
|
||||||
const begin = parameters['begin'][0];
|
const begin = parameters['begin'][0];
|
||||||
if (currentTime >= parameters.end[0]) {
|
const end = parameters['end'][0];
|
||||||
|
if (currentTime >= end) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (currentTime <= begin) {
|
if (currentTime <= begin) {
|
||||||
|
|
@ -161,6 +162,7 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||||
const curve = parameters['curve'][0];
|
const curve = parameters['curve'][0];
|
||||||
|
|
||||||
const dcoffset = parameters['dcoffset'][0];
|
const dcoffset = parameters['dcoffset'][0];
|
||||||
|
|
||||||
const min = parameters['min'][0];
|
const min = parameters['min'][0];
|
||||||
const max = parameters['max'][0];
|
const max = parameters['max'][0];
|
||||||
const shape = waveShapeNames[parameters['shape'][0]];
|
const shape = waveShapeNames[parameters['shape'][0]];
|
||||||
|
|
@ -967,7 +969,9 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
||||||
{ name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
{ name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
||||||
{ name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
{ name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
||||||
{ name: 'releaseCurve', 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 },
|
{ name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -1009,9 +1013,15 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
process(_inputs, outputs, params) {
|
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];
|
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
|
const retrigger = pv(params.retrigger, 0) >= 0.5; // convert to bool
|
||||||
if (begin !== this.beginTime && (this.state === 0 || retrigger)) {
|
if (begin !== this.beginTime && (this.state === 0 || retrigger)) {
|
||||||
// triggered
|
// triggered
|
||||||
|
|
@ -1029,7 +1039,9 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
||||||
const aCurve = pv(params.attackCurve, i);
|
const aCurve = pv(params.attackCurve, i);
|
||||||
const dCurve = pv(params.decayCurve, i);
|
const dCurve = pv(params.decayCurve, i);
|
||||||
const rCurve = pv(params.releaseCurve, 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 = [
|
const states = [
|
||||||
{ time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle
|
{ time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle
|
||||||
{ time: attack, start: this.attackStart, target: 1, curve: aCurve },
|
{ time: attack, start: this.attackStart, target: 1, curve: aCurve },
|
||||||
|
|
@ -1043,7 +1055,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
||||||
this.state = (this.state + 1) % states.length;
|
this.state = (this.state + 1) % states.length;
|
||||||
time = states[this.state].time;
|
time = states[this.state].time;
|
||||||
}
|
}
|
||||||
out[i] = this.val * peak;
|
out[i] = clamp(this.val * depth, min, max);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ export function registerZZFXSounds() {
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
node: o,
|
node: o,
|
||||||
|
nodes: { source: [o] },
|
||||||
stop: () => {},
|
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`] = `
|
exports[`runs examples > example "euclid" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:c3 ]",
|
"[ 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`] = `
|
exports[`runs examples > example "linger" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | s:lt ]",
|
"[ 0/1 → 1/4 | s:lt ]",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ const skippedExamples = [
|
||||||
'accelerationX',
|
'accelerationX',
|
||||||
'defaultmidimap',
|
'defaultmidimap',
|
||||||
'midimaps',
|
'midimaps',
|
||||||
|
'bmod',
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('runs examples', () => {
|
describe('runs examples', () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue