mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 06:19:33 -04:00
Merge remote-tracking branch 'origin/main' into nm/midi-input-state
This commit is contained in:
@@ -2,8 +2,62 @@
|
||||
|
||||
NOTE: you can generate this with `node warm.js`. it might still not be perfectly sorted...
|
||||
|
||||
## january 2026
|
||||
|
||||
- 2026-01-11T19:00:25+01:00 Fix sounds example to work in the REPL by JesCoding in: [#1851](https://codeberg.org/uzu/strudel/pulls/1851)
|
||||
- 2026-01-11T13:52:02+01:00 Improve hint text when sound search has no results by floy in: [#1883](https://codeberg.org/uzu/strudel/pulls/1883)
|
||||
- 2026-01-11T12:45:07+01:00 fix: remove faulty default readme by froos in: [#1889](https://codeberg.org/uzu/strudel/pulls/1889)
|
||||
- 2026-01-11T12:15:34+01:00 fix/self-hosted-config by alienmind in: [#1880](https://codeberg.org/uzu/strudel/pulls/1880)
|
||||
- 2026-01-11T12:07:48+01:00 Feat: Support External AudioContext Injection by 1d10t in: [#1833](https://codeberg.org/uzu/strudel/pulls/1833)
|
||||
- 2026-01-11T11:37:37+01:00 fix: add trem to top level by froos in: [#1887](https://codeberg.org/uzu/strudel/pulls/1887)
|
||||
- 2026-01-11T11:37:18+01:00 fix: export start cycle min 0 by froos in: [#1888](https://codeberg.org/uzu/strudel/pulls/1888)
|
||||
- 2026-01-11T11:05:44+01:00 fix: repl package init audio properly by froos in: [#1836](https://codeberg.org/uzu/strudel/pulls/1836)
|
||||
- 2026-01-11T06:51:24+01:00 Fix: show reload dialog when uploading prebake script by daslyfe in: [#1886](https://codeberg.org/uzu/strudel/pulls/1886)
|
||||
- 2026-01-11T06:01:42+01:00 fixes Serial onTrigger() params #1633 by gueejla in: [#1885](https://codeberg.org/uzu/strudel/pulls/1885)
|
||||
- 2026-01-10T20:52:40+01:00 Allow top level distortions for the purpose of FX by glossing in: [#1884](https://codeberg.org/uzu/strudel/pulls/1884)
|
||||
- 2026-01-10T23:12:48+01:00 Perf: Targeted node pools by glossing in: [#1810](https://codeberg.org/uzu/strudel/pulls/1810)
|
||||
- 2026-01-09T03:43:37+01:00 Bake in scaling by `freq` for FM with a gain node by glossing in: [#1878](https://codeberg.org/uzu/strudel/pulls/1878)
|
||||
- 2026-01-09T02:53:38+01:00 Bug fix: Properly handle subcontrols by glossing in: [#1877](https://codeberg.org/uzu/strudel/pulls/1877)
|
||||
- 2026-01-07T20:06:18+01:00 Feat: Add ability to turn mini parsing off with mini-off decorator by glossing in: [#1786](https://codeberg.org/uzu/strudel/pulls/1786)
|
||||
- 2026-01-07T19:22:24+01:00 Bug Fix: Update loopStart/End to not be offset by glossing in: [#1826](https://codeberg.org/uzu/strudel/pulls/1826)
|
||||
- 2026-01-06T06:35:49+01:00 Update modulator docstrings and allow ids to be patterns by glossing in: [#1874](https://codeberg.org/uzu/strudel/pulls/1874)
|
||||
- 2026-01-04T17:14:11+01:00 Fix doc link in @strudel/osc README.md by forrcaho in: [#1872](https://codeberg.org/uzu/strudel/pulls/1872)
|
||||
- 2026-01-04T02:01:07+01:00 Make stretch modulatable by glossing in: [#1870](https://codeberg.org/uzu/strudel/pulls/1870)
|
||||
- 2026-01-02T18:44:53+01:00 Make pan modulatable by glossing in: [#1865](https://codeberg.org/uzu/strudel/pulls/1865)
|
||||
- 2026-01-01T21:42:23+01:00 Fix transpilation example to have same mini-notation by JesCoding in: [#1850](https://codeberg.org/uzu/strudel/pulls/1850)
|
||||
- 2026-01-01T21:39:05+01:00 fix: missing punctuation by eddyflux in: [#1860](https://codeberg.org/uzu/strudel/pulls/1860)
|
||||
- 2026-01-01T20:20:34+01:00 Fix formatting of docstring by glossing in: [#1864](https://codeberg.org/uzu/strudel/pulls/1864)
|
||||
- 2026-01-01T19:52:15+01:00 Feat: FX Chains by glossing in: [#1861](https://codeberg.org/uzu/strudel/pulls/1861)
|
||||
|
||||
## december 2025
|
||||
|
||||
- 2025-12-29T21:59:11+01:00 Bugfix: Fix modulator clamping when min/max not specified by glossing in: [#1859](https://codeberg.org/uzu/strudel/pulls/1859)
|
||||
- 2025-12-29T16:07:18+01:00 Fix AudioContext change detection. Use AudioNode.context by jeromew in: [#1858](https://codeberg.org/uzu/strudel/pulls/1858)
|
||||
- 2025-12-28T14:20:03+01:00 dough repl fixes by froos in: [#1855](https://codeberg.org/uzu/strudel/pulls/1855)
|
||||
- 2025-12-19T08:11:50+01:00 fix: visual block selection mode for vim bindings by Dsm0 in: [#1839](https://codeberg.org/uzu/strudel/pulls/1839)
|
||||
- 2025-12-28T22:58:38+01:00 Say that @license should use SPDX identifier by Wuzzy in: [#1817](https://codeberg.org/uzu/strudel/pulls/1817)
|
||||
- 2025-12-28T22:56:07+01:00 Expose Vim object in order to create custom keybindings by JohnBjrk in: [#1816](https://codeberg.org/uzu/strudel/pulls/1816)
|
||||
- 2025-12-20T22:27:52+01:00 Document "-" in mini-notation by Wuzzy in: [#1818](https://codeberg.org/uzu/strudel/pulls/1818)
|
||||
- 2025-12-20T22:20:27+01:00 simplify envValAtTime and remove asymmetric behavior (fix #1653) by pulu in: [#1815](https://codeberg.org/uzu/strudel/pulls/1815)
|
||||
- 2025-12-14T15:09:12+01:00 delta -> per / perx / cyclesPer refinements by yaxu in: [#1832](https://codeberg.org/uzu/strudel/pulls/1832)
|
||||
- 2025-12-14T19:33:37+01:00 Fix: wrong warning in build environments by jeromew in: [#1835](https://codeberg.org/uzu/strudel/pulls/1835)
|
||||
- 2025-12-12T10:28:27+01:00 Add delta signal for representing the duration of events in patterns that are combined with it by yaxu in: [#1831](https://codeberg.org/uzu/strudel/pulls/1831)
|
||||
- 2025-12-11T16:37:58+01:00 Updates relating to LLM, github, etc by yaxu in: [#1830](https://codeberg.org/uzu/strudel/pulls/1830)
|
||||
- 2025-12-10T15:32:44+01:00 fix .as so it doesn't set undefined values by yaxu in: [#1827](https://codeberg.org/uzu/strudel/pulls/1827)
|
||||
- 2025-12-08T19:27:31+01:00 [perf] propagate `onceEnded` and `releaseAudioNode` by jeromew in: [#1809](https://codeberg.org/uzu/strudel/pulls/1809)
|
||||
- 2025-12-07T19:15:43+01:00 Add vel as a synonym for velocity & update a few docstrings by glossing in: [#1781](https://codeberg.org/uzu/strudel/pulls/1781)
|
||||
- 2025-12-07T18:53:04+01:00 [perf] fix phaser leak of unused biquads by jeromew in: [#1800](https://codeberg.org/uzu/strudel/pulls/1800)
|
||||
- 2025-12-07T18:42:50+01:00 Bug fix: Remove failing tests due to shabda removal by glossing in: [#1820](https://codeberg.org/uzu/strudel/pulls/1820)
|
||||
- 2025-12-03T17:35:05+01:00 [perf] release unused AudioBufferSourceNode + releaseAudioNode by jeromew in: [#1805](https://codeberg.org/uzu/strudel/pulls/1805)
|
||||
- **2025-12-01 strudel.cc deployed**
|
||||
|
||||
## november 2025
|
||||
|
||||
- 2025-11-29T01:00:42+01:00 added export to getSuperdoughAudioController() so that its possible to route superdough audio through other webaudio applications. by ndr0n in: [#1796](https://codeberg.org/uzu/strudel/pulls/1796)
|
||||
- 2025-11-28T23:26:20+01:00 add revv() for reversing whole patterns by yaxu in: [#1791](https://codeberg.org/uzu/strudel/pulls/1791)
|
||||
- 2025-11-28T20:19:16+01:00 [perf] Disconnect lfos for phaser and filters by glossing in: [#1787](https://codeberg.org/uzu/strudel/pulls/1787)
|
||||
- 2025-11-27T23:08:52+01:00 fix: return silence when no pattern is returned by froos in: [#1795](https://codeberg.org/uzu/strudel/pulls/1795)
|
||||
- 2025-11-27T22:38:05+01:00 add CHANGELOG.md + basic script to generate by froos in: [#1794](https://codeberg.org/uzu/strudel/pulls/1794)
|
||||
- 2025-11-27T22:03:53+01:00 [perf] in `noise`, let noiseMix do the disconnect when it exists by jeromew in: [#1783](https://codeberg.org/uzu/strudel/pulls/1783)
|
||||
- 2025-11-27T22:01:01+01:00 Bug Fix: Retries for sounds tab by glossing in: [#1754](https://codeberg.org/uzu/strudel/pulls/1754)
|
||||
- 2025-11-27T20:36:46+01:00 [hydra] return the hydra object when await initHydra(..) is called by jeromew in: [#1784](https://codeberg.org/uzu/strudel/pulls/1784)
|
||||
@@ -52,6 +106,7 @@ NOTE: you can generate this with `node warm.js`. it might still not be perfectly
|
||||
- 2025-10-28T22:51:16+01:00 Repurpose vim shortcuts for usability by dtricks in: [#1624](https://codeberg.org/uzu/strudel/pulls/1624)
|
||||
- 2025-10-28T22:35:21+01:00 Add support for euclidian in mondo with `bd&3:8` by TristanCacqueray in: [#1630](https://codeberg.org/uzu/strudel/pulls/1630)
|
||||
- 2025-10-27T10:42:07+01:00 Use bunny cdn for all samples and json files by yaxu in: [#1701](https://codeberg.org/uzu/strudel/pulls/1701)
|
||||
- **2025-10-27 @strudel/core@1.2.5**
|
||||
- 2025-10-26T22:52:54+01:00 degithub - switch some samples to bunnycdn by yaxu in: [#1697](https://codeberg.org/uzu/strudel/pulls/1697)
|
||||
- 2025-10-26T17:09:22+01:00 Fix ZZFX example by moumar in: [#1685](https://codeberg.org/uzu/strudel/pulls/1685)
|
||||
- 2025-10-23T15:56:04+02:00 Make osc port and host configurable. Changes dependencies. by yaxu in: [#1682](https://codeberg.org/uzu/strudel/pulls/1682)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script src="https://unpkg.com/@strudel/repl@1.0.2"></script>
|
||||
<script src="https://unpkg.com/@strudel/repl@1.2.7"></script>
|
||||
<!-- <script src="../../packages/repl/dist/index.js"></script> -->
|
||||
<strudel-editor>
|
||||
<!--
|
||||
// @date 23-08-15
|
||||
|
||||
@@ -24,6 +24,7 @@ import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||
import { activateTheme, initTheme, theme } from './themes.mjs';
|
||||
import { isTooltipEnabled } from './tooltip.mjs';
|
||||
import { updateWidgets, widgetPlugin } from './widget.mjs';
|
||||
import { jumpToCharacter } from './labelJump.mjs';
|
||||
|
||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||
|
||||
@@ -119,6 +120,14 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
preventDefault: true,
|
||||
run: () => onStop?.(),
|
||||
},
|
||||
{
|
||||
key: 'Alt-w',
|
||||
run: (view) => jumpToCharacter(view, '$', 1),
|
||||
},
|
||||
{
|
||||
key: 'Alt-q',
|
||||
run: (view) => jumpToCharacter(view, '$', -1),
|
||||
},
|
||||
/* {
|
||||
key: 'Ctrl-Shift-.',
|
||||
run: () => (onPanic ? onPanic() : onStop?.()),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import { SearchCursor } from '@codemirror/search';
|
||||
|
||||
export function jumpToCharacter(view, character, direction = 1) {
|
||||
const { state, dispatch } = view;
|
||||
const pos = state.selection.main.head;
|
||||
const cursor = new SearchCursor(state.doc, character);
|
||||
|
||||
let characterPositions = [];
|
||||
let jumpPos;
|
||||
while (!cursor.next().done) {
|
||||
characterPositions.push(cursor.value.to);
|
||||
}
|
||||
if (!characterPositions.length) {
|
||||
return false;
|
||||
}
|
||||
if (direction > 0) {
|
||||
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
|
||||
} else {
|
||||
jumpPos = characterPositions.reverse().find((x) => x < pos + 1) ?? characterPositions.at(0);
|
||||
}
|
||||
|
||||
if (jumpPos == null) {
|
||||
return false;
|
||||
}
|
||||
dispatch({
|
||||
selection: EditorSelection.cursor(jumpPos - 1),
|
||||
scrollIntoView: true,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -877,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
|
||||
|
||||
@@ -22,7 +22,7 @@ export * from './repl.mjs';
|
||||
export * from './signal.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * from './state.mjs';
|
||||
export * from './time.mjs';
|
||||
export * from './schedulerState.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './ui.mjs';
|
||||
export * from './util.mjs';
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
},
|
||||
"homepage": "https://strudel.cc",
|
||||
"dependencies": {
|
||||
"@kabelsalat/web": "^0.4.0",
|
||||
"fraction.js": "^5.2.1"
|
||||
},
|
||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||
|
||||
+109
-8
@@ -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
|
||||
@@ -3753,3 +3774,83 @@ Pattern.prototype.FX = function (...effects) {
|
||||
return { ...v, FX: currFX.concat(vEff) };
|
||||
}).appLeft(parray(effects));
|
||||
};
|
||||
|
||||
const _asArrayPattern = (pats) => {
|
||||
const pack = (...xs) => xs;
|
||||
let acc = pure(curry(pack, null, pats.length));
|
||||
for (const p of pats) acc = acc.appLeft(p);
|
||||
return acc;
|
||||
};
|
||||
|
||||
/**
|
||||
* Produces a [Kabelsalat](https://kabel.salat.dev/) modular sound engine.
|
||||
* This can be used as either an effect (by including `audioin()` at the beginning
|
||||
* of your kabel expression) or as a sound source (via any expression which doesn't
|
||||
* start with `audioin()`).
|
||||
*
|
||||
* Some helpers you have available to you:
|
||||
* * Strudel mini notation works fine in K(..) via "" or ``
|
||||
* * More complex Strudel expressions (like "0 1 2".fast(4) or irand(24)) can be
|
||||
* written by wrapping them in `S(..)` inside your Kabel code
|
||||
* * We expose Strudel's note frequency under `sFreq` and Strudel's gate
|
||||
* information under `sGate`
|
||||
* * You can use more complex multi-line expressions (like `let x = a; let y = b; x.lpf(y);`)
|
||||
* by wrapping them inside a function in K (see example).
|
||||
*
|
||||
* @name K
|
||||
* @param {KabelsalatExpression | Function} expr Kabelsalat graph definition
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
*
|
||||
* @example
|
||||
* note("A c e".fast(4)).transpose("<0 2 4 6 8>")
|
||||
* .scale("F:minor").transpose("12")
|
||||
* .s("saw")
|
||||
* .K(
|
||||
* // audioin().mul(sGate.adsr(0.001, 0.3, 0, 0.2)) // as effect
|
||||
* saw(saw(sFreq / "2!3 16").mul(8).add(sFreq).lag("0!3 0.1")).mul(0.3) // as source
|
||||
* .mul(sGate.adsr(0, 0.15, 0.5, "0.1!3 1"))
|
||||
* .lpf(sGate.adsr(0, 0.2, 0.3, 0.2).mul(1).add(0))
|
||||
* .add(x => x.delay(S("0.3 0.2".fast(2))).mul(0.7))
|
||||
* .add(x => x.delay("0.03 [0.08 0.01] 0.01 0.013").mul(0.77)).mul(0.7)
|
||||
* .add(x => x.delay(.13).mul(0.7))
|
||||
* .out()
|
||||
* )
|
||||
*
|
||||
* @example
|
||||
* n("<0 1 <2 3 2 4>>*16")
|
||||
* .scale("G#2:minor").sometimes(x => x.transpose("12 | 24"))
|
||||
* .K(() => {
|
||||
* const att = S(rand.range(0, 0.05))
|
||||
* const dec = S(rand.range(0.05, 0.2))
|
||||
* let f = n(sFreq);
|
||||
* const mod = sine(f).mul("0.1 | 0.2 | 0.3")
|
||||
* .add("[[1.5 1] | 1 | 2 | 4 | [6 4@3]]*2")
|
||||
* saw(f.mul(mod))
|
||||
* .mul(sGate.ad(att, dec))
|
||||
* .add(x => x.delay(0.4).mul(0.3))
|
||||
* .out()
|
||||
* }).fxr(1).room(0.3)
|
||||
*/
|
||||
/**
|
||||
* Creates a worklet effect. Typically derived by writing K(...) in the REPL which will parse
|
||||
* Kabelsalat code.
|
||||
*
|
||||
* @name worklet
|
||||
* @param {string} src Source code of the worklet update function
|
||||
* @param {...number | ...Pattern} inputs Worklet inputs
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
Pattern.prototype.worklet = function (src, ...inputs) {
|
||||
inputs = inputs.map(reify);
|
||||
return this.outerBind((v) => {
|
||||
return _asArrayPattern(inputs).withValue((vInput) => {
|
||||
const currInputs = v.workletInputs ?? [];
|
||||
return { ...v, workletSrc: src, workletInputs: currInputs.concat(vInput) };
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const worklet = (...args) => pure({}).worklet(...args);
|
||||
|
||||
+19
-1
@@ -2,10 +2,17 @@ import { NeoCyclist } from './neocyclist.mjs';
|
||||
import { Cyclist } from './cyclist.mjs';
|
||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||
import { errorLogger, logger } from './logger.mjs';
|
||||
import { setTime } from './time.mjs';
|
||||
import {
|
||||
setCpsFunc,
|
||||
setIsStarted,
|
||||
setPattern as exposeSchedulerPattern,
|
||||
setTime,
|
||||
setTriggerFunc,
|
||||
} from './schedulerState.mjs';
|
||||
import { evalScope } from './evaluate.mjs';
|
||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||
import { reset_state } from './impure.mjs';
|
||||
import { SalatRepl } from '@kabelsalat/web';
|
||||
|
||||
export function repl({
|
||||
defaultOutput,
|
||||
@@ -24,6 +31,7 @@ export function repl({
|
||||
id,
|
||||
mondo = false,
|
||||
}) {
|
||||
const kabel = new SalatRepl({ localScope: true });
|
||||
const state = {
|
||||
schedulerError: undefined,
|
||||
evalError: undefined,
|
||||
@@ -52,6 +60,7 @@ export function repl({
|
||||
getTime,
|
||||
onToggle: (started) => {
|
||||
updateState({ started });
|
||||
setIsStarted(started);
|
||||
onToggle?.(started);
|
||||
if (!started) {
|
||||
reset_state();
|
||||
@@ -65,6 +74,8 @@ export function repl({
|
||||
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
||||
const scheduler =
|
||||
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
||||
setTriggerFunc(schedulerOptions.onTrigger);
|
||||
setCpsFunc(() => scheduler.cps);
|
||||
let pPatterns = {};
|
||||
let anonymousIndex = 0;
|
||||
let allTransform;
|
||||
@@ -78,6 +89,11 @@ export function repl({
|
||||
return silence;
|
||||
};
|
||||
|
||||
const compileKabel = (code) => {
|
||||
const node = kabel.evaluate(code);
|
||||
return node.compile({ log: false });
|
||||
};
|
||||
|
||||
// helper to get a patternified pure value out
|
||||
function unpure(pat) {
|
||||
if (pat._Pattern) {
|
||||
@@ -89,6 +105,7 @@ export function repl({
|
||||
const setPattern = async (pattern, autostart = true) => {
|
||||
pattern = editPattern?.(pattern) || pattern;
|
||||
await scheduler.setPattern(pattern, autostart);
|
||||
exposeSchedulerPattern(pattern);
|
||||
return pattern;
|
||||
};
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
@@ -198,6 +215,7 @@ export function repl({
|
||||
setcps: setCps,
|
||||
setCpm,
|
||||
setcpm: setCpm,
|
||||
compileKabel,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
schedulerState.mjs - Module to pipe out various parameters from the scheduler for global consumption
|
||||
Copyright (C) 2026 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/schedulerState.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 time;
|
||||
let cpsFunc;
|
||||
let pattern;
|
||||
let triggerFunc;
|
||||
let isStarted;
|
||||
export function getTime() {
|
||||
if (!time) {
|
||||
throw new Error('no time set! use setTime to define a time source');
|
||||
}
|
||||
return time();
|
||||
}
|
||||
|
||||
export function setTime(func) {
|
||||
time = func;
|
||||
}
|
||||
|
||||
export function setCpsFunc(func) {
|
||||
cpsFunc = func;
|
||||
}
|
||||
|
||||
export function getCps() {
|
||||
return cpsFunc?.();
|
||||
}
|
||||
|
||||
export function setPattern(pat) {
|
||||
pattern = pat;
|
||||
}
|
||||
|
||||
export function getPattern() {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
export function setTriggerFunc(func) {
|
||||
triggerFunc = func;
|
||||
}
|
||||
|
||||
export function getTriggerFunc() {
|
||||
return triggerFunc;
|
||||
}
|
||||
|
||||
export function setIsStarted(val) {
|
||||
isStarted = !!val;
|
||||
}
|
||||
|
||||
export function getIsStarted() {
|
||||
return isStarted;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
let time;
|
||||
export function getTime() {
|
||||
if (!time) {
|
||||
throw new Error('no time set! use setTime to define a time source');
|
||||
}
|
||||
return time();
|
||||
}
|
||||
|
||||
export function setTime(func) {
|
||||
time = func;
|
||||
}
|
||||
+189
-25
@@ -5,9 +5,23 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import * as _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern, logger } from '@strudel/core';
|
||||
import {
|
||||
Hap,
|
||||
Pattern,
|
||||
TimeSpan,
|
||||
getCps,
|
||||
getIsStarted,
|
||||
getPattern,
|
||||
getTime,
|
||||
getTriggerFunc,
|
||||
isPattern,
|
||||
logger,
|
||||
ref,
|
||||
reify,
|
||||
} from '@strudel/core';
|
||||
import { noteToMidi, getControlName } from '@strudel/core';
|
||||
import { Note } from 'webmidi';
|
||||
import { getAudioContext } from '@strudel/webaudio';
|
||||
import { scheduleAtTime } from '../superdough/helpers.mjs';
|
||||
import { getMidiDeviceNamesString, getDevice } from './util.mjs';
|
||||
import { MidiInput } from './input.mjs';
|
||||
@@ -452,6 +466,39 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize a midi input device
|
||||
*/
|
||||
async function _initializeInput(input) {
|
||||
if (isPattern(input)) {
|
||||
throw new Error(
|
||||
`[midi] Midi input cannot be a pattern. Make sure to pass device name with single quotes. Example: midin('${
|
||||
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
||||
}')`,
|
||||
);
|
||||
}
|
||||
|
||||
const initial = await enableWebMidi(); // only returns on first init
|
||||
|
||||
const instance = midiInputs[input] || new MidiInput(input);
|
||||
midiInputs[input] = instance;
|
||||
|
||||
if (initial) {
|
||||
const device = instance.initialDevice;
|
||||
|
||||
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
||||
logger(
|
||||
device
|
||||
? `[midi] Midi enabled! Using "${device.name}". ${
|
||||
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
|
||||
}`
|
||||
: `[midi] Midi enabled! Waiting for device "${input}"... Currently connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
// MIDI input wrappers, by specified input string/index
|
||||
const midiInputs = {};
|
||||
|
||||
@@ -459,6 +506,8 @@ const midiInputs = {};
|
||||
* MIDI input: Opens a MIDI input port to receive MIDI control change messages.
|
||||
*
|
||||
* The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel
|
||||
*
|
||||
* @name midin
|
||||
* @param {string | number} input MIDI device name or index defaulting to 0
|
||||
* @returns {function(number, number=): Pattern} A function from (cc, channel?) to a pattern.
|
||||
* When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1)
|
||||
@@ -473,30 +522,145 @@ const midiInputs = {};
|
||||
* .when(cc(0).gt(0), x => x.postgain(0))
|
||||
*/
|
||||
export async function midin(input) {
|
||||
if (isPattern(input)) {
|
||||
throw new Error(
|
||||
`midin: does not accept Pattern as input. Make sure to pass device name with single quotes. Example: midin('${
|
||||
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
||||
}')`,
|
||||
);
|
||||
}
|
||||
const initial = await enableWebMidi(); // only returns on first init
|
||||
|
||||
const instance = midiInputs[input] || new MidiInput(input);
|
||||
midiInputs[input] = instance;
|
||||
|
||||
if (initial) {
|
||||
const device = instance.initialDevice;
|
||||
|
||||
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
||||
logger(
|
||||
device
|
||||
? `Midi enabled! Using "${device.name}". ${
|
||||
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
|
||||
}`
|
||||
: `Midi enabled! Waiting for device "${input}"... Currently connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
|
||||
);
|
||||
}
|
||||
const instance = await _initializeInput(input);
|
||||
|
||||
return instance.createCC.bind(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* MIDI keyboard: Opens a MIDI input port to receive MIDI keyboard messages.
|
||||
*
|
||||
* The note length is fixed as Superdough is not currently set up for undetermined
|
||||
* note durations
|
||||
*
|
||||
* @name midikeys
|
||||
* @param {string | number} input MIDI device name or index defaulting to 0
|
||||
* @returns {function((number | Pattern)=): Pattern} A function that produces a pattern.
|
||||
* When queried, the pattern will produces the most recently played midi notes and velocities,
|
||||
* lasting for the specified duration
|
||||
* @example
|
||||
* const kb = await midikeys('Arturia KeyStep 32')
|
||||
* kb().s("tri").lpf(80).lpe(6).lpd(0.1).room(2).delay(0.35)
|
||||
* @example
|
||||
* const kb = await midikeys('Arturia KeyStep 32')
|
||||
* kb("0.5 1")
|
||||
* .s("saw")
|
||||
* .add(note(rand.mul(0.3)))
|
||||
* .lpf(1000).lpe(2).room(0.5)
|
||||
*/
|
||||
const kHaps = {};
|
||||
const kListeners = {};
|
||||
|
||||
function _triggerKeyboard(input, cps, now, latencyCycles) {
|
||||
const pattern = getPattern();
|
||||
const trigger = getTriggerFunc();
|
||||
if (!pattern || !trigger) {
|
||||
return false;
|
||||
}
|
||||
const t = now + latencyCycles;
|
||||
const eps = 1e-6;
|
||||
const haps = pattern.queryArc(t - eps, t + eps, { _cps: cps });
|
||||
// Only keep haps coming from `midikeys`
|
||||
const kbHaps = haps.filter((hap) => hap.value?.midikey?.startsWith(`${input}_`));
|
||||
const ctxNow = getAudioContext().currentTime;
|
||||
if (!kbHaps.length) {
|
||||
return false;
|
||||
}
|
||||
kbHaps.forEach((hap) => {
|
||||
if (!hap.hasOnset()) {
|
||||
return;
|
||||
}
|
||||
const t = ctxNow + (hap.whole.begin - now) / cps;
|
||||
const duration = hap.duration / cps;
|
||||
trigger(hap, t - ctxNow, duration, cps, t);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
export async function midikeys(input) {
|
||||
const instance = await _initializeInput(input);
|
||||
|
||||
// TODO: support unpluggable device usage
|
||||
const device = instance.initialDevice;
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`[midi] Midi device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!kHaps[input]) {
|
||||
kHaps[input] = [];
|
||||
}
|
||||
kListeners[input] && device.removeListener('midimessage', kListeners[input]);
|
||||
kListeners[input] = (e) => {
|
||||
const { dataBytes, message } = e;
|
||||
const noteon = message.command === 9;
|
||||
let noteoff = message.command === 8;
|
||||
// Don't enqueue or trigger midi notes if scheduler is not started
|
||||
const notStarted = !getIsStarted();
|
||||
// Ignore non-note messages (e.g. CC, pitchbend, modwheel, etc.)
|
||||
const notANote = !noteon && !noteoff;
|
||||
if (notStarted || notANote) {
|
||||
return;
|
||||
}
|
||||
const [note, velocity] = dataBytes;
|
||||
noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff
|
||||
const key = `${input}_${note}`;
|
||||
const cps = getCps() ?? 0.5;
|
||||
const triggerAvailable = !!(getPattern() && getTriggerFunc());
|
||||
const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency
|
||||
const now = getTime();
|
||||
const t = now + latencySeconds * cps;
|
||||
const span = new TimeSpan(t, t);
|
||||
let value = { midikey: key };
|
||||
if (noteoff) {
|
||||
/* TODO: It's a big effort, but we could modify superdough to allow for situations where
|
||||
we don't know the hap duration in advance. This would mean, for example, that if the hap
|
||||
is flagged as such a special note-on event, we have all effects be persistent & all ADSR
|
||||
envelopes stop at the S stage [and store references to them by `midikey`]
|
||||
If this is implemented, then getting full keyboard functionality should be as simple
|
||||
as sending the corresponding note-off event below and triggering `release` on each of those
|
||||
referenced effects/envelopes
|
||||
|
||||
value = { ...value, noteoff: true };
|
||||
|
||||
If this is achieved, we can remove the noteLength parameter
|
||||
*/
|
||||
return;
|
||||
} else {
|
||||
value = { ...value, note: Math.round(note), velocity: velocity / 127 };
|
||||
}
|
||||
kHaps[input].push(new Hap(span, span, value, {}));
|
||||
if (!noteoff && triggerAvailable) {
|
||||
// If we have access to a trigger function, we call it to immediately
|
||||
// dispatch to the audio engine, rather than waiting for cyclist to catch these haps
|
||||
const triggered = _triggerKeyboard(input, cps, now, latencySeconds * cps);
|
||||
if (triggered) {
|
||||
kHaps[input] = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
device.addListener('midimessage', kListeners[input]);
|
||||
const kb = (noteLength = 0.5) => {
|
||||
const nlPat = reify(noteLength);
|
||||
const query = (state) => {
|
||||
const haps = kHaps[input].flatMap((hap) => {
|
||||
const lenHaps = nlPat.query(state.setSpan(hap.wholeOrPart()));
|
||||
return lenHaps.map((lenHap) => {
|
||||
const nl = lenHap.value ?? 0.5;
|
||||
const whole = new TimeSpan(hap.whole.begin, hap.whole.begin.add(nl));
|
||||
const part = new TimeSpan(hap.part.begin, hap.part.begin.add(nl));
|
||||
const context = hap.combineContext(lenHap);
|
||||
return new Hap(whole, part, hap.value, context);
|
||||
});
|
||||
});
|
||||
if (state.controls.cyclist) {
|
||||
// Notes have been sent; clear them
|
||||
kHaps[input] = [];
|
||||
}
|
||||
return haps;
|
||||
};
|
||||
return new Pattern(query);
|
||||
};
|
||||
return kb;
|
||||
}
|
||||
|
||||
Executable → Regular
@@ -1,11 +1,12 @@
|
||||
import { silence } from '@strudel/core';
|
||||
import { getDrawContext } from '@strudel/draw';
|
||||
import { transpiler } from '@strudel/transpiler';
|
||||
import { getAudioContext, webaudioOutput } from '@strudel/webaudio';
|
||||
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
|
||||
import { StrudelMirror, codemirrorSettings } from '@strudel/codemirror';
|
||||
import { prebake } from './prebake.mjs';
|
||||
|
||||
if (typeof HTMLElement !== 'undefined') {
|
||||
initAudioOnFirstClick();
|
||||
class StrudelRepl extends HTMLElement {
|
||||
static observedAttributes = ['code'];
|
||||
settings = codemirrorSettings.get();
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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'];
|
||||
@@ -145,6 +146,7 @@ export function getLfo(audioContext, 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,
|
||||
@@ -152,7 +154,10 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) {
|
||||
attack: attack ?? 0.005,
|
||||
release: release ?? 0.05,
|
||||
};
|
||||
return new DynamicsCompressorNode(ac, options);
|
||||
Object.entries(options).forEach(([key, value]) => {
|
||||
node[key].value = value;
|
||||
});
|
||||
return node;
|
||||
}
|
||||
|
||||
// changes the default values of the envelope based on what parameters the user has defined
|
||||
@@ -233,10 +238,12 @@ 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;
|
||||
Object.entries({ Q: q, frequency }).forEach(([key, value]) => {
|
||||
filter[key].value = value;
|
||||
});
|
||||
frequencyParam = filter.frequency;
|
||||
}
|
||||
const envelopeValues = [params.attack, params.decay, params.sustain, params.release];
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
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');
|
||||
|
||||
export const isPoolable = (node) => !!node[POOL_KEY];
|
||||
|
||||
const getNodeTime = (node) => {
|
||||
return node.context?.currentTime ?? 0;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
const key = node[POOL_KEY];
|
||||
if (key == null) return;
|
||||
const now = getNodeTime(node);
|
||||
getParams(node).forEach((param) => param.cancelScheduledValues(now));
|
||||
const pool = nodePools.get(key) ?? [];
|
||||
pool.push(new WeakRef(node));
|
||||
nodePools.set(key, pool);
|
||||
};
|
||||
|
||||
// Audio worklets are given a grace period to survive (`return true`) after
|
||||
// being released. This concludes at time `end + 0.5`. We test here whether we are
|
||||
// within some safe distance of that (`end + 0.45`) and if so, permit the node to be
|
||||
// released. This helps to prevent race conditions between node termination and node
|
||||
// re-use
|
||||
const isNodeAlive = (node) => {
|
||||
// Skip check if node is not a worklet
|
||||
if (!(node instanceof AudioWorkletNode)) return true;
|
||||
const now = getNodeTime(node);
|
||||
const end = node?.parameters?.get('end').value ?? 0;
|
||||
return now < end + 0.45;
|
||||
};
|
||||
|
||||
// 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;
|
||||
let found = false;
|
||||
while (pool.length) {
|
||||
const ref = pool.pop();
|
||||
node = ref?.deref();
|
||||
if (node != null && isNodeAlive(node)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
node = factory();
|
||||
}
|
||||
node[POOL_KEY] = key;
|
||||
return node;
|
||||
};
|
||||
@@ -36,6 +36,7 @@
|
||||
"vite-plugin-bundle-audioworklet": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kabelsalat/lib": "^0.4.0",
|
||||
"nanostores": "^0.11.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -9,12 +9,14 @@ import './reverb.mjs';
|
||||
import './vowel.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,
|
||||
getFrequencyFromValue,
|
||||
getLfo,
|
||||
getWorklet,
|
||||
releaseAudioNode,
|
||||
@@ -342,7 +344,7 @@ function getPhaser(begin, end, frequency = 1, depth = 0.5, centerFrequency = 100
|
||||
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;
|
||||
@@ -407,9 +409,9 @@ function mapChannelNumbers(channels) {
|
||||
}
|
||||
|
||||
class Chain {
|
||||
constructor(head) {
|
||||
this.audioNodes = [head];
|
||||
this.tails = [head];
|
||||
constructor() {
|
||||
this.audioNodes = [];
|
||||
this.tails = [];
|
||||
}
|
||||
connect(...nodes) {
|
||||
nodes.forEach((node) => {
|
||||
@@ -428,7 +430,7 @@ class Chain {
|
||||
return this;
|
||||
}
|
||||
releaseNodes() {
|
||||
this.audioNodes.forEach((n) => releaseAudioNode(n));
|
||||
this.audioNodes.forEach((n) => (isPoolable(n) ? releaseNodeToPool(n) : releaseAudioNode(n)));
|
||||
this.audioNodes = [];
|
||||
this.tails = [];
|
||||
}
|
||||
@@ -538,6 +540,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
value.s = s;
|
||||
}
|
||||
|
||||
const chain = new Chain(); // connection manager which tracks audio nodes for releasing
|
||||
|
||||
// get source AudioNode
|
||||
let sourceNode;
|
||||
if (source) {
|
||||
@@ -545,8 +549,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
nodes.main['source'] = [sourceNode];
|
||||
} else if (getSound(s)) {
|
||||
const { onTrigger } = getSound(s);
|
||||
// We have to use onEnded because some sources (e.g. `sampler`) have
|
||||
// an internal duration which is longer than `value.duration`
|
||||
|
||||
const onEnded = () =>
|
||||
webAudioTimeout(
|
||||
ac,
|
||||
@@ -557,6 +560,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
0,
|
||||
endWithRelease,
|
||||
);
|
||||
|
||||
const soundHandle = await onTrigger(t, value, onEnded, cps);
|
||||
|
||||
if (soundHandle) {
|
||||
@@ -578,7 +582,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
return;
|
||||
}
|
||||
|
||||
const chain = new Chain(sourceNode); // connection manager which tracks audio nodes for releasing
|
||||
chain.connect(sourceNode);
|
||||
|
||||
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;
|
||||
@@ -596,7 +601,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
delayfeedback = getDefaultValue('delayfeedback'),
|
||||
delaysync = getDefaultValue('delaysync'),
|
||||
delaytime,
|
||||
stretch = getDefaultValue('stretch'),
|
||||
i = getDefaultValue('i'),
|
||||
} = fx;
|
||||
gain = applyGainCurve(nanFallback(gain, 1));
|
||||
@@ -607,8 +611,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
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 });
|
||||
// Kabelsalat
|
||||
if (fx.workletSrc !== undefined) {
|
||||
const workletNode = getWorklet(ac, 'generic-processor', {});
|
||||
chain.connect(workletNode);
|
||||
const workletSrc = fx.workletSrc
|
||||
.replace(/\bpat\[(\d+)\]/g, (_, i) => fx.workletInputs[i])
|
||||
.replaceAll('sFreq', getFrequencyFromValue(value))
|
||||
.replaceAll('sGate', `cc('strudel-gate-${chainID}')`);
|
||||
/* global compileKabel */
|
||||
const { src, ugens, registers } = compileKabel(workletSrc);
|
||||
workletNode.port.postMessage({ src, schema: { ugens, registers }, start: t, gateEnd: end, end: endWithRelease });
|
||||
}
|
||||
|
||||
if (fx.stretch !== undefined) {
|
||||
const phaseVocoder = getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: fx.stretch });
|
||||
chain.connect(phaseVocoder);
|
||||
fxNodes['stretch'] = [phaseVocoder];
|
||||
}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/*
|
||||
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';
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||
import { getNodeFromPool, releaseNodeToPool } from './nodePools.mjs';
|
||||
|
||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user', 'one'];
|
||||
const waveformAliases = [
|
||||
@@ -167,22 +168,22 @@ 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);
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
const param = o.parameters.get(key);
|
||||
const target = value !== undefined ? value : param.defaultValue;
|
||||
param.value = target;
|
||||
});
|
||||
o.port.postMessage({ type: 'initialize' });
|
||||
const gainAdjustment = 1 / Math.sqrt(voices);
|
||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||
const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||
@@ -195,7 +196,7 @@ export function registerSynthSounds() {
|
||||
let timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
releaseAudioNode(o);
|
||||
releaseNodeToPool(o);
|
||||
onended();
|
||||
fmHandle?.stop();
|
||||
vibratoHandle?.stop();
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
getParamADSR,
|
||||
getPitchEnvelope,
|
||||
getVibratoOscillator,
|
||||
getWorklet,
|
||||
releaseAudioNode,
|
||||
webAudioTimeout,
|
||||
releaseAudioNode,
|
||||
} from './helpers.mjs';
|
||||
import { getNodeFromPool, releaseNodeToPool } from './nodePools.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
export const Warpmode = Object.freeze({
|
||||
@@ -230,24 +230,26 @@ 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);
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
const param = source.parameters.get(key);
|
||||
const target = value !== undefined ? value : param.defaultValue;
|
||||
param.value = target;
|
||||
});
|
||||
source.port.postMessage({ type: 'initialize', payload });
|
||||
if (ac.currentTime > t) {
|
||||
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
||||
return;
|
||||
@@ -333,7 +335,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
const timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
releaseAudioNode(source);
|
||||
releaseNodeToPool(source);
|
||||
vibratoHandle?.stop();
|
||||
fmHandle?.stop();
|
||||
releaseAudioNode(wtPosModulators);
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
import OLAProcessor from './ola-processor';
|
||||
import FFT from './fft.js';
|
||||
import { getDistortionAlgorithm } from './helpers.mjs';
|
||||
import * as ugens from '@kabelsalat/lib/src/ugens.js';
|
||||
|
||||
const UGENS = new Map(Object.entries(ugens));
|
||||
|
||||
const blockSize = 128;
|
||||
const PI = Math.PI;
|
||||
@@ -463,22 +466,31 @@ registerProcessor('distort-processor', DistortProcessor);
|
||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.port.onmessage = (e) => {
|
||||
const { type, payload } = e.data || {};
|
||||
if (type === 'initialize') {
|
||||
this.initialize(payload);
|
||||
}
|
||||
};
|
||||
this.initialize();
|
||||
}
|
||||
initialize(_options) {
|
||||
this.phase = [];
|
||||
}
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{
|
||||
name: 'begin',
|
||||
defaultValue: 0,
|
||||
defaultValue: -1,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
min: -1,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'end',
|
||||
defaultValue: 0,
|
||||
defaultValue: -1,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
min: -1,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -513,12 +525,17 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
];
|
||||
}
|
||||
process(_input, outputs, params) {
|
||||
if (currentTime >= params.end[0]) {
|
||||
// should terminate
|
||||
const begin = params.begin[0];
|
||||
const end = params.end[0];
|
||||
const beginDefined = begin >= 0;
|
||||
const endDefined = end >= 0;
|
||||
// We give a 0.5s grace period (for node pooling) before termination
|
||||
const shouldTerminate = endDefined && currentTime >= end + 0.5;
|
||||
const ended = endDefined && currentTime >= end;
|
||||
const notStarted = currentTime <= begin;
|
||||
if (shouldTerminate) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= params.begin[0]) {
|
||||
// keep alive
|
||||
} else if (ended || notStarted || !beginDefined) {
|
||||
return true;
|
||||
}
|
||||
const output = outputs[0];
|
||||
@@ -1134,8 +1151,8 @@ const tablesCache = {};
|
||||
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'begin', defaultValue: -1, min: -1, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'end', defaultValue: -1, min: -1, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
||||
{ name: 'detune', defaultValue: 0 },
|
||||
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
||||
@@ -1150,37 +1167,28 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.frameLen = 0;
|
||||
this.numFrames = 0;
|
||||
this.phase = [];
|
||||
|
||||
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) {
|
||||
@@ -1325,25 +1333,23 @@ 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]) {
|
||||
const begin = parameters.begin[0];
|
||||
const end = parameters.end[0];
|
||||
const beginDefined = begin >= 0;
|
||||
const endDefined = end >= 0;
|
||||
// We give a 0.5s grace period (for node pooling) before termination
|
||||
const shouldTerminate = endDefined && currentTime >= end + 0.5;
|
||||
const ended = endDefined && currentTime >= end;
|
||||
const notStarted = currentTime <= begin;
|
||||
if (shouldTerminate) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= parameters.begin[0]) {
|
||||
} else if (ended || notStarted || !beginDefined) {
|
||||
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;
|
||||
@@ -1377,14 +1383,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;
|
||||
@@ -1478,3 +1482,73 @@ class TransientProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
registerProcessor('transient-processor', TransientProcessor);
|
||||
|
||||
class GenericProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.playPos = 0;
|
||||
const channels = 16;
|
||||
this.outputs = new Array(channels).fill(0);
|
||||
this.sources = new Array(channels).fill(0);
|
||||
this.gateEnded = false;
|
||||
this.started = false;
|
||||
this.port.onmessage = (event) => {
|
||||
let {
|
||||
src,
|
||||
schema: { ugens, registers },
|
||||
start,
|
||||
gateEnd,
|
||||
end,
|
||||
} = event.data;
|
||||
this.start = start;
|
||||
this.gateEnd = gateEnd;
|
||||
this.end = end;
|
||||
this.registers = new Array(registers).fill(0);
|
||||
this.src = `o.fill(0); // reset outputs\n${src}`;
|
||||
this.nodes = [];
|
||||
for (let i = 0; i < ugens.length; i++) {
|
||||
const ugen = ugens[i];
|
||||
const nodeClass = UGENS.get(ugen.type);
|
||||
const node = new nodeClass(i, ugen, sampleRate);
|
||||
if (node.type === 'cc' && ugen.inputs?.[0]?.includes('strudel-gate')) {
|
||||
node.setValue(1);
|
||||
this.gateNode = node;
|
||||
}
|
||||
this.nodes[i] = node;
|
||||
}
|
||||
this.genSample = new Function(
|
||||
'time',
|
||||
'nodes',
|
||||
'input',
|
||||
'r', // registers
|
||||
'o', // outputs
|
||||
's', // sources
|
||||
this.src,
|
||||
);
|
||||
};
|
||||
}
|
||||
process(inputs, outputs) {
|
||||
const input = inputs[0]?.[0];
|
||||
if (currentTime >= this.end) {
|
||||
return false;
|
||||
} else if (this.genSample === undefined || currentTime < this.start) {
|
||||
// pending
|
||||
return true;
|
||||
}
|
||||
this.started = true;
|
||||
if (!this.gateEnded && currentTime > this.gateEnd) {
|
||||
this.gateNode?.setValue(0);
|
||||
this.gateEnded = true;
|
||||
}
|
||||
const outL = outputs[0][0];
|
||||
const outR = outputs[0][1] ?? outputs[0][0];
|
||||
for (let n = 0; n < blockSize; n++) {
|
||||
this.genSample(this.playPos, this.nodes, input ? input[n] : 0, this.registers, this.outputs, this.sources);
|
||||
outL[n] = this.outputs[0];
|
||||
outR[n] = this.outputs[1];
|
||||
this.playPos += 1 / sampleRate;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
registerProcessor('generic-processor', GenericProcessor);
|
||||
|
||||
@@ -26,6 +26,20 @@ describe('transpiler', () => {
|
||||
it('adds await to bare samples call', () => {
|
||||
expect(transpiler("samples('xxx');", simple).output).toEqual("await samples('xxx');");
|
||||
});
|
||||
it('handles mini strings in K(...)', () => {
|
||||
expect(transpiler('K("bd sd")', simple).output).toEqual("worklet('pat[0]', m('bd sd', 2));");
|
||||
});
|
||||
it('treats K(...) as kabelsalat', () => {
|
||||
expect(transpiler('K(1+2)', simple).output).toEqual("worklet('1 + 2');");
|
||||
});
|
||||
it('automatically calls functions in K(...)', () => {
|
||||
expect(transpiler('K(() => { return 1 + 2 })', simple).output).toEqual(
|
||||
"worklet('(() => {\\n return 1 + 2\\n})()');",
|
||||
);
|
||||
});
|
||||
it('handles strudel S(...) inside kabelsalat K(...)', () => {
|
||||
expect(transpiler('K(S("bd".fast(4)))', simple).output).toEqual("worklet('pat[0]', m('bd', 4).fast(4));");
|
||||
});
|
||||
/* it('parses dynamic imports', () => {
|
||||
expect(
|
||||
transpiler("const { default: foo } = await import('https://bar.com/foo.js');", {
|
||||
|
||||
@@ -119,7 +119,64 @@ export function transpiler(input, options = {}) {
|
||||
return this.replace(labelToP(node));
|
||||
}
|
||||
},
|
||||
leave(node, parent, prop, index) {},
|
||||
|
||||
leave(node, parent, prop, index) {
|
||||
if (!isKabelCall(node)) return;
|
||||
|
||||
let [expr, ...rest] = node.arguments;
|
||||
if (!expr) throw new Error('K(...) requires an expression');
|
||||
|
||||
if (shouldCallKabelExpression(expr)) {
|
||||
expr = {
|
||||
type: 'CallExpression',
|
||||
callee: expr,
|
||||
arguments: [],
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { template, patternExprs } = extractPatternPlaceholders(expr);
|
||||
if (patternExprs.length) {
|
||||
const workletArgs = [{ type: 'Literal', value: template }, ...patternExprs, ...rest];
|
||||
|
||||
let callee = node.callee;
|
||||
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||
if (callee.type === 'MemberExpression') {
|
||||
return this.replace({
|
||||
type: 'CallExpression',
|
||||
callee: workletMemberAst(callee.object),
|
||||
arguments: workletArgs,
|
||||
optional: false,
|
||||
});
|
||||
}
|
||||
return this.replace({
|
||||
type: 'CallExpression',
|
||||
callee: { type: 'Identifier', name: 'worklet' },
|
||||
arguments: workletArgs,
|
||||
optional: false,
|
||||
});
|
||||
}
|
||||
|
||||
const kabelSrc = genExprSource(expr);
|
||||
const workletArgs = [{ type: 'Literal', value: kabelSrc }, ...rest];
|
||||
|
||||
let callee = node.callee;
|
||||
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||
if (callee.type === 'MemberExpression') {
|
||||
return this.replace({
|
||||
type: 'CallExpression',
|
||||
callee: workletMemberAst(callee.object),
|
||||
arguments: workletArgs,
|
||||
optional: false,
|
||||
});
|
||||
}
|
||||
return this.replace({
|
||||
type: 'CallExpression',
|
||||
callee: { type: 'Identifier', name: 'worklet' },
|
||||
arguments: workletArgs,
|
||||
optional: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
let { body } = ast;
|
||||
@@ -155,6 +212,166 @@ export function transpiler(input, options = {}) {
|
||||
return { output, miniLocations, widgets };
|
||||
}
|
||||
|
||||
function isKabelCall(node) {
|
||||
if (node.type !== 'CallExpression') return false;
|
||||
let callee = node.callee;
|
||||
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||
if (callee.type === 'MemberExpression') return !callee.computed && callee.property?.name === 'K';
|
||||
return callee.type === 'Identifier' && callee.name === 'K';
|
||||
}
|
||||
|
||||
function shouldCallKabelExpression(expr) {
|
||||
if (expr.type !== 'ArrowFunctionExpression' && expr.type !== 'FunctionExpression') {
|
||||
return false;
|
||||
}
|
||||
if (expr.params.length) {
|
||||
return false;
|
||||
}
|
||||
return expr.body?.type === 'BlockStatement';
|
||||
}
|
||||
|
||||
function genExprSource(expr) {
|
||||
return escodegen.generate(expr, { format: { semicolons: false } });
|
||||
}
|
||||
|
||||
function extractPatternPlaceholders(expr) {
|
||||
const templateExpr = cloneNode(expr);
|
||||
const parents = new Map();
|
||||
const targets = [];
|
||||
|
||||
walk(templateExpr, {
|
||||
enter(node, parent, prop, index) {
|
||||
parents.set(node, { parent, prop, index });
|
||||
const patternExpr = getStrudelPatternExpr(node);
|
||||
if (patternExpr) {
|
||||
targets.push({ node, patternExpr });
|
||||
this.skip();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!targets.length) {
|
||||
return { template: genExprSource(templateExpr), patternExprs: [] };
|
||||
}
|
||||
|
||||
targets.sort((a, b) => getPatternNodeOrder(a.node) - getPatternNodeOrder(b.node));
|
||||
|
||||
const patternExprs = targets.map(({ patternExpr }) => cloneNode(patternExpr));
|
||||
|
||||
let currentExpr = templateExpr;
|
||||
targets.forEach(({ node }, index) => {
|
||||
currentExpr = replaceNode(node, placeholderAst(index), parents, currentExpr);
|
||||
});
|
||||
|
||||
const template = genExprSource(currentExpr);
|
||||
return { template, patternExprs };
|
||||
}
|
||||
|
||||
function getStrudelPatternExpr(node) {
|
||||
if (isStrudelPatternWrap(node)) {
|
||||
const arg = node.arguments?.[0];
|
||||
if (!arg) {
|
||||
throw new Error('S(...) requires an argument');
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
if (isMiniCall(node)) {
|
||||
return node;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isStrudelPatternWrap(node) {
|
||||
if (node.type !== 'CallExpression') {
|
||||
return false;
|
||||
}
|
||||
const callee = node.callee;
|
||||
if (callee.type === 'Identifier') {
|
||||
return callee.name === 'S';
|
||||
}
|
||||
if (callee.type === 'MemberExpression' && !callee.computed) {
|
||||
return callee.property?.name === 'S';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getMinilangName() {
|
||||
const minilang = languages.get('minilang');
|
||||
return minilang?.name || 'm';
|
||||
}
|
||||
|
||||
// Used to identify transpiled `m(...)` calls for proper conversion
|
||||
// to, say, kabelsalat placeholders
|
||||
function isMiniCall(node) {
|
||||
if (node.type !== 'CallExpression') {
|
||||
return false;
|
||||
}
|
||||
const callee = node.callee;
|
||||
if (callee.type !== 'Identifier') {
|
||||
return false;
|
||||
}
|
||||
if (callee.name !== getMinilangName()) {
|
||||
return false;
|
||||
}
|
||||
const firstArg = node.arguments?.[0];
|
||||
return firstArg?.type === 'Literal' && typeof firstArg.value === 'string';
|
||||
}
|
||||
|
||||
// If `start` is available, we use it. If it's already been transpiled
|
||||
// to `m(...)`, use the provided offset
|
||||
function getPatternNodeOrder(node) {
|
||||
if (typeof node.start === 'number') {
|
||||
return node.start;
|
||||
}
|
||||
if (isMiniCall(node)) {
|
||||
const offsetArg = node.arguments?.[1];
|
||||
if (offsetArg?.type === 'Literal' && typeof offsetArg.value === 'number') {
|
||||
return offsetArg.value;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function placeholderAst(index) {
|
||||
return {
|
||||
type: 'MemberExpression',
|
||||
object: { type: 'Identifier', name: 'pat' },
|
||||
property: { type: 'Literal', value: index },
|
||||
computed: true,
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
function replaceNode(node, replacement, parents, currentRoot) {
|
||||
const info = parents.get(node);
|
||||
if (!info || !info.parent) {
|
||||
return replacement;
|
||||
}
|
||||
|
||||
const { parent, prop, index } = info;
|
||||
if (Array.isArray(parent[prop])) {
|
||||
parent[prop][index] = replacement;
|
||||
} else {
|
||||
parent[prop] = replacement;
|
||||
}
|
||||
parents.set(replacement, { parent, prop, index });
|
||||
return currentRoot;
|
||||
}
|
||||
|
||||
function cloneNode(node) {
|
||||
return JSON.parse(JSON.stringify(node));
|
||||
}
|
||||
|
||||
function workletMemberAst(objectExpr) {
|
||||
return {
|
||||
type: 'MemberExpression',
|
||||
object: objectExpr,
|
||||
property: { type: 'Identifier', name: 'worklet' },
|
||||
computed: false,
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
function isStringWithDoubleQuotes(node, locations, code) {
|
||||
if (node.type !== 'Literal') {
|
||||
return false;
|
||||
|
||||
@@ -103,8 +103,10 @@ export async function renderPatternAudio(
|
||||
}
|
||||
|
||||
export function webaudioRepl(options = {}) {
|
||||
const audioContext = options.audioContext ?? getAudioContext();
|
||||
setAudioContext(audioContext);
|
||||
options = {
|
||||
getTime: () => getAudioContext().currentTime,
|
||||
getTime: () => audioContext.currentTime,
|
||||
defaultOutput: webaudioOutput,
|
||||
...options,
|
||||
};
|
||||
|
||||
Generated
+26
@@ -234,6 +234,9 @@ importers:
|
||||
|
||||
packages/core:
|
||||
dependencies:
|
||||
'@kabelsalat/web':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
fraction.js:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
@@ -518,6 +521,9 @@ importers:
|
||||
|
||||
packages/superdough:
|
||||
dependencies:
|
||||
'@kabelsalat/lib':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
nanostores:
|
||||
specifier: ^0.11.3
|
||||
version: 0.11.3
|
||||
@@ -1969,6 +1975,15 @@ packages:
|
||||
resolution: {integrity: sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==}
|
||||
engines: {node: '>=v12.0.0'}
|
||||
|
||||
'@kabelsalat/core@0.3.1':
|
||||
resolution: {integrity: sha512-y2wHZyKnwbhJdGuwXvkaIb5vqbNTW6rMDnBslHGxQHTFX5xYyxJLOcuF2/EOB698t09kOVhmmpLXKbyokf3pOA==}
|
||||
|
||||
'@kabelsalat/lib@0.4.0':
|
||||
resolution: {integrity: sha512-UoOUhYOFShDjYCTne1edevCHHxSC07FsxG9tQ5GGOOzNoKMUpGVY8Ecq9X1GVyseQFvQBgz3aqAC6K8FPZp4ZQ==}
|
||||
|
||||
'@kabelsalat/web@0.4.0':
|
||||
resolution: {integrity: sha512-uvcOJjpKf8PG6KrgY0QEpoV4IAbIPDXh73bPtFSdDcXApbkSyEEyeNE8IoqIlqfcjIX926IOCAx9nM6FsFp6DA==}
|
||||
|
||||
'@lerna/create@8.1.9':
|
||||
resolution: {integrity: sha512-DPnl5lPX4v49eVxEbJnAizrpMdMTBz1qykZrAbBul9rfgk531v8oAt+Pm6O/rpAleRombNM7FJb5rYGzBJatOQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -9261,6 +9276,17 @@ snapshots:
|
||||
dependencies:
|
||||
lodash: 4.17.21
|
||||
|
||||
'@kabelsalat/core@0.3.1': {}
|
||||
|
||||
'@kabelsalat/lib@0.4.0':
|
||||
dependencies:
|
||||
'@kabelsalat/core': 0.3.1
|
||||
|
||||
'@kabelsalat/web@0.4.0':
|
||||
dependencies:
|
||||
'@kabelsalat/core': 0.3.1
|
||||
'@kabelsalat/lib': 0.4.0
|
||||
|
||||
'@lerna/create@8.1.9(encoding@0.1.13)(typescript@5.7.3)':
|
||||
dependencies:
|
||||
'@npmcli/arborist': 7.5.4
|
||||
|
||||
@@ -611,6 +611,132 @@ exports[`runs examples > example "FX" example index 1 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "K" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/12 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 1/12 → 1/6 | note:C4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 1/6 → 1/4 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 1/4 → 1/3 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ (1/3 → 3/8) ⇝ 5/12 | note:C4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ 1/3 ⇜ (3/8 → 5/12) | note:C4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 5/12 → 1/2 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 1/2 → 7/12 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 7/12 → 2/3 | note:C4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 2/3 → 3/4 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 3/4 → 5/6 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 5/6 → 11/12 | note:C4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 11/12 → 1/1 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 1/1 → 13/12 | note:C5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 13/12 → 7/6 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 7/6 → 5/4 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 5/4 → 4/3 | note:C5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ (4/3 → 11/8) ⇝ 17/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ 4/3 ⇜ (11/8 → 17/12) | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 17/12 → 3/2 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 3/2 → 19/12 | note:C5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 19/12 → 5/3 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 5/3 → 7/4 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 7/4 → 11/6 | note:C5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 11/6 → 23/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 23/12 → 2/1 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 2/1 → 25/12 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 25/12 → 13/6 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 13/6 → 9/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 9/4 → 7/3 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ (7/3 → 19/8) ⇝ 29/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ 7/3 ⇜ (19/8 → 29/12) | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 29/12 → 5/2 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 5/2 → 31/12 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 31/12 → 8/3 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 8/3 → 11/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 11/4 → 17/6 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 17/6 → 35/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 35/12 → 3/1 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 3/1 → 37/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 37/12 → 19/6 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 19/6 → 13/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 13/4 → 10/3 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ (10/3 → 27/8) ⇝ 41/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ 10/3 ⇜ (27/8 → 41/12) | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 41/12 → 7/2 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 7/2 → 43/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 43/12 → 11/3 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 11/3 → 15/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 15/4 → 23/6 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 23/6 → 47/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 47/12 → 4/1 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "K" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/16 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 1/16 → 1/8 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 1/8 → 3/16 | note:B2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 3/16 → 1/4 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 1/4 → 5/16 | note:A#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 5/16 → 3/8 | note:C#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 3/8 → 7/16 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 7/16 → 1/2 | note:A#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 1/2 → 9/16 | note:B3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 9/16 → 5/8 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 5/8 → 11/16 | note:A#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 11/16 → 3/4 | note:D#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 3/4 → 13/16 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 13/16 → 7/8 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 7/8 → 15/16 | note:B3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 15/16 → 1/1 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 1/1 → 17/16 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 6] FXrelease:1 room:0.3 ]",
|
||||
"[ 17/16 → 9/8 | note:C#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 6] FXrelease:1 room:0.3 ]",
|
||||
"[ 9/8 → 19/16 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 4] FXrelease:1 room:0.3 ]",
|
||||
"[ 19/16 → 5/4 | note:A#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 4] FXrelease:1 room:0.3 ]",
|
||||
"[ 5/4 → 21/16 | note:B2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 4] FXrelease:1 room:0.3 ]",
|
||||
"[ 21/16 → 11/8 | note:G#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 4] FXrelease:1 room:0.3 ]",
|
||||
"[ 11/8 → 23/16 | note:A#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 4] FXrelease:1 room:0.3 ]",
|
||||
"[ 23/16 → 3/2 | note:D#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 4] FXrelease:1 room:0.3 ]",
|
||||
"[ 3/2 → 25/16 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 25/16 → 13/8 | note:A#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 13/8 → 27/16 | note:B4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 27/16 → 7/4 | note:G#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 7/4 → 29/16 | note:A#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 29/16 → 15/8 | note:C#5 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 15/8 → 31/16 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 31/16 → 2/1 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 2/1 → 33/16 | note:B2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 33/16 → 17/8 | note:G#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 17/8 → 35/16 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 35/16 → 9/4 | note:D#5 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 9/4 → 37/16 | note:G#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 37/16 → 19/8 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 19/8 → 39/16 | note:B2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 39/16 → 5/2 | note:G#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 5/2 → 41/16 | note:A#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 41/16 → 21/8 | note:C#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 21/8 → 43/16 | note:G#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 43/16 → 11/4 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 11/4 → 45/16 | note:B2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 45/16 → 23/8 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 23/8 → 47/16 | note:A#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 47/16 → 3/1 | note:D#5 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 3/1 → 49/16 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 49/16 → 25/8 | note:A#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 25/8 → 51/16 | note:B3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 51/16 → 13/4 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 13/4 → 53/16 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 53/16 → 27/8 | note:C#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 27/8 → 55/16 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 55/16 → 7/2 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 7/2 → 57/16 | note:B3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 57/16 → 29/8 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 29/8 → 59/16 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 59/16 → 15/4 | note:D#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 15/4 → 61/16 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 61/16 → 31/8 | note:A#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 31/8 → 63/16 | note:B2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 63/16 → 4/1 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "accelerate" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 2/1 | s:sax accelerate:0 ]",
|
||||
@@ -7241,6 +7367,10 @@ exports[`runs examples > example "midicmd" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "midikeys" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "midikeys" example index 1 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "midin" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||
|
||||
@@ -131,6 +131,10 @@ const midin = () => {
|
||||
return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0
|
||||
};
|
||||
|
||||
const midikeys = async () => {
|
||||
return () => strudel.silence;
|
||||
};
|
||||
|
||||
const sysex = ([id, data]) => {};
|
||||
|
||||
// TODO: refactor to evalScope
|
||||
@@ -150,6 +154,7 @@ evalScope(
|
||||
*/
|
||||
{
|
||||
midin,
|
||||
midikeys,
|
||||
sysex,
|
||||
// gist,
|
||||
// euclid,
|
||||
|
||||
+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)
|
||||
@@ -10,8 +10,8 @@ import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import AstroPWA from '@vite-pwa/astro';
|
||||
|
||||
const site = `https://strudel.cc/`; // root url without a path
|
||||
const base = '/'; // base path of the strudel site
|
||||
const site = process.env.STRUDEL_SITE || `https://strudel.cc/`; // root url without a path
|
||||
const base = process.env.STRUDEL_BASE || '/'; // base path of the strudel site
|
||||
const baseNoTrailing = base.endsWith('/') ? base.slice(0, -1) : base;
|
||||
|
||||
// this rehype plugin fixes relative links
|
||||
|
||||
@@ -25,7 +25,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
||||
<base href={BASE_URL} />
|
||||
|
||||
<!-- Scrollable a11y code helper -->
|
||||
<script {`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
||||
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
||||
|
||||
<script src="/src/pwa.ts"></script>
|
||||
<!-- this does not work for some reason: -->
|
||||
|
||||
@@ -24,7 +24,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
||||
<base href={BASE_URL} />
|
||||
|
||||
<!-- Scrollable a11y code helper -->
|
||||
<script src{`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
||||
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
||||
|
||||
<script src="/src/pwa.ts"></script>
|
||||
<!-- this does not work for some reason: -->
|
||||
|
||||
@@ -76,6 +76,7 @@ export const SIDEBAR: Sidebar = {
|
||||
{ text: 'MIDI & OSC', link: 'learn/input-output' },
|
||||
],
|
||||
More: [
|
||||
{ text: 'FAQ', link: 'learn/faq' },
|
||||
{ text: 'Recipes', link: 'recipes/recipes' },
|
||||
{ text: 'Mini-Notation', link: 'learn/mini-notation' },
|
||||
{ text: 'Visual Feedback', link: 'learn/visual-feedback' },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const ALLOW_MANY = ['by', 'url', 'genre', 'license'];
|
||||
const ALLOW_MANY = ['by', 'url', 'genre', 'license', 'tag'];
|
||||
|
||||
export function getMetadata(raw_code) {
|
||||
if (raw_code == null) {
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
---
|
||||
title: Frequently Asked Questions
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MiniRepl } from '../../docs/MiniRepl';
|
||||
import { JsDoc } from '../../docs/JsDoc';
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
This page contains frequently asked questions, with answers. Usually, the topic is explained in more detail in a section which is linked in the answer.
|
||||
|
||||
## Is Strudel/Tidal free?
|
||||
|
||||
Yes - there is no charge, this is a collective open source project, and the music you make with it is your own. However if you can, please make a one-off or regular donation to our [opencollective fund](https://opencollective.com/tidalcycles), that supports the software and cultural development of Strudel and other Uzu languages.
|
||||
|
||||
While there is no charge there are some caveats, e.g.:
|
||||
|
||||
- the source code must stay free, i.e. you cannot distribute strudel or tidal as part of projects with incompatible licenses - see the [license](https://www.gnu.org/licenses/agpl-3.0.en.html) for details.
|
||||
- the contributed examples and tracks are also separately licensed, and must not e.g. be used to train AI models without permission.
|
||||
|
||||
## How do I try out the latest features?
|
||||
|
||||
The main, stable strudel website is [strudel.cc](https://strudel.cc/). There is also [warm.strudel.cc](https://warm.strudel.cc), known as "warm strudel", which has the latest development features. You might find warm strudel has bug fixes and features that the main website doesn't, but it will often be less stable and probably not suitable for important performances.
|
||||
|
||||
Alternatively, you can run strudel locally to try out the latest features. You can find development-oriented [instructions for that here](https://codeberg.org/uzu/strudel/src/branch/main/CONTRIBUTING.md#project-setup).
|
||||
|
||||
You can see the [latest changes here](https://codeberg.org/uzu/strudel/pulls?q=&type=all&sort=recentupdate&state=closed&labels=&milestone=0&project=0&assignee=0&poster=0), as 'pull requests'.
|
||||
|
||||
## How to record or export audio?
|
||||
|
||||
Strudel is not a digital audio workstation and does not operate following the same principles shared by most traditional audio softwares. However, there are multiple ways to record the audio -- and video -- output of Strudel:
|
||||
|
||||
- Use the 'export' tab to render and download as an audio file.
|
||||
- capture the raw stereo signal coming out of your web browser. You will need an external audio editor/DAW such as Reaper/Audacity/Ardour, etc.
|
||||
- use the alternative SuperDirt audio engine. Read [this page](/learn/input-output/#oscsuperdirtstrudeldirt) to know more about it.
|
||||
- capture the audio/video stream using a capture tool such as [OBS](https://obsproject.com/fr), which is designed for live streaming, but also works very well for recording.
|
||||
- don't record anything and code it again in front of your friends.
|
||||
|
||||
## Can I use strudel with my IDE?
|
||||
|
||||
Yes you can. There are experimental modes, made by community members, for several IDEs such as:
|
||||
|
||||
- VS Code: [Strudel VS](https://marketplace.visualstudio.com/items?itemName=cmillsdev.strudelvs): an experimental mode for Microsoft VSCode. A revived version of [TidalStrudel](https://marketplace.visualstudio.com/items?itemName=roipoussiere.tidal-strudel), which is defunct.
|
||||
- nvim: [strudel.nvim](https://github.com/gruvw/strudel.nvim)
|
||||
|
||||
## How can I use my own samples?
|
||||
|
||||
There are multiple ways to load your sample collection. Some methods are good for quick experimentation, some others are good to share your audio collection with other musicians:
|
||||
|
||||
- Import folders [from the interface](/learn/samples/#from-disk-via-import-sounds-folder). These are stored locally in your web browser, and not uploaded.
|
||||
- Serve a folder of samples locally using the [strudel 'sampler' commandline tool](https://strudel.cc/learn/samples/#from-disk-via-strudelsampler). This can be most reliable method, but requires [nodejs](https://nodejs.org) to be installed.
|
||||
- Host your sound library online on the web and [load them from an URL](/learn/samples/#loading-custom-samples)
|
||||
|
||||
## Can I use Strudel with AI/LLM tools?
|
||||
|
||||
You are free to do what you like with Strudel, within the terms of the free/open source AGPLv3 license.
|
||||
However as a community we are interested in exploring human creativity. AI is _way_ over-hyped right now,
|
||||
including by people with very shady motives. Many in the community are very wary of people training models
|
||||
on their tunes that they've poured their love into. So please keep discussion and questions around AI and
|
||||
LLMs to channels dedicated to the topic and be fully respectful of other people's work.
|
||||
|
||||
Furthermore, tools like ChatGPT generally give wrong answers. Please don't ask the community to fix those
|
||||
answers for you, as generally they will be timewasting nonsense.
|
||||
|
||||
Human questions are always welcome!
|
||||
|
||||
## Where can I download loads of patterns to train my LLM?
|
||||
|
||||
You cannot, as there is no such place. For details regarding our stance towards AI/LLM, see [above](/learn/faq/#can-i-use-strudel-with-aillm-tools)
|
||||
|
||||
## How to run offline?
|
||||
|
||||
Strudel works offline just fine! There are multiple techniques for this, see [this explanation](learn/pwa/#using-strudel-offline).
|
||||
|
||||
## How to change tempo? How do I translate BPM to cpm?
|
||||
|
||||
Strudel works in cycles, rather than beats, but if you assume a certain number of beats per cycle, you can convert between them.
|
||||
|
||||
For example, if you have your tempo in beats per minute and use 4 beats per cycle (e.g. if your track is in 4/4ths) then you can do `setcpm(BPM/4)`
|
||||
where BPM is your beats per minute.
|
||||
|
||||
If you have a different number of beats per bar or are using more or less beats per cycle (e.g. If you want to put only half a bar or
|
||||
two bars into one cycle), adjust accordingly.
|
||||
|
||||
## Where can I see all the functions?
|
||||
|
||||
If you pop open the sidetab of strudel.cc (small white < on the right hand side), there is a tab "reference" which lists all the functions of strudel.
|
||||
|
||||
## Where can I see all the samples and synths?
|
||||
|
||||
If you pop open the sidetab of strudel.cc (small white < on the right hand side), there is a tab "sounds" which lists all the drum machines, samples and synths currently loaded.
|
||||
|
||||
## How do I use this exactly like a DAW?
|
||||
|
||||
Strudel has different design aims for a DAW, and so treating it like one will likely be frustrating. DAWs are geared towards
|
||||
sequencing notes over time in predictable ways, whereas Strudel and similar Uzu languages are geared towards combining and
|
||||
transforming patterns in ways that can be hard to predict.
|
||||
|
||||
If you want to emulate the functionality of a DAW in Strudel, you'll have to identify the operations
|
||||
executed by the DAW (sequencing, repeating, applying filters and envelopes) and write code that is equivalent to these
|
||||
operations. For example in Strudel, the 'arrange' and 'pick' methods are useful for sequencing patterns over time (see question on these later in this document).
|
||||
|
||||
You might still find that the typical DAW workflow is not really adapted to live coding because, despite
|
||||
both being ways of making music on the computer, they are two very different tools. You could then adapt your way of proceeding
|
||||
to the medium of code, which might mean leaving more place to serendipity and writing code that you don't predict the output of.
|
||||
|
||||
## Why doesn't everyone just use a DAW?
|
||||
|
||||
There is no easy answer to this question. Here are some thoughts:
|
||||
|
||||
- Live coding tools such as Strudel are excellent for improvising music and visuals using a computer. DAWs are valuable and robust companions for other activities such as producing, mastering and mixing audio, among other usages. Using a tool does not exclude from using any another tool, just build a toolbox.
|
||||
|
||||
- Live coding has developed over decades as a distinct creative practice. For example, live coding artists like to show their screens while playing in front of an audience. It is an essential part of what they do, of the way they share their activity with everybody.
|
||||
|
||||
- Code is a human language, it is made for other humans to read it. You can read the code and enjoy the music too. It has meaning, value, and there might even be something poetic/important about it! - Strudel is free and open source, you can inspect the code, reshape it, contribute to it if you can/want. It is not opaque and this matters for many people. There is no black box, no obscure abstractions, no business model, no user tracking or hidden features. We need open tools in the arts! - Live coders don't all shy away from using DAWs. Many use them all of the time, especially when it makes their life easier for... live coding!
|
||||
|
||||
- Code is an artistic material like any other. There is something valuable in the process of making music through code. More generally speaking, it is nice to tackle creative problems through the use of a programming language: creative thinking, building up your own solutions, DIY approach to music-making, unexpected outcome of algorithms, funny human errors, etc.
|
||||
|
||||
- There are pianos and trumpets in your DAW: why do people continue playing the piano or the trumpet? Think of live coding tools as instruments that you activate through the act of programming.
|
||||
|
||||
## How can I interface Strudel with my favorite music software? What can I do with it?
|
||||
|
||||
Strudel can send [MIDI and OSC](/learn/input-output/), which are protocols for communicating musical information.
|
||||
|
||||
Other music software (or hardware!) can then listen to these messages and process them according to its capabilities.
|
||||
|
||||
A simple example would be to send livecoded audio to a DAW like Ardour on different tracks and then use it to mix them.
|
||||
|
||||
You could also send the MIDI of a sequenced pattern to Musescore and then have it transcribe your livecoded work as a musical score.
|
||||
|
||||
You could also send MIDI to your hardware synths, if you like their sound.
|
||||
|
||||
## How do I use this in my closed source webgame or other software?
|
||||
|
||||
You don't. You need to license your game to a free/open source license fulfill the [AGPLv3 license](https://codeberg.org/uzu/strudel/src/branch/main/LICENSE) Strudel is distributed under.
|
||||
|
||||
## How to play different patterns simultaneously?
|
||||
|
||||
Using the $ operator, several patterns can be played at once:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`$: s("bd*4").bank("tr707")
|
||||
$: s("- sd").bank("tr909")`}
|
||||
/>
|
||||
|
||||
See also [stack](intro/#combining-patterns)
|
||||
|
||||
## Is it possible to mute a pattern?
|
||||
|
||||
With an additional underscore, a pattern can be muted.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`$: s("bd*4").bank("tr707")
|
||||
_$: s("- sd").bank("tr909")`}
|
||||
/>
|
||||
|
||||
See also [hush](/learn/conditional-modifiers/#hush)
|
||||
|
||||
## How can I arrange in Strudel using `mask`?
|
||||
|
||||
With mini-notation, using the `<>` and `!` operators, you can try something like
|
||||
|
||||
```
|
||||
.mask("<0!24 1!40>")
|
||||
```
|
||||
|
||||
It mutes a pattern for 24 cycles and plays it for 40. You would gain 64 cycles total, a multiple of 2/4/8 commonly used in western music.
|
||||
|
||||
If each cycle is a bar, as a starting point, you could write a mask like that for any pattern:
|
||||
|
||||
```
|
||||
.mask("<0!16 0!16 0!16 0!16 0!16 0!10>")
|
||||
```
|
||||
|
||||
It mutes it throughout.
|
||||
|
||||
For arranging, you could add the same mask to each part and replace some zeroes with ones in your different masks to make parts play.
|
||||
|
||||
If you use `.mask()` on different patterns mess up your counting, then patterns do not align anymore.
|
||||
On the other hand, doing that on purpose is one of the things that could be considered a strength of tidalcycles and Strudel.
|
||||
You can make things quite lively and more organic with a little (controlled) interference, according to your own taste.
|
||||
And you are free to arrange in cycles like 3, 6 or 9 too.
|
||||
|
||||
To modify everything at once, you could try all and when, for example:
|
||||
|
||||
```
|
||||
all(x=>x.when("<0!7 1>", x=>x.lpf(saw.range(200, 2000))))
|
||||
```
|
||||
|
||||
This would lowpass filter sweep everything every 8 cycles.
|
||||
|
||||
## How can I arrange in Strudel using `arrange` or `pick`?
|
||||
|
||||
Take [Pachelbel's Canon in D](https://en.wikipedia.org/wiki/Pachelbel%27s_Canon#Analysis) as an example which has 4 voices (one cello and 3 violins) which have repeating patterns, as seen in the link above.
|
||||
|
||||
The following snipped defines the patterns as constants which can then be used for the different voices. `arrange` takes multiple arguments, which are each a number of cycles and a pattern which is played for the number of cycles, wrapped in `[]` If the pattern is shorter than the number, it is repeated.
|
||||
|
||||
<MiniRepl client:idle tune={`const cello = note(
|
||||
"<[d3 a2 b2 f#2] [g2 d3 g2 a2]>")
|
||||
.color("grey").sound("gm_tremolo_strings:3")
|
||||
const violin_p1 = note(
|
||||
"<[f#5 e5 d5 c#5] [b4 a4 b4 c#5]>")
|
||||
.color("blue")
|
||||
const violin_p2 = note(
|
||||
"<[d5 c#5 b4 a4] [ g4 f#4 g4 f#4]>")
|
||||
.color("green")
|
||||
const violin_p3 = note(
|
||||
"<[d4 f#4 a4 g4 f#4 d4 f#4 e4] [d4 b3 d4 a4 g4 b4 a4 g4]>")
|
||||
.color("purple")
|
||||
const violin_p4 = note(
|
||||
"<[f#4 d4 e4 c#5 d5 f#5 a5 a4] [b4 g4 a4 f#4 d4 d5 [d5@3 c#5]@2]>")
|
||||
.color("red")
|
||||
|
||||
cello$: arrange(
|
||||
[2, silence],
|
||||
[18,cello])
|
||||
violin1$: arrange(
|
||||
[4,silence],
|
||||
[2,violin_p1], [2,violin_p2],
|
||||
[2,violin_p3], [2,violin_p4],
|
||||
[2,violin_p1], [2,violin_p2],
|
||||
[2,violin_p3], [2,violin_p4]
|
||||
).sound("gm_tremolo_strings:0")
|
||||
violin2$: arrange(
|
||||
[6,silence], [2,violin_p1],
|
||||
[2,violin_p2], [2,violin_p3],
|
||||
[2,violin_p4], [2,violin_p1],
|
||||
[2,violin_p2], [2,violin_p3]
|
||||
).sound("gm_tremolo_strings:1")
|
||||
violin3$: arrange(
|
||||
[8,silence],
|
||||
[2,violin_p1], [2,violin_p2],
|
||||
[2,violin_p3], [2,violin_p4],
|
||||
[2,violin_p1], [2,violin_p2]
|
||||
).sound("gm_tremolo_strings:2")
|
||||
|
||||
all(x => x.release(.2))
|
||||
|
||||
`} />
|
||||
|
||||
Alternatively, you can also put the different patterns for the violins into one single array (`const violins = [violin_p1, violin_p2, violin_p3, violin_p4]`) and use a pattern as an index to `pick` the nth element of that array. This replaces the voices defined above. Here you use `0@2` to specifiy that the first item (i.e. with index `0`) is played for `2` cycles.
|
||||
|
||||
`pick` has better highlighting than `arrange`:
|
||||
|
||||
<MiniRepl client:idle tune={`const cello = note(
|
||||
"<[d3 a2 b2 f#2] [g2 d3 g2 a2]>")
|
||||
.color("grey").sound("gm_tremolo_strings:3")
|
||||
const violin_p1 = note(
|
||||
"<[f#5 e5 d5 c#5] [b4 a4 b4 c#5]>")
|
||||
.color("blue")
|
||||
const violin_p2 = note(
|
||||
"<[d5 c#5 b4 a4] [ g4 f#4 g4 f#4]>")
|
||||
.color("green")
|
||||
const violin_p3 = note(
|
||||
"<[d4 f#4 a4 g4 f#4 d4 f#4 e4] [d4 b3 d4 a4 g4 b4 a4 g4]>")
|
||||
.color("purple")
|
||||
const violin_p4 = note(
|
||||
"<[f#4 d4 e4 c#5 d5 f#5 a5 a4] [b4 g4 a4 f#4 d4 d5 [d5@3 c#5]@2]>")
|
||||
.color("red")
|
||||
|
||||
const violins = [violin_p1, violin_p2, violin_p3, violin_p4]
|
||||
|
||||
cello$: "<~@2 0@18>".pick([cello])
|
||||
violin1$: "<~@4 0@2 1@2 2@2 3@2 0@2 1@2 2@2 3@2>".pick(violins)
|
||||
.sound("gm_tremolo_strings:0")
|
||||
violin2$: "<~@6 0@2 1@2 2@2 3@2 0@2 1@2 2@2>".pick(violins)
|
||||
.sound("gm_tremolo_strings:1")
|
||||
violin3$: "<~@8 0@2 1@2 2@2 3@2 0@2 1@2 >".pick(violins)
|
||||
.sound("gm_tremolo_strings:2")
|
||||
all(x => x.release(.2))
|
||||
`} />
|
||||
|
||||
The `pick` method also works with jsons which have named elements, which makes it easier to read, see the [here](/learn/conditional-modifiers/#pick). `pickRestart` restarts the pattern upon picking it which can make a difference if the duration of the pick indexes doesn't line up with the patterns which are picked - which is not the case here.
|
||||
|
||||
Try adding `.punchcard()` after the `release(.2)` for a visualization.
|
||||
|
||||
## I saw Switch Angel using functions which I cannot find in the reference (e.g. `trancegate`). How do I make it work?
|
||||
|
||||
Methods like `trancegate()`, `rlpf()` and `acidenv()` are currently not pattern methods which come natively with strudel.
|
||||
|
||||
They are part of a script/prebake for strudel which was written by Switch Angel and published [here](https://github.com/switchangel/strudel-scripts)
|
||||
|
||||
You can find the instructions how to use that script in the readme.md there.
|
||||
|
||||
## Is there difference between `n` and `note`?
|
||||
|
||||
They are not aliases of each other, in contrast to `s` and `sound`.
|
||||
|
||||
The method `note` is used to reference a certain note (either as its name, such as `c` or `b2` or the midi number `69`, for example `note("c3 e3 g3")`).
|
||||
|
||||
On the other hand, `n` is a way to reference the nth index of something. This something can be a scale (eg `n("0 2 4").scale("C:major")`) , but it can also be a particular note in a chord (see https://strudel.cc/recipes/recipes/#arpeggios for an example) .
|
||||
|
||||
The method `n` can also be used for something completely unrelated to notes, in particular the nth sample from a sample map `s("hh*8").bank("RolandTR909").n("0 1 2 3")`.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
n("<[0 1 2 3@3 -@2] [3 2 1 0@3 -@2] >")
|
||||
.scale("A:minor:pentatonic")
|
||||
.s("gm_acoustic_guitar_steel").n("<0 1 2 3>/2")`}
|
||||
/>
|
||||
|
||||
Note that `n` is not the only way that functions use indices, some take numbered patterns instead.
|
||||
|
||||
## Is there a cheat sheet for all symbols?
|
||||
|
||||
Yes!
|
||||
|
||||
```
|
||||
' marks start and end of strings, is different from "
|
||||
" marks start and end of single line patterns in mini notation, is different from '
|
||||
` marks start and end of patterns with line breaks in mini notation, is different from '
|
||||
[] used for patterns in mini notation, each item in it has the same length
|
||||
<> used for patterns, alternates between items each cycle
|
||||
{} historically used for polyrhythmic patterns. {a b c}%4 is the same as <a b c>*4.
|
||||
@3 elongates the item by a factor of 3 (other numbers work too, even non-integer, but for numbers between 0 and 1 you need a leading zero like this: @0.5)
|
||||
@ after an item: elongates the item once (multiple @ work too c @ @ is the same as c@3)
|
||||
_ after an item: also elongates an item once (multiple _ work too c _ _ is the same as c@3), see below for a different usage.
|
||||
. this divides equal parts of a pattern and is called a foot. Can be used instead of [] like this: "1 6 7 8 . 2 . 3 . 4" is the same as "[1 6 7 8] 2 3 4"
|
||||
- silence
|
||||
~ also silence
|
||||
x not silence (for the use in struct, any non-silence symbol works there)
|
||||
b decrease by one semitone, i.e. flat, works for steps of scales, note names (but not midi numbers) and chord names
|
||||
s increase by one semitone, i.e. sharp, works for steps of scales, note names (but not midi numbers) but not chord names
|
||||
# increase by one semitone, i.e. sharp works for steps of scales, note names (but not midi numbers) and chord names
|
||||
# also used in mondo notation
|
||||
*3 play the sample or pattern at thrice the speed, fast(3)
|
||||
!3 play the sample or pattern three times
|
||||
/2 play the sample or pattern at half speed, slow(2)
|
||||
? play the pattern sometimes
|
||||
| once per cycle, choose randomly a pattern of those separated by i.e. chooseCycles()
|
||||
, play all items separated by it at the same time, i.e. stack()
|
||||
: is used to separate multiple parameters, such as adsr(".1:.1:.5:.2"), this is is an operator which creates a list of these objects.
|
||||
$: at the start of a line, defines a member of the stack. is the only stack name that should occur multiple names
|
||||
_ before a stack name: mutes the stack, i.e. hush(), for example _$: s("bd"), see above for a different usage.
|
||||
```
|
||||
@@ -16,10 +16,14 @@ It is also possible to pattern other things with Strudel, such as software and h
|
||||
|
||||
Strudel supports MIDI without any additional software (thanks to [webmidi](https://npmjs.com/package/webmidi)), just by adding methods to your pattern:
|
||||
|
||||
## midiin(inputName?)
|
||||
## midin(inputName?)
|
||||
|
||||
<JsDoc client:idle name="midin" h={0} />
|
||||
|
||||
## midikeys(inputName?)
|
||||
|
||||
<JsDoc client:idle name="midikeys" h={0} />
|
||||
|
||||
## midi(outputName?,options?)
|
||||
|
||||
Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages.
|
||||
|
||||
@@ -13,7 +13,7 @@ Let's take a closer look about how sounds are implemented in the webaudio output
|
||||
|
||||
All sounds are registered in the sound map, using the the `registerSound` function:
|
||||
|
||||
```ts
|
||||
```js
|
||||
function registerSound(
|
||||
name: string, // The name of the sound that should be given to `s`, e.g. `mysaw`
|
||||
// The function called by the scheduler to trigger the sound:
|
||||
@@ -35,34 +35,36 @@ When `registerSound` is called, it registers `{ onTrigger, data }` under the giv
|
||||
|
||||
This might be a bit abstract, so here is a minimal example:
|
||||
|
||||
```js
|
||||
registerSound(
|
||||
'mysaw',
|
||||
(time, value, onended) => {
|
||||
let { freq } = value; // destructure control params
|
||||
const ctx = getAudioContext();
|
||||
// create oscillator
|
||||
const o = new OscillatorNode(ctx, { type: 'sawtooth', frequency: Number(freq) });
|
||||
o.start(time);
|
||||
// add gain node to level down osc
|
||||
const g = new GainNode(ctx, { gain: 0.3 });
|
||||
// connect osc to gain
|
||||
const node = o.connect(g);
|
||||
// this function can be called from outside to stop the sound
|
||||
const stop = (time) => o.stop(time);
|
||||
// ended will be fired when stop has been fired
|
||||
o.addEventListener('ended', () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
onended();
|
||||
});
|
||||
return { node, stop };
|
||||
},
|
||||
{ type: 'synth' },
|
||||
);
|
||||
// use the sound
|
||||
freq(220, 440, 330).s('mysaw');
|
||||
```
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
registerSound(
|
||||
'mysaw',
|
||||
(time, value, onended) => {
|
||||
let { freq } = value; // destructure control params
|
||||
const ctx = getAudioContext();
|
||||
// create oscillator
|
||||
const o = new OscillatorNode(ctx, { type: 'sawtooth', frequency: Number(freq) });
|
||||
o.start(time);
|
||||
// add gain node to level down osc
|
||||
const g = new GainNode(ctx, { gain: 0.3 });
|
||||
// connect osc to gain
|
||||
const node = o.connect(g);
|
||||
// this function can be called from outside to stop the sound
|
||||
const stop = (time) => o.stop(time);
|
||||
// ended will be fired when stop has been fired
|
||||
o.addEventListener('ended', () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
onended();
|
||||
});
|
||||
return { node, stop };
|
||||
},
|
||||
{ type: 'synth' },
|
||||
);
|
||||
// use the sound
|
||||
freq("220 440 330").s('mysaw');`}
|
||||
/>
|
||||
|
||||
You can actually use this code in the [REPL](https://strudel.cc/) and it'll work.
|
||||
After evaluating the code, you should see `mysaw` in listed in the sounds tab.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Pagination } from '../pagination/Pagination.jsx';
|
||||
import { useState } from 'react';
|
||||
import { useDebounce } from '../usedebounce.jsx';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { Textbox } from '../textbox/Textbox.jsx';
|
||||
|
||||
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
|
||||
@@ -28,12 +29,12 @@ export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||
if (!isNaN(date)) {
|
||||
title = date.toLocaleDateString();
|
||||
} else {
|
||||
title = 'unnamed';
|
||||
title = pattern.id || 'unnamed';
|
||||
}
|
||||
}
|
||||
|
||||
const author = Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous';
|
||||
return <>{`${pattern.id}: ${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
|
||||
return <>{`${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
|
||||
}
|
||||
|
||||
function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
|
||||
@@ -79,73 +80,115 @@ const updateCodeWindow = (context, patternData, reset = false) => {
|
||||
context.handleUpdate(patternData, reset);
|
||||
};
|
||||
|
||||
function UserPatterns({ context }) {
|
||||
export function PatternsTab({ context }) {
|
||||
const [search, setSearch] = useState('');
|
||||
const activePattern = useActivePattern();
|
||||
const viewingPatternStore = useViewingPatternData();
|
||||
const viewingPatternData = parseJSON(viewingPatternStore);
|
||||
const { userPatterns, patternFilter, patternAutoStart } = useSettings();
|
||||
const { userPatterns, patternAutoStart } = useSettings();
|
||||
const viewingPatternID = viewingPatternData?.id;
|
||||
return (
|
||||
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
|
||||
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
|
||||
<ActionButton
|
||||
label="new"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.createAndAddToDB();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="duplicate"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.duplicate(viewingPatternData);
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="delete"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.delete(viewingPatternID);
|
||||
updateCodeWindow(context, { ...data, collection: userPattern.collection });
|
||||
}}
|
||||
/>
|
||||
<label className="hover:opacity-50 cursor-pointer">
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
multiple
|
||||
accept="text/plain,text/x-markdown,application/json"
|
||||
onChange={(e) => importPatterns(e.target.files)}
|
||||
/>
|
||||
import
|
||||
</label>
|
||||
<ActionButton label="export" onClick={exportPatterns} />
|
||||
|
||||
<ActionButton
|
||||
label="delete-all"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.clearAll();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
const visiblePatterns = useMemo(() => {
|
||||
if (!search) {
|
||||
return userPatterns;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(userPatterns).filter(([_key, pattern]) => {
|
||||
const meta = getMetadata(pattern.code);
|
||||
|
||||
<div className="overflow-auto h-full bg-background p-2 rounded-md">
|
||||
{/* {patternFilter === patternFilterName.user && ( */}
|
||||
<PatternButtons
|
||||
onClick={(id) => {
|
||||
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
|
||||
|
||||
if (context.started && activePattern === id) {
|
||||
context.handleEvaluate();
|
||||
// Search for specific meta keys
|
||||
const searchLowercaseTrimmed = search.trim().toLowerCase();
|
||||
if (searchLowercaseTrimmed.includes(':')) {
|
||||
const [metaKey, metaSearch] = searchLowercaseTrimmed.split(/:\s*/);
|
||||
if (metaKey !== undefined && metaSearch !== undefined && metaKey in meta) {
|
||||
const metaValues = meta[metaKey];
|
||||
if (Array.isArray(metaValues)) {
|
||||
return metaValues.some((metaValue) => metaValue.toLowerCase().includes(metaSearch));
|
||||
} else if (typeof metaValues === 'string') {
|
||||
return metaValues.toLowerCase().includes(metaSearch);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
patterns={userPatterns}
|
||||
started={context.started}
|
||||
activePattern={activePattern}
|
||||
viewingPatternID={viewingPatternID}
|
||||
/>
|
||||
{/* )} */}
|
||||
}
|
||||
}
|
||||
const title = meta.title ? meta.title : 'unnamed';
|
||||
const authors = meta.by ? meta.by : ['anonymous'];
|
||||
const tags = meta.tag ? meta.tag : [];
|
||||
return (
|
||||
title.toLowerCase().includes(searchLowercaseTrimmed) ||
|
||||
authors.some((author) => author.toLowerCase().includes(searchLowercaseTrimmed)) ||
|
||||
tags.some((tag) => tag.toLowerCase().includes(searchLowercaseTrimmed))
|
||||
);
|
||||
}),
|
||||
);
|
||||
}, [search, viewingPatternStore]);
|
||||
|
||||
return (
|
||||
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
|
||||
<div className="w-full flex">
|
||||
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
|
||||
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
|
||||
<ActionButton
|
||||
label="new"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.createAndAddToDB();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="duplicate"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.duplicate(viewingPatternData);
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="delete"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.delete(viewingPatternID);
|
||||
updateCodeWindow(context, { ...data, collection: userPattern.collection });
|
||||
}}
|
||||
/>
|
||||
<label className="hover:opacity-50 cursor-pointer">
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
multiple
|
||||
accept="text/plain,text/x-markdown,application/json"
|
||||
onChange={(e) => importPatterns(e.target.files)}
|
||||
/>
|
||||
import
|
||||
</label>
|
||||
<ActionButton label="export" onClick={exportPatterns} />
|
||||
|
||||
<ActionButton
|
||||
label="delete-all"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.clearAll();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-auto h-full bg-background p-2 rounded-md">
|
||||
{/* {patternFilter === patternFilterName.user && ( */}
|
||||
<PatternButtons
|
||||
onClick={(id) => {
|
||||
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
|
||||
|
||||
if (context.started && activePattern === id) {
|
||||
context.handleEvaluate();
|
||||
}
|
||||
}}
|
||||
patterns={visiblePatterns}
|
||||
started={context.started}
|
||||
activePattern={activePattern}
|
||||
viewingPatternID={viewingPatternID}
|
||||
/>
|
||||
{/* )} */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -232,28 +275,3 @@ function PublicPatterns({ context }) {
|
||||
}
|
||||
return <LatestPatterns context={context} />;
|
||||
}
|
||||
|
||||
export function PatternsTab({ context }) {
|
||||
const { patternFilter } = useSettings();
|
||||
|
||||
return (
|
||||
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
|
||||
<UserPatterns context={context} />
|
||||
</div>
|
||||
);
|
||||
/* return (
|
||||
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
|
||||
<ButtonGroup
|
||||
value={patternFilter}
|
||||
onChange={(value) => settingsMap.setKey('patternFilter', value)}
|
||||
items={patternFilterName}
|
||||
></ButtonGroup>
|
||||
|
||||
{patternFilter === patternFilterName.user ? (
|
||||
<UserPatterns context={context} />
|
||||
) : (
|
||||
<PublicPatterns context={context} />
|
||||
)}
|
||||
</div>
|
||||
); */
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export function Reference() {
|
||||
return (
|
||||
<div className="flex h-full w-full p-2 overflow-hidden">
|
||||
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
|
||||
<div class="w-full flex">
|
||||
<div className="w-full flex">
|
||||
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
|
||||
</div>
|
||||
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -210,7 +210,11 @@ export function SoundsTab() {
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{!soundEntries.length && soundsFilter !== 'importSounds' ? 'No sounds loaded' : ''}
|
||||
{!soundEntries.length && soundsFilter !== 'importSounds'
|
||||
? search == ''
|
||||
? 'No sounds loaded'
|
||||
: 'No sounds found'
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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