mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 05:05:26 -04:00
Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25dbfe4c7d | |||
| 0a586eddb3 | |||
| b6d81ba2a0 | |||
| 96626fce25 | |||
| c74707e804 | |||
| 0174e08639 | |||
| af855a0c77 | |||
| 604a1b770c | |||
| 2aa978ca5b | |||
| 0571a8dc7d | |||
| a8a919f581 | |||
| bf21c4cf02 | |||
| 741ff3f3f0 | |||
| 4476921069 | |||
| 8c5e4a5780 | |||
| 34ae1bb305 | |||
| 0cf11adc2c | |||
| 7a5020aa71 | |||
| a205f7d873 | |||
| 0e9c50bf0a | |||
| e837c516c5 | |||
| 3b2e9fa0b4 | |||
| 6a47139e0d | |||
| f7f8c56c0a | |||
| c7e88e1ff2 | |||
| f6a43f1b10 | |||
| a3b183d304 | |||
| f7175c0505 | |||
| e26040d154 | |||
| 17a1b75e5b | |||
| 7df9688bb9 | |||
| d557bb59f0 | |||
| 679e81eb0f | |||
| 37dd834398 | |||
| 959e8ac7bc | |||
| 16524ca805 | |||
| b4b0194455 | |||
| fd472a64d9 | |||
| 58094973b0 | |||
| db49b37780 | |||
| bf243c04b9 | |||
| bc05cffdbc | |||
| 7b1e02e8cf | |||
| 257f09da4f | |||
| ecd58fa501 | |||
| c4ece4932e | |||
| 5c9855bf12 | |||
| e54b9c3a32 | |||
| 6afe0dc714 | |||
| ebaf7624f9 | |||
| 882bcc513f | |||
| aec33710b7 | |||
| d41ebf190b | |||
| 7953fa61e4 | |||
| 6051b922c0 | |||
| 367e07da0c | |||
| 0edede106a | |||
| b6264a1513 | |||
| 680b7d78ec | |||
| e79b4b8ccb | |||
| 98367868ac | |||
| 616bc64e70 | |||
| c0f4f47150 | |||
| b4496fcc24 | |||
| 48f6a41683 | |||
| 7bce739068 | |||
| 06f9c2dfb5 | |||
| 30fe5b47e3 | |||
| 1823993e7c | |||
| 1935195e9a | |||
| 300a1bbfd5 | |||
| 2f43dcab0c | |||
| 125eece223 | |||
| 53e49952d8 | |||
| e286ab2830 | |||
| e4e2d60b28 | |||
| 99bec835f8 | |||
| d349d2a0cd | |||
| c7a2c2bb9c | |||
| 5a51b4ec71 | |||
| 3922defab9 | |||
| a9752fb2a4 | |||
| 0a2f2eaa3d | |||
| 0de212c12f | |||
| 18b3c66eb8 | |||
| 63de46ae96 | |||
| c598059dc7 | |||
| dec037bed5 | |||
| 29f7f5c1f8 | |||
| bae5571df0 | |||
| 4b08dd8447 | |||
| 11e3da0552 | |||
| ef2ee0969a | |||
| 12f9af227c | |||
| dc5f6827f9 | |||
| 8baecf78b9 | |||
| 415b6d5f1d | |||
| 4169b764c3 | |||
| e7d1613bfb | |||
| 45aa1f5384 | |||
| 3a17aeabf3 | |||
| 2aeba2e53f | |||
| a79491dd16 | |||
| 53c101005c | |||
| 82ca77be9d | |||
| 173e359a65 | |||
| ad43259353 | |||
| 32924b3fcf | |||
| 4c99f4866b | |||
| d50af71ba1 | |||
| 0664f90178 | |||
| 4321814d36 | |||
| 6610713018 | |||
| 600ab0a83e | |||
| 8529516c66 | |||
| 0e3840aff4 | |||
| a929b36770 | |||
| 2794541dad | |||
| 3e118ce0a8 | |||
| c3ad0d1789 | |||
| 19b710c401 | |||
| 0dbca05de0 | |||
| f8796d039f | |||
| 442f487973 | |||
| 7ccdefe1c6 | |||
| b73f624058 | |||
| e4466082eb | |||
| 29b6729246 | |||
| b3df7f438f | |||
| 3b1c75d3de | |||
| 48407c309c | |||
| 23aa8ef509 | |||
| 945b325336 | |||
| 502f0b5c23 | |||
| 2ccbb0596d | |||
| 862b88b6c4 | |||
| cd7865a66f | |||
| 9c8e39d83d | |||
| 6a9739bed5 | |||
| d8f0d70908 | |||
| e25eab11ad | |||
| efb337677c | |||
| 4a70ac5a99 |
@@ -1,14 +1,17 @@
|
||||
# strudel
|
||||
|
||||
Live coding patterns on the web
|
||||
https://strudel.cc/
|
||||
|
||||
|
||||
- Try it here: <https://strudel.cc>
|
||||
- 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>
|
||||
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
||||
- 2 Years of Strudel Blog Post: <https://strudel.cc/blog/#year-2>
|
||||
|
||||
|
||||
## Running Locally
|
||||
|
||||
After cloning the project, you can run the REPL locally:
|
||||
|
||||
@@ -27,7 +27,7 @@ import { updateWidgets, widgetPlugin } from './widget.mjs';
|
||||
|
||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||
|
||||
const extensions = {
|
||||
export const extensions = {
|
||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
|
||||
isBracketClosingEnabled: (on) => (on ? closeBrackets() : []),
|
||||
@@ -48,7 +48,7 @@ const extensions = {
|
||||
]
|
||||
: [],
|
||||
};
|
||||
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||
export const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||
|
||||
export const defaultSettings = {
|
||||
keybindings: 'codemirror',
|
||||
@@ -411,7 +411,7 @@ export class StrudelMirror {
|
||||
}
|
||||
}
|
||||
|
||||
function parseBooleans(value) {
|
||||
export function parseBooleans(value) {
|
||||
return { true: true, false: false }[value] ?? value;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from './flash.mjs';
|
||||
export * from './slider.mjs';
|
||||
export * from './themes.mjs';
|
||||
export * from './widget.mjs';
|
||||
export { Vim } from './keybindings.mjs';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import { Prec } from '@codemirror/state';
|
||||
import { Prec, EditorState } from '@codemirror/state';
|
||||
import { keymap, ViewPlugin } from '@codemirror/view';
|
||||
// import { searchKeymap } from '@codemirror/search';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
@@ -131,7 +131,13 @@ const keymaps = {
|
||||
vscode: vscodeExtension,
|
||||
};
|
||||
|
||||
export { Vim } from '@replit/codemirror-vim';
|
||||
|
||||
export function keybindings(name) {
|
||||
const active = keymaps[name];
|
||||
return [active ? Prec.high(active()) : []];
|
||||
const extensions = active ? [Prec.high(active())] : [];
|
||||
if (name === 'vim') {
|
||||
extensions.push(EditorState.allowMultipleSelections.of(true));
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
+276
-5
@@ -4,7 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Pattern, register, reify } from './pattern.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { Pattern, pure, register, reify } from './pattern.mjs';
|
||||
|
||||
export function createParam(names) {
|
||||
let isMulti = Array.isArray(names);
|
||||
@@ -876,7 +877,7 @@ export const { coarse } = registerControl('coarse');
|
||||
* note("d d d# d".fast(4)).s("supersaw").tremolo("<3 2 100> ").tremoloskew("<.5>")
|
||||
*
|
||||
*/
|
||||
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
|
||||
export const { tremolo, trem } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
|
||||
|
||||
/**
|
||||
* Modulate the amplitude of a sound with a continuous waveform
|
||||
@@ -2044,6 +2045,28 @@ export const { octave, oct } = registerControl('octave', 'oct');
|
||||
* )
|
||||
*/
|
||||
export const { orbit } = registerControl('orbit', 'o');
|
||||
|
||||
/**
|
||||
* A `bus` is a send which can be used for mixing patterns. It combines with..
|
||||
* s("bus") to play that bus through another pattern (for, say, applying non-linear
|
||||
* effects like distortion to multiple signals)
|
||||
*
|
||||
* otherPat.bmod(..) (to modulate another pattern with the bus)
|
||||
*
|
||||
* @name bus
|
||||
* @param {number | Pattern} number
|
||||
*/
|
||||
export const { bus } = registerControl('bus');
|
||||
|
||||
/**
|
||||
* Postgain multiplier prior to sending the signal to the audio bus.
|
||||
*
|
||||
* @name busgain
|
||||
* @synonyms bgain
|
||||
* @param {number | Pattern} number
|
||||
*/
|
||||
export const { busgain, bgain } = registerControl('busgain', 'bgain');
|
||||
|
||||
// TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that
|
||||
export const { overgain } = registerControl('overgain');
|
||||
// TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
|
||||
@@ -2088,8 +2111,7 @@ export const { panorient } = registerControl('panorient');
|
||||
// ['pitch2'],
|
||||
// ['pitch3'],
|
||||
// ['portamento'],
|
||||
// TODO: LFO rate see https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
||||
export const { rate } = registerControl('rate');
|
||||
|
||||
// TODO: slide param for certain synths
|
||||
export const { slide } = registerControl('slide');
|
||||
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
||||
@@ -2515,7 +2537,6 @@ export const { curve } = registerControl('curve');
|
||||
export const { deltaSlide } = registerControl('deltaSlide');
|
||||
export const { pitchJump } = registerControl('pitchJump');
|
||||
export const { pitchJumpTime } = registerControl('pitchJumpTime');
|
||||
export const { lfo, repeatTime } = registerControl('lfo', 'repeatTime');
|
||||
// noise on the frequency or as bubo calls it "frequency fog" :)
|
||||
export const { znoise } = registerControl('znoise');
|
||||
export const { zmod } = registerControl('zmod');
|
||||
@@ -2792,6 +2813,254 @@ export const scrub = register(
|
||||
false,
|
||||
);
|
||||
|
||||
const subControlAliases = new Map();
|
||||
const registerSubControl = (control, subControl, ...aliases) => {
|
||||
const aliasMap = subControlAliases.get(control) ?? new Map();
|
||||
const allKeys = new Set([subControl, ...aliases]);
|
||||
for (const alias of allKeys) {
|
||||
aliasMap.set(String(alias).toLowerCase(), subControl);
|
||||
}
|
||||
subControlAliases.set(control, aliasMap);
|
||||
};
|
||||
|
||||
const registerSubControls = (control, subControlAliases = []) => {
|
||||
for (const [subControl, ...aliases] of subControlAliases) {
|
||||
registerSubControl(control, subControl, ...aliases);
|
||||
}
|
||||
};
|
||||
|
||||
const getMainSubcontrolName = (control, subKey) => {
|
||||
const aliasMap = subControlAliases.get(control);
|
||||
if (!aliasMap) return subKey;
|
||||
return aliasMap.get(String(subKey).toLowerCase()) ?? subKey;
|
||||
};
|
||||
|
||||
registerSubControls('lfo', [
|
||||
['control', 'c'],
|
||||
['subControl', 'sc'],
|
||||
['rate', 'r'],
|
||||
['depth', 'dep', 'dr'],
|
||||
['depthabs', 'da'],
|
||||
['dcoffset', 'dc'],
|
||||
['shape', 'sh'],
|
||||
['skew', 'sk'],
|
||||
['curve', '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
|
||||
* and sustains
|
||||
@@ -2805,3 +3074,5 @@ export const scrub = register(
|
||||
* s("hh*16").bank("tr909").transient("<-1:1 1:-1>")
|
||||
*/
|
||||
export const { transient } = registerControl(['transient', 'transsustain']);
|
||||
|
||||
export const { FXrelease, FXrel, FXr, fxr } = registerControl('FXrelease', 'FXrel', 'FXr', 'fxr');
|
||||
|
||||
@@ -3586,6 +3586,20 @@ export const morph = (frompat, topat, bypat) => {
|
||||
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
||||
};
|
||||
|
||||
const _distortWithAlg = function (name) {
|
||||
const func = function (args, pat) {
|
||||
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
|
||||
if (!pat) {
|
||||
return pure({}).distort(argsPat);
|
||||
}
|
||||
return pat.distort(argsPat);
|
||||
};
|
||||
Pattern.prototype[name] = function (args) {
|
||||
return func(args, this);
|
||||
};
|
||||
return func;
|
||||
};
|
||||
|
||||
/**
|
||||
* Soft-clipping distortion
|
||||
*
|
||||
@@ -3594,6 +3608,8 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
export const soft = _distortWithAlg('soft');
|
||||
|
||||
/**
|
||||
* Hard-clipping distortion
|
||||
*
|
||||
@@ -3602,6 +3618,8 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
export const hard = _distortWithAlg('hard');
|
||||
|
||||
/**
|
||||
* Cubic polynomial distortion
|
||||
*
|
||||
@@ -3610,6 +3628,8 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
export const cubic = _distortWithAlg('cubic');
|
||||
|
||||
/**
|
||||
* Diode-emulating distortion
|
||||
*
|
||||
@@ -3618,6 +3638,8 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
export const diode = _distortWithAlg('diode');
|
||||
|
||||
/**
|
||||
* Asymmetrical diode distortion
|
||||
*
|
||||
@@ -3626,6 +3648,8 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
export const asym = _distortWithAlg('asym');
|
||||
|
||||
/**
|
||||
* Wavefolding distortion
|
||||
*
|
||||
@@ -3634,6 +3658,8 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
export const fold = _distortWithAlg('fold');
|
||||
|
||||
/**
|
||||
* Wavefolding distortion composed with sinusoid
|
||||
*
|
||||
@@ -3642,6 +3668,8 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
export const sinefold = _distortWithAlg('sinefold');
|
||||
|
||||
/**
|
||||
* Distortion via Chebyshev polynomials
|
||||
*
|
||||
@@ -3650,14 +3678,7 @@ export const morph = (frompat, topat, bypat) => {
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev'];
|
||||
for (const name of distAlgoNames) {
|
||||
// Add aliases for distortion algorithms
|
||||
Pattern.prototype[name] = function (args) {
|
||||
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
|
||||
return this.distort(argsPat);
|
||||
};
|
||||
}
|
||||
export const chebyshev = _distortWithAlg('chebyshev');
|
||||
|
||||
/**
|
||||
* Turns a list of patterns into a single pattern which outputs list-values
|
||||
@@ -3721,3 +3742,35 @@ Pattern.prototype.phases = function (list) {
|
||||
export const phases = (list) => {
|
||||
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));
|
||||
};
|
||||
|
||||
@@ -46,4 +46,4 @@ all(osc)
|
||||
|
||||
[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D)
|
||||
|
||||
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
|
||||
You can read more about [how to use Superdirt with Strudel](https://strudel.cc/learn/input-output/#oscsuperdirtstrudeldirt) in the tutorial.
|
||||
|
||||
@@ -68,7 +68,7 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
|
||||
if (!(name in writeMessagers)) {
|
||||
getWriter(name, br);
|
||||
}
|
||||
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
const onTrigger = (hap, currentTime, _cps, targetTime) => {
|
||||
var message = '';
|
||||
var chk = 0;
|
||||
if (typeof hap.value === 'object') {
|
||||
|
||||
@@ -166,7 +166,7 @@ export function registerSoundfonts() {
|
||||
let envEnd = holdEnd + release + 0.01;
|
||||
|
||||
// vibrato
|
||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, time);
|
||||
const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, time);
|
||||
// pitch envelope
|
||||
getPitchEnvelope(bufferSource.detune, value, time, holdEnd);
|
||||
|
||||
@@ -174,10 +174,10 @@ export function registerSoundfonts() {
|
||||
const stop = (releaseTime) => {};
|
||||
onceEnded(bufferSource, () => {
|
||||
releaseAudioNode(bufferSource);
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
vibratoHandle?.stop();
|
||||
onended();
|
||||
});
|
||||
return { node, stop };
|
||||
return { node, stop, nodes: { source: [bufferSource], ...vibratoHandle?.nodes } };
|
||||
},
|
||||
{ type: 'soundfont', prebake: true, fonts },
|
||||
);
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/*
|
||||
audioContext.mjs - Audio Context manager
|
||||
|
||||
Sets up a common and accessible audio context for all superdough operations
|
||||
|
||||
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/audiocontext.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/>.
|
||||
*/
|
||||
|
||||
let audioContext;
|
||||
|
||||
export const setDefaultAudioContext = () => {
|
||||
|
||||
@@ -5,19 +5,18 @@ if (typeof DelayNode !== 'undefined') {
|
||||
wet = Math.abs(wet);
|
||||
this.delayTime.value = time;
|
||||
|
||||
const feedbackGain = ac.createGain();
|
||||
feedbackGain.gain.value = Math.min(Math.abs(feedback), 0.995);
|
||||
this.feedback = feedbackGain.gain;
|
||||
this.feedbackGain = ac.createGain();
|
||||
this.feedbackGain.gain.value = Math.min(Math.abs(feedback), 0.995);
|
||||
this.feedback = this.feedbackGain.gain;
|
||||
|
||||
const delayGain = ac.createGain();
|
||||
delayGain.gain.value = wet;
|
||||
this.delayGain = delayGain;
|
||||
this.delayGain = ac.createGain();
|
||||
this.delayGain.gain.value = wet;
|
||||
|
||||
this.connect(feedbackGain);
|
||||
this.connect(delayGain);
|
||||
feedbackGain.connect(this);
|
||||
this.connect(this.feedbackGain);
|
||||
this.connect(this.delayGain);
|
||||
this.feedbackGain.connect(this);
|
||||
|
||||
this.connect = (target) => delayGain.connect(target);
|
||||
this.connect = (target) => this.delayGain.connect(target);
|
||||
return this;
|
||||
}
|
||||
start(t) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { getNoiseBuffer } from './noise.mjs';
|
||||
import { getNodeFromPool } from './nodePools.mjs';
|
||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||
|
||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||
@@ -42,6 +43,7 @@ export const getParamADSR = (
|
||||
decay,
|
||||
sustain,
|
||||
release,
|
||||
// min = value at start of attack, max = value at end of attack; it is possible that max < min
|
||||
min,
|
||||
max,
|
||||
begin,
|
||||
@@ -59,17 +61,15 @@ export const getParamADSR = (
|
||||
max = max === 0 ? 0.001 : max;
|
||||
}
|
||||
const range = max - min;
|
||||
const peak = max;
|
||||
const sustainVal = min + sustain * range;
|
||||
const duration = end - begin;
|
||||
|
||||
const envValAtTime = (time) => {
|
||||
let val;
|
||||
if (attack > time) {
|
||||
let slope = getSlope(min, peak, 0, attack);
|
||||
val = time * slope + (min > peak ? min : 0);
|
||||
val = time * getSlope(min, max, 0, attack) + min;
|
||||
} else {
|
||||
val = (time - attack) * getSlope(peak, sustainVal, 0, decay) + peak;
|
||||
val = (time - attack) * getSlope(max, sustainVal, 0, decay) + max;
|
||||
}
|
||||
if (curve === 'exponential') {
|
||||
val = val || 0.001;
|
||||
@@ -105,22 +105,40 @@ function getModulationShapeInput(val) {
|
||||
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
|
||||
}
|
||||
|
||||
export function getLfo(audioContext, begin, end, properties = {}) {
|
||||
const { shape = 0, ...props } = properties;
|
||||
const { dcoffset = -0.5, depth = 1 } = properties;
|
||||
export function getEnvelope(audioContext, properties = {}) {
|
||||
return getWorklet(audioContext, 'envelope-processor', properties);
|
||||
}
|
||||
|
||||
export function getLfo(audioContext, properties = {}) {
|
||||
const {
|
||||
shape = 0,
|
||||
begin = 0,
|
||||
end = 0,
|
||||
time,
|
||||
depth = 1,
|
||||
dcoffset = -0.5,
|
||||
frequency = 1,
|
||||
skew = 0.5,
|
||||
phaseoffset = 0,
|
||||
curve = 1,
|
||||
min,
|
||||
max,
|
||||
...props
|
||||
} = properties;
|
||||
|
||||
const lfoprops = {
|
||||
frequency: 1,
|
||||
depth,
|
||||
skew: 0.5,
|
||||
phaseoffset: 0,
|
||||
time: begin,
|
||||
begin,
|
||||
end,
|
||||
shape: getModulationShapeInput(shape),
|
||||
time: time ?? begin,
|
||||
depth,
|
||||
dcoffset,
|
||||
min: dcoffset * depth,
|
||||
max: dcoffset * depth + depth,
|
||||
curve: 1,
|
||||
frequency,
|
||||
skew,
|
||||
phaseoffset,
|
||||
curve,
|
||||
shape: getModulationShapeInput(shape),
|
||||
min: min ?? dcoffset * depth,
|
||||
max: max ?? dcoffset * depth + depth,
|
||||
...props,
|
||||
};
|
||||
|
||||
@@ -128,6 +146,7 @@ export function getLfo(audioContext, begin, end, properties = {}) {
|
||||
}
|
||||
|
||||
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
|
||||
const node = getNodeFromPool('compressor', () => new DynamicsCompressorNode(ac, {}));
|
||||
const options = {
|
||||
threshold: threshold ?? -3,
|
||||
ratio: ratio ?? 10,
|
||||
@@ -135,7 +154,11 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) {
|
||||
attack: attack ?? 0.005,
|
||||
release: release ?? 0.05,
|
||||
};
|
||||
return new DynamicsCompressorNode(ac, options);
|
||||
const now = ac.currentTime;
|
||||
Object.entries(options).forEach(([key, value]) => {
|
||||
node[key].setValueAtTime(value, now);
|
||||
});
|
||||
return node;
|
||||
}
|
||||
|
||||
// changes the default values of the envelope based on what parameters the user has defined
|
||||
@@ -162,7 +185,9 @@ export function getParamLfo(audioContext, param, start, end, lfoValues) {
|
||||
}
|
||||
let lfo;
|
||||
if (depth) {
|
||||
lfo = getLfo(audioContext, start, end, {
|
||||
lfo = getLfo(audioContext, {
|
||||
begin: start,
|
||||
end,
|
||||
depth,
|
||||
dcoffset,
|
||||
...getLfoInputs,
|
||||
@@ -214,10 +239,13 @@ export function createFilter(context, start, end, params, cps, cycle) {
|
||||
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
|
||||
frequencyParam = filter.parameters.get('frequency');
|
||||
} else {
|
||||
filter = context.createBiquadFilter();
|
||||
const factory = () => context.createBiquadFilter();
|
||||
filter = getNodeFromPool('filter', factory);
|
||||
filter.type = type;
|
||||
filter.Q.value = q;
|
||||
filter.frequency.value = frequency;
|
||||
const now = context.currentTime;
|
||||
Object.entries({ Q: q, frequency }).forEach(([key, value]) => {
|
||||
filter[key].setValueAtTime(value, now);
|
||||
});
|
||||
frequencyParam = filter.frequency;
|
||||
}
|
||||
const envelopeValues = [params.attack, params.decay, params.sustain, params.release];
|
||||
@@ -332,7 +360,7 @@ export function getVibratoOscillator(param, value, t) {
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
});
|
||||
vibratoOscillator.start(t);
|
||||
return vibratoOscillator;
|
||||
return { stop: (t) => vibratoOscillator.stop(t), nodes: { vib: [vibratoOscillator], vib_gain: [gain] } };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,6 +416,7 @@ export function applyFM(param, value, begin) {
|
||||
const ac = getAudioContext();
|
||||
const toStop = []; // fm oscillators we will expose `stop` for
|
||||
const fms = {};
|
||||
const nodes = {};
|
||||
// Matrix
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
for (let j = 0; j <= 8; j++) {
|
||||
@@ -438,11 +467,14 @@ export function applyFM(param, value, begin) {
|
||||
output = osc.connect(envGain);
|
||||
}
|
||||
fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup };
|
||||
nodes[`fm_${idx}`] = [osc];
|
||||
}
|
||||
const { input, output, freq, osc, toCleanup } = fms[idx];
|
||||
const g = gainNode(amt * freq);
|
||||
io.push(isMod ? output.connect(g) : input);
|
||||
cleanupOnEnd(osc, [...toCleanup, g]);
|
||||
const gAmt = gainNode(amt);
|
||||
const gFreq = gainNode(freq);
|
||||
io.push(isMod ? output.connect(gAmt).connect(gFreq) : input);
|
||||
cleanupOnEnd(osc, [...toCleanup, gAmt, gFreq]);
|
||||
nodes[`fm_${idx}_gain`] = [gAmt];
|
||||
}
|
||||
if (!io[1]) {
|
||||
logger(
|
||||
@@ -455,6 +487,7 @@ export function applyFM(param, value, begin) {
|
||||
}
|
||||
}
|
||||
return {
|
||||
nodes,
|
||||
stop: (t) => toStop.forEach((m) => m?.stop(t)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from './helpers.mjs';
|
||||
export * from './synth.mjs';
|
||||
export * from './zzfx.mjs';
|
||||
export * from './logger.mjs';
|
||||
export * from './modulators.mjs';
|
||||
export * from './dspworklet.mjs';
|
||||
export * from './audioContext.mjs';
|
||||
export * from './wavetable.mjs';
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
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 };
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
nodePools.mjs - Helper functions related to pooling and re-using audio nodes
|
||||
|
||||
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/nodePools.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/>.
|
||||
*/
|
||||
|
||||
const nodePools = new Map();
|
||||
const POOL_KEY = Symbol('nodePoolKey');
|
||||
const IS_WORKLET_DEAD = Symbol('nodePoolIsWorkletDead');
|
||||
const MAX_POOL_SIZE = 64;
|
||||
|
||||
export const isPoolable = (node) => !!node[POOL_KEY];
|
||||
|
||||
const getParams = (node) => {
|
||||
const params = new Set();
|
||||
node.parameters?.forEach((param) => params.add(param));
|
||||
const visited = new Set(); // prioritize deepest definition
|
||||
let proto = node;
|
||||
// Move up the prototype chain
|
||||
while (proto !== Object.prototype) {
|
||||
for (const key of Object.getOwnPropertyNames(proto)) {
|
||||
if (visited.has(key)) continue;
|
||||
visited.add(key);
|
||||
const value = node[key];
|
||||
if (value instanceof AudioParam) {
|
||||
params.add(value);
|
||||
}
|
||||
}
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
export const releaseNodeToPool = (node) => {
|
||||
node.disconnect();
|
||||
if (node instanceof AudioScheduledSourceNode) {
|
||||
// not reusable
|
||||
return;
|
||||
}
|
||||
if (node[IS_WORKLET_DEAD]) {
|
||||
// Worklet already terminated, don't pool it
|
||||
return;
|
||||
}
|
||||
const key = node[POOL_KEY];
|
||||
if (key == null) return;
|
||||
const now = node.context?.currentTime ?? 0;
|
||||
getParams(node).forEach((param) => param.cancelScheduledValues(now));
|
||||
const pool = nodePools.get(key) ?? [];
|
||||
if (pool.length < MAX_POOL_SIZE) {
|
||||
pool.push(new WeakRef(node));
|
||||
nodePools.set(key, pool);
|
||||
}
|
||||
};
|
||||
|
||||
export const markWorkletAsDead = (worklet) => (worklet[IS_WORKLET_DEAD] = true);
|
||||
|
||||
// Attempt to get node from the pool. If this fails, fall back
|
||||
// to building it with the factory
|
||||
export const getNodeFromPool = (key, factory) => {
|
||||
const pool = nodePools.get(key) ?? [];
|
||||
let node;
|
||||
while (pool.length) {
|
||||
const ref = pool.pop();
|
||||
node = ref?.deref();
|
||||
if (node != null && !node[IS_WORKLET_DEAD]) break;
|
||||
}
|
||||
if (node == null || node[IS_WORKLET_DEAD]) {
|
||||
node = factory();
|
||||
}
|
||||
node[POOL_KEY] = key;
|
||||
return node;
|
||||
};
|
||||
@@ -12,6 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { releaseAudioNode } from './helpers.mjs';
|
||||
|
||||
var reverbGen = {};
|
||||
|
||||
/** Generates a reverb impulse response.
|
||||
@@ -104,8 +106,8 @@ var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt,
|
||||
player.start();
|
||||
context.oncomplete = function (event) {
|
||||
callback(event.renderedBuffer);
|
||||
filter.disconnect();
|
||||
player.disconnect();
|
||||
releaseAudioNode(filter);
|
||||
releaseAudioNode(player);
|
||||
};
|
||||
context.startRendering();
|
||||
|
||||
|
||||
@@ -65,21 +65,22 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
||||
bufferSource.playbackRate.value = playbackRate;
|
||||
|
||||
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 sample rate,
|
||||
// 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."
|
||||
const offset = begin * bufferSource.buffer.duration;
|
||||
// The computation of the offset into the sound is performed using the sound buffer's natural duration,
|
||||
// rather than the playback duration, so that even if the sound is playing at twice its normal speed,
|
||||
// the midway point through a 10-second audio buffer is still 5.
|
||||
const offset = begin * bufferDuration;
|
||||
|
||||
const loop = hapValue.loop;
|
||||
if (loop) {
|
||||
bufferSource.loop = true;
|
||||
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
|
||||
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
|
||||
bufferSource.loopStart = loopBegin * bufferDuration;
|
||||
bufferSource.loopEnd = loopEnd * bufferDuration;
|
||||
}
|
||||
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||
const sliceDuration = (end - begin) * bufferDuration;
|
||||
return { bufferSource, offset, bufferDuration, sliceDuration };
|
||||
const playbackDuration = bufferDuration / bufferSource.playbackRate.value;
|
||||
const sliceDuration = (end - begin) * playbackDuration;
|
||||
return { bufferSource, offset, bufferDuration, playbackDuration, sliceDuration };
|
||||
};
|
||||
|
||||
export const loadBuffer = (url, ac, s, n = 0) => {
|
||||
@@ -301,7 +302,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
}
|
||||
|
||||
// vibrato
|
||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
||||
const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, t);
|
||||
|
||||
const time = t + nudge;
|
||||
bufferSource.start(time, offset);
|
||||
@@ -324,7 +325,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
node.connect(out);
|
||||
onceEnded(bufferSource, function () {
|
||||
releaseAudioNode(bufferSource);
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
vibratoHandle?.stop();
|
||||
releaseAudioNode(node);
|
||||
releaseAudioNode(out);
|
||||
onended();
|
||||
@@ -334,7 +335,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
const stop = (endTime) => {
|
||||
bufferSource.stop(endTime);
|
||||
};
|
||||
const handle = { node: out, bufferSource, stop };
|
||||
const handle = { node: out, nodes: { source: [bufferSource], ...vibratoHandle?.nodes }, stop };
|
||||
|
||||
// cut groups
|
||||
if (cut !== undefined) {
|
||||
|
||||
+442
-242
@@ -7,22 +7,25 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
import './feedbackdelay.mjs';
|
||||
import './reverb.mjs';
|
||||
import './vowel.mjs';
|
||||
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
||||
import { clamp, nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
||||
import workletsUrl from './worklets.mjs?audioworklet';
|
||||
import { getNodeFromPool, isPoolable, releaseNodeToPool } from './nodePools.mjs';
|
||||
import {
|
||||
createFilter,
|
||||
effectSend,
|
||||
gainNode,
|
||||
getCompressor,
|
||||
getDistortion,
|
||||
getLfo,
|
||||
getWorklet,
|
||||
effectSend,
|
||||
releaseAudioNode,
|
||||
webAudioTimeout,
|
||||
} from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.mjs';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
import { getAudioContext, setAudioContext } from './audioContext.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||
import { resetSeenKeys } from './wavetable.mjs';
|
||||
|
||||
@@ -184,6 +187,7 @@ let defaultDefaultValues = {
|
||||
distortvol: 1,
|
||||
distorttype: 0,
|
||||
delay: 0,
|
||||
busgain: 1,
|
||||
byteBeatExpression: '0',
|
||||
delayfeedback: 0.5,
|
||||
delaysync: 3 / 16,
|
||||
@@ -191,6 +195,9 @@ let defaultDefaultValues = {
|
||||
i: 1,
|
||||
velocity: 1,
|
||||
fft: 8,
|
||||
tremolodepth: 1,
|
||||
tremolophase: 0,
|
||||
release: 0.01,
|
||||
};
|
||||
|
||||
const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues });
|
||||
@@ -327,16 +334,16 @@ export function connectToDestination(input, channels) {
|
||||
controller.output.connectToDestination(input, channels);
|
||||
}
|
||||
|
||||
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
|
||||
function getPhaser(begin, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
|
||||
const ac = getAudioContext();
|
||||
const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
|
||||
const lfo = getLfo(ac, { frequency, depth: sweep * 2, begin, end });
|
||||
|
||||
//filters
|
||||
const numStages = 1; //num of filters in series
|
||||
let fOffset = 282; //for backward compat in #1800
|
||||
const filterChain = [];
|
||||
for (let i = 0; i < numStages; i++) {
|
||||
const filter = ac.createBiquadFilter();
|
||||
const filter = getNodeFromPool('filter', () => ac.createBiquadFilter());
|
||||
filter.type = 'notch';
|
||||
filter.gain.value = 1;
|
||||
filter.frequency.value = centerFrequency + fOffset;
|
||||
@@ -359,7 +366,7 @@ export let analysers = {},
|
||||
analysersData = {};
|
||||
|
||||
export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) {
|
||||
if (!analysers[id] || analysers[id].audioContext != getAudioContext()) {
|
||||
if (!analysers[id] || analysers[id].context != getAudioContext()) {
|
||||
// make sure this doesn't happen too often as it piles up garbage
|
||||
const analyserNode = getAudioContext().createAnalyser();
|
||||
analyserNode.fftSize = fftSize;
|
||||
@@ -400,7 +407,37 @@ function mapChannelNumbers(channels) {
|
||||
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) => (isPoolable(n) ? releaseNodeToPool(n) : releaseAudioNode(n)));
|
||||
this.audioNodes = [];
|
||||
this.tails = [];
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
const ac = getAudioContext();
|
||||
const audioController = getSuperdoughAudioController();
|
||||
@@ -429,49 +466,24 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
}
|
||||
// destructure
|
||||
let {
|
||||
tremolo,
|
||||
tremolosync,
|
||||
tremolodepth = 1,
|
||||
tremoloskew,
|
||||
tremolophase = 0,
|
||||
tremoloshape,
|
||||
s = getDefaultValue('s'),
|
||||
bank,
|
||||
source,
|
||||
gain = getDefaultValue('gain'),
|
||||
postgain = getDefaultValue('postgain'),
|
||||
density = getDefaultValue('density'),
|
||||
duckorbit,
|
||||
duckonset,
|
||||
duckattack,
|
||||
duckdepth,
|
||||
djf,
|
||||
// filters
|
||||
fanchor = getDefaultValue('fanchor'),
|
||||
release = 0,
|
||||
|
||||
//phaser
|
||||
phaserrate,
|
||||
phaserdepth = getDefaultValue('phaserdepth'),
|
||||
phasersweep,
|
||||
phasercenter,
|
||||
//
|
||||
coarse,
|
||||
|
||||
crush,
|
||||
release = getDefaultValue('release'),
|
||||
dry,
|
||||
shape,
|
||||
shapevol = getDefaultValue('shapevol'),
|
||||
distort,
|
||||
distortvol = getDefaultValue('distortvol'),
|
||||
distorttype = getDefaultValue('distorttype'),
|
||||
pan,
|
||||
vowel,
|
||||
delay = getDefaultValue('delay'),
|
||||
delayfeedback = getDefaultValue('delayfeedback'),
|
||||
delaysync = getDefaultValue('delaysync'),
|
||||
delaytime,
|
||||
orbit = getDefaultValue('orbit'),
|
||||
bus,
|
||||
busgain = getDefaultValue('busgain'),
|
||||
room,
|
||||
roomfade,
|
||||
roomlp,
|
||||
@@ -481,16 +493,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
irspeed,
|
||||
irbegin,
|
||||
i = getDefaultValue('i'),
|
||||
velocity = getDefaultValue('velocity'),
|
||||
analyze, // analyser wet
|
||||
fft = getDefaultValue('fft'), // fftSize 0 - 10
|
||||
compressor: compressorThreshold,
|
||||
compressorRatio,
|
||||
compressorKnee,
|
||||
compressorAttack,
|
||||
compressorRelease,
|
||||
transient,
|
||||
transsustain,
|
||||
FX = [],
|
||||
FXrelease,
|
||||
} = value;
|
||||
|
||||
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
||||
@@ -505,17 +511,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth);
|
||||
}
|
||||
|
||||
gain = applyGainCurve(nanFallback(gain, 1));
|
||||
postgain = applyGainCurve(postgain);
|
||||
shapevol = applyGainCurve(shapevol);
|
||||
distortvol = applyGainCurve(distortvol);
|
||||
delay = applyGainCurve(delay);
|
||||
velocity = applyGainCurve(velocity);
|
||||
tremolodepth = applyGainCurve(tremolodepth);
|
||||
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||
busgain = applyGainCurve(busgain);
|
||||
|
||||
const end = t + hapDuration;
|
||||
const endWithRelease = end + release;
|
||||
const fullRelease = Math.max(release, FXrelease ?? 0);
|
||||
const endWithRelease = end + fullRelease;
|
||||
const chainID = Math.round(Math.random() * 1000000);
|
||||
|
||||
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
|
||||
@@ -529,8 +531,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
activeSoundSources.delete(chainID);
|
||||
}
|
||||
|
||||
let audioNodes = [];
|
||||
|
||||
if (['-', '~', '_'].includes(s)) {
|
||||
return;
|
||||
}
|
||||
@@ -543,17 +543,27 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
let sourceNode;
|
||||
if (source) {
|
||||
sourceNode = source(t, value, hapDuration, cps);
|
||||
nodes.main['source'] = [sourceNode];
|
||||
} else if (getSound(s)) {
|
||||
const { onTrigger } = getSound(s);
|
||||
const onEnded = () => {
|
||||
audioNodes.forEach((n) => releaseAudioNode(n));
|
||||
activeSoundSources.delete(chainID);
|
||||
};
|
||||
|
||||
const onEnded = () =>
|
||||
webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
chain.releaseNodes();
|
||||
activeSoundSources.delete(chainID);
|
||||
},
|
||||
0,
|
||||
endWithRelease,
|
||||
);
|
||||
|
||||
const soundHandle = await onTrigger(t, value, onEnded, cps);
|
||||
|
||||
if (soundHandle) {
|
||||
sourceNode = soundHandle.node;
|
||||
activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC
|
||||
nodes.main = { ...nodes.main, ...soundHandle.nodes };
|
||||
}
|
||||
} else {
|
||||
throw new Error(`sound ${s} not found! Is it loaded?`);
|
||||
@@ -568,206 +578,345 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
|
||||
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 }));
|
||||
|
||||
transient !== undefined &&
|
||||
chain.push(
|
||||
getWorklet(
|
||||
const chain = new Chain(sourceNode); // connection manager which tracks audio nodes for releasing
|
||||
FX = [...FX, value]; // run through the FX chain and then run through all FX outside of it as well
|
||||
for (let [idx, fx] of Object.entries(FX)) {
|
||||
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,
|
||||
'transient-processor',
|
||||
{},
|
||||
{
|
||||
processorOptions: {
|
||||
attack: transient,
|
||||
sustain: transsustain,
|
||||
attack: fx.transient,
|
||||
sustain: fx.transsustain,
|
||||
begin: t,
|
||||
end: endWithRelease,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
);
|
||||
chain.connect(transProcessor);
|
||||
fxNodes['transient'] = transProcessor;
|
||||
}
|
||||
|
||||
// gain stage
|
||||
chain.push(gainNode(gain));
|
||||
// gain stage
|
||||
const initialGain = gainNode(gain);
|
||||
fxNodes['gain'] = [initialGain];
|
||||
chain.connect(initialGain);
|
||||
|
||||
// filter
|
||||
const ftype = getFilterType(value.ftype);
|
||||
// filter
|
||||
const ftype = getFilterType(value.ftype);
|
||||
|
||||
if (value.cutoff !== undefined) {
|
||||
const lpMap = {
|
||||
frequency: 'cutoff',
|
||||
q: 'resonance',
|
||||
attack: 'lpattack',
|
||||
decay: 'lpdecay',
|
||||
sustain: 'lpsustain',
|
||||
release: 'lprelease',
|
||||
env: 'lpenv',
|
||||
anchor: 'fanchor',
|
||||
model: 'ftype',
|
||||
drive: 'drive',
|
||||
rate: 'lprate',
|
||||
sync: 'lpsync',
|
||||
depth: 'lpdepth',
|
||||
depthfrequency: 'lpdepthfrequency',
|
||||
shape: 'lpshape',
|
||||
dcoffset: 'lpdc',
|
||||
skew: 'lpskew',
|
||||
};
|
||||
const lpParams = pickAndRename(value, lpMap);
|
||||
lpParams.type = 'lowpass';
|
||||
const lp = () => createFilter(ac, t, end, lpParams, cps, cycle);
|
||||
const { filter: lpf1, lfo: lfo1 } = lp();
|
||||
chain.push(lpf1);
|
||||
lfo1 && audioNodes.push(lfo1);
|
||||
if (ftype === '24db') {
|
||||
const { filter: lpf2, lfo: lfo2 } = lp();
|
||||
chain.push(lpf2);
|
||||
lfo2 && audioNodes.push(lfo2);
|
||||
const filt = (params) => createFilter(ac, t, end, params, cps, cycle);
|
||||
if (fx.cutoff !== undefined) {
|
||||
const lpMap = {
|
||||
frequency: 'cutoff',
|
||||
q: 'resonance',
|
||||
attack: 'lpattack',
|
||||
decay: 'lpdecay',
|
||||
sustain: 'lpsustain',
|
||||
release: 'lprelease',
|
||||
env: 'lpenv',
|
||||
anchor: 'fanchor',
|
||||
model: 'ftype',
|
||||
drive: 'drive',
|
||||
rate: 'lprate',
|
||||
sync: 'lpsync',
|
||||
depth: 'lpdepth',
|
||||
depthfrequency: 'lpdepthfrequency',
|
||||
shape: 'lpshape',
|
||||
dcoffset: 'lpdc',
|
||||
skew: 'lpskew',
|
||||
};
|
||||
const lpParams = pickAndRename(fx, lpMap);
|
||||
lpParams.type = 'lowpass';
|
||||
const { filter: lpf1, lfo: lfo1 } = filt(lpParams);
|
||||
fxNodes['lpf'] = [lpf1];
|
||||
fxNodes['lpf_lfo'] = [lfo1];
|
||||
chain.connect(lpf1);
|
||||
lfo1 && chain.audioNodes.push(lfo1);
|
||||
if (ftype === '24db') {
|
||||
const { filter: lpf2, lfo: lfo2 } = filt(lpParams);
|
||||
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 (value.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(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);
|
||||
if (FXrelease !== undefined && FXrelease > release) {
|
||||
const releaseNode = gainNode(1);
|
||||
releaseNode.gain.setValueAtTime(1, end + release);
|
||||
releaseNode.gain.linearRampToValueAtTime(0, endWithRelease);
|
||||
chain.connect(releaseNode);
|
||||
}
|
||||
|
||||
// last gain
|
||||
const post = new GainNode(ac, { gain: postgain });
|
||||
chain.push(post);
|
||||
nodes.main['post'] = [post];
|
||||
chain.connect(post);
|
||||
|
||||
// delay
|
||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||
orbitBus.getDelay(delaytime, delayfeedback, t);
|
||||
const send = orbitBus.sendDelay(post, delay);
|
||||
audioNodes.push(send);
|
||||
const delayNode = orbitBus.getDelay(delaytime, delayfeedback, t);
|
||||
nodes.main['delay'] = [delayNode];
|
||||
const delaySend = orbitBus.sendDelay(post, delay);
|
||||
nodes.main['delay_mix'] = [delaySend];
|
||||
chain.audioNodes.push(delaySend);
|
||||
}
|
||||
// reverb
|
||||
if (room > 0) {
|
||||
@@ -782,33 +931,84 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
}
|
||||
roomIR = await loadBuffer(url, ac, ir, 0);
|
||||
}
|
||||
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
||||
const send = orbitBus.sendReverb(post, room);
|
||||
audioNodes.push(send);
|
||||
const roomNode = orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
||||
nodes.main['room'] = [roomNode];
|
||||
const reverbSend = orbitBus.sendReverb(post, room);
|
||||
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) {
|
||||
orbitBus.getDjf(djf, t);
|
||||
const djfNode = orbitBus.getDjf(djf, t);
|
||||
nodes.main['djf'] = [djfNode];
|
||||
}
|
||||
|
||||
// analyser
|
||||
if (analyze && !(ac instanceof OfflineAudioContext)) {
|
||||
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
||||
const analyserSend = effectSend(post, analyserNode, 1);
|
||||
audioNodes.push(analyserSend);
|
||||
chain.audioNodes.push(analyserSend);
|
||||
}
|
||||
if (dry != null) {
|
||||
dry = applyGainCurve(dry);
|
||||
const dryGain = new GainNode(ac, { gain: dry });
|
||||
chain.push(dryGain);
|
||||
chain.connect(dryGain);
|
||||
orbitBus.connectToOutput(dryGain);
|
||||
} else {
|
||||
orbitBus.connectToOutput(post);
|
||||
}
|
||||
|
||||
// connect chain elements together
|
||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||
audioNodes = audioNodes.concat(chain);
|
||||
// finally, now that `nodes` is populated, set up modulators
|
||||
FX.forEach((fx, idx) => {
|
||||
const key = idx === FX.length - 1 ? 'main' : idx;
|
||||
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) => {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
superdoughdata.mjs - Data needed for running superdough (defaults, mappings, etc.)
|
||||
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdoughdata.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Mapping from control name to webaudio node and parameter
|
||||
const CONTROL_TARGETS = {
|
||||
stretch: { node: 'stretch', param: 'pitchFactor' },
|
||||
gain: { node: 'gain', param: 'gain' },
|
||||
postgain: { node: 'post', param: 'gain' },
|
||||
pan: { node: 'pan', param: 'pan' },
|
||||
tremolo: { node: 'tremolo', param: 'frequency' },
|
||||
tremolosync: { node: 'tremolo', param: 'frequency' },
|
||||
tremolodepth: { node: 'tremolo_gain', param: 'gain' },
|
||||
tremoloskew: { node: 'tremolo', param: 'skew' },
|
||||
tremolophase: { node: 'tremolo', param: 'phase' },
|
||||
tremoloshape: { node: 'tremolo', param: 'shape' },
|
||||
|
||||
// MODULATORS
|
||||
lfo: { node: 'lfo', param: 'frequency' },
|
||||
lfo_rate: { node: 'lfo', param: 'frequency' },
|
||||
lfo_sync: { node: 'lfo', param: 'frequency' },
|
||||
lfo_depth: { node: 'lfo', param: 'depth' },
|
||||
lfo_depthabs: { node: 'lfo', param: 'depth' },
|
||||
lfo_skew: { node: 'lfo', param: 'skew' },
|
||||
lfo_curve: { node: 'lfo', param: 'curve' },
|
||||
lfo_dcoffset: { node: 'lfo', param: 'dcoffset' },
|
||||
env: { node: 'env', param: 'depth' },
|
||||
env_attack: { node: 'env', param: 'attack' },
|
||||
env_decay: { node: 'env', param: 'decay' },
|
||||
env_sustain: { node: 'env', param: 'sustain' },
|
||||
env_release: { node: 'env', param: 'release' },
|
||||
bmod: { node: 'bmod', param: 'depth' },
|
||||
bmod_depth: { node: 'bmod', param: 'depth' },
|
||||
bmod_depthabs: { node: 'bmod', param: 'depth' },
|
||||
|
||||
// LPF
|
||||
cutoff: { node: 'lpf', param: 'frequency' },
|
||||
resonance: { node: 'lpf', param: 'Q' },
|
||||
lprate: { node: 'lpf_lfo', param: 'rate' },
|
||||
lpsync: { node: 'lpf_lfo', param: 'sync' },
|
||||
lpdepth: { node: 'lpf_lfo', param: 'depth' },
|
||||
lpdepthfrequency: { node: 'lpf_lfo', param: 'depth' },
|
||||
lpshape: { node: 'lpf_lfo', param: 'shape' },
|
||||
lpdc: { node: 'lpf_lfo', param: 'dcoffset' },
|
||||
lpskew: { node: 'lpf_lfo', param: 'skew' },
|
||||
|
||||
// HPF
|
||||
hcutoff: { node: 'hpf', param: 'frequency' },
|
||||
hresonance: { node: 'hpf', param: 'Q' },
|
||||
hprate: { node: 'hpf_lfo', param: 'rate' },
|
||||
hpsync: { node: 'hpf_lfo', param: 'sync' },
|
||||
hpdepth: { node: 'hpf_lfo', param: 'depth' },
|
||||
hpdepthfrequency: { node: 'hpf_lfo', param: 'depth' },
|
||||
hpshape: { node: 'hpf_lfo', param: 'shape' },
|
||||
hpdc: { node: 'hpf_lfo', param: 'dcoffset' },
|
||||
hpskew: { node: 'hpf_lfo', param: 'skew' },
|
||||
|
||||
// BPF
|
||||
bandf: { node: 'bpf', param: 'frequency' },
|
||||
bandq: { node: 'bpf', param: 'Q' },
|
||||
bprate: { node: 'bpf_lfo', param: 'rate' },
|
||||
bpsync: { node: 'bpf_lfo', param: 'sync' },
|
||||
bpdepth: { node: 'bpf_lfo', param: 'depth' },
|
||||
bpdepthfrequency: { node: 'bpf_lfo', param: 'depth' },
|
||||
bpshape: { node: 'bpf_lfo', param: 'shape' },
|
||||
bpdc: { node: 'bpf_lfo', param: 'dcoffset' },
|
||||
bpskew: { node: 'bpf_lfo', param: 'skew' },
|
||||
|
||||
vowel: { node: 'vowel', param: 'frequency' },
|
||||
|
||||
// DISTORTION
|
||||
coarse: { node: 'coarse', param: 'coarse' },
|
||||
crush: { node: 'crush', param: 'crush' },
|
||||
shape: { node: 'shape', param: 'shape' },
|
||||
shapevol: { node: 'shape', param: 'postgain' },
|
||||
distort: { node: 'distort', param: 'distort' },
|
||||
distortvol: { node: 'distort', param: 'postgain' },
|
||||
distorttype: { node: 'distort', param: 'distort' },
|
||||
|
||||
// COMPRESSOR
|
||||
compressor: { node: 'compressor', param: 'threshold' },
|
||||
compressorRatio: { node: 'compressor', param: 'ratio' },
|
||||
compressorKnee: { node: 'compressor', param: 'knee' },
|
||||
compressorAttack: { node: 'compressor', param: 'attack' },
|
||||
compressorRelease: { node: 'compressor', param: 'release' },
|
||||
|
||||
// PHASER
|
||||
phaserrate: { node: 'phaser_lfo', param: '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;
|
||||
}
|
||||
@@ -1,8 +1,20 @@
|
||||
/*
|
||||
superdoughoutput.mjs - Output controller for superdough
|
||||
|
||||
Handles setting up and mixing to the outputs as well as all global (orbit) effects
|
||||
|
||||
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdoughoutput.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 { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
|
||||
import { errorLogger } from './logger.mjs';
|
||||
import { clamp } from './util.mjs';
|
||||
|
||||
let hasChanged = (now, before) => now !== undefined && now !== before;
|
||||
const hasChanged = (now, before) => now !== undefined && now !== before;
|
||||
// Node with fixed stereo channel count to prevent clicking when the input signal
|
||||
// switches from mono to stereo
|
||||
const getStereoNode = (ac) => new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||
|
||||
export class Orbit {
|
||||
reverbNode;
|
||||
@@ -11,10 +23,11 @@ export class Orbit {
|
||||
summingNode;
|
||||
djfNode;
|
||||
audioContext;
|
||||
|
||||
constructor(audioContext) {
|
||||
this.audioContext = audioContext;
|
||||
this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||
this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||
this.output = getStereoNode(audioContext);
|
||||
this.summingNode = getStereoNode(audioContext);
|
||||
this.summingNode.connect(this.output);
|
||||
}
|
||||
|
||||
@@ -34,6 +47,7 @@ export class Orbit {
|
||||
}
|
||||
const val = this.djfNode.parameters.get('value');
|
||||
val.setValueAtTime(value, t);
|
||||
return this.djfNode;
|
||||
}
|
||||
|
||||
getDelay(delaytime = 0, feedback = 0.5, t) {
|
||||
@@ -164,6 +178,7 @@ export class SuperdoughAudioController {
|
||||
audioContext;
|
||||
output;
|
||||
nodes = {};
|
||||
buses = {};
|
||||
|
||||
constructor(audioContext) {
|
||||
this.audioContext = audioContext;
|
||||
@@ -171,10 +186,14 @@ export class SuperdoughAudioController {
|
||||
}
|
||||
|
||||
reset() {
|
||||
Array.from(this.nodes).forEach((node) => {
|
||||
Object.values(this.nodes).forEach((node) => {
|
||||
node.disconnect();
|
||||
});
|
||||
Object.values(this.buses).forEach((bus) => {
|
||||
bus.disconnect();
|
||||
});
|
||||
this.nodes = {};
|
||||
this.buses = {};
|
||||
this.output.reset();
|
||||
}
|
||||
|
||||
@@ -206,4 +225,11 @@ export class SuperdoughAudioController {
|
||||
}
|
||||
return this.nodes[orbitNum];
|
||||
}
|
||||
|
||||
getBus(busNum) {
|
||||
if (this.buses[busNum] == null) {
|
||||
this.buses[busNum] = getStereoNode(this.audioContext);
|
||||
}
|
||||
return this.buses[busNum];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { clamp } from './util.mjs';
|
||||
import { registerSound, soundMap } from './superdough.mjs';
|
||||
import { getSuperdoughAudioController, registerSound, soundMap } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import {
|
||||
applyFM,
|
||||
@@ -18,8 +18,9 @@ import {
|
||||
} from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||
import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs';
|
||||
|
||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
|
||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user', 'one'];
|
||||
const waveformAliases = [
|
||||
['tri', 'triangle'],
|
||||
['sqr', 'square'],
|
||||
@@ -52,17 +53,17 @@ export function registerSynthSounds() {
|
||||
// turn down
|
||||
const g = gainNode(0.3);
|
||||
|
||||
let sound = getOscillator(s, t, value, () => {
|
||||
const sound = getOscillator(s, t, value, () => {
|
||||
releaseAudioNode(g);
|
||||
onended();
|
||||
});
|
||||
|
||||
let { node: o, stop, triggerRelease } = sound;
|
||||
const { node: o, nodes, stop, triggerRelease } = sound;
|
||||
|
||||
const { duration } = value;
|
||||
|
||||
const envGain = gainNode(1);
|
||||
let node = o.connect(g).connect(envGain);
|
||||
const node = o.connect(g).connect(envGain);
|
||||
const holdEnd = t + duration;
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
|
||||
const envEnd = holdEnd + release + 0.01;
|
||||
@@ -70,6 +71,7 @@ export function registerSynthSounds() {
|
||||
stop(envEnd);
|
||||
return {
|
||||
node,
|
||||
nodes,
|
||||
stop: (endTime) => {
|
||||
stop(endTime);
|
||||
},
|
||||
@@ -139,6 +141,7 @@ export function registerSynthSounds() {
|
||||
|
||||
return {
|
||||
node,
|
||||
nodes: { source: [o] },
|
||||
stop: (endTime) => {
|
||||
o.stop(endTime);
|
||||
},
|
||||
@@ -165,26 +168,31 @@ export function registerSynthSounds() {
|
||||
const end = holdend + release + 0.01;
|
||||
const voices = clamp(unison, 1, 100);
|
||||
let panspread = voices > 1 ? clamp(spread, 0, 1) : 0;
|
||||
let o = getWorklet(
|
||||
ac,
|
||||
'supersaw-oscillator',
|
||||
{
|
||||
frequency,
|
||||
begin,
|
||||
end,
|
||||
freqspread: detune,
|
||||
voices,
|
||||
panspread,
|
||||
},
|
||||
{
|
||||
outputChannelCount: [2],
|
||||
},
|
||||
);
|
||||
|
||||
const params = {
|
||||
frequency,
|
||||
begin,
|
||||
end,
|
||||
freqspread: detune,
|
||||
voices,
|
||||
panspread,
|
||||
};
|
||||
const factory = () => new AudioWorkletNode(ac, 'supersaw-oscillator', { outputChannelCount: [2] });
|
||||
const o = getNodeFromPool('supersaw', factory);
|
||||
const now = ac.currentTime;
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
const param = o.parameters.get(key);
|
||||
const target = value !== undefined ? value : param.defaultValue;
|
||||
param.setValueAtTime(target, now);
|
||||
});
|
||||
o.port.postMessage({ type: 'initialize' });
|
||||
o.port.onmessage = (e) => {
|
||||
if (e.data.type === 'died') markWorkletAsDead(o);
|
||||
o.port.onmessage = null;
|
||||
};
|
||||
const gainAdjustment = 1 / Math.sqrt(voices);
|
||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
||||
const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
const fmHandle = applyFM(o.parameters.get('frequency'), value, begin);
|
||||
let envGain = gainNode(1);
|
||||
envGain = o.connect(envGain);
|
||||
|
||||
@@ -193,10 +201,10 @@ export function registerSynthSounds() {
|
||||
let timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
releaseAudioNode(o);
|
||||
releaseNodeToPool(o);
|
||||
onended();
|
||||
fm?.stop();
|
||||
vibratoOscillator?.stop();
|
||||
fmHandle?.stop();
|
||||
vibratoHandle?.stop();
|
||||
},
|
||||
begin,
|
||||
end,
|
||||
@@ -204,6 +212,7 @@ export function registerSynthSounds() {
|
||||
|
||||
return {
|
||||
node: envGain,
|
||||
nodes: { source: [o], ...fmHandle?.nodes, ...vibratoHandle?.nodes },
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
@@ -279,6 +288,7 @@ export function registerSynthSounds() {
|
||||
|
||||
return {
|
||||
node: envGain,
|
||||
source: o,
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
@@ -329,25 +339,25 @@ export function registerSynthSounds() {
|
||||
);
|
||||
|
||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
||||
const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
const fmHandle = applyFM(o.parameters.get('frequency'), value, begin);
|
||||
let envGain = gainNode(1);
|
||||
envGain = o.connect(envGain);
|
||||
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||
let lfo;
|
||||
let pw_lfo;
|
||||
if (pwsweep != 0) {
|
||||
lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep });
|
||||
lfo.connect(o.parameters.get('pulsewidth'));
|
||||
pw_lfo = getLfo(ac, { frequency: pwrate, depth: pwsweep, begin, end });
|
||||
pw_lfo.connect(o.parameters.get('pulsewidth'));
|
||||
}
|
||||
let timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
releaseAudioNode(o);
|
||||
releaseAudioNode(lfo);
|
||||
releaseAudioNode(pw_lfo);
|
||||
onended();
|
||||
fm?.stop();
|
||||
vibratoOscillator?.stop();
|
||||
fmHandle?.stop();
|
||||
vibratoHandle?.stop();
|
||||
},
|
||||
begin,
|
||||
end,
|
||||
@@ -355,6 +365,7 @@ export function registerSynthSounds() {
|
||||
|
||||
return {
|
||||
node: envGain,
|
||||
nodes: { source: [o], pw_lfo: [pw_lfo], ...fmHandle?.nodes, ...vibratoHandle?.nodes },
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
@@ -363,6 +374,41 @@ export function registerSynthSounds() {
|
||||
{ prebake: true, type: 'synth' },
|
||||
);
|
||||
|
||||
registerSound(
|
||||
'bus',
|
||||
(begin, value, onended) => {
|
||||
const ac = getAudioContext();
|
||||
const [attack, decay, sustain, release] = getADSRValues(
|
||||
[value.attack, value.decay, value.sustain, value.release],
|
||||
'linear',
|
||||
[0.001, 0.05, 1, 0.01],
|
||||
);
|
||||
const holdend = begin + value.duration;
|
||||
const end = holdend + release + 0.01;
|
||||
const bus = getSuperdoughAudioController().getBus(value.n ?? 0);
|
||||
const envGain = bus.connect(gainNode(0));
|
||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||
const timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
bus.disconnect(envGain);
|
||||
onended();
|
||||
},
|
||||
begin,
|
||||
end,
|
||||
);
|
||||
|
||||
return {
|
||||
node: envGain,
|
||||
nodes: { source: [bus] },
|
||||
stop: (time) => {
|
||||
timeoutNode.stop(time);
|
||||
},
|
||||
};
|
||||
},
|
||||
{ prebake: true, type: 'input' },
|
||||
);
|
||||
|
||||
[...noises].forEach((s) => {
|
||||
registerSound(
|
||||
s,
|
||||
@@ -400,6 +446,7 @@ export function registerSynthSounds() {
|
||||
stop(envEnd);
|
||||
return {
|
||||
node,
|
||||
nodes: { source: [o] },
|
||||
stop: (endTime) => {
|
||||
stop(endTime);
|
||||
},
|
||||
@@ -467,8 +514,17 @@ export function getOscillator(s, t, value, onended) {
|
||||
s = 'triangle';
|
||||
}
|
||||
s = s === 'user' && !partials ? 'triangle' : s;
|
||||
// If no partials are given, use stock waveforms
|
||||
if (!partials || partials?.length === 0 || s === 'sine') {
|
||||
if (s === 'one') {
|
||||
// Constant 1 oscillator (used for modulation)
|
||||
o = new ConstantSourceNode(getAudioContext(), { offset: 1 });
|
||||
o.start(t);
|
||||
return {
|
||||
node: o,
|
||||
nodes: { source: o },
|
||||
stop: (time) => o?.stop(time),
|
||||
};
|
||||
} else if (!partials || partials?.length === 0 || s === 'sine') {
|
||||
// If no partials are given, use stock waveforms
|
||||
o = getAudioContext().createOscillator();
|
||||
o.type = s || 'triangle';
|
||||
}
|
||||
@@ -479,11 +535,11 @@ export function getOscillator(s, t, value, onended) {
|
||||
// set frequency
|
||||
o.frequency.value = getFrequencyFromValue(value);
|
||||
|
||||
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
|
||||
const vibratoHandle = getVibratoOscillator(o.detune, value, t);
|
||||
|
||||
// pitch envelope
|
||||
getPitchEnvelope(o.detune, value, t, t + duration);
|
||||
const fmModulator = applyFM(o.frequency, value, t);
|
||||
const fmHandle = applyFM(o.frequency, value, t);
|
||||
|
||||
let noiseMix;
|
||||
if (noise) {
|
||||
@@ -500,9 +556,10 @@ export function getOscillator(s, t, value, onended) {
|
||||
|
||||
return {
|
||||
node: noiseMix?.node || o,
|
||||
nodes: { source: [o], ...vibratoHandle?.nodes, ...fmHandle?.nodes },
|
||||
stop: (time) => {
|
||||
fmModulator.stop(time);
|
||||
vibratoOscillator?.stop(time);
|
||||
fmHandle.stop(time);
|
||||
vibratoHandle?.stop(time);
|
||||
noiseMix?.stop(time);
|
||||
o.stop(time);
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { releaseAudioNode } from './helpers.mjs';
|
||||
|
||||
// credits to webdirt: https://github.com/dktr0/WebDirt/blob/41342e81d6ad694a2310d491fef7b7e8b0929efe/js-src/Graph.js#L597
|
||||
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] },
|
||||
@@ -46,7 +48,8 @@ if (typeof GainNode !== 'undefined') {
|
||||
}
|
||||
const { gains, qs, freqs } = vowelFormant[letter];
|
||||
this.makeupGain = ac.createGain();
|
||||
this.audioNodes = [];
|
||||
this.filters = [];
|
||||
this.gains = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const gain = ac.createGain();
|
||||
gain.gain.value = gains[i];
|
||||
@@ -56,9 +59,9 @@ if (typeof GainNode !== 'undefined') {
|
||||
filter.frequency.value = freqs[i];
|
||||
super.connect(filter);
|
||||
filter.connect(gain);
|
||||
this.audioNodes.push(filter);
|
||||
this.filters.push(filter);
|
||||
gain.connect(this.makeupGain);
|
||||
this.audioNodes.push(gain);
|
||||
this.gains.push(gain);
|
||||
}
|
||||
this.makeupGain.gain.value = 8; // how much makeup gain to add?
|
||||
return this;
|
||||
@@ -67,11 +70,13 @@ if (typeof GainNode !== 'undefined') {
|
||||
this.makeupGain.connect(target);
|
||||
}
|
||||
disconnect() {
|
||||
this.makeupGain.disconnect();
|
||||
this.audioNodes.forEach((n) => n.disconnect());
|
||||
releaseAudioNode(this.makeupGain);
|
||||
this.filters.forEach(releaseAudioNode);
|
||||
this.gains.forEach(releaseAudioNode);
|
||||
super.disconnect();
|
||||
this.makeupGain = null;
|
||||
this.audioNodes = null;
|
||||
this.filters = null;
|
||||
this.gains = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
getParamADSR,
|
||||
getPitchEnvelope,
|
||||
getVibratoOscillator,
|
||||
getWorklet,
|
||||
releaseAudioNode,
|
||||
webAudioTimeout,
|
||||
releaseAudioNode,
|
||||
} from './helpers.mjs';
|
||||
import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
export const Warpmode = Object.freeze({
|
||||
@@ -230,24 +230,31 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
}
|
||||
const endWithRelease = holdEnd + release;
|
||||
const envEnd = endWithRelease + 0.01;
|
||||
const source = getWorklet(
|
||||
ac,
|
||||
'wavetable-oscillator-processor',
|
||||
{
|
||||
begin: t,
|
||||
end: envEnd,
|
||||
frequency,
|
||||
freqspread: value.detune,
|
||||
position: value.wt,
|
||||
warp: value.warp,
|
||||
warpMode: warpmode,
|
||||
voices: Math.max(value.unison ?? 1, 1),
|
||||
panspread: value.spread,
|
||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||
},
|
||||
{ outputChannelCount: [2] },
|
||||
);
|
||||
source.port.postMessage({ type: 'table', payload });
|
||||
const params = {
|
||||
begin: t,
|
||||
end: envEnd,
|
||||
frequency,
|
||||
freqspread: value.detune,
|
||||
position: value.wt,
|
||||
warp: value.warp,
|
||||
warpMode: warpmode,
|
||||
voices: Math.max(value.unison ?? 1, 1),
|
||||
panspread: value.spread,
|
||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||
};
|
||||
const factory = () => new AudioWorkletNode(ac, 'wavetable-oscillator-processor', { outputChannelCount: [2] });
|
||||
const source = getNodeFromPool('wavetable', factory);
|
||||
const now = ac.currentTime;
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
const param = source.parameters.get(key);
|
||||
const target = value !== undefined ? value : param.defaultValue;
|
||||
param.setValueAtTime(target, now);
|
||||
});
|
||||
source.port.postMessage({ type: 'initialize', payload });
|
||||
source.port.onmessage = (e) => {
|
||||
if (e.data.type === 'died') markWorkletAsDead(source);
|
||||
source.port.onmessage = null;
|
||||
};
|
||||
if (ac.currentTime > t) {
|
||||
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
||||
return;
|
||||
@@ -314,19 +321,28 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
dcoffset: value.warpdc ?? 0,
|
||||
},
|
||||
);
|
||||
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
||||
const fm = applyFM(source.parameters.get('frequency'), value, t);
|
||||
const vibratoHandle = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
||||
const fmHandle = applyFM(source.parameters.get('frequency'), value, t);
|
||||
const envGain = ac.createGain();
|
||||
const node = source.connect(envGain);
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
|
||||
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
|
||||
const handle = { node, source };
|
||||
const handle = {
|
||||
node,
|
||||
nodes: {
|
||||
source: [source],
|
||||
wt_lfo: [wtPosModulators],
|
||||
warp_lfo: [wtWarpModulators],
|
||||
...fmHandle?.nodes,
|
||||
...vibratoHandle?.nodes,
|
||||
},
|
||||
};
|
||||
const timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
releaseAudioNode(source);
|
||||
releaseAudioNode(vibratoOscillator);
|
||||
fm?.stop();
|
||||
releaseNodeToPool(source);
|
||||
vibratoHandle?.stop();
|
||||
fmHandle?.stop();
|
||||
releaseAudioNode(wtPosModulators);
|
||||
releaseAudioNode(wtWarpModulators);
|
||||
onended();
|
||||
|
||||
@@ -124,8 +124,8 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
{ name: 'shape', defaultValue: 0 },
|
||||
{ name: 'curve', defaultValue: 1 },
|
||||
{ name: 'dcoffset', defaultValue: 0 },
|
||||
{ name: 'min', defaultValue: 0 },
|
||||
{ name: 'max', defaultValue: 1 },
|
||||
{ name: 'min', defaultValue: -1e9 },
|
||||
{ name: 'max', defaultValue: 1e9 },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -143,7 +143,8 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
|
||||
process(_inputs, outputs, parameters) {
|
||||
const begin = parameters['begin'][0];
|
||||
if (currentTime >= parameters.end[0]) {
|
||||
const end = parameters['end'][0];
|
||||
if (currentTime >= end) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= begin) {
|
||||
@@ -161,6 +162,7 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
const curve = parameters['curve'][0];
|
||||
|
||||
const dcoffset = parameters['dcoffset'][0];
|
||||
|
||||
const min = parameters['min'][0];
|
||||
const max = parameters['max'][0];
|
||||
const shape = waveShapeNames[parameters['shape'][0]];
|
||||
@@ -461,6 +463,16 @@ registerProcessor('distort-processor', DistortProcessor);
|
||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.isAlive = true; // used internally to prevent multiple death messages
|
||||
this.port.onmessage = (e) => {
|
||||
const { type, payload } = e.data || {};
|
||||
if (type === 'initialize') {
|
||||
this.initialize(payload);
|
||||
}
|
||||
};
|
||||
this.initialize();
|
||||
}
|
||||
initialize(_options) {
|
||||
this.phase = [];
|
||||
}
|
||||
static get parameterDescriptors() {
|
||||
@@ -511,12 +523,16 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
];
|
||||
}
|
||||
process(_input, outputs, params) {
|
||||
if (currentTime >= params.end[0]) {
|
||||
// should terminate
|
||||
if (currentTime >= params.end[0] + 0.5) {
|
||||
// Outside of grace period - should terminate
|
||||
if (this.isAlive) {
|
||||
this.port.postMessage({ type: 'died' });
|
||||
this.isAlive = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= params.begin[0]) {
|
||||
// keep alive
|
||||
if (currentTime >= params.end[0] || currentTime <= params.begin[0]) {
|
||||
// Inside of grace period or not yet started
|
||||
return true;
|
||||
}
|
||||
const output = outputs[0];
|
||||
@@ -967,7 +983,9 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
||||
{ name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
||||
{ name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
||||
{ name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
||||
{ name: 'peak', defaultValue: 1 },
|
||||
{ name: 'depth', defaultValue: 1 },
|
||||
{ name: 'min', defaultValue: -1e9 },
|
||||
{ name: 'max', defaultValue: 1e9 },
|
||||
{ name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 },
|
||||
];
|
||||
}
|
||||
@@ -1009,9 +1027,15 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
process(_inputs, outputs, params) {
|
||||
const begin = params['begin'][0];
|
||||
const end = params['end'][0];
|
||||
if (currentTime >= end) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= begin) {
|
||||
return true;
|
||||
}
|
||||
const out = outputs[0][0];
|
||||
if (!out) return true;
|
||||
const begin = pv(params.begin, 0);
|
||||
const retrigger = pv(params.retrigger, 0) >= 0.5; // convert to bool
|
||||
if (begin !== this.beginTime && (this.state === 0 || retrigger)) {
|
||||
// triggered
|
||||
@@ -1029,7 +1053,9 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
||||
const aCurve = pv(params.attackCurve, i);
|
||||
const dCurve = pv(params.decayCurve, i);
|
||||
const rCurve = pv(params.releaseCurve, i);
|
||||
const peak = pv(params.peak, i);
|
||||
const depth = pv(params.depth, i);
|
||||
const min = pv(params.min, i);
|
||||
const max = pv(params.max, i);
|
||||
const states = [
|
||||
{ time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle
|
||||
{ time: attack, start: this.attackStart, target: 1, curve: aCurve },
|
||||
@@ -1043,7 +1069,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
||||
this.state = (this.state + 1) % states.length;
|
||||
time = states[this.state].time;
|
||||
}
|
||||
out[i] = this.val * peak;
|
||||
out[i] = clamp(this.val * depth, min, max);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1138,37 +1164,29 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.frameLen = 0;
|
||||
this.numFrames = 0;
|
||||
this.phase = [];
|
||||
|
||||
this.isAlive = true; // used internally to prevent multiple death messages
|
||||
this.port.onmessage = (e) => {
|
||||
const { type, payload } = e.data || {};
|
||||
if (type === 'table') {
|
||||
const key = payload.key;
|
||||
this.frameLen = payload.frameLen;
|
||||
if (!tablesCache[key]) {
|
||||
const tables = [payload.frames];
|
||||
let table = tables[0];
|
||||
for (let level = 1; level < 1; level++) {
|
||||
const nextLen = table.length >> 1;
|
||||
const nextTable = table.map((frame) => {
|
||||
const avg = new Float32Array(nextLen);
|
||||
for (let i = 0; i < nextLen; i++) {
|
||||
avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2;
|
||||
}
|
||||
return avg;
|
||||
});
|
||||
tables.push(nextTable);
|
||||
table = nextTable;
|
||||
if (nextLen <= 32) break;
|
||||
}
|
||||
tablesCache[key] = tables;
|
||||
}
|
||||
this.tables = tablesCache[key];
|
||||
this.numFrames = this.tables[0].length;
|
||||
if (type === 'initialize') {
|
||||
this.initialize(payload);
|
||||
}
|
||||
};
|
||||
this.initialize();
|
||||
}
|
||||
initialize(options) {
|
||||
this.table = null;
|
||||
this.frameLen = null;
|
||||
this.numFrames = null;
|
||||
this.phase = [];
|
||||
if (options?.key) {
|
||||
const key = options.key;
|
||||
this.frameLen = options.frameLen;
|
||||
if (!tablesCache[key]) {
|
||||
tablesCache[key] = options.frames;
|
||||
}
|
||||
this.table = tablesCache[key];
|
||||
this.numFrames = this.table.length;
|
||||
}
|
||||
}
|
||||
|
||||
_mirror(x) {
|
||||
@@ -1313,25 +1331,22 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
return a + (b - a) * frac;
|
||||
}
|
||||
|
||||
_chooseMip(dphi) {
|
||||
const approxHarm = clamp(dphi, 1e-6, 64);
|
||||
let level = 0;
|
||||
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
||||
level++;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
process(_inputs, outputs, parameters) {
|
||||
if (currentTime >= parameters.end[0]) {
|
||||
if (currentTime >= parameters.end[0] + 0.5) {
|
||||
// Outside of grace period - should terminate
|
||||
if (this.isAlive) {
|
||||
this.port.postMessage({ type: 'died' });
|
||||
this.isAlive = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= parameters.begin[0]) {
|
||||
if (currentTime >= parameters.end[0] || currentTime <= parameters.begin[0]) {
|
||||
// Inside of grace period or not yet started
|
||||
return true;
|
||||
}
|
||||
const outL = outputs[0][0];
|
||||
const outR = outputs[0][1] || outputs[0][0];
|
||||
if (!this.tables) {
|
||||
if (!this.table) {
|
||||
outL.fill(0);
|
||||
if (outR !== outL) outR.set(outL);
|
||||
return true;
|
||||
@@ -1365,14 +1380,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune
|
||||
const dPhase = fVoice * INVSR;
|
||||
const level = this._chooseMip(dPhase);
|
||||
const table = this.tables[level];
|
||||
|
||||
// warp phase then sample
|
||||
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
|
||||
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
||||
const s0 = this._sampleFrame(table[fIdx], ph);
|
||||
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
||||
const s0 = this._sampleFrame(this.table[fIdx], ph);
|
||||
const s1 = this._sampleFrame(this.table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
||||
let s = lerp(s0, s1, interpT);
|
||||
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
||||
s = -s;
|
||||
|
||||
@@ -90,6 +90,7 @@ export function registerZZFXSounds() {
|
||||
});
|
||||
return {
|
||||
node: o,
|
||||
nodes: { source: [o] },
|
||||
stop: () => {},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -42,4 +42,18 @@ describe('transpiler', () => {
|
||||
[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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,12 +21,15 @@ export function registerLanguage(type, config) {
|
||||
export function transpiler(input, options = {}) {
|
||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
||||
|
||||
const comments = [];
|
||||
let ast = parse(input, {
|
||||
ecmaVersion: 2022,
|
||||
allowAwaitOutsideFunction: true,
|
||||
locations: true,
|
||||
onComment: comments,
|
||||
});
|
||||
|
||||
const miniDisableRanges = findMiniDisableRanges(comments, input.length);
|
||||
let miniLocations = [];
|
||||
const collectMiniLocations = (value, node) => {
|
||||
const minilang = languages.get('minilang');
|
||||
@@ -66,6 +69,9 @@ export function transpiler(input, options = {}) {
|
||||
return this.replace(tidalWithLocation(raw, offset));
|
||||
}
|
||||
if (isBackTickString(node, parent)) {
|
||||
if (isMiniDisabled(node.start, miniDisableRanges)) {
|
||||
return;
|
||||
}
|
||||
const { quasis } = node;
|
||||
const { raw } = quasis[0].value;
|
||||
this.skip();
|
||||
@@ -73,6 +79,9 @@ export function transpiler(input, options = {}) {
|
||||
return this.replace(miniWithLocation(raw, node));
|
||||
}
|
||||
if (isStringWithDoubleQuotes(node)) {
|
||||
if (isMiniDisabled(node.start, miniDisableRanges)) {
|
||||
return;
|
||||
}
|
||||
const { value } = node;
|
||||
this.skip();
|
||||
emitMiniLocations && collectMiniLocations(value, node);
|
||||
@@ -327,3 +336,32 @@ function languageWithLocation(name, value, offset) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -565,6 +565,52 @@ exports[`runs examples > example "_euclidRot" example index 20 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "FX" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 1/8 → 1/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 1/4 → 3/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 3/8 → 1/2 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 1/2 → 5/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 5/8 → 3/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 3/4 → 7/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 7/8 → 1/1 | s:lt decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 1/1 → 9/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 9/8 → 5/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 5/4 → 11/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 11/8 → 3/2 | s:oh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 3/2 → 13/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 13/8 → 7/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 7/4 → 15/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 15/8 → 2/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 2/1 → 17/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 17/8 → 9/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 9/4 → 19/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 19/8 → 5/2 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 5/2 → 21/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 21/8 → 11/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 11/4 → 23/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 23/8 → 3/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 3/1 → 25/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 25/8 → 13/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 13/4 → 27/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 27/8 → 7/2 | s:lt decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 7/2 → 29/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 29/8 → 15/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 15/4 → 31/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
"[ 31/8 → 4/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "FX" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
|
||||
"[ 1/1 → 2/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
|
||||
"[ 2/1 → 3/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
|
||||
"[ 3/1 → 4/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "accelerate" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 2/1 | s:sax accelerate:0 ]",
|
||||
@@ -3803,6 +3849,51 @@ exports[`runs examples > example "end" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "env" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]",
|
||||
"[ 1/1 → 2/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]",
|
||||
"[ 2/1 → 3/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]",
|
||||
"[ 3/1 → 4/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "env" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]",
|
||||
"[ 1/1 → 2/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]",
|
||||
"[ 2/1 → 3/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]",
|
||||
"[ 3/1 → 4/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "env" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]",
|
||||
"[ 1/1 → 2/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]",
|
||||
"[ 2/1 → 3/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]",
|
||||
"[ 3/1 → 4/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "env" example index 3 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:0.5} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 1/1 → 2/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:1} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 2/1 → 3/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:1} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 3/1 → 4/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:0.5} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "env" example index 4 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:0.5}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 1/1 → 2/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:1}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 2/1 → 3/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:1}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 3/1 → 4/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:0.5}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "euclid" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:c3 ]",
|
||||
@@ -6113,6 +6204,51 @@ exports[`runs examples > example "leslie" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lfo" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
"[ 1/1 → 2/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
"[ 2/1 → 3/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
"[ 3/1 → 4/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lfo" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]",
|
||||
"[ 1/1 → 2/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]",
|
||||
"[ 2/1 → 3/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]",
|
||||
"[ 3/1 → 4/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lfo" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
"[ 1/1 → 2/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
"[ 2/1 → 3/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
"[ 3/1 → 4/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lfo" example index 3 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s sync:8} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 1/1 → 2/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 2/1 → 3/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 3/1 → 4/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s sync:8} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "lfo" example index 4 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4 sync:8}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 1/1 → 2/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 2/1 → 3/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
"[ 3/1 → 4/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4 sync:8}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "linger" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:lt ]",
|
||||
|
||||
@@ -20,6 +20,7 @@ const skippedExamples = [
|
||||
'accelerationX',
|
||||
'defaultmidimap',
|
||||
'midimaps',
|
||||
'bmod',
|
||||
];
|
||||
|
||||
describe('runs examples', () => {
|
||||
|
||||
+1
-183
@@ -3,186 +3,4 @@
|
||||
This is the website for Strudel, deployed at [strudel.cc](https://strudel.cc).
|
||||
It includes the REPL live coding editor and the documentation site.
|
||||
|
||||
## Run locally
|
||||
|
||||
```bash
|
||||
# from project root
|
||||
npm run setup
|
||||
npm run repl
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd website
|
||||
npm run build # <- builds repl + tutorial to ../docs
|
||||
npm run preview # <- test static build
|
||||
```
|
||||
|
||||
## Generate PWA icons
|
||||
|
||||
```sh
|
||||
cd website/public
|
||||
npx pwa-asset-generator icon.png icons
|
||||
```
|
||||
|
||||
# Standard Readme of Astro Starter Kit: Docs Site
|
||||
|
||||
```bash
|
||||
npm create astro@latest -- --template docs
|
||||
```
|
||||
|
||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/docs)
|
||||
[](https://codesandbox.io/s/github/withastro/astro/tree/latest/examples/docs)
|
||||
|
||||

|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Full Markdown support**
|
||||
- ✅ **Responsive mobile-friendly design**
|
||||
- ✅ **Sidebar navigation**
|
||||
- ✅ **Search (powered by Algolia)**
|
||||
- ✅ **Multi-language i18n**
|
||||
- ✅ **Automatic table of contents**
|
||||
- ✅ **Automatic list of contributors**
|
||||
- ✅ (and, best of all) **dark mode**
|
||||
|
||||
## Commands Cheatsheet
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :--------------------- | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro --help` | Get help using the Astro CLI |
|
||||
|
||||
To deploy your site to production, check out our [Deploy an Astro Website](https://docs.astro.build/guides/deploy) guide.
|
||||
|
||||
## New to Astro?
|
||||
|
||||
Welcome! Check out [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||
|
||||
## Customize This Theme
|
||||
|
||||
edit: removed, as css styles have been replaced with tailwind styles
|
||||
|
||||
### Site metadata
|
||||
|
||||
`src/config.ts` contains several data objects that describe metadata about your site like title, description, default language, and Open Graph details. You can customize these to match your project.
|
||||
|
||||
## Page metadata
|
||||
|
||||
Astro uses frontmatter in Markdown pages to choose layouts and pass properties to those layouts. If you are using the default layout, you can customize the page in many different ways to optimize SEO and other things. For example, you can use the `title` and `description` properties to set the document title, meta title, meta description, and Open Graph description.
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Example title
|
||||
description: Really cool docs example that uses Astro
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
# Page content...
|
||||
```
|
||||
|
||||
For more SEO related properties, look at `src/components/HeadSEO.astro`
|
||||
|
||||
### Sidebar navigation
|
||||
|
||||
The sidebar navigation is controlled by the `SIDEBAR` variable in your `src/config.ts` file. You can customize the sidebar by modifying this object. A default, starter navigation has already been created for you.
|
||||
|
||||
```ts
|
||||
export const SIDEBAR = {
|
||||
en: [
|
||||
{ text: "Section Header", header: true },
|
||||
{ text: "Introduction", link: "en/introduction" },
|
||||
{ text: "Page 2", link: "en/page-2" },
|
||||
{ text: "Page 3", link: "en/page-3" },
|
||||
|
||||
{ text: "Another Section", header: true },
|
||||
{ text: "Page 4", link: "en/page-4" },
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
Note the top-level `en` key: This is needed for multi-language support. You can change it to whatever language you'd like, or add new languages as you go. More details on this below.
|
||||
|
||||
### Multiple Languages support
|
||||
|
||||
The Astro docs template supports multiple languages out of the box. The default theme only shows `en` documentation, but you can enable multi-language support features by adding a second language to your project.
|
||||
|
||||
To add a new language to your project, you'll want to extend the current `src/pages/[lang]/...` layout:
|
||||
|
||||
```diff
|
||||
📂 src/pages
|
||||
┣ 📂 en
|
||||
┃ ┣ 📜 page-1.md
|
||||
┃ ┣ 📜 page-2.md
|
||||
┃ ┣ 📜 page-3.astro
|
||||
+ ┣ 📂 es
|
||||
+ ┃ ┣ 📜 page-1.md
|
||||
+ ┃ ┣ 📜 page-2.md
|
||||
+ ┃ ┣ 📜 page-3.astro
|
||||
```
|
||||
|
||||
You'll also need to add the new language name to the `KNOWN_LANGUAGES` map in your `src/config.ts` file. This will enable your new language switcher in the site header.
|
||||
|
||||
```diff
|
||||
// src/config.ts
|
||||
export const KNOWN_LANGUAGES = {
|
||||
English: 'en',
|
||||
+ Spanish: 'es',
|
||||
};
|
||||
```
|
||||
|
||||
Last step: you'll need to add a new entry to your sidebar, to create the table of contents for that language. While duplicating every page might not sound ideal to everyone, this extra control allows you to create entirely custom content for every language.
|
||||
|
||||
> Make sure the sidebar `link` value points to the correct language!
|
||||
|
||||
```diff
|
||||
// src/config.ts
|
||||
export const SIDEBAR = {
|
||||
en: [
|
||||
{ text: 'Section Header', header: true, },
|
||||
{ text: 'Introduction', link: 'en/introduction' },
|
||||
// ...
|
||||
],
|
||||
+ es: [
|
||||
+ { text: 'Encabezado de sección', header: true, },
|
||||
+ { text: 'Introducción', link: 'es/introduction' },
|
||||
+ // ...
|
||||
+ ],
|
||||
};
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
If you plan to use Spanish as the default language, you just need to modify the redirect path in `src/pages/index.astro`:
|
||||
|
||||
```diff
|
||||
<script>
|
||||
- window.location.pathname = `/en/introduction`;
|
||||
+ window.location.pathname = `/es/introduction`;
|
||||
</script>
|
||||
```
|
||||
|
||||
You can also remove the above script and write a landing page in Spanish instead.
|
||||
|
||||
### What if I don't plan to support multiple languages?
|
||||
|
||||
That's totally fine! Not all projects need (or can support) multiple languages. You can continue to use this theme without ever adding a second language.
|
||||
|
||||
If that single language is not English, you can just replace `en` in directory layouts and configurations with the preferred language.
|
||||
|
||||
### Search (Powered by Algolia)
|
||||
|
||||
[Algolia](https://www.algolia.com/) offers a free service to qualified open source projects called [DocSearch](https://docsearch.algolia.com/). If you are accepted to the DocSearch program, provide your API Key & index name in `src/config.ts` and a search box will automatically appear in your site header.
|
||||
|
||||
Note that Aglolia and Astro are not affiliated. We have no say over acceptance to the DocSearch program.
|
||||
|
||||
If you'd prefer to remove Algolia's search and replace it with your own, check out the `src/components/Header.astro` component to see where the component is added.
|
||||
more setup info, see [project setup](../CONTRIBUTING.md#project-setup)
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
import '../../repl/Repl.css';
|
||||
---
|
||||
|
||||
<div id="code"></div>
|
||||
|
||||
<script>
|
||||
import { DoughMirror } from './dough-mirror.mjs';
|
||||
const root = document.getElementById('code');
|
||||
const doughmirror = new DoughMirror({ root });
|
||||
globalThis.settings = doughmirror.settings.bind(doughmirror);
|
||||
</script>
|
||||
<style>
|
||||
#code {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,153 @@
|
||||
import {
|
||||
initEditor,
|
||||
codemirrorSettings,
|
||||
flash,
|
||||
compartments,
|
||||
extensions,
|
||||
parseBooleans,
|
||||
activateTheme,
|
||||
updateMiniLocations,
|
||||
highlightMiniLocations,
|
||||
} from '@strudel/codemirror';
|
||||
import { evalScope, hash2code, code2hash } from '@strudel/core';
|
||||
import { Framer } from '@strudel/draw';
|
||||
import { persistentAtom } from '@nanostores/persistent';
|
||||
import { DoughRepl } from './dough-repl.mjs';
|
||||
|
||||
let initialCode = '$: note("c a f e")';
|
||||
|
||||
export const code = persistentAtom('vanilla-repl-code', initialCode, {
|
||||
encode: JSON.stringify,
|
||||
decode: JSON.parse,
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const codeParam = window.location.href.split('#')[1] || '';
|
||||
if (codeParam) {
|
||||
const codeFromHash = hash2code(codeParam);
|
||||
code.set(codeFromHash);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('could not init code from url');
|
||||
}
|
||||
}
|
||||
|
||||
export class DoughMirror {
|
||||
constructor(options) {
|
||||
const { root, initialCode = code.get(), bgFill = true } = options;
|
||||
this.root = root;
|
||||
this.code = initialCode;
|
||||
this.repl = new DoughRepl();
|
||||
this.prebaked = this.prebake();
|
||||
|
||||
// init codemirror
|
||||
this.editor = initEditor({
|
||||
root,
|
||||
initialCode: this.code,
|
||||
onChange: (v) => {
|
||||
if (v.docChanged) {
|
||||
this.code = v.state.doc.toString();
|
||||
code.set(this.code);
|
||||
}
|
||||
},
|
||||
onEvaluate: this.evaluate.bind(this),
|
||||
onStop: () => this.stop(),
|
||||
mondo: false,
|
||||
});
|
||||
const settings = codemirrorSettings.get();
|
||||
this.setFontSize(settings.fontSize);
|
||||
this.setFontFamily(settings.fontFamily);
|
||||
|
||||
// init event highlighting
|
||||
this.framer = new Framer(
|
||||
(time) => {
|
||||
const frameHaps = this.repl.processHaps();
|
||||
highlightMiniLocations(this.editor, time, frameHaps);
|
||||
},
|
||||
(err) => console.log('Framer error', err),
|
||||
);
|
||||
}
|
||||
prebake() {
|
||||
const modulesLoading = evalScope(import('@strudel/core'), import('@strudel/tonal'), import('@strudel/mini'));
|
||||
return Promise.all([modulesLoading, this.repl.prebake()]);
|
||||
}
|
||||
async evaluate() {
|
||||
this.framer.start();
|
||||
this.flash();
|
||||
await this.prebaked;
|
||||
const { miniLocations } = await this.repl.evaluate(this.code);
|
||||
window.location.hash = '#' + code2hash(this.code);
|
||||
updateMiniLocations(this.editor, miniLocations);
|
||||
}
|
||||
stop() {
|
||||
this.repl.stop();
|
||||
this.framer.stop();
|
||||
highlightMiniLocations(this.editor, 0, []);
|
||||
}
|
||||
// added synonym (compared to StrudelMirror)
|
||||
settings(settings = {}) {
|
||||
this.updateSettings(settings);
|
||||
}
|
||||
|
||||
// the rest is copy pasted from StrudelMirror:
|
||||
|
||||
updateSettings(settings = {}) {
|
||||
settings.fontSize && this.setFontSize(settings.fontSize);
|
||||
settings.fontFamily && this.setFontFamily(settings.fontFamily);
|
||||
for (let key in extensions) {
|
||||
if (key in settings) {
|
||||
this.reconfigureExtension(key, settings[key]);
|
||||
}
|
||||
}
|
||||
const updated = { ...codemirrorSettings.get(), ...settings };
|
||||
// console.log(updated);
|
||||
codemirrorSettings.set(updated);
|
||||
}
|
||||
reconfigureExtension(key, value) {
|
||||
if (!extensions[key]) {
|
||||
console.warn(`extension ${key} is not known`);
|
||||
return;
|
||||
}
|
||||
value = parseBooleans(value);
|
||||
const newValue = extensions[key](value, this);
|
||||
this.editor.dispatch({
|
||||
effects: compartments[key].reconfigure(newValue),
|
||||
});
|
||||
if (key === 'theme') {
|
||||
activateTheme(value);
|
||||
}
|
||||
}
|
||||
flash(ms) {
|
||||
flash(this.editor, ms);
|
||||
}
|
||||
setFontSize(size) {
|
||||
this.root.style.fontSize = size + 'px';
|
||||
}
|
||||
setFontFamily(family) {
|
||||
this.root.style.fontFamily = family;
|
||||
const scroller = this.root.querySelector('.cm-scroller');
|
||||
if (scroller) {
|
||||
scroller.style.fontFamily = family;
|
||||
}
|
||||
}
|
||||
setCode(code, offset = 0) {
|
||||
const changes = {
|
||||
from: 0,
|
||||
to: this.editor.state.doc.length + offset,
|
||||
insert: code,
|
||||
};
|
||||
this.editor.dispatch({ changes });
|
||||
}
|
||||
getCursorLocation() {
|
||||
return this.editor.state.selection.main.head;
|
||||
}
|
||||
setCursorLocation(col) {
|
||||
return this.editor.dispatch({ selection: { anchor: col } });
|
||||
}
|
||||
appendCode(code) {
|
||||
const cursor = this.getCursorLocation();
|
||||
this.setCode(this.code + code);
|
||||
this.setCursorLocation(cursor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// import { Dough, doughsamples } from 'dough-synth';
|
||||
import { Dough, doughsamples } from 'https://unpkg.com/dough-synth@0.1.9/dough.js';
|
||||
import { Pattern, noteToMidi, evaluate, stack } from '@strudel/core';
|
||||
// import doughUrl from 'dough-synth?url';
|
||||
import { transpiler } from '@strudel/transpiler';
|
||||
//const doughBaseUrl = doughUrl.split('/').slice(0, -1).join('/') + '/';
|
||||
const doughBaseUrl = 'https://unpkg.com/dough-synth@0.1.9/';
|
||||
|
||||
Object.assign(globalThis, { doughsamples });
|
||||
|
||||
export class DoughRepl {
|
||||
pattern;
|
||||
latency = 0.1;
|
||||
cps = 0.5;
|
||||
origin;
|
||||
t0;
|
||||
lasttime;
|
||||
strudel;
|
||||
q = [];
|
||||
constructor() {
|
||||
this.ready = this.init();
|
||||
}
|
||||
async init() {
|
||||
// init dough immediately, so that it can attach the document click event to initAudio immediately
|
||||
this.dough = new Dough({
|
||||
base: doughBaseUrl,
|
||||
//base: "../", // local dev
|
||||
onTick: ({ t0, t1 }) => {
|
||||
if (!this.pattern) {
|
||||
return;
|
||||
}
|
||||
this.origin ??= t0;
|
||||
this.t0 = t0;
|
||||
this.lasttime = performance.now();
|
||||
const a = (t0 - this.origin) * this.cps;
|
||||
const b = (t1 - this.origin) * this.cps;
|
||||
|
||||
const haps = this.pattern.queryArc(a, b).filter((hap) => hap.hasOnset());
|
||||
if (!haps.length) {
|
||||
return;
|
||||
}
|
||||
haps.forEach((hap) => {
|
||||
const time = hap.whole.begin.valueOf() / this.cps + this.origin + this.latency;
|
||||
const duration = hap.duration.valueOf() / this.cps;
|
||||
const event = {
|
||||
dough: 'play',
|
||||
...hap.value,
|
||||
time,
|
||||
duration,
|
||||
};
|
||||
if (event.note && typeof event.note === 'string') {
|
||||
event.note = noteToMidi(event.note);
|
||||
}
|
||||
//console.log("event", JSON.stringify(Object.entries(event)));
|
||||
this.dough.evaluate(event);
|
||||
this.q.push({ event, hap });
|
||||
});
|
||||
},
|
||||
});
|
||||
// miniAllStrings();
|
||||
const setcps = (cps) => (this.cps = cps);
|
||||
const setcpm = (cpm) => setcps(cpm / 60);
|
||||
const replScope = { setcps, setcpm };
|
||||
Object.assign(globalThis, replScope);
|
||||
}
|
||||
async evaluate(code) {
|
||||
await this.ready;
|
||||
let patterns = [];
|
||||
Pattern.prototype.p = function (id) {
|
||||
if (!id.startsWith('_')) {
|
||||
patterns.push(this);
|
||||
}
|
||||
};
|
||||
|
||||
let { meta } = await evaluate(code, transpiler, { addReturn: false, wrapAsync: true, emitWidgets: false });
|
||||
const { miniLocations } = meta;
|
||||
|
||||
this.pattern = stack(...patterns);
|
||||
return { miniLocations, pattern: this.pattern };
|
||||
}
|
||||
stop() {
|
||||
this.pattern = undefined;
|
||||
this.origin = undefined;
|
||||
}
|
||||
prebake() {
|
||||
return Promise.all([
|
||||
// doughsamples('github:eddyflux/crate')
|
||||
]);
|
||||
}
|
||||
// tbd: move this to dough-synth
|
||||
get time() {
|
||||
return this.t0 + (performance.now() - this.lasttime) / 1000 + this.latency;
|
||||
}
|
||||
processHaps() {
|
||||
const currentHaps = [];
|
||||
const time = this.time;
|
||||
this.q = this.q.filter(({ event, hap }) => {
|
||||
const end = event.time + event.duration;
|
||||
const isActive = time >= event.time && time <= end;
|
||||
if (isActive) {
|
||||
currentHaps.push(hap);
|
||||
}
|
||||
return end > time; // delete old events
|
||||
// we do NOT return !isActive, because a frame might miss an event, which would cause a leak
|
||||
});
|
||||
// console.log(this.q.length); // to check for leaks
|
||||
return currentHaps;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
import HeadCommon from '../../components/HeadCommon.astro';
|
||||
import Dough from '../../components/Dough/Dough.astro';
|
||||
---
|
||||
|
||||
<html lang="en" class="m-0 dark">
|
||||
<head>
|
||||
<HeadCommon />
|
||||
<title>Strudel Dough REPL</title>
|
||||
</head>
|
||||
<body class="h-app-height bg-background m-0">
|
||||
<Dough />
|
||||
<a rel="me" href="https://social.toplap.org/@strudel" target="_blank" class="hidden">mastodon</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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:
|
||||
|
||||
```js
|
||||
// @title Hey Hoo
|
||||
// @by Sam Tagada
|
||||
// @license CC BY-NC-SA
|
||||
// @title My Cool Song
|
||||
// @by John Doe
|
||||
// @license CC-BY-SA-4.0
|
||||
```
|
||||
|
||||
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
|
||||
/*
|
||||
@title Hey Hoo
|
||||
@by Sam Tagada
|
||||
@license CC BY-NC-SA
|
||||
@title My Cool Song
|
||||
@by John Doe
|
||||
@license CC-BY-SA-4.0
|
||||
*/
|
||||
```
|
||||
|
||||
Or define multiple tags in one line:
|
||||
|
||||
```js
|
||||
// @title Hey Hoo @by Sam Tagada @license CC BY-NC-SA
|
||||
// @title My Cool Song @by John Doe @license CC-BY-SA-4.0
|
||||
```
|
||||
|
||||
The `title` tag has an alternative syntax using quotes (must be defined at the very begining):
|
||||
|
||||
```js
|
||||
// "Hey Hoo" @by Sam Tagada
|
||||
// "My Cool Song" @by John Doe
|
||||
```
|
||||
|
||||
## Tags list
|
||||
@@ -48,20 +48,22 @@ Available tags are:
|
||||
|
||||
- `@title`: music title
|
||||
- `@by`: music author(s), separated by comma, eventually followed with a link in `<>` (ex: `@by John Doe <https://example.com>`)
|
||||
- `@license`: music license(s), e.g. CC BY-NC-SA. Unsure? [Choose a creative commons license here](https://creativecommons.org/choose/)
|
||||
- `@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/).
|
||||
- `@details`: some additional information about the music
|
||||
- `@url`: web page(s) related to the music (git repo, soundcloud link, etc.)
|
||||
- `@genre`: music genre(s) (pop, jazz, etc)
|
||||
- `@url`: web page(s) related to the music (git repository, Soundcloud link, etc.)
|
||||
- `@genre`: music genre(s) (pop, jazz, etc.)
|
||||
- `@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
|
||||
|
||||
Some of them accepts several values, using the comma or new line separator, or duplicating the tag:
|
||||
|
||||
```js
|
||||
/*
|
||||
@by Sam Tagada
|
||||
Jimmy
|
||||
@by John Doe
|
||||
Jane Doe
|
||||
@genre pop, jazz
|
||||
@url https://example.com
|
||||
@url https://example.org
|
||||
@@ -72,11 +74,11 @@ You can also add optional prefixes and use tags where you want:
|
||||
|
||||
```js
|
||||
/*
|
||||
song @by Sam Tagada
|
||||
samples @by Jimmy
|
||||
song @by John Doe
|
||||
samples @by Jane Doe
|
||||
*/
|
||||
...
|
||||
note("a3 c#4 e4 a4") // @by Sandy
|
||||
note("a3 c#4 e4 a4") // @by Sandy Sue
|
||||
```
|
||||
|
||||
## Multiline
|
||||
|
||||
@@ -142,6 +142,8 @@ The "~" represents a rest, and will create silence between other events:
|
||||
|
||||
<MiniRepl client:idle tune={`note("[b4 [~ c5] d5 e5]")`} punchcard />
|
||||
|
||||
Alternatively, "-" can be used instead of "~". It means the same thing.
|
||||
|
||||
## Parallel / polyphony
|
||||
|
||||
Using commas, we can play chords.
|
||||
|
||||
@@ -41,7 +41,7 @@ note("c3 [e3 g3]*2")
|
||||
is transpiled to:
|
||||
|
||||
```strudel
|
||||
note(m('c3 [e3 g3]', 5))
|
||||
note(m('c3 [e3 g3]*2', 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.
|
||||
|
||||
@@ -35,3 +35,24 @@ 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.
|
||||
- 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)
|
||||
|
||||
@@ -377,4 +377,4 @@ insect [crow metal] - -,
|
||||
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).
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function ExportTab(Props) {
|
||||
<div className="flex flex-row gap-4 w-full">
|
||||
<FormItem label="Start cycle" disabled={exporting}>
|
||||
<Textbox
|
||||
min={1}
|
||||
min={0}
|
||||
max={Infinity}
|
||||
onBlur={(e) => {
|
||||
let v = parseInt(e.target.value);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { errorLogger } from '@strudel/core';
|
||||
import { useSettings, storePrebakeScript } from '../../../settings.mjs';
|
||||
import { SpecialActionInput } from '../button/action-button';
|
||||
import { confirmDialog, SETTING_CHANGE_RELOAD_MSG } from '@src/repl/util.mjs';
|
||||
|
||||
async function importScript(script) {
|
||||
const reader = new FileReader();
|
||||
@@ -23,7 +24,19 @@ export function ImportPrebakeScriptButton() {
|
||||
type="file"
|
||||
label="import prebake script"
|
||||
accept=".strudel"
|
||||
onChange={(e) => importScript(e.target.files[0])}
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files[0];
|
||||
const confirmed = await confirmDialog(SETTING_CHANGE_RELOAD_MSG);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await importScript(file);
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
errorLogger(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { themes } from '@strudel/codemirror';
|
||||
import { Textbox } from '../textbox/Textbox.jsx';
|
||||
import { isUdels } from '../../util.mjs';
|
||||
import { confirmAndReloadPage, isUdels } from '../../util.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
||||
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
||||
import { confirmDialog } from '../../util.mjs';
|
||||
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
|
||||
import { ActionButton, SpecialActionButton } from '../button/action-button.jsx';
|
||||
import { SpecialActionButton } from '../button/action-button.jsx';
|
||||
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
|
||||
|
||||
function Checkbox({ label, value, onChange, disabled = false }) {
|
||||
@@ -86,8 +86,6 @@ const fontFamilyOptions = {
|
||||
galactico: 'galactico',
|
||||
};
|
||||
|
||||
const RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
|
||||
|
||||
export function SettingsTab({ started }) {
|
||||
const {
|
||||
theme,
|
||||
@@ -127,11 +125,8 @@ export function SettingsTab({ started }) {
|
||||
isDisabled={started}
|
||||
audioDeviceName={audioDeviceName}
|
||||
onChange={(audioDeviceName) => {
|
||||
confirmDialog(RELOAD_MSG).then((r) => {
|
||||
if (r == true) {
|
||||
settingsMap.setKey('audioDeviceName', audioDeviceName);
|
||||
return window.location.reload();
|
||||
}
|
||||
confirmAndReloadPage(() => {
|
||||
settingsMap.setKey('audioDeviceName', audioDeviceName);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -141,11 +136,8 @@ export function SettingsTab({ started }) {
|
||||
<AudioEngineTargetSelector
|
||||
target={audioEngineTarget}
|
||||
onChange={(target) => {
|
||||
confirmDialog(RELOAD_MSG).then((r) => {
|
||||
if (r == true) {
|
||||
settingsMap.setKey('audioEngineTarget', target);
|
||||
return window.location.reload();
|
||||
}
|
||||
confirmAndReloadPage(() => {
|
||||
settingsMap.setKey('audioEngineTarget', target);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -175,12 +167,9 @@ export function SettingsTab({ started }) {
|
||||
label="Multi Channel Orbits"
|
||||
onChange={(cbEvent) => {
|
||||
const val = cbEvent.target.checked;
|
||||
confirmDialog(RELOAD_MSG).then((r) => {
|
||||
if (r == true) {
|
||||
settingsMap.setKey('multiChannelOrbits', val);
|
||||
setMultiChannelOrbits(val);
|
||||
return window.location.reload();
|
||||
}
|
||||
confirmAndReloadPage(() => {
|
||||
settingsMap.setKey('multiChannelOrbits', val);
|
||||
setMultiChannelOrbits(val);
|
||||
});
|
||||
}}
|
||||
value={multiChannelOrbits}
|
||||
@@ -297,11 +286,8 @@ export function SettingsTab({ started }) {
|
||||
label="Sync across Browser Tabs / Windows"
|
||||
onChange={(cbEvent) => {
|
||||
const newVal = cbEvent.target.checked;
|
||||
confirmDialog(RELOAD_MSG).then((r) => {
|
||||
if (r) {
|
||||
settingsMap.setKey('isSyncEnabled', newVal);
|
||||
window.location.reload();
|
||||
}
|
||||
confirmAndReloadPage(() => {
|
||||
settingsMap.setKey('isSyncEnabled', newVal);
|
||||
});
|
||||
}}
|
||||
disabled={shouldAlwaysSync}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { code2hash, evalScope, hash2code, logger } from '@strudel/core';
|
||||
import { code2hash, errorLogger, evalScope, hash2code, logger } from '@strudel/core';
|
||||
import { settingPatterns } from '../settings.mjs';
|
||||
import { setVersionDefaults } from '@strudel/webaudio';
|
||||
import { getMetadata } from '../metadata_parser';
|
||||
@@ -107,7 +107,19 @@ export function confirmDialog(msg) {
|
||||
resolve(confirmed);
|
||||
});
|
||||
}
|
||||
|
||||
export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
|
||||
export function confirmAndReloadPage(onSuccess) {
|
||||
confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => {
|
||||
if (r == true) {
|
||||
try {
|
||||
onSuccess();
|
||||
return window.location.reload();
|
||||
} catch (e) {
|
||||
errorLogger(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
//RIP due to SPAM
|
||||
// let lastShared;
|
||||
// export async function shareCode(codeToShare) {
|
||||
|
||||
Reference in New Issue
Block a user