mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 13:13:10 -04:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62bf9abed3 | |||
| 8ec1d4d034 | |||
| 6981638f70 | |||
| f5823ce981 | |||
| f8750901f0 | |||
| c1fd8c82c6 | |||
| 6e36bdfff7 | |||
| 8dbdd0a9b8 | |||
| f4c0281613 | |||
| f610965f43 | |||
| 17ef84ed70 | |||
| f0cb96dcad | |||
| f4f04a7d57 | |||
| a3ac9646f5 | |||
| 12b89f9f88 | |||
| 166985a055 | |||
| 85e787214b | |||
| d198910a86 | |||
| 8ce6a881e6 | |||
| 7bc8576a68 | |||
| 99cf256ee2 | |||
| 42d0bfadce | |||
| 22f78fbb2f | |||
| 77076a1c3e | |||
| a1d31df556 | |||
| f9d0e154e8 | |||
| bbe95b3995 | |||
| 2dc921e375 | |||
| 3d13955c41 | |||
| ae9638c353 | |||
| 97cd3a5946 | |||
| ca3452bf52 | |||
| 7f528870b7 | |||
| 60e5682829 | |||
| d605fbbde8 | |||
| 21e6d81d34 | |||
| 85e79d9932 | |||
| 71dfbeea88 | |||
| ffcab7bf6c | |||
| c3f6a1d6ba | |||
| eb96d3ad63 | |||
| 06e10cd693 | |||
| 80875cca95 | |||
| b954a25b2b | |||
| cca346d2c2 | |||
| 3343c7bcf0 | |||
| 6e3c5b23dd | |||
| c12a77a0a1 | |||
| e5ab6b3c93 | |||
| e82bb6c410 | |||
| 8b1fb12388 | |||
| 1f1f3288a6 | |||
| d642c0e12f | |||
| 1eb657e6fe | |||
| 7c5f53f9ae | |||
| efe7c9394f | |||
| 35bfd8b3ff | |||
| c2720a5738 | |||
| 5355a0c563 | |||
| 75db9ff16e | |||
| 12cc1e9b5e | |||
| cd48e67675 | |||
| 3a284696c9 | |||
| 31cacc3c29 | |||
| 9ffd0e496f | |||
| 550d7ff6cb | |||
| af88471f6b | |||
| f9a9993c42 | |||
| dab1676761 | |||
| d753eedb90 | |||
| 726e5ef14a | |||
| cc15e3cd36 | |||
| 4743334a17 | |||
| 7cff2050c8 | |||
| e6e96c8603 | |||
| d4b3a3b972 | |||
| fd2ff255e5 | |||
| 5903070c02 | |||
| e92865cecb | |||
| a5c4fd5bc1 | |||
| 5caaf423ff | |||
| aa8209ad4c | |||
| 6908ea38e4 | |||
| 103b27c21f | |||
| 597c941991 | |||
| f0aac2098a | |||
| c1a185e852 | |||
| b9fce15042 | |||
| 2fa8ed1424 | |||
| b7827cb89f | |||
| 29fd541f1f | |||
| 0830a9df69 | |||
| 4159c8394d | |||
| 466bf819d8 | |||
| 7e4cbefc8f | |||
| 2483c673a4 | |||
| 69360b3ec8 | |||
| 96d0f2053e | |||
| b952e4c735 | |||
| a4212589e5 | |||
| ee1a894f3b | |||
| c62428ca27 | |||
| d516d765d5 | |||
| 5032b4e966 | |||
| a4d7803d43 | |||
| 4a395f372d | |||
| c754dab1ef | |||
| df54404733 |
+1061
-123
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,13 @@ function defineTags(dictionary) {
|
||||
doclet.synonyms = doclet.synonyms_text.split(/[ ,]+/);
|
||||
},
|
||||
});
|
||||
|
||||
dictionary.defineTag('tags', {
|
||||
mustHaveValue: true,
|
||||
onTagged: function (doclet, tag) {
|
||||
doclet.tags = tag.value.split(/[ ,]+/);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { defineTags: defineTags };
|
||||
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
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)
|
||||
@@ -24,6 +24,7 @@ import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||
import { activateTheme, initTheme, theme } from './themes.mjs';
|
||||
import { isTooltipEnabled } from './tooltip.mjs';
|
||||
import { updateWidgets, widgetPlugin } from './widget.mjs';
|
||||
import { jumpToCharacter } from './labelJump.mjs';
|
||||
|
||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||
|
||||
@@ -119,6 +120,14 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
preventDefault: true,
|
||||
run: () => onStop?.(),
|
||||
},
|
||||
{
|
||||
key: 'Alt-w',
|
||||
run: (view) => jumpToCharacter(view, '$', 1),
|
||||
},
|
||||
{
|
||||
key: 'Alt-q',
|
||||
run: (view) => jumpToCharacter(view, '$', -1),
|
||||
},
|
||||
/* {
|
||||
key: 'Ctrl-Shift-.',
|
||||
run: () => (onPanic ? onPanic() : onStop?.()),
|
||||
@@ -425,6 +434,7 @@ function s4() {
|
||||
/**
|
||||
* Overrides the css of highlighted events. Make sure to use single quotes!
|
||||
* @name markcss
|
||||
* @tag visualization
|
||||
* @example
|
||||
* note("c a f e")
|
||||
* .markcss('text-decoration:underline')
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import { SearchCursor } from '@codemirror/search';
|
||||
|
||||
export function jumpToCharacter(view, character, direction = 1) {
|
||||
const { state, dispatch } = view;
|
||||
const pos = state.selection.main.head;
|
||||
const cursor = new SearchCursor(state.doc, character);
|
||||
|
||||
let characterPositions = [];
|
||||
let jumpPos;
|
||||
while (!cursor.next().done) {
|
||||
characterPositions.push(cursor.value.to);
|
||||
}
|
||||
if (!characterPositions.length) {
|
||||
return false;
|
||||
}
|
||||
if (direction > 0) {
|
||||
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
|
||||
} else {
|
||||
jumpPos = characterPositions.reverse().find((x) => x < pos + 1) ?? characterPositions.at(0);
|
||||
}
|
||||
|
||||
if (jumpPos == null) {
|
||||
return false;
|
||||
}
|
||||
dispatch({
|
||||
selection: EditorSelection.cursor(jumpPos - 1),
|
||||
scrollIntoView: true,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/codemirror",
|
||||
"version": "1.2.6",
|
||||
"version": "1.3.0",
|
||||
"description": "Codemirror Extensions for Strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -117,6 +117,7 @@ export const sliderPlugin = ViewPlugin.fromClass(
|
||||
* Displays a slider widget to allow the user manipulate a value
|
||||
*
|
||||
* @name slider
|
||||
* @tags external_io, visualization
|
||||
* @param {number} value Initial value
|
||||
* @param {number} min Minimum value - optional, defaults to 0
|
||||
* @param {number} max Maximum value - optional, defaults to 1
|
||||
|
||||
+207
-13
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ import Fraction, { gcd } from './fraction.mjs';
|
||||
* - "-" hold previous value
|
||||
* - "." silence
|
||||
*
|
||||
* @tags visualization
|
||||
* @param {Pattern} pattern the pattern to use
|
||||
* @param {number} chars max number of characters (approximately)
|
||||
* @returns string
|
||||
|
||||
@@ -61,6 +61,7 @@ export const bjorklund = function (ons, steps) {
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name euclid
|
||||
* @tags temporal
|
||||
* @param {number} pulses the number of onsets/beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @returns Pattern
|
||||
@@ -73,6 +74,7 @@ export const bjorklund = function (ons, steps) {
|
||||
* Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence.
|
||||
* @memberof Pattern
|
||||
* @name euclidRot
|
||||
* @tags temporal
|
||||
* @param {number} pulses the number of onsets/beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param {number} rotation offset in steps
|
||||
@@ -156,6 +158,7 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
|
||||
* so there will be no gaps.
|
||||
* @name euclidLegato
|
||||
* @memberof Pattern
|
||||
* @tags temporal
|
||||
* @param {number} pulses the number of onsets/beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param rotation offset in steps
|
||||
@@ -187,6 +190,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
|
||||
* the resulting sequence
|
||||
* @name euclidLegatoRot
|
||||
* @memberof Pattern
|
||||
* @tags temporal
|
||||
* @param {number} pulses the number of onsets/beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param {number} rotation offset in steps
|
||||
@@ -208,6 +212,7 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s
|
||||
* @name euclidish
|
||||
* @synonyms eish
|
||||
* @memberof Pattern
|
||||
* @tags temporal
|
||||
* @param {number} pulses the number of onsets
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse)
|
||||
|
||||
@@ -22,7 +22,7 @@ export * from './repl.mjs';
|
||||
export * from './signal.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * from './state.mjs';
|
||||
export * from './time.mjs';
|
||||
export * from './schedulerState.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './ui.mjs';
|
||||
export * from './util.mjs';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/core",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
+253
-8
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@ const _pick = function (lookup, pat, modulo = true) {
|
||||
|
||||
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
||||
* Similar to `inhabit`, but maintains the structure of the original patterns.
|
||||
* @tags combiners
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
@@ -57,6 +58,7 @@ const __pick = register('pick', function (lookup, pat) {
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||
* second one.
|
||||
* @tags combiners
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
@@ -67,6 +69,7 @@ export const pickmod = register('pickmod', function (lookup, pat) {
|
||||
});
|
||||
|
||||
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
|
||||
* @tags combiners, functional
|
||||
* @param {Pattern} pat
|
||||
* @param {Pattern} lookup a pattern of indices
|
||||
* @param {function[]} funcs the array of functions from which to pull
|
||||
@@ -83,6 +86,7 @@ export const pickF = register('pickF', function (lookup, funcs, pat) {
|
||||
|
||||
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @tags combiners
|
||||
* @param {Pattern} pat
|
||||
* @param {Pattern} lookup a pattern of indices
|
||||
* @param {function[]} funcs the array of functions from which to pull
|
||||
@@ -93,6 +97,7 @@ export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
|
||||
* @tags combiners
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
@@ -103,6 +108,7 @@ export const pickOut = register('pickOut', function (lookup, pat) {
|
||||
|
||||
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @tags combiners
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
@@ -112,6 +118,7 @@ export const pickmodOut = register('pickmodOut', function (lookup, pat) {
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
|
||||
* @tags combiners
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
@@ -122,6 +129,7 @@ export const pickRestart = register('pickRestart', function (lookup, pat) {
|
||||
|
||||
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @tags combiners
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
@@ -138,6 +146,7 @@ export const pickmodRestart = register('pickmodRestart', function (lookup, pat)
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
|
||||
* @tags combiners
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
@@ -148,6 +157,7 @@ export const pickReset = register('pickReset', function (lookup, pat) {
|
||||
|
||||
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @tags combiners
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
@@ -159,6 +169,7 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) {
|
||||
/** Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
||||
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
|
||||
* @name inhabit
|
||||
* @tags combiners
|
||||
* @synonyms pickSqueeze
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
@@ -180,6 +191,7 @@ export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], fun
|
||||
* second one.
|
||||
* @name inhabitmod
|
||||
* @synonyms pickmodSqueeze
|
||||
* @tags combiners
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
@@ -192,6 +204,7 @@ export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSq
|
||||
/**
|
||||
* Pick from the list of values (or patterns of values) via the index using the given
|
||||
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
|
||||
* @tags combiners
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
|
||||
+19
-3
@@ -2,7 +2,13 @@ import { NeoCyclist } from './neocyclist.mjs';
|
||||
import { Cyclist } from './cyclist.mjs';
|
||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||
import { errorLogger, logger } from './logger.mjs';
|
||||
import { setTime } from './time.mjs';
|
||||
import {
|
||||
setCpsFunc,
|
||||
setIsStarted,
|
||||
setPattern as exposeSchedulerPattern,
|
||||
setTime,
|
||||
setTriggerFunc,
|
||||
} from './schedulerState.mjs';
|
||||
import { evalScope } from './evaluate.mjs';
|
||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||
import { reset_state } from './impure.mjs';
|
||||
@@ -52,6 +58,7 @@ export function repl({
|
||||
getTime,
|
||||
onToggle: (started) => {
|
||||
updateState({ started });
|
||||
setIsStarted(started);
|
||||
onToggle?.(started);
|
||||
if (!started) {
|
||||
reset_state();
|
||||
@@ -65,6 +72,8 @@ export function repl({
|
||||
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
||||
const scheduler =
|
||||
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
||||
setTriggerFunc(schedulerOptions.onTrigger);
|
||||
setCpsFunc(() => scheduler.cps);
|
||||
let pPatterns = {};
|
||||
let anonymousIndex = 0;
|
||||
let allTransform;
|
||||
@@ -89,6 +98,7 @@ export function repl({
|
||||
const setPattern = async (pattern, autostart = true) => {
|
||||
pattern = editPattern?.(pattern) || pattern;
|
||||
await scheduler.setPattern(pattern, autostart);
|
||||
exposeSchedulerPattern(pattern);
|
||||
return pattern;
|
||||
};
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
@@ -106,6 +116,7 @@ export function repl({
|
||||
* Changes the global tempo to the given cycles per minute
|
||||
*
|
||||
* @name setcpm
|
||||
* @tags temporal
|
||||
* @alias setCpm
|
||||
* @param {number} cpm cycles per minute
|
||||
* @example
|
||||
@@ -119,7 +130,9 @@ export function repl({
|
||||
|
||||
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
|
||||
|
||||
/** Applies a function to all the running patterns. Note that the patterns are groups together into a single `stack` before the function is applied. This is probably what you want, but see `each` for
|
||||
let allTransforms = [];
|
||||
/**
|
||||
* Applies a function to all the running patterns. Note that the patterns are groups together into a single `stack` before the function is applied. This is probably what you want, but see `each` for
|
||||
* a version that applies the function to each pattern separately.
|
||||
* ```
|
||||
* $: sound("bd - cp sd")
|
||||
@@ -131,18 +144,21 @@ export function repl({
|
||||
* $: sound("hh*8")
|
||||
* all(x => x.pianoroll())
|
||||
* ```
|
||||
*
|
||||
* @tags combiners
|
||||
*/
|
||||
let allTransforms = [];
|
||||
const all = function (transform) {
|
||||
allTransforms.push(transform);
|
||||
return silence;
|
||||
};
|
||||
/** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern.
|
||||
*
|
||||
* ```
|
||||
* $: sound("bd - cp sd")
|
||||
* $: sound("hh*8")
|
||||
* each(fast("<2 3>"))
|
||||
* ```
|
||||
* @tags combiners
|
||||
*/
|
||||
const each = function (transform) {
|
||||
eachTransform = transform;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
schedulerState.mjs - Module to pipe out various parameters from the scheduler for global consumption
|
||||
Copyright (C) 2026 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/schedulerState.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
let time;
|
||||
let cpsFunc;
|
||||
let pattern;
|
||||
let triggerFunc;
|
||||
let isStarted;
|
||||
export function getTime() {
|
||||
if (!time) {
|
||||
throw new Error('no time set! use setTime to define a time source');
|
||||
}
|
||||
return time();
|
||||
}
|
||||
|
||||
export function setTime(func) {
|
||||
time = func;
|
||||
}
|
||||
|
||||
export function setCpsFunc(func) {
|
||||
cpsFunc = func;
|
||||
}
|
||||
|
||||
export function getCps() {
|
||||
return cpsFunc?.();
|
||||
}
|
||||
|
||||
export function setPattern(pat) {
|
||||
pattern = pat;
|
||||
}
|
||||
|
||||
export function getPattern() {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
export function setTriggerFunc(func) {
|
||||
triggerFunc = func;
|
||||
}
|
||||
|
||||
export function getTriggerFunc() {
|
||||
return triggerFunc;
|
||||
}
|
||||
|
||||
export function setIsStarted(val) {
|
||||
isStarted = !!val;
|
||||
}
|
||||
|
||||
export function getIsStarted() {
|
||||
return isStarted;
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export const signal = (func) => {
|
||||
* A sawtooth signal between 0 and 1.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
* @example
|
||||
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
|
||||
* .clip(saw.slow(2))
|
||||
@@ -38,6 +39,7 @@ export const saw = signal((t) => t % 1);
|
||||
* A sawtooth signal between -1 and 1 (like `saw`, but bipolar).
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
*/
|
||||
export const saw2 = saw.toBipolar();
|
||||
|
||||
@@ -45,6 +47,7 @@ export const saw2 = saw.toBipolar();
|
||||
* A sawtooth signal between 1 and 0 (like `saw`, but flipped).
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
* @example
|
||||
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
|
||||
* .clip(isaw.slow(2))
|
||||
@@ -59,6 +62,7 @@ export const isaw = signal((t) => 1 - (t % 1));
|
||||
* A sawtooth signal between 1 and -1 (like `saw2`, but flipped).
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
*/
|
||||
export const isaw2 = isaw.toBipolar();
|
||||
|
||||
@@ -66,12 +70,14 @@ export const isaw2 = isaw.toBipolar();
|
||||
* A sine signal between -1 and 1 (like `sine`, but bipolar).
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
*/
|
||||
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||
|
||||
/**
|
||||
* A sine signal between 0 and 1.
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
* @example
|
||||
* n(sine.segment(16).range(0,15))
|
||||
* .scale("C:minor")
|
||||
@@ -83,6 +89,7 @@ export const sine = sine2.fromBipolar();
|
||||
* A cosine signal between 0 and 1.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
* @example
|
||||
* n(stack(sine,cosine).segment(16).range(0,15))
|
||||
* .scale("C:minor")
|
||||
@@ -94,12 +101,14 @@ export const cosine = sine._early(Fraction(1).div(4));
|
||||
* A cosine signal between -1 and 1 (like `cosine`, but bipolar).
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
*/
|
||||
export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||
|
||||
/**
|
||||
* A square signal between 0 and 1.
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
* @example
|
||||
* n(square.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -110,6 +119,7 @@ export const square = signal((t) => Math.floor((t * 2) % 2));
|
||||
* A square signal between -1 and 1 (like `square`, but bipolar).
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
*/
|
||||
export const square2 = square.toBipolar();
|
||||
|
||||
@@ -117,6 +127,7 @@ export const square2 = square.toBipolar();
|
||||
* A triangle signal between 0 and 1.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
* @example
|
||||
* n(tri.segment(8).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -127,6 +138,7 @@ export const tri = fastcat(saw, isaw);
|
||||
* A triangle signal between -1 and 1 (like `tri`, but bipolar).
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
*/
|
||||
export const tri2 = fastcat(saw2, isaw2);
|
||||
|
||||
@@ -134,6 +146,7 @@ export const tri2 = fastcat(saw2, isaw2);
|
||||
* An inverted triangle signal between 1 and 0 (like `tri`, but flipped).
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
* @example
|
||||
* n(itri.segment(8).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -144,6 +157,7 @@ export const itri = fastcat(isaw, saw);
|
||||
* An inverted triangle signal between -1 and 1 (like `itri`, but bipolar).
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
*/
|
||||
export const itri2 = fastcat(isaw2, saw2);
|
||||
|
||||
@@ -151,6 +165,7 @@ export const itri2 = fastcat(isaw2, saw2);
|
||||
* A signal representing the cycle time.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @tags generators
|
||||
*/
|
||||
export const time = signal(id);
|
||||
|
||||
@@ -158,6 +173,7 @@ export const time = signal(id);
|
||||
* The mouse's x position value ranges from 0 to 1.
|
||||
* @name mousex
|
||||
* @return {Pattern}
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(mousex.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -167,6 +183,7 @@ export const time = signal(id);
|
||||
* The mouse's y position value ranges from 0 to 1.
|
||||
* @name mousey
|
||||
* @return {Pattern}
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(mousey.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -269,6 +286,7 @@ export const getRandsAtTime = (t, n = 1, seed = 0) => {
|
||||
* precise RNG, try `useRNG('precise')`.
|
||||
*
|
||||
* @name useRNG
|
||||
* @tags generators, math
|
||||
* @param {string} mod - Mode. One of 'legacy', 'precise'
|
||||
* @example
|
||||
* useRNG('legacy')
|
||||
@@ -280,6 +298,7 @@ export const useRNG = (mode = 'legacy') => (RNG_MODE = mode);
|
||||
|
||||
/**
|
||||
* A discrete pattern of numbers from 0 to n-1
|
||||
* @tags generators
|
||||
* @example
|
||||
* n(run(4)).scale("C4:pentatonic")
|
||||
* // n("0 1 2 3").scale("C4:pentatonic")
|
||||
@@ -290,6 +309,7 @@ export const run = (n) => saw.range(0, n).round().segment(n);
|
||||
* Creates a binary pattern from a number.
|
||||
*
|
||||
* @name binary
|
||||
* @tags generators
|
||||
* @param {number} n - input number to convert to binary
|
||||
* @example
|
||||
* "hh".s().struct(binary(5))
|
||||
@@ -304,6 +324,7 @@ export const binary = (n) => {
|
||||
* Creates a binary pattern from a number, padded to n bits long.
|
||||
*
|
||||
* @name binaryN
|
||||
* @tags generators
|
||||
* @param {number} n - input number to convert to binary
|
||||
* @param {number} nBits - pattern length, defaults to 16
|
||||
* @example
|
||||
@@ -321,6 +342,7 @@ export const binaryN = (n, nBits = 16) => {
|
||||
* Creates a binary list pattern from a number.
|
||||
*
|
||||
* @name binaryL
|
||||
* @tags generators
|
||||
* @param {number} n - input number to convert to binary
|
||||
* s("saw").seg(8)
|
||||
* .partials(binaryL(irand(4096).add(1)))
|
||||
@@ -334,6 +356,7 @@ export const binaryL = (n) => {
|
||||
* Creates a binary list pattern from a number, padded to n bits long.
|
||||
*
|
||||
* @name binaryNL
|
||||
* @tags generators
|
||||
* @param {number} n - input number to convert to binary
|
||||
* @param {number} nBits - pattern length, defaults to 16
|
||||
*/
|
||||
@@ -353,6 +376,7 @@ export const binaryNL = (n, nBits = 16) => {
|
||||
* Creates a list of random numbers of the given length
|
||||
*
|
||||
* @name randL
|
||||
* @tags generators
|
||||
* @param {number} n Number of random numbers to sample
|
||||
* @example
|
||||
* s("saw").seg(16).n(irand(12)).scale("F1:minor")
|
||||
@@ -384,6 +408,7 @@ const _rearrangeWith = (ipat, n, pat) => {
|
||||
* Slices a pattern into the given number of parts, then plays those parts in random order.
|
||||
* Each part will be played exactly once per cycle.
|
||||
* @name shuffle
|
||||
* @tags temporal
|
||||
* @example
|
||||
* note("c d e f").sound("piano").shuffle(4)
|
||||
* @example
|
||||
@@ -397,6 +422,7 @@ export const shuffle = register('shuffle', (n, pat) => {
|
||||
* Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`,
|
||||
* but parts might be played more than once, or not at all, per cycle.
|
||||
* @name scramble
|
||||
* @tags temporal
|
||||
* @example
|
||||
* note("c d e f").sound("piano").scramble(4)
|
||||
* @example
|
||||
@@ -409,6 +435,7 @@ export const scramble = register('scramble', (n, pat) => {
|
||||
/**
|
||||
* Modify a pattern by applying a function to the `randomSeed` control if present
|
||||
*
|
||||
* @tags math
|
||||
* @param {Function} func Function from seed (or undefined) to seed (or undefined)
|
||||
* @param {Pattern} pat Pattern to update
|
||||
* @returns Pattern
|
||||
@@ -428,6 +455,7 @@ export const withSeed = (func, pat) => {
|
||||
* that use randomness, like `shuffle` and `sometimes`.
|
||||
*
|
||||
* @name seed
|
||||
* @tags math
|
||||
* @param {number} n A new seed. Can be any number.
|
||||
* @example
|
||||
* $: s("hh*4").degrade();
|
||||
@@ -441,6 +469,7 @@ export const seed = register('seed', (n, pat) => {
|
||||
* A continuous pattern of random numbers, between 0 and 1.
|
||||
*
|
||||
* @name rand
|
||||
* @tags generators
|
||||
* @example
|
||||
* // randomly change the cutoff
|
||||
* s("bd*4,hh*8").cutoff(rand.range(500,8000))
|
||||
@@ -449,6 +478,7 @@ export const seed = register('seed', (n, pat) => {
|
||||
export const rand = signal((t, controls) => getRandsAtTime(t, 1, controls.randSeed));
|
||||
/**
|
||||
* A continuous pattern of random numbers, between -1 and 1
|
||||
* @tags generators
|
||||
*/
|
||||
export const rand2 = rand.toBipolar();
|
||||
|
||||
@@ -458,6 +488,7 @@ export const _brandBy = (p) => rand.fmap((x) => x < p);
|
||||
* A continuous pattern of 0 or 1 (binary random), with a probability for the value being 1
|
||||
*
|
||||
* @name brandBy
|
||||
* @tags generators
|
||||
* @param {number} probability - a number between 0 and 1
|
||||
* @example
|
||||
* s("hh*10").pan(brandBy(0.2))
|
||||
@@ -468,6 +499,7 @@ export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
||||
* A continuous pattern of 0 or 1 (binary random)
|
||||
*
|
||||
* @name brand
|
||||
* @tags generators
|
||||
* @example
|
||||
* s("hh*10").pan(brand)
|
||||
*/
|
||||
@@ -479,6 +511,7 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
||||
* A continuous pattern of random integers, between 0 and n-1.
|
||||
*
|
||||
* @name irand
|
||||
* @tags generators
|
||||
* @param {number} n max value (exclusive)
|
||||
* @example
|
||||
* // randomly select scale notes from 0 - 7 (= C to C)
|
||||
@@ -501,6 +534,7 @@ export const __chooseWith = (pat, xs) => {
|
||||
/**
|
||||
* Choose from the list of values (or patterns of values) using the given
|
||||
* pattern of numbers, which should be in the range of 0..1
|
||||
* @tags temporal
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
@@ -514,6 +548,7 @@ export const chooseWith = (pat, xs) => {
|
||||
/**
|
||||
* As with {chooseWith}, but the structure comes from the chosen values, rather
|
||||
* than the pattern you're using to choose with.
|
||||
* @tags temporal
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
@@ -524,6 +559,7 @@ export const chooseInWith = (pat, xs) => {
|
||||
|
||||
/**
|
||||
* Chooses randomly from the given list of elements.
|
||||
* @tags temporal
|
||||
* @param {...any} xs values / patterns to choose from.
|
||||
* @returns {Pattern} - a continuous pattern.
|
||||
* @example
|
||||
@@ -539,6 +575,7 @@ export const chooseOut = choose;
|
||||
* Chooses from the given list of values (or patterns of values), according
|
||||
* to the pattern that the method is called on. The pattern should be in
|
||||
* the range 0 .. 1.
|
||||
* @tags temporal
|
||||
* @param {...any} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
@@ -549,6 +586,7 @@ Pattern.prototype.choose = function (...xs) {
|
||||
/**
|
||||
* As with choose, but the pattern that this method is called on should be
|
||||
* in the range -1 .. 1
|
||||
* @tags temporal
|
||||
* @param {...any} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
@@ -558,6 +596,7 @@ Pattern.prototype.choose2 = function (...xs) {
|
||||
|
||||
/**
|
||||
* Picks one of the elements at random each cycle.
|
||||
* @tags temporal
|
||||
* @synonyms randcat
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
@@ -600,6 +639,7 @@ const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
|
||||
|
||||
/**
|
||||
* Chooses randomly from the given list of elements by giving a probability to each element
|
||||
* @tags temporal
|
||||
* @param {...any} pairs arrays of value and weight
|
||||
* @returns {Pattern} - a continuous pattern.
|
||||
* @example
|
||||
@@ -609,6 +649,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
||||
|
||||
/**
|
||||
* Picks one of the elements at random each cycle by giving a probability to each element
|
||||
* @tags temporal
|
||||
* @synonyms wrandcat
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
@@ -652,6 +693,7 @@ function _berlin(t, seed = 0) {
|
||||
/**
|
||||
* Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1.
|
||||
*
|
||||
* @tags generators
|
||||
* @name perlin
|
||||
* @example
|
||||
* // randomly change the cutoff
|
||||
@@ -664,6 +706,7 @@ export const perlin = signal((t, controls) => _perlin(t, controls.randSeed));
|
||||
* Generates a continuous pattern of [berlin noise](conceived by Jame Coyne and Jade Rowland as a joke but turned out to be surprisingly cool and useful,
|
||||
* like perlin noise but with sawtooth waves), in the range 0..1.
|
||||
*
|
||||
* @tags generators
|
||||
* @name berlin
|
||||
* @example
|
||||
* // ascending arpeggios
|
||||
@@ -684,6 +727,7 @@ export const degradeByWith = register(
|
||||
* 0 = 0% chance of removal
|
||||
* 1 = 100% chance of removal
|
||||
*
|
||||
* @tags temporal
|
||||
* @name degradeBy
|
||||
* @memberof Pattern
|
||||
* @param {number} amount - a number between 0 and 1
|
||||
@@ -709,6 +753,7 @@ export const degradeBy = register(
|
||||
*
|
||||
* Randomly removes 50% of events from the pattern. Shorthand for `.degradeBy(0.5)`
|
||||
*
|
||||
* @tags temporal
|
||||
* @name degrade
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
@@ -725,6 +770,7 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, t
|
||||
* 1 = 0% chance of removal
|
||||
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
|
||||
*
|
||||
* @tags temporal
|
||||
* @name undegradeBy
|
||||
* @memberof Pattern
|
||||
* @param {number} amount - a number between 0 and 1
|
||||
@@ -753,6 +799,7 @@ export const undegradeBy = register(
|
||||
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
|
||||
* Events that would be removed by degrade are let through by undegrade and vice versa (see second example).
|
||||
*
|
||||
* @tags temporal
|
||||
* @name undegrade
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
@@ -771,6 +818,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5), t
|
||||
* Randomly applies the given function by the given probability.
|
||||
* Similar to `someCyclesBy`
|
||||
*
|
||||
* @tags temporal
|
||||
* @name sometimesBy
|
||||
* @memberof Pattern
|
||||
* @param {number | Pattern} probability - a number between 0 and 1
|
||||
@@ -790,6 +838,7 @@ export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
||||
*
|
||||
* Applies the given function with a 50% chance
|
||||
*
|
||||
* @tags temporal
|
||||
* @name sometimes
|
||||
* @memberof Pattern
|
||||
* @param {function} function - the transformation to apply
|
||||
@@ -811,6 +860,7 @@ export const sometimes = register('sometimes', function (func, pat) {
|
||||
* @param {number | Pattern} probability - a number between 0 and 1
|
||||
* @param {function} function - the transformation to apply
|
||||
* @returns Pattern
|
||||
* @tags temporal
|
||||
* @example
|
||||
* s("bd,hh*8").someCyclesBy(.3, x=>x.speed("0.5"))
|
||||
*/
|
||||
@@ -833,6 +883,7 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat)
|
||||
* @name someCycles
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @tags temporal
|
||||
* @example
|
||||
* s("bd,hh*8").someCycles(x=>x.speed("0.5"))
|
||||
*/
|
||||
@@ -847,6 +898,7 @@ export const someCycles = register('someCycles', function (func, pat) {
|
||||
* @name often
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @tags temporal
|
||||
* @example
|
||||
* s("hh*8").often(x=>x.speed("0.5"))
|
||||
*/
|
||||
@@ -861,6 +913,7 @@ export const often = register('often', function (func, pat) {
|
||||
* @name rarely
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @tags temporal
|
||||
* @example
|
||||
* s("hh*8").rarely(x=>x.speed("0.5"))
|
||||
*/
|
||||
@@ -872,6 +925,7 @@ export const rarely = register('rarely', function (func, pat) {
|
||||
*
|
||||
* Shorthand for `.sometimesBy(0.1, fn)`
|
||||
*
|
||||
* @tags temporal
|
||||
* @name almostNever
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
@@ -886,6 +940,7 @@ export const almostNever = register('almostNever', function (func, pat) {
|
||||
*
|
||||
* Shorthand for `.sometimesBy(0.9, fn)`
|
||||
*
|
||||
* @tags temporal
|
||||
* @name almostAlways
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
@@ -900,6 +955,7 @@ export const almostAlways = register('almostAlways', function (func, pat) {
|
||||
*
|
||||
* Shorthand for `.sometimesBy(0, fn)` (never calls fn)
|
||||
*
|
||||
* @tags temporal
|
||||
* @name never
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
@@ -914,6 +970,7 @@ export const never = register('never', function (_, pat) {
|
||||
*
|
||||
* Shorthand for `.sometimesBy(1, fn)` (always calls fn)
|
||||
*
|
||||
* @tags temporal
|
||||
* @name always
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
@@ -942,6 +999,7 @@ export function _keyDown(keyname) {
|
||||
* Do something on a keypress, or array of keypresses
|
||||
* [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
|
||||
*
|
||||
* @tags external_io
|
||||
* @name whenKey
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
@@ -958,6 +1016,7 @@ export const whenKey = register('whenKey', function (input, func, pat) {
|
||||
* returns true when a key or array of keys is held
|
||||
* [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
|
||||
*
|
||||
* @tags external_io
|
||||
* @name keyDown
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
@@ -975,6 +1034,8 @@ export const keyDown = register('keyDown', function (pat) {
|
||||
* event durations, from the pattern that it is combined with.
|
||||
* For example `cyclesPer.struct("1 1 [1 1] 1")` would give the same as `"0.25 0.25 [0.125 0.125] 0.25"`.
|
||||
* See also its reciprocal, `per`, also known as `perCycle`.
|
||||
*
|
||||
* @tags temporal
|
||||
* @example
|
||||
* // Shorter events are lower in pitch
|
||||
* sound("saw saw [saw saw] saw")
|
||||
@@ -993,6 +1054,7 @@ export const cyclesPer = new Pattern(function (state) {
|
||||
* event durations, from the pattern that it is combined with.
|
||||
* For example `per.struct("1 1 [1 1] 1")` would give the same as `"4 4 [8 8] 4"`.
|
||||
* See also its reciprocal, `cyclesPer`.
|
||||
* @tags temporal
|
||||
* @synonyms perCycle
|
||||
* @example
|
||||
* // Shorter events are more distorted
|
||||
@@ -1010,6 +1072,7 @@ export const perCycle = per;
|
||||
* particular, where the event duration halves, the
|
||||
* returned value increases by one. `perx.struct("1 1 [1 [1 1]] 1")` would therefore be
|
||||
* the same as `"3 3 [4 [5 5]] 3"`.
|
||||
* @tags temporal
|
||||
*/
|
||||
export const perx = new Pattern(function (state) {
|
||||
const n = Fraction(1).div(state.span.duration);
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
let time;
|
||||
export function getTime() {
|
||||
if (!time) {
|
||||
throw new Error('no time set! use setTime to define a time source');
|
||||
}
|
||||
return time();
|
||||
}
|
||||
|
||||
export function setTime(func) {
|
||||
time = func;
|
||||
}
|
||||
@@ -135,6 +135,8 @@ export async function loadOrc(url) {
|
||||
* p4 -- MIDI key number (as a real number, not an integer but in [0, 127].
|
||||
* p5 -- MIDI velocity (as a real number, not an integer but in [0, 127].
|
||||
* p6 -- Strudel controls, as a string.
|
||||
*
|
||||
* @tags external_io
|
||||
*/
|
||||
export const csoundm = register('csoundm', (instrument, pat) => {
|
||||
let p1 = instrument;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/csound",
|
||||
"version": "1.2.6",
|
||||
"version": "1.3.0",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/draw",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"description": "Helpers for drawing with Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -42,6 +42,7 @@ const getValue = (e) => {
|
||||
*
|
||||
* @name pianoroll
|
||||
* @synonyms punchcard
|
||||
* @tags visualization
|
||||
* @param {Object} options Object containing all the optional following parameters as key value pairs:
|
||||
* @param {integer} cycles number of cycles to be displayed at the same time - defaults to 4
|
||||
* @param {number} playhead location of the active notes on the time axis - 0 to 1, defaults to 0.5
|
||||
@@ -299,6 +300,7 @@ Pattern.prototype.punchcard = function (options) {
|
||||
* Supports all the same options as pianoroll.
|
||||
*
|
||||
* @name wordfall
|
||||
* @tags visualization
|
||||
*/
|
||||
Pattern.prototype.wordfall = function (options) {
|
||||
return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options });
|
||||
|
||||
@@ -116,6 +116,7 @@ export function pitchwheel({
|
||||
/**
|
||||
* Renders a pitch circle to visualize frequencies within one octave
|
||||
* @name pitchwheel
|
||||
* @tags visualization
|
||||
* @param {number} hapcircles
|
||||
* @param {number} circle
|
||||
* @param {number} edo
|
||||
|
||||
@@ -129,6 +129,7 @@ function drawSpiral(options) {
|
||||
* Displays a spiral visual.
|
||||
*
|
||||
* @name spiral
|
||||
* @tags visualization
|
||||
* @param {Object} options Object containing all the optional following parameters as key value pairs:
|
||||
* @param {number} stretch controls the rotations per cycle ratio, where 1 = 1 cycle / 360 degrees
|
||||
* @param {number} size the diameter of the spiral
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/embed",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
||||
"main": "embed.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/gamepad",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"description": "Gamepad Inputs for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/hydra",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"description": "Hydra integration for strudel",
|
||||
"main": "hydra.mjs",
|
||||
"type": "module",
|
||||
|
||||
+184
-28
@@ -5,9 +5,23 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import * as _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern, logger, ref } from '@strudel/core';
|
||||
import {
|
||||
Hap,
|
||||
Pattern,
|
||||
TimeSpan,
|
||||
getCps,
|
||||
getIsStarted,
|
||||
getPattern,
|
||||
getTime,
|
||||
getTriggerFunc,
|
||||
isPattern,
|
||||
logger,
|
||||
ref,
|
||||
reify,
|
||||
} from '@strudel/core';
|
||||
import { noteToMidi, getControlName } from '@strudel/core';
|
||||
import { Note } from 'webmidi';
|
||||
import { getAudioContext } from '@strudel/webaudio';
|
||||
import { scheduleAtTime } from '../superdough/helpers.mjs';
|
||||
|
||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||
@@ -124,6 +138,7 @@ function githubPath(base, subpath = '') {
|
||||
|
||||
/**
|
||||
* configures the default midimap, which is used when no "midimap" port is set
|
||||
* @tags external_io
|
||||
* @example
|
||||
* defaultmidimap({ lpf: 74 })
|
||||
* $: note("c a f e").midi();
|
||||
@@ -137,6 +152,7 @@ let loadCache = {};
|
||||
|
||||
/**
|
||||
* Adds midimaps to the registry. Inside each midimap, control names (e.g. lpf) are mapped to cc numbers.
|
||||
* @tags external_io
|
||||
* @example
|
||||
* midimaps({ mymap: { lpf: 74 } })
|
||||
* $: note("c a f e")
|
||||
@@ -291,6 +307,7 @@ function sendNote(note, velocity, duration, device, midichan, targetTime) {
|
||||
|
||||
/**
|
||||
* MIDI output: Opens a MIDI output port.
|
||||
* @tags external_io
|
||||
* @param {string | number} midiport MIDI device name or index defaulting to 0
|
||||
* @param {object} options Additional MIDI configuration options
|
||||
* @example
|
||||
@@ -477,14 +494,42 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
||||
});
|
||||
};
|
||||
|
||||
let listeners = {};
|
||||
const refs = {};
|
||||
const refsByChan = {};
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @tags external_io
|
||||
* @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)
|
||||
@@ -498,29 +543,11 @@ const refsByChan = {};
|
||||
* 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) {
|
||||
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)}` : ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
const device = await _initialize(input);
|
||||
refs[input] ??= {};
|
||||
refsByChan[input] ??= {};
|
||||
const cc = (cc, chan) => {
|
||||
@@ -532,8 +559,7 @@ export async function midin(input) {
|
||||
|
||||
listeners[input] && device.removeListener('midimessage', listeners[input]);
|
||||
listeners[input] = (e) => {
|
||||
const ccNum = e.dataBytes[0];
|
||||
const v = e.dataBytes[1];
|
||||
const [ccNum, v] = e.dataBytes;
|
||||
const chan = e.message.channel;
|
||||
const scaled = v / 127;
|
||||
refsByChan[input][ccNum] ??= {};
|
||||
@@ -543,3 +569,133 @@ 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
|
||||
* @tags external_io
|
||||
* @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,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/midi",
|
||||
"version": "1.2.6",
|
||||
"version": "1.3.0",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mini",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mondolang",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"description": "a language for functional composition that translates to js",
|
||||
"main": "mondo.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mondo",
|
||||
"version": "1.1.5",
|
||||
"version": "1.1.6",
|
||||
"description": "mondo notation for strudel",
|
||||
"main": "mondough.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name accelerationX
|
||||
* @return {Pattern}
|
||||
* @synonyms accX
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(accelerationX.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -17,6 +18,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name accelerationY
|
||||
* @return {Pattern}
|
||||
* @synonyms accY
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(accelerationY.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -27,6 +29,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name accelerationZ
|
||||
* @return {Pattern}
|
||||
* @synonyms accZ
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(accelerationZ.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -37,6 +40,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name gravityX
|
||||
* @return {Pattern}
|
||||
* @synonyms gravX
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(gravityX.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -47,6 +51,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name gravityY
|
||||
* @return {Pattern}
|
||||
* @synonyms gravY
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(gravityY.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -57,6 +62,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name gravityZ
|
||||
* @return {Pattern}
|
||||
* @synonyms gravZ
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(gravityZ.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -67,6 +73,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name rotationAlpha
|
||||
* @return {Pattern}
|
||||
* @synonyms rotA, rotZ, rotationZ
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(rotationAlpha.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -77,6 +84,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name rotationBeta
|
||||
* @return {Pattern}
|
||||
* @synonyms rotB, rotX, rotationX
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(rotationBeta.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -87,6 +95,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name rotationGamma
|
||||
* @return {Pattern}
|
||||
* @synonyms rotG, rotY, rotationY
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(rotationGamma.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -97,6 +106,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name orientationAlpha
|
||||
* @return {Pattern}
|
||||
* @synonyms oriA, oriZ, orientationZ
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(orientationAlpha.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -107,6 +117,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name orientationBeta
|
||||
* @return {Pattern}
|
||||
* @synonyms oriB, oriX, orientationX
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(orientationBeta.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -117,6 +128,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name orientationGamma
|
||||
* @return {Pattern}
|
||||
* @synonyms oriG, oriY, orientationY
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(orientationGamma.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -127,6 +139,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name absoluteOrientationAlpha
|
||||
* @return {Pattern}
|
||||
* @synonyms absOriA, absOriZ, absoluteOrientationZ
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(absoluteOrientationAlpha.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -137,6 +150,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name absoluteOrientationBeta
|
||||
* @return {Pattern}
|
||||
* @synonyms absOriB, absOriX, absoluteOrientationX
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(absoluteOrientationBeta.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
@@ -147,6 +161,7 @@ import { signal } from '../core/signal.mjs';
|
||||
* @name absoluteOrientationGamma
|
||||
* @return {Pattern}
|
||||
* @synonyms absOriG, absOriY, absoluteOrientationY
|
||||
* @tags external_io
|
||||
* @example
|
||||
* n(absoluteOrientationGamma.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/motion",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"description": "DeviceMotion API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mqtt",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"description": "MQTT API for strudel",
|
||||
"main": "mqtt.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -79,6 +79,7 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
||||
* For more info, read [MIDI & OSC in the docs](https://strudel.cc/learn/input-output/)
|
||||
*
|
||||
* @name osc
|
||||
* @tags external_io
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/osc",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.2",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"bin": "./server.js",
|
||||
|
||||
Regular → Executable
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/reference",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.2",
|
||||
"description": "Headless reference of all strudel functions",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/repl",
|
||||
"version": "1.2.7",
|
||||
"version": "1.3.0",
|
||||
"description": "Strudel REPL as a Web Component",
|
||||
"module": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/sampler",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/serial",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"description": "Webserial API for strudel",
|
||||
"main": "serial.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/soundfonts",
|
||||
"version": "1.2.6",
|
||||
"version": "1.3.0",
|
||||
"description": "Soundsfont support for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -154,9 +154,8 @@ 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].setValueAtTime(value, now);
|
||||
node[key].value = value;
|
||||
});
|
||||
return node;
|
||||
}
|
||||
@@ -173,6 +172,7 @@ 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,9 +242,8 @@ 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].setValueAtTime(value, now);
|
||||
filter[key].value = value;
|
||||
});
|
||||
frequencyParam = filter.frequency;
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ export const connectLFO = (id, params, nodeTracker) => {
|
||||
fxi = 'main',
|
||||
depth = 1,
|
||||
depthabs,
|
||||
retrig = 0,
|
||||
...filteredParams
|
||||
} = params;
|
||||
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
|
||||
@@ -109,7 +110,7 @@ export const connectLFO = (id, params, nodeTracker) => {
|
||||
const modParams = {
|
||||
...filteredParams,
|
||||
frequency: sync !== undefined ? sync * cps : rate,
|
||||
time: cycle / cps,
|
||||
time: retrig > 0.5 ? 0 : cycle / cps,
|
||||
depth: depthValue,
|
||||
min,
|
||||
max,
|
||||
|
||||
@@ -7,11 +7,13 @@ 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));
|
||||
@@ -38,34 +40,43 @@ 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 = node.context?.currentTime ?? 0;
|
||||
const now = getNodeTime(node);
|
||||
getParams(node).forEach((param) => param.cancelScheduledValues(now));
|
||||
const pool = nodePools.get(key) ?? [];
|
||||
if (pool.length < MAX_POOL_SIZE) {
|
||||
pool.push(new WeakRef(node));
|
||||
nodePools.set(key, pool);
|
||||
}
|
||||
pool.push(new WeakRef(node));
|
||||
nodePools.set(key, pool);
|
||||
};
|
||||
|
||||
export const markWorkletAsDead = (worklet) => (worklet[IS_WORKLET_DEAD] = true);
|
||||
// Audio worklets are given a grace period to survive (`return true`) after
|
||||
// being released. This concludes at time `end + 0.5`. We test here whether we are
|
||||
// within some safe distance of that (`end + 0.45`) and if so, permit the node to be
|
||||
// released. This helps to prevent race conditions between node termination and node
|
||||
// re-use
|
||||
const isNodeAlive = (node) => {
|
||||
// Skip check if node is not a worklet
|
||||
if (!(node instanceof AudioWorkletNode)) return true;
|
||||
const now = getNodeTime(node);
|
||||
const end = node?.parameters?.get('end').value ?? 0;
|
||||
return now < end + 0.45;
|
||||
};
|
||||
|
||||
// Attempt to get node from the pool. If this fails, fall back
|
||||
// to building it with the factory
|
||||
export const getNodeFromPool = (key, factory) => {
|
||||
const pool = nodePools.get(key) ?? [];
|
||||
let node;
|
||||
let found = false;
|
||||
while (pool.length) {
|
||||
const ref = pool.pop();
|
||||
node = ref?.deref();
|
||||
if (node != null && !node[IS_WORKLET_DEAD]) break;
|
||||
if (node != null && isNodeAlive(node)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (node == null || node[IS_WORKLET_DEAD]) {
|
||||
if (!found) {
|
||||
node = factory();
|
||||
}
|
||||
node[POOL_KEY] = key;
|
||||
|
||||
@@ -35,7 +35,9 @@ class OLAProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
/** Handles dynamic reallocation of input/output channels buffer
|
||||
(channel numbers may lety during lifecycle) **/
|
||||
* (channel numbers may vary during lifecycle)
|
||||
* @tags internals
|
||||
**/
|
||||
reallocateChannelsIfNeeded(inputs, outputs) {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
let nbChannels = inputs[i].length;
|
||||
@@ -88,7 +90,10 @@ class OLAProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/** Read next web audio block to input buffers **/
|
||||
/**
|
||||
* Read next web audio block to input buffers
|
||||
* @tags internals
|
||||
**/
|
||||
readInputs(inputs) {
|
||||
// when playback is paused, we may stop receiving new samples
|
||||
if (inputs[0].length && inputs[0][0].length == 0) {
|
||||
@@ -108,7 +113,9 @@ class OLAProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/** Write next web audio block from output buffers **/
|
||||
/** Write next web audio block from output buffers
|
||||
* @tags internals
|
||||
**/
|
||||
writeOutputs(outputs) {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
@@ -118,7 +125,9 @@ class OLAProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/** Shift left content of input buffers to receive new web audio block **/
|
||||
/** Shift left content of input buffers to receive new web audio block
|
||||
* @tags internals
|
||||
**/
|
||||
shiftInputBuffers() {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
@@ -127,7 +136,9 @@ class OLAProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/** Shift left content of output buffers to receive new web audio block **/
|
||||
/** Shift left content of output buffers to receive new web audio block
|
||||
* @tags internals
|
||||
**/
|
||||
shiftOutputBuffers() {
|
||||
for (let i = 0; i < this.nbOutputs; i++) {
|
||||
for (let j = 0; j < this.outputBuffers[i].length; j++) {
|
||||
@@ -137,7 +148,9 @@ class OLAProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/** Copy contents of input buffers to buffer actually sent to process **/
|
||||
/** Copy contents of input buffers to buffer actually sent to process
|
||||
* @tags internals
|
||||
**/
|
||||
prepareInputBuffersToSend() {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
@@ -146,7 +159,9 @@ class OLAProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/** Add contents of output buffers just processed to output buffers **/
|
||||
/** Add contents of output buffers just processed to output buffers
|
||||
* @tags internals
|
||||
**/
|
||||
handleOutputBuffersToRetrieve() {
|
||||
for (let i = 0; i < this.nbOutputs; i++) {
|
||||
for (let j = 0; j < this.outputBuffers[i].length; j++) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superdough",
|
||||
"version": "1.2.6",
|
||||
"version": "1.3.0",
|
||||
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -36,6 +36,8 @@
|
||||
"vite-plugin-bundle-audioworklet": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kabelsalat/lib": "^0.4.1",
|
||||
"@kabelsalat/web": "^0.4.1",
|
||||
"nanostores": "^0.11.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -18,6 +18,7 @@ var reverbGen = {};
|
||||
|
||||
/** Generates a reverb impulse response.
|
||||
|
||||
@tags internals
|
||||
@param {!Object} params TODO: Document the properties.
|
||||
@param {!function(!AudioBuffer)} callback Function to call when
|
||||
the impulse response has been generated. The impulse response
|
||||
@@ -50,7 +51,7 @@ reverbGen.generateReverb = function (params, callback) {
|
||||
|
||||
/** Creates a canvas element showing a graph of the given data.
|
||||
|
||||
|
||||
@tags internals
|
||||
@param {!Float32Array} data An array of numbers, or a Float32Array.
|
||||
@param {number} width Width in pixels of the canvas.
|
||||
@param {number} height Height in pixels of the canvas.
|
||||
@@ -81,7 +82,9 @@ reverbGen.generateGraph = function (data, width, height, min, max) {
|
||||
@param {number} lpFreqEnd
|
||||
@param {number} lpFreqEndAt
|
||||
@param {!function(!AudioBuffer)} callback May be called
|
||||
immediately within the current execution context, or later.*/
|
||||
immediately within the current execution context, or later.
|
||||
@tags internals
|
||||
*/
|
||||
var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt, callback) {
|
||||
if (lpFreqStart == 0) {
|
||||
callback(input);
|
||||
|
||||
@@ -235,6 +235,7 @@ export async function fetchSampleMap(url) {
|
||||
|
||||
/**
|
||||
* Loads a collection of samples to use with `s`
|
||||
* @tags samples
|
||||
* @example
|
||||
* samples('github:tidalcycles/dirt-samples');
|
||||
* s("[bd ~]*2, [~ hh]*2, ~ sd")
|
||||
|
||||
@@ -14,11 +14,10 @@ import {
|
||||
createFilter,
|
||||
effectSend,
|
||||
gainNode,
|
||||
getADSRValues,
|
||||
getCompressor,
|
||||
getDistortion,
|
||||
getFrequencyFromValue,
|
||||
getLfo,
|
||||
getParamADSR,
|
||||
getWorklet,
|
||||
releaseAudioNode,
|
||||
webAudioTimeout,
|
||||
@@ -41,6 +40,7 @@ export let maxPolyphony = DEFAULT_MAX_POLYPHONY;
|
||||
* start to die out in first-in-first-out order once the max polyphony has been hit
|
||||
*
|
||||
* @name setMaxPolyphony
|
||||
* @tags fx, superdough
|
||||
* @param {number} Max polyphony. Defaults to 128
|
||||
* @example
|
||||
* setMaxPolyphony(4)
|
||||
@@ -74,6 +74,7 @@ export function applyGainCurve(val) {
|
||||
* quadratic, exponential, etc. rather than linear
|
||||
*
|
||||
* @name setGainCurve
|
||||
* @tags fx, superdough
|
||||
* @param {Function} function to apply to all gain values
|
||||
* @example
|
||||
* setGainCurve((x) => x * x) // quadratic gain
|
||||
@@ -129,6 +130,8 @@ async function aliasBankPath(path) {
|
||||
* Optionally accepts a single argument string of a path to a JSON file containing bank aliases.
|
||||
* @param {string} bank - The bank to alias
|
||||
* @param {string} alias - The alias to use for the bank
|
||||
*
|
||||
* @tags samples
|
||||
*/
|
||||
export async function aliasBank(...args) {
|
||||
switch (args.length) {
|
||||
@@ -147,6 +150,7 @@ export async function aliasBank(...args) {
|
||||
|
||||
/**
|
||||
* Register an alias for a sound.
|
||||
* @tags samples
|
||||
* @param {string} original - The original sound name
|
||||
* @param {string} alias - The alias to use for the sound
|
||||
*/
|
||||
@@ -178,19 +182,6 @@ 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,
|
||||
@@ -269,7 +260,14 @@ export function loadWorklets() {
|
||||
return workletsLoading;
|
||||
}
|
||||
|
||||
let AUDIO_OUTPUTS;
|
||||
let kabel;
|
||||
async function initKabelsalat() {
|
||||
const { SalatRepl } = await import('@kabelsalat/web');
|
||||
logger('[kabelsalat] ready');
|
||||
kabel = new SalatRepl({ localScope: true });
|
||||
return kabel;
|
||||
}
|
||||
|
||||
// this function should be called on first user interaction (to avoid console warning)
|
||||
export async function initAudio(options = {}) {
|
||||
const {
|
||||
@@ -290,8 +288,8 @@ export async function initAudio(options = {}) {
|
||||
|
||||
if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) {
|
||||
try {
|
||||
AUDIO_OUTPUTS = await getAudioDevices();
|
||||
const id = AUDIO_OUTPUTS.get(audioDeviceName);
|
||||
const devices = await getAudioDevices();
|
||||
const id = devices.get(audioDeviceName);
|
||||
const isValidID = (id ?? '').length > 0;
|
||||
if (audioCtx.sinkId !== id && isValidID) {
|
||||
await audioCtx.setSinkId(id);
|
||||
@@ -316,6 +314,7 @@ export async function initAudio(options = {}) {
|
||||
} catch (err) {
|
||||
console.warn('could not load AudioWorklet effects', err);
|
||||
}
|
||||
await initKabelsalat();
|
||||
logger('[superdough] ready');
|
||||
}
|
||||
let audioReady;
|
||||
@@ -332,51 +331,6 @@ 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) {
|
||||
@@ -496,6 +450,14 @@ class Chain {
|
||||
}
|
||||
}
|
||||
|
||||
const compileKabel = (code) => {
|
||||
if (!kabel) {
|
||||
throw new Error('kabelsalat not loaded');
|
||||
}
|
||||
const node = kabel.evaluate(code);
|
||||
return node.compile({ log: false });
|
||||
};
|
||||
|
||||
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
|
||||
// mapping from main FX and numbered FX chains to nodes
|
||||
const nodes = { main: {} };
|
||||
@@ -558,7 +520,6 @@ 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);
|
||||
@@ -608,21 +569,6 @@ 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);
|
||||
|
||||
@@ -659,6 +605,7 @@ 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;
|
||||
@@ -676,7 +623,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
delayfeedback = getDefaultValue('delayfeedback'),
|
||||
delaysync = getDefaultValue('delaysync'),
|
||||
delaytime,
|
||||
stretch = getDefaultValue('stretch'),
|
||||
i = getDefaultValue('i'),
|
||||
} = fx;
|
||||
gain = applyGainCurve(nanFallback(gain, 1));
|
||||
@@ -687,8 +633,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
||||
|
||||
if (stretch !== undefined) {
|
||||
const phaseVocoder = getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch });
|
||||
// Kabelsalat
|
||||
if (fx.workletSrc !== undefined) {
|
||||
const workletNode = getWorklet(ac, 'generic-processor', {}, { 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 });
|
||||
chain.connect(phaseVocoder);
|
||||
fxNodes['stretch'] = [phaseVocoder];
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||
import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs';
|
||||
import { getNodeFromPool, releaseNodeToPool } from './nodePools.mjs';
|
||||
|
||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user', 'one'];
|
||||
const waveformAliases = [
|
||||
@@ -178,17 +178,12 @@ 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.setValueAtTime(target, now);
|
||||
param.value = target;
|
||||
});
|
||||
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);
|
||||
|
||||
@@ -110,7 +110,9 @@ export function getCommonSampleInfo(hapValue, bank) {
|
||||
return { transpose, url, index, midi, label };
|
||||
}
|
||||
|
||||
/** Selects entries from `source` and renames them via `map` */
|
||||
/** Selects entries from `source` and renames them via `map`
|
||||
* @tags internals
|
||||
*/
|
||||
export const pickAndRename = (source, map) => {
|
||||
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
webAudioTimeout,
|
||||
releaseAudioNode,
|
||||
} from './helpers.mjs';
|
||||
import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs';
|
||||
import { getNodeFromPool, releaseNodeToPool } from './nodePools.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
export const Warpmode = Object.freeze({
|
||||
@@ -186,6 +186,7 @@ export function registerWaveTable(key, tables, params) {
|
||||
* Loads a collection of wavetables to use with `s`
|
||||
*
|
||||
* @name tables
|
||||
* @tags fx
|
||||
*/
|
||||
export const tables = async (url, frameLen, json, options = {}) => {
|
||||
if (json !== undefined) return _processTables(json, url, frameLen);
|
||||
@@ -244,17 +245,12 @@ 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.setValueAtTime(target, now);
|
||||
param.value = target;
|
||||
});
|
||||
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;
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
import OLAProcessor from './ola-processor';
|
||||
import FFT from './fft.js';
|
||||
import { getDistortionAlgorithm } from './helpers.mjs';
|
||||
import * as ugens from '@kabelsalat/lib/src/ugens.js';
|
||||
|
||||
const UGENS = new Map(Object.entries(ugens));
|
||||
|
||||
const blockSize = 128;
|
||||
const PI = Math.PI;
|
||||
@@ -463,7 +466,6 @@ 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') {
|
||||
@@ -479,16 +481,16 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
return [
|
||||
{
|
||||
name: 'begin',
|
||||
defaultValue: 0,
|
||||
defaultValue: -1,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
min: -1,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'end',
|
||||
defaultValue: 0,
|
||||
defaultValue: -1,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
min: 0,
|
||||
min: -1,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -523,16 +525,17 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
];
|
||||
}
|
||||
process(_input, outputs, params) {
|
||||
if (currentTime >= params.end[0] + 0.5) {
|
||||
// Outside of grace period - should terminate
|
||||
if (this.isAlive) {
|
||||
this.port.postMessage({ type: 'died' });
|
||||
this.isAlive = false;
|
||||
}
|
||||
const begin = params.begin[0];
|
||||
const end = params.end[0];
|
||||
const beginDefined = begin >= 0;
|
||||
const endDefined = end >= 0;
|
||||
// We give a 0.5s grace period (for node pooling) before termination
|
||||
const shouldTerminate = endDefined && currentTime >= end + 0.5;
|
||||
const ended = endDefined && currentTime >= end;
|
||||
const notStarted = currentTime <= begin;
|
||||
if (shouldTerminate) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime >= params.end[0] || currentTime <= params.begin[0]) {
|
||||
// Inside of grace period or not yet started
|
||||
} else if (ended || notStarted || !beginDefined) {
|
||||
return true;
|
||||
}
|
||||
const output = outputs[0];
|
||||
@@ -643,14 +646,18 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
||||
this.timeCursor += this.hopSize;
|
||||
}
|
||||
|
||||
/** Apply Hann window in-place */
|
||||
/** Apply Hann window in-place
|
||||
* @tags internals
|
||||
*/
|
||||
applyHannWindow(input) {
|
||||
for (let i = 0; i < this.blockSize; i++) {
|
||||
input[i] *= this.hannWindow[i] * 1.62;
|
||||
}
|
||||
}
|
||||
|
||||
/** Compute squared magnitudes for peak finding **/
|
||||
/** Compute squared magnitudes for peak finding
|
||||
* @tags internals
|
||||
**/
|
||||
computeMagnitudes() {
|
||||
let i = 0,
|
||||
j = 0;
|
||||
@@ -664,7 +671,9 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/** Find peaks in spectrum magnitudes **/
|
||||
/** Find peaks in spectrum magnitudes
|
||||
* @tags internals
|
||||
**/
|
||||
findPeaks() {
|
||||
this.nbPeaks = 0;
|
||||
let i = 2;
|
||||
@@ -685,7 +694,9 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/** Shift peaks and regions of influence by pitchFactor into new specturm */
|
||||
/** Shift peaks and regions of influence by pitchFactor into new specturm
|
||||
* @tags internals
|
||||
*/
|
||||
shiftPeaks(pitchFactor) {
|
||||
// zero-fill new spectrum
|
||||
this.freqComplexBufferShifted.fill(0);
|
||||
@@ -838,7 +849,9 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
||||
|
||||
registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
|
||||
|
||||
/** BYTE BEATS */
|
||||
/** BYTE BEATS
|
||||
* @tags internals
|
||||
*/
|
||||
const chyx = {
|
||||
/*bit*/ bitC: function (x, y, z) {
|
||||
return x & y ? z : 0;
|
||||
@@ -1148,8 +1161,8 @@ const tablesCache = {};
|
||||
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'begin', defaultValue: -1, min: -1, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'end', defaultValue: -1, min: -1, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
||||
{ name: 'detune', defaultValue: 0 },
|
||||
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
||||
@@ -1164,7 +1177,6 @@ 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') {
|
||||
@@ -1332,16 +1344,17 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
process(_inputs, outputs, parameters) {
|
||||
if (currentTime >= parameters.end[0] + 0.5) {
|
||||
// Outside of grace period - should terminate
|
||||
if (this.isAlive) {
|
||||
this.port.postMessage({ type: 'died' });
|
||||
this.isAlive = false;
|
||||
}
|
||||
const begin = parameters.begin[0];
|
||||
const end = parameters.end[0];
|
||||
const beginDefined = begin >= 0;
|
||||
const endDefined = end >= 0;
|
||||
// We give a 0.5s grace period (for node pooling) before termination
|
||||
const shouldTerminate = endDefined && currentTime >= end + 0.5;
|
||||
const ended = endDefined && currentTime >= end;
|
||||
const notStarted = currentTime <= begin;
|
||||
if (shouldTerminate) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime >= parameters.end[0] || currentTime <= parameters.begin[0]) {
|
||||
// Inside of grace period or not yet started
|
||||
} else if (ended || notStarted || !beginDefined) {
|
||||
return true;
|
||||
}
|
||||
const outL = outputs[0][0];
|
||||
@@ -1479,3 +1492,81 @@ 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);
|
||||
|
||||
@@ -19,6 +19,7 @@ function applyGainCurve(val) {
|
||||
* @param {number} a - Signal A (can be a single value or an array value in buffer processing).
|
||||
* @param {number} b - Signal B (can be a single value or an array value in buffer processing).
|
||||
* @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix).
|
||||
* @tags internals
|
||||
* @returns {number} Crossfaded output value.
|
||||
*/
|
||||
function crossfade(a, b, m) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/tonal",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -100,6 +100,7 @@ function scaleOffset(scale, offset, note) {
|
||||
* - 5P = perfect fifth
|
||||
* - 5d = diminished fifth
|
||||
*
|
||||
* @tags music_theory
|
||||
* @param {string | number} amount Either number of semitones or interval string.
|
||||
* @returns Pattern
|
||||
* @memberof Pattern
|
||||
@@ -154,6 +155,7 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name scaleTranspose
|
||||
* @tags music_theory
|
||||
* @param {offset} offset number of steps inside the scale
|
||||
* @returns Pattern
|
||||
* @synonyms scaleTrans, strans
|
||||
@@ -244,6 +246,7 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
|
||||
* The root note defaults to octave 3, if no octave number is given.
|
||||
*
|
||||
* @name scale
|
||||
* @tags music_theory
|
||||
* @param {string} scale Name of scale
|
||||
* @returns Pattern
|
||||
* @example
|
||||
|
||||
@@ -90,6 +90,7 @@ export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistr
|
||||
* Adds a new custom voicing dictionary.
|
||||
*
|
||||
* @name addVoicings
|
||||
* @tags music_theory
|
||||
* @memberof Pattern
|
||||
* @param {string} name identifier for the voicing dictionary
|
||||
* @param {Object} dictionary maps chord symbol to possible voicings
|
||||
@@ -133,6 +134,7 @@ const getVoicing = (chord, dictionaryName, lastVoicing) => {
|
||||
* Uses [chord-voicings package](https://github.com/felixroos/chord-voicings#chord-voicings).
|
||||
*
|
||||
* @name voicings
|
||||
* @tags music_theory
|
||||
* @memberof Pattern
|
||||
* @param {string} dictionary which voicing dictionary to use.
|
||||
* @returns Pattern
|
||||
@@ -157,6 +159,7 @@ export const voicings = register('voicings', function (dictionary, pat) {
|
||||
* Maps the chords of the incoming pattern to root notes in the given octave.
|
||||
*
|
||||
* @name rootNotes
|
||||
* @tags music_theory
|
||||
* @memberof Pattern
|
||||
* @param {octave} octave octave to use
|
||||
* @returns Pattern
|
||||
@@ -189,6 +192,7 @@ export const rootNotes = register('rootNotes', function (octave, pat) {
|
||||
* If you pass a pattern of strings to voicing, they will be interpreted as chords.
|
||||
*
|
||||
* @name voicing
|
||||
* @tags music_theory
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* n("0 1 2 3").chord("<C Am F G>").voicing()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/transpiler",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -26,6 +26,20 @@ describe('transpiler', () => {
|
||||
it('adds await to bare samples call', () => {
|
||||
expect(transpiler("samples('xxx');", simple).output).toEqual("await samples('xxx');");
|
||||
});
|
||||
it('handles mini strings in K(...)', () => {
|
||||
expect(transpiler('K("bd sd")', simple).output).toEqual("worklet('pat[0]', m('bd sd', 2));");
|
||||
});
|
||||
it('treats K(...) as kabelsalat', () => {
|
||||
expect(transpiler('K(1+2)', simple).output).toEqual("worklet('1 + 2');");
|
||||
});
|
||||
it('automatically calls functions in K(...)', () => {
|
||||
expect(transpiler('K(() => { return 1 + 2 })', simple).output).toEqual(
|
||||
"worklet('(() => {\\n return 1 + 2\\n})()');",
|
||||
);
|
||||
});
|
||||
it('handles strudel S(...) inside kabelsalat K(...)', () => {
|
||||
expect(transpiler('K(S("bd".fast(4)))', simple).output).toEqual("worklet('pat[0]', m('bd', 4).fast(4));");
|
||||
});
|
||||
/* it('parses dynamic imports', () => {
|
||||
expect(
|
||||
transpiler("const { default: foo } = await import('https://bar.com/foo.js');", {
|
||||
|
||||
@@ -119,7 +119,64 @@ export function transpiler(input, options = {}) {
|
||||
return this.replace(labelToP(node));
|
||||
}
|
||||
},
|
||||
leave(node, parent, prop, index) {},
|
||||
|
||||
leave(node, parent, prop, index) {
|
||||
if (!isKabelCall(node)) return;
|
||||
|
||||
let [expr, ...rest] = node.arguments;
|
||||
if (!expr) throw new Error('K(...) requires an expression');
|
||||
|
||||
if (shouldCallKabelExpression(expr)) {
|
||||
expr = {
|
||||
type: 'CallExpression',
|
||||
callee: expr,
|
||||
arguments: [],
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { template, patternExprs } = extractPatternPlaceholders(expr);
|
||||
if (patternExprs.length) {
|
||||
const workletArgs = [{ type: 'Literal', value: template }, ...patternExprs, ...rest];
|
||||
|
||||
let callee = node.callee;
|
||||
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||
if (callee.type === 'MemberExpression') {
|
||||
return this.replace({
|
||||
type: 'CallExpression',
|
||||
callee: workletMemberAst(callee.object),
|
||||
arguments: workletArgs,
|
||||
optional: false,
|
||||
});
|
||||
}
|
||||
return this.replace({
|
||||
type: 'CallExpression',
|
||||
callee: { type: 'Identifier', name: 'worklet' },
|
||||
arguments: workletArgs,
|
||||
optional: false,
|
||||
});
|
||||
}
|
||||
|
||||
const kabelSrc = genExprSource(expr);
|
||||
const workletArgs = [{ type: 'Literal', value: kabelSrc }, ...rest];
|
||||
|
||||
let callee = node.callee;
|
||||
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||
if (callee.type === 'MemberExpression') {
|
||||
return this.replace({
|
||||
type: 'CallExpression',
|
||||
callee: workletMemberAst(callee.object),
|
||||
arguments: workletArgs,
|
||||
optional: false,
|
||||
});
|
||||
}
|
||||
return this.replace({
|
||||
type: 'CallExpression',
|
||||
callee: { type: 'Identifier', name: 'worklet' },
|
||||
arguments: workletArgs,
|
||||
optional: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
let { body } = ast;
|
||||
@@ -155,6 +212,166 @@ export function transpiler(input, options = {}) {
|
||||
return { output, miniLocations, widgets };
|
||||
}
|
||||
|
||||
function isKabelCall(node) {
|
||||
if (node.type !== 'CallExpression') return false;
|
||||
let callee = node.callee;
|
||||
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||
if (callee.type === 'MemberExpression') return !callee.computed && callee.property?.name === 'K';
|
||||
return callee.type === 'Identifier' && callee.name === 'K';
|
||||
}
|
||||
|
||||
function shouldCallKabelExpression(expr) {
|
||||
if (expr.type !== 'ArrowFunctionExpression' && expr.type !== 'FunctionExpression') {
|
||||
return false;
|
||||
}
|
||||
if (expr.params.length) {
|
||||
return false;
|
||||
}
|
||||
return expr.body?.type === 'BlockStatement';
|
||||
}
|
||||
|
||||
function genExprSource(expr) {
|
||||
return escodegen.generate(expr, { format: { semicolons: false } });
|
||||
}
|
||||
|
||||
function extractPatternPlaceholders(expr) {
|
||||
const templateExpr = cloneNode(expr);
|
||||
const parents = new Map();
|
||||
const targets = [];
|
||||
|
||||
walk(templateExpr, {
|
||||
enter(node, parent, prop, index) {
|
||||
parents.set(node, { parent, prop, index });
|
||||
const patternExpr = getStrudelPatternExpr(node);
|
||||
if (patternExpr) {
|
||||
targets.push({ node, patternExpr });
|
||||
this.skip();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!targets.length) {
|
||||
return { template: genExprSource(templateExpr), patternExprs: [] };
|
||||
}
|
||||
|
||||
targets.sort((a, b) => getPatternNodeOrder(a.node) - getPatternNodeOrder(b.node));
|
||||
|
||||
const patternExprs = targets.map(({ patternExpr }) => cloneNode(patternExpr));
|
||||
|
||||
let currentExpr = templateExpr;
|
||||
targets.forEach(({ node }, index) => {
|
||||
currentExpr = replaceNode(node, placeholderAst(index), parents, currentExpr);
|
||||
});
|
||||
|
||||
const template = genExprSource(currentExpr);
|
||||
return { template, patternExprs };
|
||||
}
|
||||
|
||||
function getStrudelPatternExpr(node) {
|
||||
if (isStrudelPatternWrap(node)) {
|
||||
const arg = node.arguments?.[0];
|
||||
if (!arg) {
|
||||
throw new Error('S(...) requires an argument');
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
if (isMiniCall(node)) {
|
||||
return node;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isStrudelPatternWrap(node) {
|
||||
if (node.type !== 'CallExpression') {
|
||||
return false;
|
||||
}
|
||||
const callee = node.callee;
|
||||
if (callee.type === 'Identifier') {
|
||||
return callee.name === 'S';
|
||||
}
|
||||
if (callee.type === 'MemberExpression' && !callee.computed) {
|
||||
return callee.property?.name === 'S';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getMinilangName() {
|
||||
const minilang = languages.get('minilang');
|
||||
return minilang?.name || 'm';
|
||||
}
|
||||
|
||||
// Used to identify transpiled `m(...)` calls for proper conversion
|
||||
// to, say, kabelsalat placeholders
|
||||
function isMiniCall(node) {
|
||||
if (node.type !== 'CallExpression') {
|
||||
return false;
|
||||
}
|
||||
const callee = node.callee;
|
||||
if (callee.type !== 'Identifier') {
|
||||
return false;
|
||||
}
|
||||
if (callee.name !== getMinilangName()) {
|
||||
return false;
|
||||
}
|
||||
const firstArg = node.arguments?.[0];
|
||||
return firstArg?.type === 'Literal' && typeof firstArg.value === 'string';
|
||||
}
|
||||
|
||||
// If `start` is available, we use it. If it's already been transpiled
|
||||
// to `m(...)`, use the provided offset
|
||||
function getPatternNodeOrder(node) {
|
||||
if (typeof node.start === 'number') {
|
||||
return node.start;
|
||||
}
|
||||
if (isMiniCall(node)) {
|
||||
const offsetArg = node.arguments?.[1];
|
||||
if (offsetArg?.type === 'Literal' && typeof offsetArg.value === 'number') {
|
||||
return offsetArg.value;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function placeholderAst(index) {
|
||||
return {
|
||||
type: 'MemberExpression',
|
||||
object: { type: 'Identifier', name: 'pat' },
|
||||
property: { type: 'Literal', value: index },
|
||||
computed: true,
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
function replaceNode(node, replacement, parents, currentRoot) {
|
||||
const info = parents.get(node);
|
||||
if (!info || !info.parent) {
|
||||
return replacement;
|
||||
}
|
||||
|
||||
const { parent, prop, index } = info;
|
||||
if (Array.isArray(parent[prop])) {
|
||||
parent[prop][index] = replacement;
|
||||
} else {
|
||||
parent[prop] = replacement;
|
||||
}
|
||||
parents.set(replacement, { parent, prop, index });
|
||||
return currentRoot;
|
||||
}
|
||||
|
||||
function cloneNode(node) {
|
||||
return JSON.parse(JSON.stringify(node));
|
||||
}
|
||||
|
||||
function workletMemberAst(objectExpr) {
|
||||
return {
|
||||
type: 'MemberExpression',
|
||||
object: objectExpr,
|
||||
property: { type: 'Identifier', name: 'worklet' },
|
||||
computed: false,
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
function isStringWithDoubleQuotes(node, locations, code) {
|
||||
if (node.type !== 'Literal') {
|
||||
return false;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vite-plugin-bundle-audioworklet",
|
||||
"main": "./vite-plugin-bundle-audioworklet.js",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"vite",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/web",
|
||||
"version": "1.2.6",
|
||||
"version": "1.3.0",
|
||||
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
||||
"module": "web.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/webaudio",
|
||||
"version": "1.2.6",
|
||||
"version": "1.3.0",
|
||||
"description": "Web Audio helpers for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -98,6 +98,7 @@ function clearScreen(smear = 0, smearRGB = `0,0,0`, ctx = getDrawContext()) {
|
||||
/**
|
||||
* Renders an oscilloscope for the frequency domain of the audio signal.
|
||||
* @name fscope
|
||||
* @tags visualization
|
||||
* @param {string} color line color as hex or color name. defaults to white.
|
||||
* @param {number} scale scales the y-axis. Defaults to 0.25
|
||||
* @param {number} pos y-position relative to screen height. 0 = top, 1 = bottom of screen
|
||||
@@ -122,6 +123,7 @@ Pattern.prototype.fscope = function (config = {}) {
|
||||
* Renders an oscilloscope for the time domain of the audio signal.
|
||||
* @name scope
|
||||
* @synonyms tscope
|
||||
* @tags visualization
|
||||
* @param {object} config optional config with options:
|
||||
* @param {boolean} align if 1, the scope will be aligned to the first zero crossing. defaults to 1
|
||||
* @param {string} color line color as hex or color name. defaults to white.
|
||||
|
||||
@@ -5,6 +5,7 @@ import { analysers, getAnalyzerData } from 'superdough';
|
||||
/**
|
||||
* Renders a spectrum analyzer for the incoming audio signal.
|
||||
* @name spectrum
|
||||
* @tags visualization
|
||||
* @param {object} config optional config with options:
|
||||
* @param {integer} thickness line thickness in px (default 3)
|
||||
* @param {integer} speed scroll speed (default 1)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/xen",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"description": "Xenharmonic API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
Generated
+28
-2
@@ -41,7 +41,7 @@ importers:
|
||||
version: 2.2.7
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 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))
|
||||
version: 3.0.4(vitest@3.0.4)
|
||||
'@vitest/ui':
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(vitest@3.0.4)
|
||||
@@ -518,6 +518,12 @@ importers:
|
||||
|
||||
packages/superdough:
|
||||
dependencies:
|
||||
'@kabelsalat/lib':
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1
|
||||
'@kabelsalat/web':
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1
|
||||
nanostores:
|
||||
specifier: ^0.11.3
|
||||
version: 0.11.3
|
||||
@@ -1969,6 +1975,15 @@ packages:
|
||||
resolution: {integrity: sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==}
|
||||
engines: {node: '>=v12.0.0'}
|
||||
|
||||
'@kabelsalat/core@0.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'}
|
||||
@@ -9261,6 +9276,17 @@ 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
|
||||
@@ -10554,7 +10580,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@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))':
|
||||
'@vitest/coverage-v8@3.0.4(vitest@3.0.4)':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
|
||||
@@ -611,6 +611,132 @@ exports[`runs examples > example "FX" example index 1 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "K" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/12 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 1/12 → 1/6 | note:C4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 1/6 → 1/4 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 1/4 → 1/3 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ (1/3 → 3/8) ⇝ 5/12 | note:C4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ 1/3 ⇜ (3/8 → 5/12) | note:C4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 5/12 → 1/2 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 1/2 → 7/12 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 7/12 → 2/3 | note:C4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 2/3 → 3/4 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 3/4 → 5/6 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 5/6 → 11/12 | note:C4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 11/12 → 1/1 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 1/1 → 13/12 | note:C5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 13/12 → 7/6 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 7/6 → 5/4 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 5/4 → 4/3 | note:C5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ (4/3 → 11/8) ⇝ 17/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ 4/3 ⇜ (11/8 → 17/12) | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 17/12 → 3/2 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 3/2 → 19/12 | note:C5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 19/12 → 5/3 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 5/3 → 7/4 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 7/4 → 11/6 | note:C5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 11/6 → 23/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 23/12 → 2/1 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 2/1 → 25/12 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 25/12 → 13/6 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 13/6 → 9/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 9/4 → 7/3 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ (7/3 → 19/8) ⇝ 29/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ 7/3 ⇜ (19/8 → 29/12) | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 29/12 → 5/2 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 5/2 → 31/12 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 31/12 → 8/3 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 8/3 → 11/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 11/4 → 17/6 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 17/6 → 35/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 35/12 → 3/1 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 3/1 → 37/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 37/12 → 19/6 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 19/6 → 13/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||
"[ 13/4 → 10/3 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ (10/3 → 27/8) ⇝ 41/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||
"[ 10/3 ⇜ (27/8 → 41/12) | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 41/12 → 7/2 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||
"[ 7/2 → 43/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 43/12 → 11/3 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 11/3 → 15/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||
"[ 15/4 → 23/6 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 23/6 → 47/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
"[ 47/12 → 4/1 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "K" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/16 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 1/16 → 1/8 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 1/8 → 3/16 | note:B2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 3/16 → 1/4 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 1/4 → 5/16 | note:A#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 5/16 → 3/8 | note:C#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 3/8 → 7/16 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 7/16 → 1/2 | note:A#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 1/2 → 9/16 | note:B3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 9/16 → 5/8 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 5/8 → 11/16 | note:A#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 11/16 → 3/4 | note:D#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 3/4 → 13/16 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 13/16 → 7/8 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 7/8 → 15/16 | note:B3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 15/16 → 1/1 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0 0.05 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 1/1 → 17/16 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 6] FXrelease:1 room:0.3 ]",
|
||||
"[ 17/16 → 9/8 | note:C#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 6] FXrelease:1 room:0.3 ]",
|
||||
"[ 9/8 → 19/16 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 4] FXrelease:1 room:0.3 ]",
|
||||
"[ 19/16 → 5/4 | note:A#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 4] FXrelease:1 room:0.3 ]",
|
||||
"[ 5/4 → 21/16 | note:B2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 4] FXrelease:1 room:0.3 ]",
|
||||
"[ 21/16 → 11/8 | note:G#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 4] FXrelease:1 room:0.3 ]",
|
||||
"[ 11/8 → 23/16 | note:A#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 4] FXrelease:1 room:0.3 ]",
|
||||
"[ 23/16 → 3/2 | note:D#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 4] FXrelease:1 room:0.3 ]",
|
||||
"[ 3/2 → 25/16 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 25/16 → 13/8 | note:A#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 13/8 → 27/16 | note:B4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 27/16 → 7/4 | note:G#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 7/4 → 29/16 | note:A#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 29/16 → 15/8 | note:C#5 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 15/8 → 31/16 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 31/16 → 2/1 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.025977108255028726 0.12793132476508617 0.2 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 2/1 → 33/16 | note:B2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 33/16 → 17/8 | note:G#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 17/8 → 35/16 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 35/16 → 9/4 | note:D#5 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 9/4 → 37/16 | note:G#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 37/16 → 19/8 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 19/8 → 39/16 | note:B2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 39/16 → 5/2 | note:G#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 5/2 → 41/16 | note:A#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 41/16 → 21/8 | note:C#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 21/8 → 43/16 | note:G#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 43/16 → 11/4 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 11/4 → 45/16 | note:B2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 45/16 → 23/8 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 23/8 → 47/16 | note:A#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 47/16 → 3/1 | note:D#5 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.047976355999708176 0.19392906799912457 0.3 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 3/1 → 49/16 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 49/16 → 25/8 | note:A#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 25/8 → 51/16 | note:B3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 51/16 → 13/4 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 13/4 → 53/16 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 53/16 → 27/8 | note:C#4 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 27/8 → 55/16 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 55/16 → 7/2 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 2] FXrelease:1 room:0.3 ]",
|
||||
"[ 7/2 → 57/16 | note:B3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 57/16 → 29/8 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 29/8 → 59/16 | note:A#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 59/16 → 15/4 | note:D#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1.5] FXrelease:1 room:0.3 ]",
|
||||
"[ 15/4 → 61/16 | note:G#2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 61/16 → 31/8 | note:A#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 31/8 → 63/16 | note:B2 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
"[ 63/16 → 4/1 | note:G#3 workletSrc:(() => {\\n const att = pat[0];\\n const dec = pat[1];\\n let f = n(sFreq);\\n const mod = sine(f).mul(pat[2]).add(pat[3]);\\n saw(f.mul(mod)).mul(sGate.ad(att dec)).add(x => x.delay(0.4).mul(0.3)).out()\\n})() workletInputs:[0.010864455532282592 0.08259336659684778 0.1 1] FXrelease:1 room:0.3 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "accelerate" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 2/1 | s:sax accelerate:0 ]",
|
||||
@@ -5484,19 +5610,6 @@ 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 ]",
|
||||
@@ -7254,6 +7367,10 @@ exports[`runs examples > example "midicmd" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "midikeys" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "midikeys" example index 1 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "midin" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||
|
||||
@@ -131,6 +131,10 @@ const midin = () => {
|
||||
return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0
|
||||
};
|
||||
|
||||
const midikeys = async () => {
|
||||
return () => strudel.silence;
|
||||
};
|
||||
|
||||
const sysex = ([id, data]) => {};
|
||||
|
||||
// TODO: refactor to evalScope
|
||||
@@ -150,6 +154,7 @@ evalScope(
|
||||
*/
|
||||
{
|
||||
midin,
|
||||
midikeys,
|
||||
sysex,
|
||||
// gist,
|
||||
// euclid,
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
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);
|
||||
});
|
||||
// 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
|
||||
|
||||
/*
|
||||
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,4 +1,4 @@
|
||||
const ALLOW_MANY = ['by', 'url', 'genre', 'license'];
|
||||
const ALLOW_MANY = ['by', 'url', 'genre', 'license', 'tag'];
|
||||
|
||||
export function getMetadata(raw_code) {
|
||||
if (raw_code == null) {
|
||||
|
||||
@@ -19,6 +19,14 @@ 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:
|
||||
|
||||
@@ -16,10 +16,14 @@ It is also possible to pattern other things with Strudel, such as software and h
|
||||
|
||||
Strudel supports MIDI without any additional software (thanks to [webmidi](https://npmjs.com/package/webmidi)), just by adding methods to your pattern:
|
||||
|
||||
## midiin(inputName?)
|
||||
## midin(inputName?)
|
||||
|
||||
<JsDoc client:idle name="midin" h={0} />
|
||||
|
||||
## midikeys(inputName?)
|
||||
|
||||
<JsDoc client:idle name="midikeys" h={0} />
|
||||
|
||||
## midi(outputName?,options?)
|
||||
|
||||
Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages.
|
||||
|
||||
@@ -53,6 +53,7 @@ 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
|
||||
|
||||
@@ -92,3 +93,21 @@ 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.
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Pagination } from '../pagination/Pagination.jsx';
|
||||
import { useState } from 'react';
|
||||
import { useDebounce } from '../usedebounce.jsx';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { Textbox } from '../textbox/Textbox.jsx';
|
||||
|
||||
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
|
||||
@@ -28,12 +29,12 @@ export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||
if (!isNaN(date)) {
|
||||
title = date.toLocaleDateString();
|
||||
} else {
|
||||
title = 'unnamed';
|
||||
title = pattern.id || 'unnamed';
|
||||
}
|
||||
}
|
||||
|
||||
const author = Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous';
|
||||
return <>{`${pattern.id}: ${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
|
||||
return <>{`${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
|
||||
}
|
||||
|
||||
function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
|
||||
@@ -79,73 +80,115 @@ const updateCodeWindow = (context, patternData, reset = false) => {
|
||||
context.handleUpdate(patternData, reset);
|
||||
};
|
||||
|
||||
function UserPatterns({ context }) {
|
||||
export function PatternsTab({ context }) {
|
||||
const [search, setSearch] = useState('');
|
||||
const activePattern = useActivePattern();
|
||||
const viewingPatternStore = useViewingPatternData();
|
||||
const viewingPatternData = parseJSON(viewingPatternStore);
|
||||
const { userPatterns, patternFilter, patternAutoStart } = useSettings();
|
||||
const { userPatterns, patternAutoStart } = useSettings();
|
||||
const viewingPatternID = viewingPatternData?.id;
|
||||
return (
|
||||
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
|
||||
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
|
||||
<ActionButton
|
||||
label="new"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.createAndAddToDB();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="duplicate"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.duplicate(viewingPatternData);
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="delete"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.delete(viewingPatternID);
|
||||
updateCodeWindow(context, { ...data, collection: userPattern.collection });
|
||||
}}
|
||||
/>
|
||||
<label className="hover:opacity-50 cursor-pointer">
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
multiple
|
||||
accept="text/plain,text/x-markdown,application/json"
|
||||
onChange={(e) => importPatterns(e.target.files)}
|
||||
/>
|
||||
import
|
||||
</label>
|
||||
<ActionButton label="export" onClick={exportPatterns} />
|
||||
|
||||
<ActionButton
|
||||
label="delete-all"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.clearAll();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
const visiblePatterns = useMemo(() => {
|
||||
if (!search) {
|
||||
return userPatterns;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(userPatterns).filter(([_key, pattern]) => {
|
||||
const meta = getMetadata(pattern.code);
|
||||
|
||||
<div className="overflow-auto h-full bg-background p-2 rounded-md">
|
||||
{/* {patternFilter === patternFilterName.user && ( */}
|
||||
<PatternButtons
|
||||
onClick={(id) => {
|
||||
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
|
||||
|
||||
if (context.started && activePattern === id) {
|
||||
context.handleEvaluate();
|
||||
// Search for specific meta keys
|
||||
const searchLowercaseTrimmed = search.trim().toLowerCase();
|
||||
if (searchLowercaseTrimmed.includes(':')) {
|
||||
const [metaKey, metaSearch] = searchLowercaseTrimmed.split(/:\s*/);
|
||||
if (metaKey !== undefined && metaSearch !== undefined && metaKey in meta) {
|
||||
const metaValues = meta[metaKey];
|
||||
if (Array.isArray(metaValues)) {
|
||||
return metaValues.some((metaValue) => metaValue.toLowerCase().includes(metaSearch));
|
||||
} else if (typeof metaValues === 'string') {
|
||||
return metaValues.toLowerCase().includes(metaSearch);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
patterns={userPatterns}
|
||||
started={context.started}
|
||||
activePattern={activePattern}
|
||||
viewingPatternID={viewingPatternID}
|
||||
/>
|
||||
{/* )} */}
|
||||
}
|
||||
}
|
||||
const title = meta.title ? meta.title : 'unnamed';
|
||||
const authors = meta.by ? meta.by : ['anonymous'];
|
||||
const tags = meta.tag ? meta.tag : [];
|
||||
return (
|
||||
title.toLowerCase().includes(searchLowercaseTrimmed) ||
|
||||
authors.some((author) => author.toLowerCase().includes(searchLowercaseTrimmed)) ||
|
||||
tags.some((tag) => tag.toLowerCase().includes(searchLowercaseTrimmed))
|
||||
);
|
||||
}),
|
||||
);
|
||||
}, [search, viewingPatternStore]);
|
||||
|
||||
return (
|
||||
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
|
||||
<div className="w-full flex">
|
||||
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
|
||||
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
|
||||
<ActionButton
|
||||
label="new"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.createAndAddToDB();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="duplicate"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.duplicate(viewingPatternData);
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="delete"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.delete(viewingPatternID);
|
||||
updateCodeWindow(context, { ...data, collection: userPattern.collection });
|
||||
}}
|
||||
/>
|
||||
<label className="hover:opacity-50 cursor-pointer">
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
multiple
|
||||
accept="text/plain,text/x-markdown,application/json"
|
||||
onChange={(e) => importPatterns(e.target.files)}
|
||||
/>
|
||||
import
|
||||
</label>
|
||||
<ActionButton label="export" onClick={exportPatterns} />
|
||||
|
||||
<ActionButton
|
||||
label="delete-all"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.clearAll();
|
||||
updateCodeWindow(context, data);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-auto h-full bg-background p-2 rounded-md">
|
||||
{/* {patternFilter === patternFilterName.user && ( */}
|
||||
<PatternButtons
|
||||
onClick={(id) => {
|
||||
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
|
||||
|
||||
if (context.started && activePattern === id) {
|
||||
context.handleEvaluate();
|
||||
}
|
||||
}}
|
||||
patterns={visiblePatterns}
|
||||
started={context.started}
|
||||
activePattern={activePattern}
|
||||
viewingPatternID={viewingPatternID}
|
||||
/>
|
||||
{/* )} */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -232,28 +275,3 @@ function PublicPatterns({ context }) {
|
||||
}
|
||||
return <LatestPatterns context={context} />;
|
||||
}
|
||||
|
||||
export function PatternsTab({ context }) {
|
||||
const { patternFilter } = useSettings();
|
||||
|
||||
return (
|
||||
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
|
||||
<UserPatterns context={context} />
|
||||
</div>
|
||||
);
|
||||
/* return (
|
||||
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
|
||||
<ButtonGroup
|
||||
value={patternFilter}
|
||||
onChange={(value) => settingsMap.setKey('patternFilter', value)}
|
||||
items={patternFilterName}
|
||||
></ButtonGroup>
|
||||
|
||||
{patternFilter === patternFilterName.user ? (
|
||||
<UserPatterns context={context} />
|
||||
) : (
|
||||
<PublicPatterns context={context} />
|
||||
)}
|
||||
</div>
|
||||
); */
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { memo, useEffect, useMemo, useState, Fragment } from 'react';
|
||||
|
||||
import jsdocJson from '../../../../../doc.json';
|
||||
import { Textbox } from '../textbox/Textbox';
|
||||
@@ -10,8 +10,16 @@ const availableFunctions = (() => {
|
||||
const functions = [];
|
||||
for (const doc of jsdocJson.docs) {
|
||||
if (!isValid(doc)) continue;
|
||||
if (seen.has(doc.name)) continue;
|
||||
|
||||
// jsdoc also uses "tags" for when you use @something in the comments and it doesn't know what
|
||||
// @something is. We only want data from comments like `@tags fx, superdough` here.
|
||||
// If nothing is specified, we default to "untagged" for debugging
|
||||
doc.tags = doc.tags?.filter((t) => t && typeof t === 'string') || ['untagged'];
|
||||
functions.push(doc);
|
||||
|
||||
const synonyms = doc.synonyms || [];
|
||||
seen.add(doc.name);
|
||||
for (const s of synonyms) {
|
||||
if (!s || seen.has(s)) continue;
|
||||
seen.add(s);
|
||||
@@ -35,11 +43,27 @@ const getInnerText = (html) => {
|
||||
return div.textContent || div.innerText || '';
|
||||
};
|
||||
|
||||
export function Reference() {
|
||||
export const Reference = memo(function Reference() {
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedTag, setSelectedTag] = useState(null);
|
||||
const [selectedFunction, setSelectedFunction] = useState(null);
|
||||
|
||||
const visibleFunctions = useMemo(() => {
|
||||
const toggleTag = (tag) => {
|
||||
if (selectedTag === tag) {
|
||||
setSelectedTag(null);
|
||||
} else {
|
||||
setSelectedTag(tag);
|
||||
}
|
||||
};
|
||||
|
||||
const searchVisibleFunctions = useMemo(() => {
|
||||
return availableFunctions.filter((entry) => {
|
||||
if (selectedTag) {
|
||||
if (!(entry.tags || ['untagged']).includes(selectedTag)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!search) {
|
||||
return true;
|
||||
}
|
||||
@@ -50,43 +74,128 @@ export function Reference() {
|
||||
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
|
||||
);
|
||||
});
|
||||
}, [search]);
|
||||
}, [search, selectedTag]);
|
||||
|
||||
const detailVisibleFunctions = useMemo(() => {
|
||||
return searchVisibleFunctions.filter((x) => {
|
||||
if (selectedTag === null) {
|
||||
if (search) {
|
||||
return true;
|
||||
}
|
||||
return x.name === selectedFunction;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}, [searchVisibleFunctions, selectedFunction, selectedTag]);
|
||||
|
||||
const tagCounts = {};
|
||||
for (const doc of availableFunctions) {
|
||||
(doc.tags || ['untagged']).forEach((t) => {
|
||||
if (typeof t === 'string' && t) {
|
||||
tagCounts[t] = (tagCounts[t] || 0) + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const onSearchTagFilterClick = () => {
|
||||
setSelectedTag(null);
|
||||
setSelectedFunction(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFunction) {
|
||||
const el = document.getElementById(`doc-${selectedFunction}`);
|
||||
const container = document.getElementById('reference-container');
|
||||
container.scrollTo(0, el.offsetTop);
|
||||
}
|
||||
}, [selectedFunction]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full p-2 overflow-hidden">
|
||||
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
|
||||
<div class="w-full flex">
|
||||
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
|
||||
<div className="h-full text-foreground flex flex-col gap-3 w-1/3 ">
|
||||
<div className="w-full flex">
|
||||
<Textbox
|
||||
className="w-full"
|
||||
placeholder="Search"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSelectedFunction(null);
|
||||
setSearch(e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<a
|
||||
key={i}
|
||||
className="cursor-pointer text-foreground flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
|
||||
onClick={() => {
|
||||
const el = document.getElementById(`doc-${i}`);
|
||||
const container = document.getElementById('reference-container');
|
||||
container.scrollTo(0, el.offsetTop);
|
||||
}}
|
||||
{selectedTag && (
|
||||
<div className="w-72">
|
||||
<span
|
||||
className="text-foreground border-2 border-gray-500 px-1 py-0.5 my-2 rounded-md cursor-pointer font-sans"
|
||||
onClick={onSearchTagFilterClick}
|
||||
>
|
||||
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
|
||||
</a>
|
||||
{selectedTag}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
|
||||
{searchVisibleFunctions.map((entry, i) => (
|
||||
<Fragment key={`entry-${entry.name}`}>
|
||||
<a
|
||||
className={
|
||||
'cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis ' +
|
||||
(entry.name === selectedFunction ? 'bg-lineHighlight font-bold' : '')
|
||||
}
|
||||
onClick={() => {
|
||||
if (entry.name === selectedFunction) {
|
||||
setSelectedFunction(null);
|
||||
} else {
|
||||
setSelectedFunction(entry.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{entry.name}
|
||||
</a>{' '}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="break-normal flex-grow flex-col overflow-y-auto overflow-x-hidden px-2 flex relative"
|
||||
className="break-normal flex-col overflow-y-auto overflow-x-hidden p-2 flex relative"
|
||||
id="reference-container"
|
||||
>
|
||||
<div className="prose dark:prose-invert min-w-full px-1 ">
|
||||
<h2>API Reference</h2>
|
||||
<p>
|
||||
<p className="font-sans text-md">
|
||||
This is the long list of functions you can use. Remember that you don't need to remember all of those and
|
||||
that you can already make music with a small set of functions!
|
||||
</p>
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<section key={i}>
|
||||
<h3 id={`doc-${i}`}>{entry.name}</h3>
|
||||
<div>
|
||||
{Object.entries(tagCounts)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([t, count]) => (
|
||||
<span key={t}>
|
||||
<a
|
||||
className={[
|
||||
'select-none text-white border-2 border-gray-500 px-1 py-0.5 my-2 cursor-pointer text-sm/8 rounded-md no-underline font-sans',
|
||||
`${selectedTag === t ? 'bg-gray-500 text-black' : ''}`,
|
||||
].join(' ')}
|
||||
onClick={() => toggleTag(t)}
|
||||
>
|
||||
{t} ({count})
|
||||
</a>{' '}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{detailVisibleFunctions.map((entry, i) => (
|
||||
<section key={i} className="font-sans">
|
||||
<div className="flex flex-row items-center mt-8 justify-between">
|
||||
<h3 className="font-mono my-0" id={`doc-${entry.name}`}>
|
||||
{entry.name}
|
||||
</h3>
|
||||
{entry.tags && (
|
||||
<span className="ml-2 text-xs text-gray-400 border-2 border-gray-500 rounded-md px-1 py-0.5">
|
||||
{entry.tags.join(', ')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!!entry.synonyms_text && (
|
||||
<p>
|
||||
Synonyms: <code>{entry.synonyms_text}</code>
|
||||
@@ -107,9 +216,10 @@ export function Reference() {
|
||||
</pre>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
)) || <p className="font-sans">Searcb or select a tag to get started.</p>}
|
||||
{detailVisibleFunctions.length > 0 && <div className="h-screen" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user