Compare commits

..

1 Commits

Author SHA1 Message Date
Felix Roos 1a5447fb81 update dough-synth 2025-12-28 22:34:54 +01:00
30 changed files with 353 additions and 1523 deletions
+1 -4
View File
@@ -1,17 +1,14 @@
# strudel # strudel
Live coding patterns on the web Live coding patterns on the web
https://strudel.cc/
- Try it here: <https://strudel.cc> - Try it here: <https://strudel.cc>
- Docs: <https://strudel.cc/learn> - Docs: <https://strudel.cc/learn>
- Source: https://codeberg.org/uzu/strudel/
* Along with many other live coding projects, we have moved from Microsoft's Github platform to Codeberg for ethical reasons. **Please don't fork the project back to github**.
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel> - Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year> - 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
- 2 Years of Strudel Blog Post: <https://strudel.cc/blog/#year-2> - 2 Years of Strudel Blog Post: <https://strudel.cc/blog/#year-2>
## Running Locally ## Running Locally
After cloning the project, you can run the REPL locally: After cloning the project, you can run the REPL locally:
-1
View File
@@ -4,4 +4,3 @@ export * from './flash.mjs';
export * from './slider.mjs'; export * from './slider.mjs';
export * from './themes.mjs'; export * from './themes.mjs';
export * from './widget.mjs'; export * from './widget.mjs';
export { Vim } from './keybindings.mjs';
-2
View File
@@ -131,8 +131,6 @@ const keymaps = {
vscode: vscodeExtension, vscode: vscodeExtension,
}; };
export { Vim } from '@replit/codemirror-vim';
export function keybindings(name) { export function keybindings(name) {
const active = keymaps[name]; const active = keymaps[name];
const extensions = active ? [Prec.high(active())] : []; const extensions = active ? [Prec.high(active())] : [];
+4 -275
View File
@@ -4,8 +4,7 @@ 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 { logger } from './logger.mjs'; import { Pattern, register, reify } from './pattern.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);
@@ -2045,28 +2044,6 @@ 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
@@ -2111,7 +2088,8 @@ 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
@@ -2537,6 +2515,7 @@ 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');
@@ -2813,254 +2792,6 @@ 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', 'cu'],
['sync', 's'],
['fxi'],
]);
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'],
['fxi'],
]);
registerSubControls('bmod', [
['bus', 'b'],
['control', 'c'],
['subControl', 'sc'],
['depth', 'dep', 'dr'],
['depthabs', 'da'],
['dc'],
['fxi'],
]);
Pattern.prototype.modulate = function (type, config, idPat) {
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;
// Copy value into a temporary `v` container and attach a single `id` (to be shared across
// each config entry). At the output we destructure and throw away the id
output = output.fmap((v) => (id) => ({ v, id })).appLeft(reify(idPat));
for (const [rawKey, value] of Object.entries(config)) {
const key = getMainSubcontrolName(type, rawKey);
const valuePat = reify(value);
output = output
.fmap(({ v, id }) => (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, id };
if (key === 'control' || key === 'subControl') {
t[id][key] = getControlName(c);
} else {
t[id][key] = c;
}
return { v, id };
})
.appLeft(valuePat);
}
return output.fmap(({ v }) => v);
};
/**
* 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: c
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
* @param {number | Pattern} [config.rate] Modulation rate. Aliases: 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: cu
* @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s
* @param {number | Pattern} [config.fxi] FX index to target
* @param {string | Pattern} id ID to use for this modulator
* @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: c
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
* @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 {number | Pattern} [config.fxi] FX index to target
* @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: c
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
* @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 {number | Pattern} [config.fxi] FX index to target
* @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
@@ -3074,5 +2805,3 @@ export const bmod = (config) => pure({}).bmod(config);
* s("hh*16").bank("tr909").transient("<-1:1 1:-1>") * s("hh*16").bank("tr909").transient("<-1:1 1:-1>")
*/ */
export const { transient } = registerControl(['transient', 'transsustain']); export const { transient } = registerControl(['transient', 'transsustain']);
export const { FXrelease, FXrel, FXr, fxr } = registerControl('FXrelease', 'FXrel', 'FXr', 'fxr');
-32
View File
@@ -3721,35 +3721,3 @@ Pattern.prototype.phases = function (list) {
export const phases = (list) => { export const phases = (list) => {
return _ensureListPattern(list).as('phases'); return _ensureListPattern(list).as('phases');
}; };
/**
* Establishes an FX chain. Can be called by chaining .FX(fx1).FX(fx2)..
* calls and/or in a single .FX(fx1, fx2, ..) call. The fx1, .. are _patterns_ which
* establish the controls of the given effect. See examples.
* @name FX
* @memberof Pattern
* @returns Pattern
* @example
* $: s("[sbd <hh [bd | lt | oh]>]*4").dec(.4)
* .FX(
* phaser(0.5).gain(2),
* bpf(800),
* distort(1.3),
* room(0.2),
* delay(0.5).gain(1.25),
* distort(0.3),
* ).fxr(1.7) // sets release time of effects (like delay)
* @example
* $: s("saw").fm(0.5)
* .delay(0.3) // outer effects are applied *last*
* .FX(coarse(4)) // first coarse
* .FX(lpf(500).lpe(4).lpa(1).lpd(2)) // then lpf
* .FX(distort(1)) // then distort
*/
Pattern.prototype.FX = function (...effects) {
effects = effects.map(reify);
return this.withValue((v) => (vEff) => {
const currFX = v.FX ?? [];
return { ...v, FX: currFX.concat(vEff) };
}).appLeft(parray(effects));
};
+1 -1
View File
@@ -46,4 +46,4 @@ all(osc)
[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D) [open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D)
You can read more about [how to use Superdirt with Strudel](https://strudel.cc/learn/input-output/#oscsuperdirtstrudeldirt) in the tutorial. You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
+3 -3
View File
@@ -166,7 +166,7 @@ export function registerSoundfonts() {
let envEnd = holdEnd + release + 0.01; let envEnd = holdEnd + release + 0.01;
// vibrato // vibrato
const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, time); let vibratoOscillator = 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);
vibratoHandle?.stop(); releaseAudioNode(vibratoOscillator);
onended(); onended();
}); });
return { node, stop, nodes: { source: [bufferSource], ...vibratoHandle?.nodes } }; return { node, stop };
}, },
{ type: 'soundfont', prebake: true, fonts }, { type: 'soundfont', prebake: true, fonts },
); );
+10 -9
View File
@@ -5,18 +5,19 @@ if (typeof DelayNode !== 'undefined') {
wet = Math.abs(wet); wet = Math.abs(wet);
this.delayTime.value = time; this.delayTime.value = time;
this.feedbackGain = ac.createGain(); const feedbackGain = ac.createGain();
this.feedbackGain.gain.value = Math.min(Math.abs(feedback), 0.995); feedbackGain.gain.value = Math.min(Math.abs(feedback), 0.995);
this.feedback = this.feedbackGain.gain; this.feedback = feedbackGain.gain;
this.delayGain = ac.createGain(); const delayGain = ac.createGain();
this.delayGain.gain.value = wet; delayGain.gain.value = wet;
this.delayGain = delayGain;
this.connect(this.feedbackGain); this.connect(feedbackGain);
this.connect(this.delayGain); this.connect(delayGain);
this.feedbackGain.connect(this); feedbackGain.connect(this);
this.connect = (target) => this.delayGain.connect(target); this.connect = (target) => delayGain.connect(target);
return this; return this;
} }
start(t) { start(t) {
+14 -38
View File
@@ -104,40 +104,22 @@ 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 getEnvelope(audioContext, properties = {}) { export function getLfo(audioContext, begin, end, properties = {}) {
return getWorklet(audioContext, 'envelope-processor', properties); const { shape = 0, ...props } = 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,
time: time ?? begin,
depth,
dcoffset,
frequency,
skew,
phaseoffset,
curve,
shape: getModulationShapeInput(shape), shape: getModulationShapeInput(shape),
min: min ?? dcoffset * depth, dcoffset,
max: max ?? dcoffset * depth + depth, min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props, ...props,
}; };
@@ -179,9 +161,7 @@ export function getParamLfo(audioContext, param, start, end, lfoValues) {
} }
let lfo; let lfo;
if (depth) { if (depth) {
lfo = getLfo(audioContext, { lfo = getLfo(audioContext, start, end, {
begin: start,
end,
depth, depth,
dcoffset, dcoffset,
...getLfoInputs, ...getLfoInputs,
@@ -351,7 +331,7 @@ export function getVibratoOscillator(param, value, t) {
releaseAudioNode(vibratoOscillator); releaseAudioNode(vibratoOscillator);
}); });
vibratoOscillator.start(t); vibratoOscillator.start(t);
return { stop: (t) => vibratoOscillator.stop(t), nodes: { vib: [vibratoOscillator], vib_gain: [gain] } }; return vibratoOscillator;
} }
} }
@@ -407,7 +387,6 @@ 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++) {
@@ -458,13 +437,11 @@ 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(
@@ -477,7 +454,6 @@ 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)),
}; };
} }
-1
View File
@@ -10,7 +10,6 @@ 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';
-178
View File
@@ -1,178 +0,0 @@
/*
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, subControl) => {
const controlNoIdx = control.split('_')[0];
return controlTargets[`${controlNoIdx}_${subControl}`] ?? controlTargets[controlNoIdx];
};
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 targetInfo = getControlData(control, subControl);
if (!targetInfo) {
errorLogger(
new Error(`Could not find control data for target '${control}'. It may not be modulatable.`),
'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,
fxi = 'main',
depth = 1,
depthabs,
...filteredParams
} = params;
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], 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.main[`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, fxi = 'main', ...filteredParams } = params;
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], 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.main[`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, fxi = 'main' } = params;
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], 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);
const toCleanup = [];
let modulator = unClamped;
if (min !== undefined && max !== undefined) {
const wsData = clampWithWaveShaper(unClamped, min, max);
modulator = wsData.modulator;
toCleanup.push(...wsData.toCleanup);
}
webAudioTimeout(
ac,
() => {
targetParams.forEach((t) => modulator.connect(t));
},
0,
params.begin,
);
toCleanup.push(dc, shifted, depthGain);
return { modulator, toCleanup };
};
+2 -4
View File
@@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { releaseAudioNode } from './helpers.mjs';
var reverbGen = {}; var reverbGen = {};
/** Generates a reverb impulse response. /** Generates a reverb impulse response.
@@ -106,8 +104,8 @@ var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt,
player.start(); player.start();
context.oncomplete = function (event) { context.oncomplete = function (event) {
callback(event.renderedBuffer); callback(event.renderedBuffer);
releaseAudioNode(filter); filter.disconnect();
releaseAudioNode(player); player.disconnect();
}; };
context.startRendering(); context.startRendering();
+12 -13
View File
@@ -65,22 +65,21 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
bufferSource.playbackRate.value = playbackRate; bufferSource.playbackRate.value = playbackRate;
const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue; const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
const bufferDuration = bufferSource.buffer.duration;
// The computation of the offset into the sound is performed using the sound buffer's natural duration, // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the playback duration, so that even if the sound is playing at twice its normal speed, // rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5. // the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferDuration; const offset = begin * bufferSource.buffer.duration;
const loop = hapValue.loop; const loop = hapValue.loop;
if (loop) { if (loop) {
bufferSource.loop = true; bufferSource.loop = true;
bufferSource.loopStart = loopBegin * bufferDuration; bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
bufferSource.loopEnd = loopEnd * bufferDuration; bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
} }
const playbackDuration = bufferDuration / bufferSource.playbackRate.value; const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
const sliceDuration = (end - begin) * playbackDuration; const sliceDuration = (end - begin) * bufferDuration;
return { bufferSource, offset, bufferDuration, playbackDuration, sliceDuration }; return { bufferSource, offset, bufferDuration, sliceDuration };
}; };
export const loadBuffer = (url, ac, s, n = 0) => { export const loadBuffer = (url, ac, s, n = 0) => {
@@ -302,7 +301,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
} }
// vibrato // vibrato
const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, t); let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
const time = t + nudge; const time = t + nudge;
bufferSource.start(time, offset); bufferSource.start(time, offset);
@@ -325,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);
vibratoHandle?.stop(); releaseAudioNode(vibratoOscillator);
releaseAudioNode(node); releaseAudioNode(node);
releaseAudioNode(out); releaseAudioNode(out);
onended(); onended();
@@ -335,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, nodes: { source: [bufferSource], ...vibratoHandle?.nodes }, stop }; const handle = { node: out, bufferSource, stop };
// cut groups // cut groups
if (cut !== undefined) { if (cut !== undefined) {
+241 -440
View File
@@ -7,24 +7,22 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs'; import './feedbackdelay.mjs';
import './reverb.mjs'; import './reverb.mjs';
import './vowel.mjs'; import './vowel.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs'; 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,
webAudioTimeout,
} from './helpers.mjs'; } from './helpers.mjs';
import { map } from 'nanostores'; import { map } from 'nanostores';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.mjs';
import { loadBuffer } from './sampler.mjs'; import { loadBuffer } from './sampler.mjs';
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext, setAudioContext } from './audioContext.mjs';
import { SuperdoughAudioController } from './superdoughoutput.mjs'; import { SuperdoughAudioController } from './superdoughoutput.mjs';
import { resetSeenKeys } from './wavetable.mjs'; import { resetSeenKeys } from './wavetable.mjs';
@@ -186,7 +184,6 @@ 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,
@@ -194,9 +191,6 @@ let defaultDefaultValues = {
i: 1, i: 1,
velocity: 1, velocity: 1,
fft: 8, fft: 8,
tremolodepth: 1,
tremolophase: 0,
release: 0.01,
}; };
const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues }); const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues });
@@ -333,9 +327,9 @@ export function connectToDestination(input, channels) {
controller.output.connectToDestination(input, channels); controller.output.connectToDestination(input, channels);
} }
function getPhaser(begin, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) { function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
const ac = getAudioContext(); const ac = getAudioContext();
const lfo = getLfo(ac, { frequency, depth: sweep * 2, begin, end }); const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
//filters //filters
const numStages = 1; //num of filters in series const numStages = 1; //num of filters in series
@@ -365,7 +359,7 @@ export let analysers = {},
analysersData = {}; analysersData = {};
export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) { export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) {
if (!analysers[id] || analysers[id].context != getAudioContext()) { if (!analysers[id] || analysers[id].audioContext != getAudioContext()) {
// make sure this doesn't happen too often as it piles up garbage // make sure this doesn't happen too often as it piles up garbage
const analyserNode = getAudioContext().createAnalyser(); const analyserNode = getAudioContext().createAnalyser();
analyserNode.fftSize = fftSize; analyserNode.fftSize = fftSize;
@@ -406,37 +400,7 @@ function mapChannelNumbers(channels) {
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
} }
class Chain {
constructor(head) {
this.audioNodes = [head];
this.tails = [head];
}
connect(...nodes) {
nodes.forEach((node) => {
this.tails.forEach((tail) => {
tail.connect(node);
});
});
this.tails = nodes;
this.audioNodes.push(...nodes);
return this;
}
connectOne(idx, node) {
this.tails[idx].connect(node);
this.tails[idx] = node;
this.audioNodes.push(node);
return this;
}
releaseNodes() {
this.audioNodes.forEach((n) => releaseAudioNode(n));
this.audioNodes = [];
this.tails = [];
}
}
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
// mapping from main FX and numbered FX chains to nodes
const nodes = { main: {} };
// new: t is always expected to be the absolute target onset time // new: t is always expected to be the absolute target onset time
const ac = getAudioContext(); const ac = getAudioContext();
const audioController = getSuperdoughAudioController(); const audioController = getSuperdoughAudioController();
@@ -465,24 +429,49 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
} }
// destructure // destructure
let { let {
tremolo,
tremolosync,
tremolodepth = 1,
tremoloskew,
tremolophase = 0,
tremoloshape,
s = getDefaultValue('s'), s = getDefaultValue('s'),
bank, bank,
source, source,
gain = getDefaultValue('gain'),
postgain = getDefaultValue('postgain'), postgain = getDefaultValue('postgain'),
density = getDefaultValue('density'),
duckorbit, duckorbit,
duckonset, duckonset,
duckattack, duckattack,
duckdepth, duckdepth,
djf, djf,
release = getDefaultValue('release'), // filters
fanchor = getDefaultValue('fanchor'),
release = 0,
//phaser
phaserrate,
phaserdepth = getDefaultValue('phaserdepth'),
phasersweep,
phasercenter,
//
coarse,
crush,
dry, dry,
shape,
shapevol = getDefaultValue('shapevol'),
distort,
distortvol = getDefaultValue('distortvol'),
distorttype = getDefaultValue('distorttype'),
pan,
vowel,
delay = getDefaultValue('delay'), delay = getDefaultValue('delay'),
delayfeedback = getDefaultValue('delayfeedback'), delayfeedback = getDefaultValue('delayfeedback'),
delaysync = getDefaultValue('delaysync'), delaysync = getDefaultValue('delaysync'),
delaytime, delaytime,
orbit = getDefaultValue('orbit'), orbit = getDefaultValue('orbit'),
bus,
busgain = getDefaultValue('busgain'),
room, room,
roomfade, roomfade,
roomlp, roomlp,
@@ -492,10 +481,16 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
irspeed, irspeed,
irbegin, irbegin,
i = getDefaultValue('i'), i = getDefaultValue('i'),
velocity = getDefaultValue('velocity'),
analyze, // analyser wet analyze, // analyser wet
fft = getDefaultValue('fft'), // fftSize 0 - 10 fft = getDefaultValue('fft'), // fftSize 0 - 10
FX = [], compressor: compressorThreshold,
FXrelease, compressorRatio,
compressorKnee,
compressorAttack,
compressorRelease,
transient,
transsustain,
} = value; } = value;
delaytime = delaytime ?? cycleToSeconds(delaysync, cps); delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
@@ -510,13 +505,17 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth); audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth);
} }
gain = applyGainCurve(nanFallback(gain, 1));
postgain = applyGainCurve(postgain); postgain = applyGainCurve(postgain);
shapevol = applyGainCurve(shapevol);
distortvol = applyGainCurve(distortvol);
delay = applyGainCurve(delay); delay = applyGainCurve(delay);
busgain = applyGainCurve(busgain); velocity = applyGainCurve(velocity);
tremolodepth = applyGainCurve(tremolodepth);
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
const end = t + hapDuration; const end = t + hapDuration;
const fullRelease = Math.max(release, FXrelease ?? 0); const endWithRelease = end + release;
const endWithRelease = end + fullRelease;
const chainID = Math.round(Math.random() * 1000000); const chainID = Math.round(Math.random() * 1000000);
// oldest audio nodes will be destroyed if maximum polyphony is exceeded // oldest audio nodes will be destroyed if maximum polyphony is exceeded
@@ -530,6 +529,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
activeSoundSources.delete(chainID); activeSoundSources.delete(chainID);
} }
let audioNodes = [];
if (['-', '~', '_'].includes(s)) { if (['-', '~', '_'].includes(s)) {
return; return;
} }
@@ -542,27 +543,17 @@ 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.main['source'] = [sourceNode];
} else if (getSound(s)) { } else if (getSound(s)) {
const { onTrigger } = getSound(s); const { onTrigger } = getSound(s);
// We have to use onEnded because some sources (e.g. `sampler`) have const onEnded = () => {
// an internal duration which is longer than `value.duration` audioNodes.forEach((n) => releaseAudioNode(n));
const onEnded = () => activeSoundSources.delete(chainID);
webAudioTimeout( };
ac,
() => {
chain.releaseNodes();
activeSoundSources.delete(chainID);
},
0,
endWithRelease,
);
const soundHandle = await onTrigger(t, value, onEnded, cps); const soundHandle = await onTrigger(t, value, onEnded, cps);
if (soundHandle) { if (soundHandle) {
sourceNode = soundHandle.node; sourceNode = soundHandle.node;
activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC
nodes.main = { ...nodes.main, ...soundHandle.nodes };
} }
} else { } else {
throw new Error(`sound ${s} not found! Is it loaded?`); throw new Error(`sound ${s} not found! Is it loaded?`);
@@ -577,345 +568,206 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
logger('[webaudio] skip hap: still loading', ac.currentTime - t); logger('[webaudio] skip hap: still loading', ac.currentTime - t);
return; return;
} }
const chain = []; // audio nodes that will be connected to each other sequentially
chain.push(sourceNode);
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
const chain = new Chain(sourceNode); // connection manager which tracks audio nodes for releasing transient !== undefined &&
FX = [...FX, value]; // run through the FX chain and then run through all FX outside of it as well chain.push(
for (let [idx, fx] of Object.entries(FX)) { getWorklet(
const key = idx == FX.length - 1 ? 'main' : idx;
nodes[key] ??= {};
const fxNodes = nodes[key];
let {
gain = getDefaultValue('gain'),
velocity = getDefaultValue('velocity'),
shapevol = getDefaultValue('shapevol'),
distorttype = getDefaultValue('distorttype'),
distortvol = getDefaultValue('distortvol'),
tremolodepth = getDefaultValue('tremolodepth'),
phaserdepth = getDefaultValue('phaserdepth'),
delay = getDefaultValue('delay'),
delayfeedback = getDefaultValue('delayfeedback'),
delaysync = getDefaultValue('delaysync'),
delaytime,
stretch = getDefaultValue('stretch'),
i = getDefaultValue('i'),
} = fx;
gain = applyGainCurve(nanFallback(gain, 1));
shapevol = applyGainCurve(shapevol);
distortvol = applyGainCurve(distortvol);
velocity = applyGainCurve(velocity);
tremolodepth = applyGainCurve(tremolodepth);
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
if (stretch !== undefined) {
const phaseVocoder = getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch });
chain.connect(phaseVocoder);
fxNodes['stretch'] = [phaseVocoder];
}
if (fx.transient !== undefined) {
const transProcessor = getWorklet(
ac, ac,
'transient-processor', 'transient-processor',
{}, {},
{ {
processorOptions: { processorOptions: {
attack: fx.transient, attack: transient,
sustain: fx.transsustain, sustain: transsustain,
begin: t, begin: t,
end: endWithRelease, end: endWithRelease,
}, },
}, },
); ),
chain.connect(transProcessor); );
fxNodes['transient'] = transProcessor;
}
// gain stage // gain stage
const initialGain = gainNode(gain); chain.push(gainNode(gain));
fxNodes['gain'] = [initialGain];
chain.connect(initialGain);
// filter // filter
const ftype = getFilterType(value.ftype); const ftype = getFilterType(value.ftype);
const filt = (params) => createFilter(ac, t, end, params, cps, cycle); if (value.cutoff !== undefined) {
if (fx.cutoff !== undefined) { const lpMap = {
const lpMap = { frequency: 'cutoff',
frequency: 'cutoff', q: 'resonance',
q: 'resonance', attack: 'lpattack',
attack: 'lpattack', decay: 'lpdecay',
decay: 'lpdecay', sustain: 'lpsustain',
sustain: 'lpsustain', release: 'lprelease',
release: 'lprelease', env: 'lpenv',
env: 'lpenv', anchor: 'fanchor',
anchor: 'fanchor', model: 'ftype',
model: 'ftype', drive: 'drive',
drive: 'drive', rate: 'lprate',
rate: 'lprate', sync: 'lpsync',
sync: 'lpsync', depth: 'lpdepth',
depth: 'lpdepth', depthfrequency: 'lpdepthfrequency',
depthfrequency: 'lpdepthfrequency', shape: 'lpshape',
shape: 'lpshape', dcoffset: 'lpdc',
dcoffset: 'lpdc', skew: 'lpskew',
skew: 'lpskew', };
}; const lpParams = pickAndRename(value, lpMap);
const lpParams = pickAndRename(fx, lpMap); lpParams.type = 'lowpass';
lpParams.type = 'lowpass'; const lp = () => createFilter(ac, t, end, lpParams, cps, cycle);
const { filter: lpf1, lfo: lfo1 } = filt(lpParams); const { filter: lpf1, lfo: lfo1 } = lp();
fxNodes['lpf'] = [lpf1]; chain.push(lpf1);
fxNodes['lpf_lfo'] = [lfo1]; lfo1 && audioNodes.push(lfo1);
chain.connect(lpf1); if (ftype === '24db') {
lfo1 && chain.audioNodes.push(lfo1); const { filter: lpf2, lfo: lfo2 } = lp();
if (ftype === '24db') { chain.push(lpf2);
const { filter: lpf2, lfo: lfo2 } = filt(lpParams); lfo2 && audioNodes.push(lfo2);
fxNodes['lpf'].push(lpf2);
fxNodes['lpf_lfo'].push(lfo2);
chain.connect(lpf2);
lfo2 && chain.audioNodes.push(lfo2);
}
}
if (fx.hcutoff !== undefined) {
const hpMap = {
frequency: 'hcutoff',
q: 'hresonance',
attack: 'hpattack',
decay: 'hpdecay',
sustain: 'hpsustain',
release: 'hprelease',
env: 'hpenv',
anchor: 'fanchor',
model: 'ftype',
drive: 'drive',
rate: 'hprate',
sync: 'hpsync',
depth: 'hpdepth',
depthfrequency: 'hpdepthfrequency',
shape: 'hpshape',
dcoffset: 'hpdc',
skew: 'hpskew',
};
const hpParams = pickAndRename(fx, hpMap);
hpParams.type = 'highpass';
const { filter: hpf1, lfo: lfo1 } = filt(hpParams);
fxNodes['hpf'] = [hpf1];
fxNodes['hpf_lfo'] = [lfo1];
lfo1 && chain.audioNodes.push(lfo1);
chain.connect(hpf1);
if (ftype === '24db') {
const { filter: hpf2, lfo: lfo2 } = filt(hpParams);
fxNodes['hpf'].push(hpf2);
fxNodes['hpf_lfo'].push(lfo2);
chain.connect(hpf2);
lfo2 && chain.audioNodes.push(lfo2);
}
}
if (fx.bandf !== undefined) {
const bpMap = {
frequency: 'bandf',
q: 'bandq',
attack: 'bpattack',
decay: 'bpdecay',
sustain: 'bpsustain',
release: 'bprelease',
env: 'bpenv',
anchor: 'fanchor',
model: 'ftype',
drive: 'drive',
rate: 'bprate',
sync: 'bpsync',
depth: 'bpdepth',
depthfrequency: 'bpdepthfrequency',
shape: 'bpshape',
dcoffset: 'bpdc',
skew: 'bpskew',
};
const bpParams = pickAndRename(fx, bpMap);
bpParams.type = 'bandpass';
const { filter: bpf1, lfo: lfo1 } = filt(bpParams);
fxNodes['bpf'] = [bpf1];
fxNodes['bpf_lfo'] = [lfo1];
chain.connect(bpf1);
lfo1 && chain.audioNodes.push(lfo1);
if (ftype === '24db') {
const { filter: bpf2, lfo: lfo2 } = filt(bpParams);
fxNodes['bpf'].push(bpf2);
fxNodes['bpf_lfo'].push(lfo2);
chain.connect(bpf2);
lfo2 && chain.audioNodes.push(lfo2);
}
}
if (fx.vowel !== undefined) {
const vowelNode = ac.createVowelFilter(fx.vowel);
fxNodes['vowel'] = vowelNode.filters;
chain.connect(vowelNode);
}
// effects
if (fx.coarse !== undefined) {
const coarseNode = getWorklet(ac, 'coarse-processor', { coarse: fx.coarse });
fxNodes['coarse'] = [coarseNode];
chain.connect(coarseNode);
}
if (fx.crush !== undefined) {
const crushNode = getWorklet(ac, 'crush-processor', { crush: fx.crush });
fxNodes['crush'] = [crushNode];
chain.connect(crushNode);
}
if (fx.shape !== undefined) {
const shapeNode = getWorklet(ac, 'shape-processor', { shape: fx.shape, postgain: shapevol });
fxNodes['shape'] = [shapeNode];
chain.connect(shapeNode);
}
if (fx.distort !== undefined) {
const distortNode = getDistortion(fx.distort, distortvol, distorttype);
fxNodes['distort'] = [distortNode];
chain.connect(distortNode);
}
let tremolo = fx.tremolo;
if (fx.tremolosync != null) {
tremolo = cps * fx.tremolosync;
}
if (tremolo !== undefined) {
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
const gain = Math.max(1 - tremolodepth, 0);
const amGain = new GainNode(ac, { gain });
const time = cycle / cps;
const lfo = getLfo(ac, {
skew: fx.tremoloskew ?? (fx.tremoloshape != null ? 0.5 : 1),
frequency: tremolo,
depth: tremolodepth,
time,
dcoffset: 0,
shape: fx.tremoloshape,
phaseoffset: fx.tremolophase,
min: 0,
max: 1,
curve: 1.5,
begin: t,
end: endWithRelease,
});
fxNodes['tremolo'] = [lfo];
fxNodes['tremolo_gain'] = [amGain];
lfo.connect(amGain.gain);
chain.audioNodes.push(lfo);
chain.connect(amGain);
}
if (fx.compressor !== undefined) {
const compressorNode = getCompressor(
ac,
fx.compressor,
fx.compressorRatio,
fx.compressorKnee,
fx.compressorAttack,
fx.compressorRelease,
);
fxNodes['compressor'] = [compressorNode];
chain.connect(compressorNode);
}
// panning
if (fx.pan !== undefined) {
const panner = ac.createStereoPanner();
fxNodes['pan'] = [panner];
panner.pan.value = 2 * fx.pan - 1;
chain.connect(panner);
}
// phaser
if (fx.phaserrate !== undefined && phaserdepth > 0) {
const { filterChain, lfo } = getPhaser(
t,
endWithRelease,
fx.phaserrate,
phaserdepth,
fx.phasercenter,
fx.phasersweep,
);
fxNodes['phaser'] = [...filterChain];
fxNodes['phaser_lfo'] = [lfo];
filterChain.forEach((f) => chain.connect(f));
chain.audioNodes.push(lfo);
}
// delay
if (key !== 'main' && delay > 0 && delaytime > 0 && delayfeedback > 0) {
const dry = gainNode(1);
delayfeedback = clamp(delayfeedback, 0, 0.98);
const delayNode = ac.createFeedbackDelay(1, delaytime, delayfeedback);
const wetDelay = gainNode(delay);
const dryDelay = gainNode(fx.dry ?? 1);
const sum = new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
chain
.connect(dry)
.connect(dryDelay, delayNode)
.connectOne(1, wetDelay) // connect delayNode -> wetDelay
.connect(sum);
chain.audioNodes.push(delayNode.feedbackGain, delayNode.delayGain);
fxNodes['delay'] = [delayNode];
fxNodes['delay_mix'] = [wetDelay];
}
// reverb
if (key !== 'main' && fx.room > 0) {
let roomIR;
if (fx.ir !== undefined) {
let url;
let sample = getSound(fx.ir);
if (Array.isArray(sample)) {
url = sample.data.samples[fx.i % sample.data.samples.length];
} else if (typeof sample === 'object') {
url = Object.values(sample.data.samples).flat()[i % Object.values(sample.data.samples).length];
}
roomIR = await loadBuffer(url, ac, fx.ir, 0);
}
const dry = gainNode(1);
const reverbNode = ac.createReverb(
fx.roomsize,
fx.roomfade,
fx.roomlp,
fx.roomdim,
roomIR,
fx.irspeed,
fx.irbegin,
);
const wetReverb = gainNode(fx.room);
const dryReverb = gainNode(fx.dry ?? 1);
const sum = new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
chain
.connect(dry)
.connect(dryReverb, reverbNode)
.connectOne(1, wetReverb) // connect reverbNode -> wetReverb
.connect(sum);
fxNodes['room'] = [reverbNode];
fxNodes['room_mix'] = [wetReverb];
} }
} }
if (FXrelease !== undefined && FXrelease > release) { if (value.hcutoff !== undefined) {
const releaseNode = gainNode(1); const hpMap = {
releaseNode.gain.setValueAtTime(1, end + release); frequency: 'hcutoff',
releaseNode.gain.linearRampToValueAtTime(0, endWithRelease); q: 'hresonance',
chain.connect(releaseNode); attack: 'hpattack',
decay: 'hpdecay',
sustain: 'hpsustain',
release: 'hprelease',
env: 'hpenv',
anchor: 'fanchor',
model: 'ftype',
drive: 'drive',
rate: 'hprate',
sync: 'hpsync',
depth: 'hpdepth',
depthfrequency: 'hpdepthfrequency',
shape: 'hpshape',
dcoffset: 'hpdc',
skew: 'hpskew',
};
const hpParams = pickAndRename(value, hpMap);
hpParams.type = 'highpass';
const hp = () => createFilter(ac, t, end, hpParams, cps, cycle);
const { filter: hpf1, lfo: lfo1 } = hp();
chain.push(hpf1);
lfo1 && audioNodes.push(lfo1);
if (ftype === '24db') {
const { filter: hpf2, lfo: lfo2 } = hp();
chain.push(hpf2);
lfo2 && audioNodes.push(lfo2);
}
}
if (value.bandf !== undefined) {
const bpMap = {
frequency: 'bandf',
q: 'bandq',
attack: 'bpattack',
decay: 'bpdecay',
sustain: 'bpsustain',
release: 'bprelease',
env: 'bpenv',
anchor: 'fanchor',
model: 'ftype',
drive: 'drive',
rate: 'bprate',
sync: 'bpsync',
depth: 'bpdepth',
depthfrequency: 'bpdepthfrequency',
shape: 'bpshape',
dcoffset: 'bpdc',
skew: 'bpskew',
};
const bpParams = pickAndRename(value, bpMap);
bpParams.type = 'bandpass';
const bp = () => createFilter(ac, t, end, bpParams, cps, cycle);
const { filter: bpf1, lfo: lfo1 } = bp();
chain.push(bpf1);
lfo1 && audioNodes.push(lfo1);
if (ftype === '24db') {
const { filter: bpf2, lfo: lfo2 } = bp();
chain.push(bpf2);
lfo2 && audioNodes.push(lfo2);
}
}
if (vowel !== undefined) {
const vowelFilter = ac.createVowelFilter(vowel);
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 (tremolosync != null) {
tremolo = cps * tremolosync;
}
if (value.wtPosSynced != null) {
value.wtPosRate /= cps;
}
if (value.wtWarpSynced != null) {
value.wtWarpRate /= cps;
}
if (tremolo !== undefined) {
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
const gain = Math.max(1 - tremolodepth, 0);
const amGain = new GainNode(ac, { gain });
const time = cycle / cps;
const lfo = getLfo(ac, t, endWithRelease, {
skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1),
frequency: tremolo,
depth: tremolodepth,
time,
dcoffset: 0,
shape: tremoloshape,
phaseoffset: tremolophase,
min: 0,
max: 1,
curve: 1.5,
});
lfo.connect(amGain.gain);
audioNodes.push(lfo);
chain.push(amGain);
}
compressorThreshold !== undefined &&
chain.push(
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease),
);
// panning
if (pan !== undefined) {
const panner = ac.createStereoPanner();
panner.pan.value = 2 * pan - 1;
chain.push(panner);
}
// phaser
if (phaserrate !== undefined && phaserdepth > 0) {
const { filterChain, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep);
audioNodes.push(lfo);
chain.push(...filterChain);
} }
// last gain // last gain
const post = new GainNode(ac, { gain: postgain }); const post = new GainNode(ac, { gain: postgain });
nodes.main['post'] = [post]; chain.push(post);
chain.connect(post);
// delay // delay
if (delay > 0 && delaytime > 0 && delayfeedback > 0) { if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delayNode = orbitBus.getDelay(delaytime, delayfeedback, t); orbitBus.getDelay(delaytime, delayfeedback, t);
nodes.main['delay'] = [delayNode]; const send = orbitBus.sendDelay(post, delay);
const delaySend = orbitBus.sendDelay(post, delay); audioNodes.push(send);
nodes.main['delay_mix'] = [delaySend];
chain.audioNodes.push(delaySend);
} }
// reverb // reverb
if (room > 0) { if (room > 0) {
@@ -930,84 +782,33 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
} }
roomIR = await loadBuffer(url, ac, ir, 0); roomIR = await loadBuffer(url, ac, ir, 0);
} }
const roomNode = orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
nodes.main['room'] = [roomNode]; const send = orbitBus.sendReverb(post, room);
const reverbSend = orbitBus.sendReverb(post, room); audioNodes.push(send);
nodes.main['room_mix'] = [reverbSend];
chain.audioNodes.push(reverbSend);
}
if (bus != null) {
const busNode = audioController.getBus(bus);
const busSend = effectSend(post, busNode, busgain);
chain.audioNodes.push(busSend);
} }
if (djf != null) { if (djf != null) {
const djfNode = orbitBus.getDjf(djf, t); orbitBus.getDjf(djf, t);
nodes.main['djf'] = [djfNode];
} }
// analyser // analyser
if (analyze && !(ac instanceof OfflineAudioContext)) { if (analyze && !(ac instanceof OfflineAudioContext)) {
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
const analyserSend = effectSend(post, analyserNode, 1); const analyserSend = effectSend(post, analyserNode, 1);
chain.audioNodes.push(analyserSend); audioNodes.push(analyserSend);
} }
if (dry != null) { if (dry != null) {
dry = applyGainCurve(dry); dry = applyGainCurve(dry);
const dryGain = new GainNode(ac, { gain: dry }); const dryGain = new GainNode(ac, { gain: dry });
chain.connect(dryGain); chain.push(dryGain);
orbitBus.connectToOutput(dryGain); orbitBus.connectToOutput(dryGain);
} else { } else {
orbitBus.connectToOutput(post); orbitBus.connectToOutput(post);
} }
// finally, now that `nodes` is populated, set up modulators // connect chain elements together
FX.forEach((fx, idx) => { chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
const key = idx === FX.length - 1 ? 'main' : idx; audioNodes = audioNodes.concat(chain);
if (fx.lfo) {
for (const id of fx.lfo.__ids) {
const params = fx.lfo[id];
params.fxi ??= key;
const lfo = connectLFO(
id,
{
...params,
cps,
cycle,
begin: t,
end: endWithRelease,
},
nodes,
);
lfo && chain.audioNodes.push(lfo);
}
}
if (fx.env) {
for (const id of fx.env.__ids) {
const params = fx.env[id];
params.fxi ??= key;
const env = connectEnvelope(
id,
{
...params,
begin: t,
end: endWithRelease,
},
nodes,
);
env && chain.audioNodes.push(env);
}
}
if (fx.bmod) {
for (const id of fx.bmod.__ids) {
const params = fx.bmod[id];
params.fxi ??= key;
const { toCleanup } = connectBusModulator({ ...params, begin: t, end: endWithRelease }, nodes, controller);
chain.audioNodes.push(...toCleanup);
}
}
});
}; };
export const superdoughTrigger = (t, hap, ct, cps) => { export const superdoughTrigger = (t, hap, ct, cps) => {
-150
View File
@@ -1,150 +0,0 @@
/*
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: 'frequency' },
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;
}
+4 -21
View File
@@ -2,10 +2,7 @@ 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';
const hasChanged = (now, before) => now !== undefined && now !== before; let 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;
@@ -14,11 +11,10 @@ export class Orbit {
summingNode; summingNode;
djfNode; djfNode;
audioContext; audioContext;
constructor(audioContext) { constructor(audioContext) {
this.audioContext = audioContext; this.audioContext = audioContext;
this.output = getStereoNode(audioContext); this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
this.summingNode = getStereoNode(audioContext); this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
this.summingNode.connect(this.output); this.summingNode.connect(this.output);
} }
@@ -38,7 +34,6 @@ 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) {
@@ -169,7 +164,6 @@ export class SuperdoughAudioController {
audioContext; audioContext;
output; output;
nodes = {}; nodes = {};
buses = {};
constructor(audioContext) { constructor(audioContext) {
this.audioContext = audioContext; this.audioContext = audioContext;
@@ -177,14 +171,10 @@ export class SuperdoughAudioController {
} }
reset() { reset() {
Object.values(this.nodes).forEach((node) => { Array.from(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();
} }
@@ -216,11 +206,4 @@ 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];
}
} }
+23 -74
View File
@@ -1,5 +1,5 @@
import { clamp } from './util.mjs'; import { clamp } from './util.mjs';
import { getSuperdoughAudioController, registerSound, soundMap } from './superdough.mjs'; import { 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', 'one']; const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
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);
const sound = getOscillator(s, t, value, () => { let sound = getOscillator(s, t, value, () => {
releaseAudioNode(g); releaseAudioNode(g);
onended(); onended();
}); });
const { node: o, nodes, stop, triggerRelease } = sound; let { node: o, stop, triggerRelease } = sound;
const { duration } = value; const { duration } = value;
const envGain = gainNode(1); const envGain = gainNode(1);
const node = o.connect(g).connect(envGain); let 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,7 +70,6 @@ export function registerSynthSounds() {
stop(envEnd); stop(envEnd);
return { return {
node, node,
nodes,
stop: (endTime) => { stop: (endTime) => {
stop(endTime); stop(endTime);
}, },
@@ -140,7 +139,6 @@ export function registerSynthSounds() {
return { return {
node, node,
nodes: { source: [o] },
stop: (endTime) => { stop: (endTime) => {
o.stop(endTime); o.stop(endTime);
}, },
@@ -185,8 +183,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 vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin); const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
const fmHandle = applyFM(o.parameters.get('frequency'), value, begin); const fm = applyFM(o.parameters.get('frequency'), value, begin);
let envGain = gainNode(1); let envGain = gainNode(1);
envGain = o.connect(envGain); envGain = o.connect(envGain);
@@ -197,8 +195,8 @@ export function registerSynthSounds() {
() => { () => {
releaseAudioNode(o); releaseAudioNode(o);
onended(); onended();
fmHandle?.stop(); fm?.stop();
vibratoHandle?.stop(); vibratoOscillator?.stop();
}, },
begin, begin,
end, end,
@@ -206,7 +204,6 @@ 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);
}, },
@@ -282,7 +279,6 @@ export function registerSynthSounds() {
return { return {
node: envGain, node: envGain,
source: o,
stop: (time) => { stop: (time) => {
timeoutNode.stop(time); timeoutNode.stop(time);
}, },
@@ -333,25 +329,25 @@ export function registerSynthSounds() {
); );
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin); const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
const fmHandle = applyFM(o.parameters.get('frequency'), value, begin); const fm = 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 pw_lfo; let lfo;
if (pwsweep != 0) { if (pwsweep != 0) {
pw_lfo = getLfo(ac, { frequency: pwrate, depth: pwsweep, begin, end }); lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep });
pw_lfo.connect(o.parameters.get('pulsewidth')); lfo.connect(o.parameters.get('pulsewidth'));
} }
let timeoutNode = webAudioTimeout( let timeoutNode = webAudioTimeout(
ac, ac,
() => { () => {
releaseAudioNode(o); releaseAudioNode(o);
releaseAudioNode(pw_lfo); releaseAudioNode(lfo);
onended(); onended();
fmHandle?.stop(); fm?.stop();
vibratoHandle?.stop(); vibratoOscillator?.stop();
}, },
begin, begin,
end, end,
@@ -359,7 +355,6 @@ 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);
}, },
@@ -368,41 +363,6 @@ 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,
@@ -440,7 +400,6 @@ export function registerSynthSounds() {
stop(envEnd); stop(envEnd);
return { return {
node, node,
nodes: { source: [o] },
stop: (endTime) => { stop: (endTime) => {
stop(endTime); stop(endTime);
}, },
@@ -508,17 +467,8 @@ export function getOscillator(s, t, value, onended) {
s = 'triangle'; s = 'triangle';
} }
s = s === 'user' && !partials ? 'triangle' : s; s = s === 'user' && !partials ? 'triangle' : s;
if (s === 'one') { // If no partials are given, use stock waveforms
// Constant 1 oscillator (used for modulation) if (!partials || partials?.length === 0 || s === 'sine') {
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';
} }
@@ -529,11 +479,11 @@ export function getOscillator(s, t, value, onended) {
// set frequency // set frequency
o.frequency.value = getFrequencyFromValue(value); o.frequency.value = getFrequencyFromValue(value);
const vibratoHandle = getVibratoOscillator(o.detune, value, t); let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
// pitch envelope // pitch envelope
getPitchEnvelope(o.detune, value, t, t + duration); getPitchEnvelope(o.detune, value, t, t + duration);
const fmHandle = applyFM(o.frequency, value, t); const fmModulator = applyFM(o.frequency, value, t);
let noiseMix; let noiseMix;
if (noise) { if (noise) {
@@ -550,10 +500,9 @@ 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) => {
fmHandle.stop(time); fmModulator.stop(time);
vibratoHandle?.stop(time); vibratoOscillator?.stop(time);
noiseMix?.stop(time); noiseMix?.stop(time);
o.stop(time); o.stop(time);
}, },
+6 -11
View File
@@ -1,5 +1,3 @@
import { releaseAudioNode } from './helpers.mjs';
// credits to webdirt: https://github.com/dktr0/WebDirt/blob/41342e81d6ad694a2310d491fef7b7e8b0929efe/js-src/Graph.js#L597 // credits to webdirt: https://github.com/dktr0/WebDirt/blob/41342e81d6ad694a2310d491fef7b7e8b0929efe/js-src/Graph.js#L597
export var vowelFormant = { export var vowelFormant = {
a: { freqs: [660, 1120, 2750, 3000, 3350], gains: [1, 0.5012, 0.0708, 0.0631, 0.0126], qs: [80, 90, 120, 130, 140] }, a: { freqs: [660, 1120, 2750, 3000, 3350], gains: [1, 0.5012, 0.0708, 0.0631, 0.0126], qs: [80, 90, 120, 130, 140] },
@@ -48,8 +46,7 @@ if (typeof GainNode !== 'undefined') {
} }
const { gains, qs, freqs } = vowelFormant[letter]; const { gains, qs, freqs } = vowelFormant[letter];
this.makeupGain = ac.createGain(); this.makeupGain = ac.createGain();
this.filters = []; this.audioNodes = [];
this.gains = [];
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
const gain = ac.createGain(); const gain = ac.createGain();
gain.gain.value = gains[i]; gain.gain.value = gains[i];
@@ -59,9 +56,9 @@ if (typeof GainNode !== 'undefined') {
filter.frequency.value = freqs[i]; filter.frequency.value = freqs[i];
super.connect(filter); super.connect(filter);
filter.connect(gain); filter.connect(gain);
this.filters.push(filter); this.audioNodes.push(filter);
gain.connect(this.makeupGain); gain.connect(this.makeupGain);
this.gains.push(gain); this.audioNodes.push(gain);
} }
this.makeupGain.gain.value = 8; // how much makeup gain to add? this.makeupGain.gain.value = 8; // how much makeup gain to add?
return this; return this;
@@ -70,13 +67,11 @@ if (typeof GainNode !== 'undefined') {
this.makeupGain.connect(target); this.makeupGain.connect(target);
} }
disconnect() { disconnect() {
releaseAudioNode(this.makeupGain); this.makeupGain.disconnect();
this.filters.forEach(releaseAudioNode); this.audioNodes.forEach((n) => n.disconnect());
this.gains.forEach(releaseAudioNode);
super.disconnect(); super.disconnect();
this.makeupGain = null; this.makeupGain = null;
this.filters = null; this.audioNodes = null;
this.gains = null;
} }
} }
+5 -14
View File
@@ -314,28 +314,19 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
dcoffset: value.warpdc ?? 0, dcoffset: value.warpdc ?? 0,
}, },
); );
const vibratoHandle = getVibratoOscillator(source.parameters.get('detune'), value, t); const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
const fmHandle = applyFM(source.parameters.get('frequency'), value, t); const fm = 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 = { const handle = { node, source };
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);
vibratoHandle?.stop(); releaseAudioNode(vibratoOscillator);
fmHandle?.stop(); fm?.stop();
releaseAudioNode(wtPosModulators); releaseAudioNode(wtPosModulators);
releaseAudioNode(wtWarpModulators); releaseAudioNode(wtWarpModulators);
onended(); onended();
+8 -20
View File
@@ -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: -1e9 }, { name: 'min', defaultValue: 0 },
{ name: 'max', defaultValue: 1e9 }, { name: 'max', defaultValue: 1 },
]; ];
} }
@@ -143,8 +143,7 @@ class LFOProcessor extends AudioWorkletProcessor {
process(_inputs, outputs, parameters) { process(_inputs, outputs, parameters) {
const begin = parameters['begin'][0]; const begin = parameters['begin'][0];
const end = parameters['end'][0]; if (currentTime >= parameters.end[0]) {
if (currentTime >= end) {
return false; return false;
} }
if (currentTime <= begin) { if (currentTime <= begin) {
@@ -162,7 +161,6 @@ 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]];
@@ -969,9 +967,7 @@ 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: 'depth', defaultValue: 1 }, { name: 'peak', 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 },
]; ];
} }
@@ -1013,15 +1009,9 @@ 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
@@ -1039,9 +1029,7 @@ 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 depth = pv(params.depth, i); const peak = pv(params.peak, 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 },
@@ -1055,7 +1043,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] = clamp(this.val * depth, min, max); out[i] = this.val * peak;
} }
return true; return true;
} }
-1
View File
@@ -90,7 +90,6 @@ export function registerZZFXSounds() {
}); });
return { return {
node: o, node: o,
nodes: { source: [o] },
stop: () => {}, stop: () => {},
}; };
}, },
@@ -42,18 +42,4 @@ describe('transpiler', () => {
[12, 14], [12, 14],
]); ]);
}); });
it('allows disabling mini', () => {
const code = `/* mini-off */
const randPrefix = Math.random() > 0.5 ? "b" : "s";
const drumPat = \`\${randPrefix}d\`;
// mini-on
s(drumPat).lpf("5000 10000") // make sure mini still runs;
`;
const { output, miniLocations } = transpiler(code, { ...simple, emitMiniLocations: true });
expect(output).not.toContain("m('b'");
expect(output).not.toContain("m('s'");
const cutoffIdx = code.indexOf('5000 10000');
expect(miniLocations).toHaveLength(2);
expect(miniLocations[0][0]).toEqual(cutoffIdx);
});
}); });
-38
View File
@@ -21,15 +21,12 @@ export function registerLanguage(type, config) {
export function transpiler(input, options = {}) { export function transpiler(input, options = {}) {
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
const comments = [];
let ast = parse(input, { let ast = parse(input, {
ecmaVersion: 2022, ecmaVersion: 2022,
allowAwaitOutsideFunction: true, allowAwaitOutsideFunction: true,
locations: true, locations: true,
onComment: comments,
}); });
const miniDisableRanges = findMiniDisableRanges(comments, input.length);
let miniLocations = []; let miniLocations = [];
const collectMiniLocations = (value, node) => { const collectMiniLocations = (value, node) => {
const minilang = languages.get('minilang'); const minilang = languages.get('minilang');
@@ -69,9 +66,6 @@ export function transpiler(input, options = {}) {
return this.replace(tidalWithLocation(raw, offset)); return this.replace(tidalWithLocation(raw, offset));
} }
if (isBackTickString(node, parent)) { if (isBackTickString(node, parent)) {
if (isMiniDisabled(node.start, miniDisableRanges)) {
return;
}
const { quasis } = node; const { quasis } = node;
const { raw } = quasis[0].value; const { raw } = quasis[0].value;
this.skip(); this.skip();
@@ -79,9 +73,6 @@ export function transpiler(input, options = {}) {
return this.replace(miniWithLocation(raw, node)); return this.replace(miniWithLocation(raw, node));
} }
if (isStringWithDoubleQuotes(node)) { if (isStringWithDoubleQuotes(node)) {
if (isMiniDisabled(node.start, miniDisableRanges)) {
return;
}
const { value } = node; const { value } = node;
this.skip(); this.skip();
emitMiniLocations && collectMiniLocations(value, node); emitMiniLocations && collectMiniLocations(value, node);
@@ -336,32 +327,3 @@ function languageWithLocation(name, value, offset) {
optional: false, optional: false,
}; };
} }
function findMiniDisableRanges(comments, codeEnd) {
const ranges = [];
const stack = []; // used to track on/off pairs
for (const comment of comments) {
const value = comment.value.trim();
if (value.startsWith('mini-off')) {
stack.push(comment.start);
} else if (value.startsWith('mini-on')) {
const start = stack.pop();
ranges.push([start, comment.end]);
}
}
while (stack.length) {
// If no closing mini-on is found, just turn it off until `codeEnd`
const start = stack.pop();
ranges.push([start, codeEnd]);
}
return ranges;
}
function isMiniDisabled(offset, miniDisableRanges) {
for (const [start, end] of miniDisableRanges) {
if (offset >= start && offset < end) {
return true;
}
}
return false;
}
-136
View File
@@ -565,52 +565,6 @@ exports[`runs examples > example "_euclidRot" example index 20 1`] = `
] ]
`; `;
exports[`runs examples > example "FX" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 1/8 → 1/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 1/4 → 3/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 3/8 → 1/2 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 1/2 → 5/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 5/8 → 3/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 3/4 → 7/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 7/8 → 1/1 | s:lt decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 1/1 → 9/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 9/8 → 5/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 5/4 → 11/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 11/8 → 3/2 | s:oh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 3/2 → 13/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 13/8 → 7/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 7/4 → 15/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 15/8 → 2/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 2/1 → 17/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 17/8 → 9/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 9/4 → 19/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 19/8 → 5/2 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 5/2 → 21/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 21/8 → 11/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 11/4 → 23/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 23/8 → 3/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 3/1 → 25/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 25/8 → 13/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 13/4 → 27/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 27/8 → 7/2 | s:lt decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 7/2 → 29/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 29/8 → 15/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 15/4 → 31/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
"[ 31/8 → 4/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
]
`;
exports[`runs examples > example "FX" example index 1 1`] = `
[
"[ 0/1 → 1/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
"[ 1/1 → 2/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
"[ 2/1 → 3/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
"[ 3/1 → 4/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
]
`;
exports[`runs examples > example "accelerate" example index 0 1`] = ` exports[`runs examples > example "accelerate" example index 0 1`] = `
[ [
"[ 0/1 → 2/1 | s:sax accelerate:0 ]", "[ 0/1 → 2/1 | s:sax accelerate:0 ]",
@@ -3849,51 +3803,6 @@ 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 ]",
@@ -6204,51 +6113,6 @@ 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 ]",
-1
View File
@@ -20,7 +20,6 @@ const skippedExamples = [
'accelerationX', 'accelerationX',
'defaultmidimap', 'defaultmidimap',
'midimaps', 'midimaps',
'bmod',
]; ];
describe('runs examples', () => { describe('runs examples', () => {
+1 -1
View File
@@ -4,7 +4,7 @@ import { Pattern, noteToMidi, evaluate, stack } from '@strudel/core';
// import doughUrl from 'dough-synth?url'; // import doughUrl from 'dough-synth?url';
import { transpiler } from '@strudel/transpiler'; import { transpiler } from '@strudel/transpiler';
//const doughBaseUrl = doughUrl.split('/').slice(0, -1).join('/') + '/'; //const doughBaseUrl = doughUrl.split('/').slice(0, -1).join('/') + '/';
const doughBaseUrl = 'https://unpkg.com/dough-synth@0.1.9/'; const doughBaseUrl = 'https://unpkg.com/dough-synth@0.2.0/';
Object.assign(globalThis, { doughsamples }); Object.assign(globalThis, { doughsamples });
+16 -18
View File
@@ -11,9 +11,9 @@ import { JsDoc } from '../../docs/JsDoc';
You can optionally add some music metadata in your Strudel code, by using tags in code comments: You can optionally add some music metadata in your Strudel code, by using tags in code comments:
```js ```js
// @title My Cool Song // @title Hey Hoo
// @by John Doe // @by Sam Tagada
// @license CC-BY-SA-4.0 // @license CC BY-NC-SA
``` ```
Like other comments, those are ignored by Strudel, but it can be used by other tools to retrieve some information about the music. Like other comments, those are ignored by Strudel, but it can be used by other tools to retrieve some information about the music.
@@ -24,22 +24,22 @@ You can also use comment blocks:
```js ```js
/* /*
@title My Cool Song @title Hey Hoo
@by John Doe @by Sam Tagada
@license CC-BY-SA-4.0 @license CC BY-NC-SA
*/ */
``` ```
Or define multiple tags in one line: Or define multiple tags in one line:
```js ```js
// @title My Cool Song @by John Doe @license CC-BY-SA-4.0 // @title Hey Hoo @by Sam Tagada @license CC BY-NC-SA
``` ```
The `title` tag has an alternative syntax using quotes (must be defined at the very begining): The `title` tag has an alternative syntax using quotes (must be defined at the very begining):
```js ```js
// "My Cool Song" @by John Doe // "Hey Hoo" @by Sam Tagada
``` ```
## Tags list ## Tags list
@@ -48,22 +48,20 @@ Available tags are:
- `@title`: music title - `@title`: music title
- `@by`: music author(s), separated by comma, eventually followed with a link in `<>` (ex: `@by John Doe <https://example.com>`) - `@by`: music author(s), separated by comma, eventually followed with a link in `<>` (ex: `@by John Doe <https://example.com>`)
- `@license`: music license(s), separated by comma. Each license should be specified by using the correct identifier in the [https://spdx.org/licenses/](SPDX License List). Example: CC-BY-SA-4.0. Unsure? [Choose a Creative Commons license here](https://creativecommons.org/choose/). - `@license`: music license(s), e.g. CC BY-NC-SA. Unsure? [Choose a creative commons license here](https://creativecommons.org/choose/)
- `@details`: some additional information about the music - `@details`: some additional information about the music
- `@url`: web page(s) related to the music (git repository, Soundcloud link, etc.) - `@url`: web page(s) related to the music (git repo, soundcloud link, etc.)
- `@genre`: music genre(s) (pop, jazz, etc.) - `@genre`: music genre(s) (pop, jazz, etc)
- `@album`: music album name - `@album`: music album name
Note to tool authors: _Never_ trust that a song has filled those fields with syntactically correct values; make sure your software is robust enough it doesn't break if it encounters bad values
## Multiple values ## Multiple values
Some of them accepts several values, using the comma or new line separator, or duplicating the tag: Some of them accepts several values, using the comma or new line separator, or duplicating the tag:
```js ```js
/* /*
@by John Doe @by Sam Tagada
Jane Doe Jimmy
@genre pop, jazz @genre pop, jazz
@url https://example.com @url https://example.com
@url https://example.org @url https://example.org
@@ -74,11 +72,11 @@ You can also add optional prefixes and use tags where you want:
```js ```js
/* /*
song @by John Doe song @by Sam Tagada
samples @by Jane Doe samples @by Jimmy
*/ */
... ...
note("a3 c#4 e4 a4") // @by Sandy Sue note("a3 c#4 e4 a4") // @by Sandy
``` ```
## Multiline ## Multiline
+1 -1
View File
@@ -41,7 +41,7 @@ note("c3 [e3 g3]*2")
is transpiled to: is transpiled to:
```strudel ```strudel
note(m('c3 [e3 g3]*2', 5)) note(m('c3 [e3 g3]', 5))
``` ```
Here, the string is wrapped in `m`, which will create a pattern from a mini-notation string. As the second parameter, it gets passed source code location of the string, which enables highlighting active events later. Here, the string is wrapped in `m`, which will create a pattern from a mini-notation string. As the second parameter, it gets passed source code location of the string, which enables highlighting active events later.
@@ -35,24 +35,3 @@ Troubleshooting
- If :w logs but evaluation doesn't apply, ensure Vim keybindings are active and try again. You can also use Ctrl+Enter as a fallback. - If :w logs but evaluation doesn't apply, ensure Vim keybindings are active and try again. You can also use Ctrl+Enter as a fallback.
- For :q / gc, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again. - For :q / gc, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again.
## Adding custom keybindings
To add custom keybindings the `Vim` object can be used (either from within a pattern or in a prebake script)
Example:
```javascript
// Map 'jk' to Escape in normal mode
Vim.map('jk', '<Esc>', 'insert');
// Map 'U' to :w (Evaluate the current code)
Vim.map('U', ':w<CR>', 'normal');
// Map 'Q' to :q — Stop/pause playback
Vim.map('Q', ':q<CR>', 'normal');
// Map 'J' to find next '$' (jump to next label)
Vim.map('J', '/\\$<CR>', 'normal');
// Map 'K' to find previous '$' (jump to previous label)
Vim.map('K', '?\\$<CR>', 'normal');
```
For more information on how to use the `Vim` object see [CodeMirror Vim](https://github.com/replit/codemirror-vim)
+1 -1
View File
@@ -377,4 +377,4 @@ insect [crow metal] - -,
punchcard punchcard
/> />
Now that we know the basics of how to make beats, let's look at how we can play [notes](/workshop/first-notes). Now that we know the basics of how to make beats, let's look at how we can play [notes](/workshop/first-notes)