Compare commits

..

2 Commits

Author SHA1 Message Date
Aria c3018a29a0 Update docstring and add an example 2026-01-11 19:10:02 -06:00
Aria 6bd1c877aa Working version of inputs as sound sources 2026-01-11 19:03:20 -06:00
56 changed files with 462 additions and 2397 deletions
+125 -1063
View File
File diff suppressed because it is too large Load Diff
-211
View File
@@ -1,211 +0,0 @@
0 silly argv {
0 silly argv _: [ 'version' ],
0 silly argv private: false,
0 silly argv lernaVersion: '8.1.9',
0 silly argv '$0': 'node_modules/lerna/dist/cli.js'
0 silly argv }
1 notice cli v8.1.9
2 verbose packageConfigs Explicit "packages" configuration found in lerna.json. Resolving packages using the configured glob(s): ["packages/*"]
3 verbose rootPath /Users/jaderose/Documents/Github/cstrudel/strudel
4 info versioning independent
5 silly isAnythingCommitted
6 verbose isAnythingCommitted 1
7 silly getCurrentBranch
8 verbose currentBranch main
9 silly remoteBranchExists
10 silly isBehindUpstream
11 silly isBehindUpstream main is behind origin/main by 0 commit(s) and ahead by 4
12 silly hasTags
13 verbose hasTags true
14 silly git-describe.sync "@strudel/codemirror@1.2.7-4-ga3ac9646" => {"lastTagName":"@strudel/codemirror@1.2.7","lastVersion":"1.2.7","refCount":"4","sha":"a3ac9646","isDirty":false}
15 info Looking for changed packages since @strudel/codemirror@1.2.7
16 silly checking diff packages/codemirror
17 silly no diff found in @strudel/codemirror
18 silly checking diff packages/core
19 silly no diff found in @strudel/core
20 silly checking diff packages/csound
21 silly no diff found in @strudel/csound
22 silly checking diff packages/desktopbridge
23 silly no diff found in @strudel/desktopbridge
24 silly checking diff packages/draw
25 silly no diff found in @strudel/draw
26 silly checking diff packages/embed
27 silly no diff found in @strudel/embed
28 silly checking diff packages/gamepad
29 silly no diff found in @strudel/gamepad
30 silly checking diff packages/hs2js
31 silly no diff found in hs2js
32 silly checking diff packages/hydra
33 silly no diff found in @strudel/hydra
34 silly checking diff packages/midi
35 silly no diff found in @strudel/midi
36 silly checking diff packages/mini
37 silly no diff found in @strudel/mini
38 silly checking diff packages/mondo
39 silly no diff found in mondolang
40 silly checking diff packages/mondough
41 silly no diff found in @strudel/mondo
42 silly checking diff packages/motion
43 silly no diff found in @strudel/motion
44 silly checking diff packages/mqtt
45 silly no diff found in @strudel/mqtt
46 silly checking diff packages/osc
47 silly found diff in packages/osc/server.js
48 verbose filtered diff [ 'packages/osc/server.js' ]
49 silly checking diff packages/reference
50 silly no diff found in @strudel/reference
51 silly checking diff packages/repl
52 silly no diff found in @strudel/repl
53 silly checking diff packages/sampler
54 silly no diff found in @strudel/sampler
55 silly checking diff packages/serial
56 silly no diff found in @strudel/serial
57 silly checking diff packages/soundfonts
58 silly no diff found in @strudel/soundfonts
59 silly checking diff packages/superdough
60 silly found diff in packages/superdough/package.json
61 verbose filtered diff [ 'packages/superdough/package.json' ]
62 silly checking diff packages/supradough
63 silly found diff in packages/supradough/package.json
64 verbose filtered diff [ 'packages/supradough/package.json' ]
65 silly checking diff packages/tidal
66 silly no diff found in @strudel/tidal
67 silly checking diff packages/tonal
68 silly no diff found in @strudel/tonal
69 silly checking diff packages/transpiler
70 silly no diff found in @strudel/transpiler
71 silly checking diff packages/vite-plugin-bundle-audioworklet
72 silly no diff found in vite-plugin-bundle-audioworklet
73 silly checking diff packages/web
74 silly no diff found in @strudel/web
75 silly checking diff packages/webaudio
76 silly found diff in packages/webaudio/package.json
77 verbose filtered diff [ 'packages/webaudio/package.json' ]
78 silly checking diff packages/xen
79 silly no diff found in @strudel/xen
80 verbose updated @strudel/codemirror
81 verbose updated @strudel/csound
82 verbose updated @strudel/midi
83 verbose updated @strudel/osc
84 verbose updated @strudel/repl
85 verbose updated @strudel/soundfonts
86 verbose updated superdough
87 verbose updated supradough
88 verbose updated @strudel/web
89 verbose updated @strudel/webaudio
90 verbose git-describe undefined => "@strudel/codemirror@1.2.7-4-ga3ac9646"
91 silly git-describe parsed => {"lastTagName":"@strudel/codemirror@1.2.7","lastVersion":"1.2.7","refCount":"4","sha":"a3ac9646","isDirty":false}
92 info execute Skipping releases
93 silly lifecycle No script for "preversion" in "@strudel/monorepo", continuing
94 silly lifecycle No script for "preversion" in "@strudel/osc", continuing
95 silly lifecycle No script for "preversion" in "superdough", continuing
96 silly lifecycle No script for "preversion" in "supradough", continuing
97 verbose version supradough has no lockfile. Skipping lockfile update.
98 verbose version @strudel/osc has no lockfile. Skipping lockfile update.
99 verbose version superdough has no lockfile. Skipping lockfile update.
100 silly lifecycle No script for "version" in "supradough", continuing
101 silly lifecycle No script for "version" in "superdough", continuing
102 silly lifecycle No script for "preversion" in "@strudel/codemirror", continuing
103 silly lifecycle No script for "preversion" in "@strudel/webaudio", continuing
104 silly lifecycle No script for "version" in "@strudel/osc", continuing
105 verbose version @strudel/webaudio has no lockfile. Skipping lockfile update.
106 verbose version @strudel/codemirror has no lockfile. Skipping lockfile update.
107 silly lifecycle No script for "version" in "@strudel/codemirror", continuing
108 silly lifecycle No script for "version" in "@strudel/webaudio", continuing
109 silly lifecycle No script for "preversion" in "@strudel/csound", continuing
110 silly lifecycle No script for "preversion" in "@strudel/midi", continuing
111 silly lifecycle No script for "preversion" in "@strudel/soundfonts", continuing
112 silly lifecycle No script for "preversion" in "@strudel/web", continuing
113 verbose version @strudel/csound has no lockfile. Skipping lockfile update.
114 verbose version @strudel/midi has no lockfile. Skipping lockfile update.
115 verbose version @strudel/soundfonts has no lockfile. Skipping lockfile update.
116 verbose version @strudel/web has no lockfile. Skipping lockfile update.
117 silly lifecycle No script for "version" in "@strudel/midi", continuing
118 silly lifecycle No script for "version" in "@strudel/csound", continuing
119 silly lifecycle No script for "version" in "@strudel/soundfonts", continuing
120 silly lifecycle No script for "preversion" in "@strudel/repl", continuing
121 verbose version @strudel/repl has no lockfile. Skipping lockfile update.
122 silly lifecycle No script for "version" in "@strudel/web", continuing
123 silly lifecycle No script for "version" in "@strudel/repl", continuing
124 silly lifecycle No script for "version" in "@strudel/monorepo", continuing
125 verbose version Updating root pnpm-lock.yaml
126 silly version Skipped applying prettier to ignored file: packages/supradough/package.json
127 silly version Skipped applying prettier to ignored file: packages/superdough/package.json
128 silly version Skipped applying prettier to ignored file: packages/osc/package.json
129 silly version Skipped applying prettier to ignored file: packages/codemirror/package.json
130 silly version Skipped applying prettier to ignored file: packages/webaudio/package.json
131 silly version Skipped applying prettier to ignored file: packages/midi/package.json
132 silly version Skipped applying prettier to ignored file: packages/csound/package.json
133 silly version Skipped applying prettier to ignored file: packages/soundfonts/package.json
134 silly version Skipped applying prettier to ignored file: packages/web/package.json
135 silly version Skipped applying prettier to ignored file: packages/repl/package.json
136 silly version Skipped applying prettier to ignored file: pnpm-lock.yaml
137 silly gitAdd [
137 silly gitAdd 'packages/supradough/package.json',
137 silly gitAdd 'packages/superdough/package.json',
137 silly gitAdd 'packages/osc/package.json',
137 silly gitAdd 'packages/codemirror/package.json',
137 silly gitAdd 'packages/webaudio/package.json',
137 silly gitAdd 'packages/midi/package.json',
137 silly gitAdd 'packages/csound/package.json',
137 silly gitAdd 'packages/soundfonts/package.json',
137 silly gitAdd 'packages/web/package.json',
137 silly gitAdd 'packages/repl/package.json',
137 silly gitAdd 'pnpm-lock.yaml'
137 silly gitAdd ]
138 silly gitCommit Publish
138 silly gitCommit
138 silly gitCommit - @strudel/codemirror@1.2.8
138 silly gitCommit - @strudel/csound@1.2.8
138 silly gitCommit - @strudel/midi@1.2.8
138 silly gitCommit - @strudel/osc@1.3.2
138 silly gitCommit - @strudel/repl@1.2.9
138 silly gitCommit - @strudel/soundfonts@1.2.8
138 silly gitCommit - superdough@1.2.5
138 silly gitCommit - supradough@1.2.4
138 silly gitCommit - @strudel/web@1.2.8
138 silly gitCommit - @strudel/webaudio@1.2.9
139 verbose git [
139 verbose git 'commit',
139 verbose git '-F',
139 verbose git '/private/var/folders/hc/yf_zr55547sbcpz5rj7q85sc0000gn/T/3c85b5d4-56ae-40e9-8df7-db2bc1048402/lerna-commit.txt'
139 verbose git ]
140 silly gitTag @strudel/codemirror@1.2.8 git tag %s -m %s
141 verbose git [
141 verbose git 'tag',
141 verbose git '@strudel/codemirror@1.2.8',
141 verbose git '-m',
141 verbose git '@strudel/codemirror@1.2.8'
141 verbose git ]
142 silly gitTag @strudel/csound@1.2.8 git tag %s -m %s
143 verbose git [ 'tag', '@strudel/csound@1.2.8', '-m', '@strudel/csound@1.2.8' ]
144 silly gitTag @strudel/midi@1.2.8 git tag %s -m %s
145 verbose git [ 'tag', '@strudel/midi@1.2.8', '-m', '@strudel/midi@1.2.8' ]
146 silly gitTag @strudel/osc@1.3.2 git tag %s -m %s
147 verbose git [ 'tag', '@strudel/osc@1.3.2', '-m', '@strudel/osc@1.3.2' ]
148 silly gitTag @strudel/repl@1.2.9 git tag %s -m %s
149 verbose git [ 'tag', '@strudel/repl@1.2.9', '-m', '@strudel/repl@1.2.9' ]
150 silly gitTag @strudel/soundfonts@1.2.8 git tag %s -m %s
151 verbose git [
151 verbose git 'tag',
151 verbose git '@strudel/soundfonts@1.2.8',
151 verbose git '-m',
151 verbose git '@strudel/soundfonts@1.2.8'
151 verbose git ]
152 silly gitTag superdough@1.2.5 git tag %s -m %s
153 verbose git [ 'tag', 'superdough@1.2.5', '-m', 'superdough@1.2.5' ]
154 silly gitTag supradough@1.2.4 git tag %s -m %s
155 verbose git [ 'tag', 'supradough@1.2.4', '-m', 'supradough@1.2.4' ]
156 silly gitTag @strudel/web@1.2.8 git tag %s -m %s
157 verbose git [ 'tag', '@strudel/web@1.2.8', '-m', '@strudel/web@1.2.8' ]
158 silly gitTag @strudel/webaudio@1.2.9 git tag %s -m %s
159 verbose git [ 'tag', '@strudel/webaudio@1.2.9', '-m', '@strudel/webaudio@1.2.9' ]
160 error Error: Command failed with exit code 128: git tag superdough@1.2.5 -m superdough@1.2.5
160 error fatal: tag 'superdough@1.2.5' already exists
160 error at makeError (/Users/jaderose/Documents/Github/cstrudel/strudel/node_modules/.pnpm/execa@5.0.0/node_modules/execa/lib/error.js:59:11)
160 error at handlePromise (/Users/jaderose/Documents/Github/cstrudel/strudel/node_modules/.pnpm/execa@5.0.0/node_modules/execa/index.js:114:26)
160 error at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
160 error at async Promise.all (index 6)
160 error at async VersionCommand.gitCommitAndTagVersionForUpdates (/Users/jaderose/Documents/Github/cstrudel/strudel/node_modules/.pnpm/lerna@8.1.9_encoding@0.1.13/node_modules/lerna/dist/index.js:9957:11)
160 error at async VersionCommand.commitAndTagUpdates (/Users/jaderose/Documents/Github/cstrudel/strudel/node_modules/.pnpm/lerna@8.1.9_encoding@0.1.13/node_modules/lerna/dist/index.js:9933:18)
160 error at async Promise.all (index 0)
-9
View File
@@ -24,7 +24,6 @@ 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';
@@ -120,14 +119,6 @@ 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?.()),
-31
View File
@@ -1,31 +0,0 @@
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;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "1.3.0",
"version": "1.2.6",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
+12
View File
@@ -3076,3 +3076,15 @@ export const bmod = (config) => pure({}).bmod(config);
export const { transient } = registerControl(['transient', 'transsustain']);
export const { FXrelease, FXrel, FXr, fxr } = registerControl('FXrelease', 'FXrel', 'FXr', 'fxr');
/**
* Input audio device to be used as a sound source in a pattern
*
* @name input
* @param {string | number | Pattern} input Name or index of the input audio device to use
* @example
* // Please be careful about feedback!
* input("1 2").gain(0.5)
* .stretch(0.5).lfo({ s: 0.5 })
*/
export const { input } = registerControl('input');
+1 -1
View File
@@ -22,7 +22,7 @@ export * from './repl.mjs';
export * from './signal.mjs';
export * from './speak.mjs';
export * from './state.mjs';
export * from './schedulerState.mjs';
export * from './time.mjs';
export * from './timespan.mjs';
export * from './ui.mjs';
export * from './util.mjs';
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "1.2.6",
"version": "1.2.5",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
@@ -31,7 +31,6 @@
},
"homepage": "https://strudel.cc",
"dependencies": {
"@kabelsalat/web": "^0.4.1",
"fraction.js": "^5.2.1"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
+1 -84
View File
@@ -26,7 +26,6 @@ import {
} from './util.mjs';
import drawLine from './drawLine.mjs';
import { errorLogger, logger } from './logger.mjs';
import { strudelScope } from './evaluate.mjs';
let stringParser;
@@ -1684,9 +1683,7 @@ export function register(name, func, patternify = true, preserveSteps = false, j
// toplevel functions get curried as well as patternified
// because pfunc uses spread args, we need to state the arity explicitly!
const curried = curry(pfunc, null, arity);
strudelScope[name] = curried;
return curried;
return curry(pfunc, null, arity);
}
// Like register, but defaults to stepJoin
@@ -3777,83 +3774,3 @@ 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);
+1 -19
View File
@@ -2,17 +2,10 @@ import { NeoCyclist } from './neocyclist.mjs';
import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs';
import { errorLogger, logger } from './logger.mjs';
import {
setCpsFunc,
setIsStarted,
setPattern as exposeSchedulerPattern,
setTime,
setTriggerFunc,
} from './schedulerState.mjs';
import { setTime } from './time.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,
@@ -31,7 +24,6 @@ export function repl({
id,
mondo = false,
}) {
const kabel = new SalatRepl({ localScope: true });
const state = {
schedulerError: undefined,
evalError: undefined,
@@ -60,7 +52,6 @@ export function repl({
getTime,
onToggle: (started) => {
updateState({ started });
setIsStarted(started);
onToggle?.(started);
if (!started) {
reset_state();
@@ -74,8 +65,6 @@ 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;
@@ -89,11 +78,6 @@ 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) {
@@ -105,7 +89,6 @@ 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?
@@ -215,7 +198,6 @@ export function repl({
setcps: setCps,
setCpm,
setcpm: setCpm,
compileKabel,
});
};
-53
View File
@@ -1,53 +0,0 @@
/*
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;
}
+11
View File
@@ -0,0 +1,11 @@
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;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "1.3.0",
"version": "1.2.6",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
"version": "1.2.6",
"version": "1.2.5",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/embed",
"version": "1.1.2",
"version": "1.1.1",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/gamepad",
"version": "1.2.6",
"version": "1.2.5",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
"version": "1.2.6",
"version": "1.2.5",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
+28 -179
View File
@@ -5,23 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import {
Hap,
Pattern,
TimeSpan,
getCps,
getIsStarted,
getPattern,
getTime,
getTriggerFunc,
isPattern,
logger,
ref,
reify,
} from '@strudel/core';
import { Pattern, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi, getControlName } from '@strudel/core';
import { Note } from 'webmidi';
import { getAudioContext } from '@strudel/webaudio';
import { scheduleAtTime } from '../superdough/helpers.mjs';
// if you use WebMidi from outside of this package, make sure to import that instance:
@@ -491,41 +477,14 @@ Pattern.prototype.midi = function (midiport, options = {}) {
});
};
/**
* Initialize a midi device
*/
async function _initialize(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 device = getDevice(input, WebMidi.inputs);
if (!device) {
throw new Error(
`[midi] Midi device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
);
}
if (initial) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
`[midi] Midi enabled! Using "${device.name}". ${
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`,
);
}
return device;
}
let listeners = {};
const refs = {};
const refsByChan = {};
/**
* 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)
@@ -539,11 +498,29 @@ async function _initialize(input) {
* note("c a f e").s("saw")
* .when(cc(0).gt(0), x => x.postgain(0))
*/
let listeners = {};
const refs = {};
const refsByChan = {};
export async function midin(input) {
const device = await _initialize(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 device = getDevice(input, WebMidi.inputs);
if (!device) {
throw new Error(
`midiin: device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
);
}
if (initial) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`,
);
}
refs[input] ??= {};
refsByChan[input] ??= {};
const cc = (cc, chan) => {
@@ -555,7 +532,8 @@ export async function midin(input) {
listeners[input] && device.removeListener('midimessage', listeners[input]);
listeners[input] = (e) => {
const [ccNum, v] = e.dataBytes;
const ccNum = e.dataBytes[0];
const v = e.dataBytes[1];
const chan = e.message.channel;
const scaled = v / 127;
refsByChan[input][ccNum] ??= {};
@@ -565,132 +543,3 @@ export async function midin(input) {
device.addListener('midimessage', listeners[input]);
return cc;
}
/**
* 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 device = await _initialize(input);
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;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "1.3.0",
"version": "1.2.6",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
"version": "1.2.6",
"version": "1.2.5",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mondolang",
"version": "1.1.2",
"version": "1.1.1",
"description": "a language for functional composition that translates to js",
"main": "mondo.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mondo",
"version": "1.1.6",
"version": "1.1.5",
"description": "mondo notation for strudel",
"main": "mondough.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/motion",
"version": "1.2.6",
"version": "1.2.5",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mqtt",
"version": "1.2.6",
"version": "1.2.5",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
"version": "1.3.2",
"version": "1.3.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"bin": "./server.js",
Executable → Regular
View File
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/reference",
"version": "1.2.2",
"version": "1.2.1",
"description": "Headless reference of all strudel functions",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
"version": "1.3.0",
"version": "1.2.7",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/sampler",
"version": "0.2.4",
"version": "0.2.3",
"description": "",
"keywords": [
"tidalcycles",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
"version": "1.2.6",
"version": "1.2.5",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
"version": "1.3.0",
"version": "1.2.6",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
+4 -3
View File
@@ -154,8 +154,9 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) {
attack: attack ?? 0.005,
release: release ?? 0.05,
};
const now = ac.currentTime;
Object.entries(options).forEach(([key, value]) => {
node[key].value = value;
node[key].setValueAtTime(value, now);
});
return node;
}
@@ -172,7 +173,6 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
if (a == null && d == null && s == null && r == null) {
return defaultValues ?? [envmin, envmin, envmax, releaseMin];
}
const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin;
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
};
@@ -242,8 +242,9 @@ export function createFilter(context, start, end, params, cps, cycle) {
const factory = () => context.createBiquadFilter();
filter = getNodeFromPool('filter', factory);
filter.type = type;
const now = context.currentTime;
Object.entries({ Q: q, frequency }).forEach(([key, value]) => {
filter[key].value = value;
filter[key].setValueAtTime(value, now);
});
frequencyParam = filter.frequency;
}
+14 -25
View File
@@ -7,13 +7,11 @@ This program is free software: you can redistribute it and/or modify it under th
const nodePools = new Map();
const POOL_KEY = Symbol('nodePoolKey');
const IS_WORKLET_DEAD = Symbol('nodePoolIsWorkletDead');
const MAX_POOL_SIZE = 64;
export const isPoolable = (node) => !!node[POOL_KEY];
const getNodeTime = (node) => {
return node.context?.currentTime ?? 0;
};
const getParams = (node) => {
const params = new Set();
node.parameters?.forEach((param) => params.add(param));
@@ -40,43 +38,34 @@ export const releaseNodeToPool = (node) => {
// not reusable
return;
}
if (node[IS_WORKLET_DEAD]) {
// Worklet already terminated, don't pool it
return;
}
const key = node[POOL_KEY];
if (key == null) return;
const now = getNodeTime(node);
const now = node.context?.currentTime ?? 0;
getParams(node).forEach((param) => param.cancelScheduledValues(now));
const pool = nodePools.get(key) ?? [];
pool.push(new WeakRef(node));
nodePools.set(key, pool);
if (pool.length < MAX_POOL_SIZE) {
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;
};
export const markWorkletAsDead = (worklet) => (worklet[IS_WORKLET_DEAD] = true);
// Attempt to get node from the pool. If this fails, fall back
// to building it with the factory
export const getNodeFromPool = (key, factory) => {
const pool = nodePools.get(key) ?? [];
let node;
let found = false;
while (pool.length) {
const ref = pool.pop();
node = ref?.deref();
if (node != null && isNodeAlive(node)) {
found = true;
break;
}
if (node != null && !node[IS_WORKLET_DEAD]) break;
}
if (!found) {
if (node == null || node[IS_WORKLET_DEAD]) {
node = factory();
}
node[POOL_KEY] = key;
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "1.3.0",
"version": "1.2.6",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
@@ -36,7 +36,6 @@
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"dependencies": {
"@kabelsalat/lib": "^0.4.1",
"nanostores": "^0.11.3"
},
"engines": {
+82 -19
View File
@@ -14,10 +14,11 @@ import {
createFilter,
effectSend,
gainNode,
getADSRValues,
getCompressor,
getDistortion,
getFrequencyFromValue,
getLfo,
getParamADSR,
getWorklet,
releaseAudioNode,
webAudioTimeout,
@@ -177,6 +178,19 @@ export const getAudioDevices = async () => {
return devicesMap;
};
let AUDIO_INPUTS;
export const getAudioInputs = async () => {
await navigator.mediaDevices.getUserMedia({ audio: true });
let mediaDevices = await navigator.mediaDevices.enumerateDevices();
mediaDevices = mediaDevices.filter((device) => device.kind === 'audioinput' && device.deviceId !== 'default');
const devicesMap = new Map();
devicesMap.set(DEFAULT_AUDIO_DEVICE_NAME, '');
mediaDevices.forEach((device) => {
devicesMap.set(device.label, device.deviceId);
});
return devicesMap;
};
let defaultDefaultValues = {
s: 'triangle',
gain: 0.8,
@@ -255,6 +269,7 @@ export function loadWorklets() {
return workletsLoading;
}
let AUDIO_OUTPUTS;
// this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) {
const {
@@ -275,8 +290,8 @@ export async function initAudio(options = {}) {
if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) {
try {
const devices = await getAudioDevices();
const id = devices.get(audioDeviceName);
AUDIO_OUTPUTS = await getAudioDevices();
const id = AUDIO_OUTPUTS.get(audioDeviceName);
const isValidID = (id ?? '').length > 0;
if (audioCtx.sinkId !== id && isValidID) {
await audioCtx.setSinkId(id);
@@ -317,6 +332,51 @@ export async function initAudioOnFirstClick(options) {
return audioReady;
}
const CACHED_INPUTS = {};
export async function getInput(input) {
let sourceNode = CACHED_INPUTS[input];
if (sourceNode === undefined) {
const ac = getAudioContext();
if (!AUDIO_INPUTS) {
AUDIO_INPUTS = await getAudioInputs();
}
const available = Array.from(AUDIO_INPUTS.keys());
// Convert numerical inputs to their corresponding device name
const deviceName = typeof input === 'number' ? available[input] : input;
const inputId = AUDIO_INPUTS.get(deviceName);
if (inputId === undefined) {
throw new Error(`[superdough] input "${input}" not found. Available inputs: ${available.join(', ')}`);
}
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: { exact: inputId },
channelCount: { ideal: 2 },
sampleRate: { ideal: ac.sampleRate },
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false,
latency: { ideal: 0.01 },
},
});
sourceNode = ac.createMediaStreamSource(stream);
CACHED_INPUTS[input] = sourceNode;
}
return sourceNode;
}
async function getInputHandle(input, value, start, end) {
const inputNode = await getInput(input);
const envGain = gainNode(0);
inputNode.connect(envGain);
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
[0.001, 0.05, 0.6, 0.01],
);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, start, end, 'linear');
return { inputNode, envGain };
}
let controller;
export function getSuperdoughAudioController() {
if (controller == null) {
@@ -498,6 +558,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
fft = getDefaultValue('fft'), // fftSize 0 - 10
FX = [],
FXrelease,
input,
} = value;
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
@@ -547,6 +608,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
if (source) {
sourceNode = source(t, value, hapDuration, cps);
nodes.main['source'] = [sourceNode];
} else if (input) {
const { inputNode, envGain } = await getInputHandle(input, value, t, end);
sourceNode = envGain;
webAudioTimeout(
ac,
() => {
chain.releaseNodes();
activeSoundSources.delete(chainID);
// We disconnect inputNode instead of releasing it
// because we want inputs to persist
inputNode.disconnect(envGain);
},
t,
endWithRelease,
);
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
@@ -583,7 +659,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
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;
@@ -601,6 +676,7 @@ 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));
@@ -611,21 +687,8 @@ 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);
// Kabelsalat
if (fx.workletSrc !== undefined) {
const workletNode = getWorklet(ac, 'generic-processor', {}, { outputChannelCount: [2] });
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 });
if (stretch !== undefined) {
const phaseVocoder = getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch });
chain.connect(phaseVocoder);
fxNodes['stretch'] = [phaseVocoder];
}
+7 -2
View File
@@ -18,7 +18,7 @@ import {
} from './helpers.mjs';
import { logger } from './logger.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
import { getNodeFromPool, releaseNodeToPool } from './nodePools.mjs';
import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs';
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user', 'one'];
const waveformAliases = [
@@ -178,12 +178,17 @@ export function registerSynthSounds() {
};
const factory = () => new AudioWorkletNode(ac, 'supersaw-oscillator', { outputChannelCount: [2] });
const o = getNodeFromPool('supersaw', factory);
const now = ac.currentTime;
Object.entries(params).forEach(([key, value]) => {
const param = o.parameters.get(key);
const target = value !== undefined ? value : param.defaultValue;
param.value = target;
param.setValueAtTime(target, now);
});
o.port.postMessage({ type: 'initialize' });
o.port.onmessage = (e) => {
if (e.data.type === 'died') markWorkletAsDead(o);
o.port.onmessage = null;
};
const gainAdjustment = 1 / Math.sqrt(voices);
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
+7 -2
View File
@@ -11,7 +11,7 @@ import {
webAudioTimeout,
releaseAudioNode,
} from './helpers.mjs';
import { getNodeFromPool, releaseNodeToPool } from './nodePools.mjs';
import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs';
import { logger } from './logger.mjs';
export const Warpmode = Object.freeze({
@@ -244,12 +244,17 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
};
const factory = () => new AudioWorkletNode(ac, 'wavetable-oscillator-processor', { outputChannelCount: [2] });
const source = getNodeFromPool('wavetable', factory);
const now = ac.currentTime;
Object.entries(params).forEach(([key, value]) => {
const param = source.parameters.get(key);
const target = value !== undefined ? value : param.defaultValue;
param.value = target;
param.setValueAtTime(target, now);
});
source.port.postMessage({ type: 'initialize', payload });
source.port.onmessage = (e) => {
if (e.data.type === 'died') markWorkletAsDead(source);
source.port.onmessage = null;
};
if (ac.currentTime > t) {
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
return;
+26 -107
View File
@@ -5,9 +5,6 @@
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;
@@ -466,6 +463,7 @@ registerProcessor('distort-processor', DistortProcessor);
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.isAlive = true; // used internally to prevent multiple death messages
this.port.onmessage = (e) => {
const { type, payload } = e.data || {};
if (type === 'initialize') {
@@ -481,16 +479,16 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
return [
{
name: 'begin',
defaultValue: -1,
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: -1,
min: 0,
},
{
name: 'end',
defaultValue: -1,
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: -1,
min: 0,
},
{
@@ -525,17 +523,16 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
];
}
process(_input, outputs, params) {
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) {
if (currentTime >= params.end[0] + 0.5) {
// Outside of grace period - should terminate
if (this.isAlive) {
this.port.postMessage({ type: 'died' });
this.isAlive = false;
}
return false;
} else if (ended || notStarted || !beginDefined) {
}
if (currentTime >= params.end[0] || currentTime <= params.begin[0]) {
// Inside of grace period or not yet started
return true;
}
const output = outputs[0];
@@ -1151,8 +1148,8 @@ const tablesCache = {};
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{ name: 'begin', defaultValue: -1, min: -1, max: Number.POSITIVE_INFINITY },
{ name: 'end', defaultValue: -1, min: -1, max: Number.POSITIVE_INFINITY },
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
{ name: 'detune', defaultValue: 0 },
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
@@ -1167,6 +1164,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
constructor(options) {
super(options);
this.isAlive = true; // used internally to prevent multiple death messages
this.port.onmessage = (e) => {
const { type, payload } = e.data || {};
if (type === 'initialize') {
@@ -1334,17 +1332,16 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
}
process(_inputs, outputs, parameters) {
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) {
if (currentTime >= parameters.end[0] + 0.5) {
// Outside of grace period - should terminate
if (this.isAlive) {
this.port.postMessage({ type: 'died' });
this.isAlive = false;
}
return false;
} else if (ended || notStarted || !beginDefined) {
}
if (currentTime >= parameters.end[0] || currentTime <= parameters.begin[0]) {
// Inside of grace period or not yet started
return true;
}
const outL = outputs[0][0];
@@ -1482,81 +1479,3 @@ 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 output = outputs[0];
const outL = output[0];
const outR = output[1];
for (let n = 0; n < blockSize; n++) {
this.genSample(this.playPos, this.nodes, input ? input[n] : 0, this.registers, this.outputs, this.sources);
const left = this.outputs[0];
const right = this.outputs[1];
// Spread to stereo if possible; else mixdown to mono
if (outR) {
outL[n] = left;
outR[n] = right;
} else {
outL[n] = 0.5 * (left + right);
}
this.playPos += 1 / sampleRate;
}
return true;
}
}
registerProcessor('generic-processor', GenericProcessor);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
"version": "1.2.6",
"version": "1.2.5",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/transpiler",
"version": "1.2.6",
"version": "1.2.5",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"type": "module",
@@ -26,20 +26,6 @@ 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');", {
+1 -218
View File
@@ -119,64 +119,7 @@ export function transpiler(input, options = {}) {
return this.replace(labelToP(node));
}
},
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,
});
},
leave(node, parent, prop, index) {},
});
let { body } = ast;
@@ -212,166 +155,6 @@ 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;
@@ -1,7 +1,7 @@
{
"name": "vite-plugin-bundle-audioworklet",
"main": "./vite-plugin-bundle-audioworklet.js",
"version": "0.1.2",
"version": "0.1.1",
"description": "",
"keywords": [
"vite",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
"version": "1.3.0",
"version": "1.2.6",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
"version": "1.3.0",
"version": "1.2.6",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/xen",
"version": "1.2.6",
"version": "1.2.5",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"type": "module",
+2 -28
View File
@@ -41,7 +41,7 @@ importers:
version: 2.2.7
'@vitest/coverage-v8':
specifier: 3.0.4
version: 3.0.4(vitest@3.0.4)
version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))
'@vitest/ui':
specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4)
@@ -234,9 +234,6 @@ importers:
packages/core:
dependencies:
'@kabelsalat/web':
specifier: ^0.4.1
version: 0.4.1
fraction.js:
specifier: ^5.2.1
version: 5.2.1
@@ -521,9 +518,6 @@ importers:
packages/superdough:
dependencies:
'@kabelsalat/lib':
specifier: ^0.4.1
version: 0.4.1
nanostores:
specifier: ^0.11.3
version: 0.11.3
@@ -1975,15 +1969,6 @@ packages:
resolution: {integrity: sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==}
engines: {node: '>=v12.0.0'}
'@kabelsalat/core@0.4.0':
resolution: {integrity: sha512-5zV8nh8HffW8aexObXs5pE0xgL0jb1cHc3o8UN6AvK0mBt87fjZvW65E4rMslHyq+OuCscoBJW7B3MbYfWrwMQ==}
'@kabelsalat/lib@0.4.1':
resolution: {integrity: sha512-gBCjrZKD9huTKNJuBC6BXM4PMQSg8otL8A/vp8j98P9v6yWTX1TuyaqdLg/1PrIAMV4hlWoav9ZD3YMFlrvouw==}
'@kabelsalat/web@0.4.1':
resolution: {integrity: sha512-ASkFhePJLx3GjadYOueI7sHKXBbRPk/a1pfslFeQNI9gU7yZq/KrnkmOmLrgrtixAsy7gyi1vWqkmRRvH3Ki6w==}
'@lerna/create@8.1.9':
resolution: {integrity: sha512-DPnl5lPX4v49eVxEbJnAizrpMdMTBz1qykZrAbBul9rfgk531v8oAt+Pm6O/rpAleRombNM7FJb5rYGzBJatOQ==}
engines: {node: '>=18.0.0'}
@@ -9276,17 +9261,6 @@ snapshots:
dependencies:
lodash: 4.17.21
'@kabelsalat/core@0.4.0': {}
'@kabelsalat/lib@0.4.1':
dependencies:
'@kabelsalat/core': 0.4.0
'@kabelsalat/web@0.4.1':
dependencies:
'@kabelsalat/core': 0.4.0
'@kabelsalat/lib': 0.4.1
'@lerna/create@8.1.9(encoding@0.1.13)(typescript@5.7.3)':
dependencies:
'@npmcli/arborist': 7.5.4
@@ -10580,7 +10554,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/coverage-v8@3.0.4(vitest@3.0.4)':
'@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
+13 -130
View File
@@ -611,132 +611,6 @@ 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 ]",
@@ -5610,6 +5484,19 @@ exports[`runs examples > example "inhabit" example index 1 1`] = `
]
`;
exports[`runs examples > example "input" example index 0 1`] = `
[
"[ 0/1 → 1/2 | input:1 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
"[ 1/2 → 1/1 | input:2 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
"[ 1/1 → 3/2 | input:1 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
"[ 3/2 → 2/1 | input:2 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
"[ 2/1 → 5/2 | input:1 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
"[ 5/2 → 3/1 | input:2 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
"[ 3/1 → 7/2 | input:1 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
"[ 7/2 → 4/1 | input:2 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
]
`;
exports[`runs examples > example "inside" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 ]",
@@ -7367,10 +7254,6 @@ 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 ]",
-5
View File
@@ -131,10 +131,6 @@ 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
@@ -154,7 +150,6 @@ evalScope(
*/
{
midin,
midikeys,
sysex,
// gist,
// euclid,
+12 -28
View File
@@ -1,30 +1,14 @@
// this script loads all merged PRs within the given page range
// it can be used to update the CHANGELOG.md file in a semi-automated way
// the problem: codeberg doesn't support loading merged PRs, so we have to filter them in memory
// luckily, we can sort after "recentupdate", which means we can do incremental changelog generation
// todo: support setting a "last_updated" date, so the script would automatically check how far it has to go
fetch('https://codeberg.org/api/v1/repos/uzu/strudel/pulls?state=closed&page=1')
.then((res) => res.json())
.then((pulls) => {
const r = pulls
.filter((pull) => pull.merged)
.sort((a, b) => new Date(b.closed_at) - new Date(a.closed_at))
.map((pull) => `${pull.closed_at} ${pull.title} by ${pull.user.login || '?'} in: [#${pull.number}](${pull.url}) `)
.join('\n');
console.log(r);
});
async function main() {
let pageStart = 1;
let pageEnd = 1;
let prs = [];
for (let p = pageStart; p <= pageEnd; p++) {
console.log(`load page ${p}/${pageEnd}`);
const res = await fetch(
`https://codeberg.org/api/v1/repos/uzu/strudel/pulls?state=closed&sort=recentupdate&page=${p}`,
);
const pulls = await res.json();
const merged = pulls.filter((pull) => pull.merged);
prs = prs.concat(merged);
}
const output = prs
.sort((a, b) => new Date(b.closed_at) - new Date(a.closed_at))
.map(
(pull) => `- ${pull.closed_at} ${pull.title} by @${pull.user.login || '?'} in: [#${pull.number}](${pull.url}) `,
)
.join('\n');
console.log('-------------');
console.log(output);
}
/*
main();
*/
+1 -1
View File
@@ -1,4 +1,4 @@
const ALLOW_MANY = ['by', 'url', 'genre', 'license', 'tag'];
const ALLOW_MANY = ['by', 'url', 'genre', 'license'];
export function getMetadata(raw_code) {
if (raw_code == null) {
-8
View File
@@ -19,14 +19,6 @@ 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:
+1 -5
View File
@@ -16,14 +16,10 @@ 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:
## midin(inputName?)
## midiin(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.
-19
View File
@@ -53,7 +53,6 @@ Available tags are:
- `@url`: web page(s) related to the music (git repository, Soundcloud link, etc.)
- `@genre`: music genre(s) (pop, jazz, etc.)
- `@album`: music album name
- `@tag`: custom tag
Note to tool authors: _Never_ trust that a song has filled those fields with syntactically correct values; make sure your software is robust enough it doesn't break if it encounters bad values
@@ -93,21 +92,3 @@ If a tag doesn't accept a list, it can take multi-line values:
the sofa in the living room.
*/
```
# Searching meta-data in the online repl
Meta-data can be used in the search field of the patterns tab in the online repl.
For example to search for all patterns by a specific author use the search term
```
by: Ada L
```
or search for patterns with a specific genre like
```
genre: unicorns
```
Hint: If no meta-data property is provided in the search all patterns with a `@title`, `@by` or `@tag` matching the search term will be shown.
+86 -104
View File
@@ -18,7 +18,6 @@ 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]);
@@ -29,12 +28,12 @@ export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
if (!isNaN(date)) {
title = date.toLocaleDateString();
} else {
title = pattern.id || 'unnamed';
title = 'unnamed';
}
}
const author = Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous';
return <>{`${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
return <>{`${pattern.id}: ${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
}
function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
@@ -80,115 +79,73 @@ const updateCodeWindow = (context, patternData, reset = false) => {
context.handleUpdate(patternData, reset);
};
export function PatternsTab({ context }) {
const [search, setSearch] = useState('');
function UserPatterns({ context }) {
const activePattern = useActivePattern();
const viewingPatternStore = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const { userPatterns, patternAutoStart } = useSettings();
const { userPatterns, patternFilter, patternAutoStart } = useSettings();
const viewingPatternID = viewingPatternData?.id;
const visiblePatterns = useMemo(() => {
if (!search) {
return userPatterns;
}
return Object.fromEntries(
Object.entries(userPatterns).filter(([_key, pattern]) => {
const meta = getMetadata(pattern.code);
// 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;
}
}
}
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 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="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);
<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>
if (context.started && activePattern === id) {
context.handleEvaluate();
}
}}
patterns={userPatterns}
started={context.started}
activePattern={activePattern}
viewingPatternID={viewingPatternID}
/>
{/* )} */}
</div>
</div>
);
@@ -275,3 +232,28 @@ 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 className="w-full flex">
<div class="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">