mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-16 23:56:53 -04:00
Compare commits
193 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12fac86012 | |||
| cea6301ffd | |||
| 0b69ce21c7 | |||
| 4f05674cb3 | |||
| feab3f5c86 | |||
| 2755a7eebf | |||
| deb69ced9f | |||
| 1d0e2bd0c7 | |||
| b5ef12a2b1 | |||
| e3dd4ab126 | |||
| 751f924a4d | |||
| 6d99a7f399 | |||
| 5921bec5f4 | |||
| b4e171f5b4 | |||
| 90ad12300a | |||
| ffc6d12d87 | |||
| 1334276ec0 | |||
| cbf472fe0a | |||
| 4439599117 | |||
| 32754ef97a | |||
| 67bce82a62 | |||
| 5da3ae8df4 | |||
| eb3818eebb | |||
| e6b824ebff | |||
| c74dd95bd0 | |||
| 897a750f70 | |||
| 81d02cf5ae | |||
| a6dc6bb831 | |||
| 00a3c1acc0 | |||
| 8ff51d4468 | |||
| 2e041e8647 | |||
| 55095ba4e8 | |||
| 530f45a7bb | |||
| 99295275ec | |||
| c2e45eb36c | |||
| 35e32a32e8 | |||
| 8def3f6af6 | |||
| aa3698c4f4 | |||
| 6b525b4819 | |||
| 77ced19144 | |||
| 14cc5d989b | |||
| 47ad4bb171 | |||
| e3ad6b1f0a | |||
| 1e771d6150 | |||
| 3ac4649536 | |||
| 14be1ddf7f | |||
| 207ce9cbec | |||
| 3abcc59874 | |||
| 7fc8f1c5c9 | |||
| 016315afbc | |||
| b3c0d9179f | |||
| 993dc53e92 | |||
| 60345db1bd | |||
| a09349e457 | |||
| 71f538b115 | |||
| 70be560a7a | |||
| 269758ebdb | |||
| 98d613fbdf | |||
| f5e21a220b | |||
| f6e6df78a8 | |||
| 05ae80b0ca | |||
| 3bbaf77b20 | |||
| a9975a2b41 | |||
| 2b3f719484 | |||
| 4051fd5241 | |||
| 55ffb59b9c | |||
| 93cf9fcb6a | |||
| a5b867f1b1 | |||
| d8645e9a71 | |||
| b824ad16ae | |||
| 03b221a2a2 | |||
| 28db6d1fd4 | |||
| dc9cadc979 | |||
| f99b13efc2 | |||
| 0001d9c27b | |||
| d79d122f68 | |||
| 6e61f3cdb6 | |||
| 4993ec4cbf | |||
| 8e5ca57edd | |||
| 107a3acd3a | |||
| 1d887e81ef | |||
| 7f2860eb14 | |||
| 3364b9f05f | |||
| f9ed71fbf8 | |||
| e9ea0bc7cc | |||
| a87daf3d21 | |||
| 784b9c08ec | |||
| 139b5376fc | |||
| a052def314 | |||
| bcce1fa68a | |||
| 9a10cac8e9 | |||
| cf999462b7 | |||
| 9fc084b291 | |||
| cb46dd7840 | |||
| b684e183a0 | |||
| 84e4dde3d7 | |||
| 5f8d028d4f | |||
| e3abdd41c0 | |||
| bce6ffde08 | |||
| 35a3e32238 | |||
| 60b5024f63 | |||
| 4f14ff00d5 | |||
| 3d3130e880 | |||
| 83d542d06a | |||
| 6105d5027d | |||
| 7e34069021 | |||
| b75ca193a2 | |||
| afac1f62ca | |||
| de83805fd9 | |||
| 38b7bcb3d2 | |||
| c96b1c2faf | |||
| 87e723649e | |||
| 62bf9abed3 | |||
| e35a2000de | |||
| 4b8d80016b | |||
| d2f2c6df02 | |||
| 8ec1d4d034 | |||
| 6981638f70 | |||
| 9bcaaa3ac4 | |||
| f5823ce981 | |||
| f8750901f0 | |||
| c1fd8c82c6 | |||
| 6e36bdfff7 | |||
| 8dbdd0a9b8 | |||
| f4c0281613 | |||
| f610965f43 | |||
| 17ef84ed70 | |||
| f0cb96dcad | |||
| f4f04a7d57 | |||
| a3ac9646f5 | |||
| 12b89f9f88 | |||
| 166985a055 | |||
| 85e787214b | |||
| 99cf256ee2 | |||
| 42d0bfadce | |||
| 22f78fbb2f | |||
| 77076a1c3e | |||
| a1d31df556 | |||
| f9d0e154e8 | |||
| 2e7ec9ea27 | |||
| bc9e97b9ac | |||
| 00f0ef56bc | |||
| 44ad05fb20 | |||
| 18cc1e4466 | |||
| 055f009357 | |||
| 9ffd0e496f | |||
| 550d7ff6cb | |||
| 2fe1712dae | |||
| 726e5ef14a | |||
| cc15e3cd36 | |||
| 0ef30d5e8d | |||
| 160ef84b47 | |||
| 1a7f464998 | |||
| ee326c709e | |||
| a0291e8a42 | |||
| ecada58edc | |||
| bb7f587025 | |||
| dab1a018e4 | |||
| 0c01a997ac | |||
| b8dc50267e | |||
| 1aaa04ab50 | |||
| 1860d31624 | |||
| 47557cf06f | |||
| 213edfb211 | |||
| 84e59645fd | |||
| 4df19f9ff8 | |||
| ba7721f53c | |||
| fd1e3d248a | |||
| bad81e3850 | |||
| a409149dae | |||
| a6b3b9ef5e | |||
| ec73c48e2e | |||
| 65dd79e374 | |||
| 2fa8ed1424 | |||
| b7827cb89f | |||
| 29fd541f1f | |||
| 0830a9df69 | |||
| 4159c8394d | |||
| 466bf819d8 | |||
| 7e4cbefc8f | |||
| 2483c673a4 | |||
| 69360b3ec8 | |||
| 96d0f2053e | |||
| b952e4c735 | |||
| a4212589e5 | |||
| ee1a894f3b | |||
| c62428ca27 | |||
| d516d765d5 | |||
| 5032b4e966 | |||
| a4d7803d43 | |||
| 4a395f372d | |||
| c754dab1ef | |||
| df54404733 |
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"devToolbar": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,13 @@ function defineTags(dictionary) {
|
|||||||
doclet.synonyms = doclet.synonyms_text.split(/[ ,]+/);
|
doclet.synonyms = doclet.synonyms_text.split(/[ ,]+/);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dictionary.defineTag('tags', {
|
||||||
|
mustHaveValue: true,
|
||||||
|
onTagged: function (doclet, tag) {
|
||||||
|
doclet.tags = tag.value.split(/[ ,]+/);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { defineTags: defineTags };
|
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)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Block-based evaluation utilities
|
||||||
|
|
||||||
|
export function getBlockRegions(code) {
|
||||||
|
const chars = code.split('');
|
||||||
|
let i = 0,
|
||||||
|
blanks = [],
|
||||||
|
blockStart = 0,
|
||||||
|
regions = [];
|
||||||
|
while (i < chars.length) {
|
||||||
|
const isBlank = chars[i] === '\n';
|
||||||
|
if (isBlank) {
|
||||||
|
blanks.push(i);
|
||||||
|
} else if (chars[i].trim() !== '') {
|
||||||
|
if (blanks.length > 1) {
|
||||||
|
regions.push([blockStart, blanks[0]]);
|
||||||
|
blockStart = i;
|
||||||
|
}
|
||||||
|
blanks = [];
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
regions.push([blockStart, blanks.length ? blanks[0] : i]);
|
||||||
|
return regions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBlockAt(code, cursor) {
|
||||||
|
const regions = getBlockRegions(code);
|
||||||
|
for (const [start, end] of regions) {
|
||||||
|
if (cursor >= start && cursor <= end) {
|
||||||
|
return [start, end];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const evalBlock = (strudelMirror) => {
|
||||||
|
const { state } = strudelMirror.editor;
|
||||||
|
const code = state.doc.toString();
|
||||||
|
const cursor = state.selection.main.head;
|
||||||
|
const range = getBlockAt(code, cursor);
|
||||||
|
if (range) {
|
||||||
|
const [a, b] = range;
|
||||||
|
const block = code.slice(a, b);
|
||||||
|
if (block) {
|
||||||
|
// Flash the block being evaluated
|
||||||
|
strudelMirror.flash(200, { from: a, to: b });
|
||||||
|
strudelMirror.repl.evaluateBlock(block, true, { range });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
@@ -13,18 +13,18 @@ import {
|
|||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { persistentAtom } from '@nanostores/persistent';
|
import { persistentAtom } from '@nanostores/persistent';
|
||||||
import { logger, registerControl, repl } from '@strudel/core';
|
import { logger, registerControl, repl } from '@strudel/core';
|
||||||
import { cleanupDraw, Drawer } from '@strudel/draw';
|
import { cleanupDraw, cleanupDrawContext, Drawer } from '@strudel/draw';
|
||||||
|
|
||||||
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
||||||
import { basicSetup } from './basicSetup.mjs';
|
import { basicSetup } from './basicSetup.mjs';
|
||||||
|
import { evalBlock } from './block_utilities.mjs';
|
||||||
import { flash, isFlashEnabled } from './flash.mjs';
|
import { flash, isFlashEnabled } from './flash.mjs';
|
||||||
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
||||||
import { keybindings } from './keybindings.mjs';
|
import { keybindings } from './keybindings.mjs';
|
||||||
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
import { jumpToCharacter } from './labelJump.mjs';
|
||||||
|
import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||||
import { activateTheme, initTheme, theme } from './themes.mjs';
|
import { activateTheme, initTheme, theme } from './themes.mjs';
|
||||||
import { isTooltipEnabled } from './tooltip.mjs';
|
import { isTooltipEnabled } from './tooltip.mjs';
|
||||||
import { updateWidgets, widgetPlugin } from './widget.mjs';
|
import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs';
|
||||||
import { jumpToCharacter } from './labelJump.mjs';
|
|
||||||
|
|
||||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||||
|
|
||||||
@@ -64,6 +64,7 @@ export const defaultSettings = {
|
|||||||
isLineWrappingEnabled: false,
|
isLineWrappingEnabled: false,
|
||||||
isTabIndentationEnabled: false,
|
isTabIndentationEnabled: false,
|
||||||
isMultiCursorEnabled: false,
|
isMultiCursorEnabled: false,
|
||||||
|
isBlockBasedEvalEnabled: false,
|
||||||
theme: 'strudelTheme',
|
theme: 'strudelTheme',
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@@ -75,7 +76,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
|
|||||||
});
|
});
|
||||||
|
|
||||||
// https://codemirror.net/docs/guide/
|
// https://codemirror.net/docs/guide/
|
||||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) {
|
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) {
|
||||||
const settings = codemirrorSettings.get();
|
const settings = codemirrorSettings.get();
|
||||||
const initialSettings = Object.keys(compartments).map((key) =>
|
const initialSettings = Object.keys(compartments).map((key) =>
|
||||||
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
||||||
@@ -105,11 +106,26 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
keymap.of([
|
keymap.of([
|
||||||
{
|
{
|
||||||
key: 'Ctrl-Enter',
|
key: 'Ctrl-Enter',
|
||||||
run: () => onEvaluate?.(),
|
run: () => {
|
||||||
|
// issue with referencing settings, this works more reliably
|
||||||
|
if (strudelMirror?.isBlockBasedEvalEnabled) {
|
||||||
|
evalBlock(strudelMirror);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return onEvaluate?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Alt-Enter',
|
key: 'Alt-Enter',
|
||||||
run: () => onEvaluate?.(),
|
run: () => {
|
||||||
|
if (strudelMirror?.isBlockBasedEvalEnabled) {
|
||||||
|
evalBlock(strudelMirror);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return onEvaluate?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Ctrl-.',
|
key: 'Ctrl-.',
|
||||||
@@ -171,6 +187,7 @@ export class StrudelMirror {
|
|||||||
this.onDraw = onDraw || this.draw;
|
this.onDraw = onDraw || this.draw;
|
||||||
this.id = id || s4();
|
this.id = id || s4();
|
||||||
this.solo = solo;
|
this.solo = solo;
|
||||||
|
this.isBlockBasedEvalEnabled = false; // Will be updated via updateSettings()
|
||||||
|
|
||||||
this.drawer = new Drawer((haps, time, _, painters) => {
|
this.drawer = new Drawer((haps, time, _, painters) => {
|
||||||
const currentFrame = haps.filter((hap) => hap.isActive(time));
|
const currentFrame = haps.filter((hap) => hap.isActive(time));
|
||||||
@@ -201,20 +218,28 @@ export class StrudelMirror {
|
|||||||
cleanupDraw(true, id);
|
cleanupDraw(true, id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeEval: async () => {
|
beforeEval: async ({ blockBased } = {}) => {
|
||||||
cleanupDraw(true, id);
|
// Only clean up all drawings for full evaluation
|
||||||
|
// Block-based eval should preserve animations (like .scope()) from other blocks
|
||||||
|
if (!blockBased) {
|
||||||
|
cleanupDraw(true, id);
|
||||||
|
}
|
||||||
await this.prebaked;
|
await this.prebaked;
|
||||||
await replOptions?.beforeEval?.();
|
await replOptions?.beforeEval?.();
|
||||||
},
|
},
|
||||||
afterEval: (options) => {
|
afterEval: (options) => {
|
||||||
// remember for when highlighting is toggled on
|
// remember for when highlighting is toggled on
|
||||||
this.miniLocations = options.meta?.miniLocations;
|
this.miniLocations = options.meta?.miniLocations || [];
|
||||||
this.widgets = options.meta?.widgets;
|
this.widgets = options.meta?.widgets;
|
||||||
|
|
||||||
const sliders = this.widgets.filter((w) => w.type === 'slider');
|
const sliders = this.widgets.filter((w) => w.type === 'slider');
|
||||||
updateSliderWidgets(this.editor, sliders);
|
|
||||||
const widgets = this.widgets.filter((w) => w.type !== 'slider');
|
const widgets = this.widgets.filter((w) => w.type !== 'slider');
|
||||||
updateWidgets(this.editor, widgets);
|
// range-aware update for block-based evaluation
|
||||||
updateMiniLocations(this.editor, this.miniLocations);
|
const range = options.range && options.range.length >= 2 ? options.range : null;
|
||||||
|
|
||||||
|
updateSliderWidgets(this.editor, sliders, range);
|
||||||
|
updateWidgets(this.editor, widgets, range);
|
||||||
|
updateMiniLocations(this.editor, this.miniLocations, range);
|
||||||
replOptions?.afterEval?.(options);
|
replOptions?.afterEval?.(options);
|
||||||
// if no painters are set (.onPaint was not called), then we only need
|
// if no painters are set (.onPaint was not called), then we only need
|
||||||
// the present moment (for highlighting)
|
// the present moment (for highlighting)
|
||||||
@@ -222,8 +247,14 @@ export class StrudelMirror {
|
|||||||
this.drawer.setDrawTime(drawTime);
|
this.drawer.setDrawTime(drawTime);
|
||||||
// invalidate drawer after we've set the appropriate drawTime
|
// invalidate drawer after we've set the appropriate drawTime
|
||||||
this.drawer.invalidate(this.repl.scheduler);
|
this.drawer.invalidate(this.repl.scheduler);
|
||||||
|
|
||||||
|
// Clean up draw context if a non-inline widget was removed
|
||||||
|
if (options.widgetRemoved) {
|
||||||
|
cleanupDrawContext(id);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.cleanupDrawContext = () => cleanupDrawContext(id);
|
||||||
this.editor = initEditor({
|
this.editor = initEditor({
|
||||||
root,
|
root,
|
||||||
initialCode,
|
initialCode,
|
||||||
@@ -236,7 +267,9 @@ export class StrudelMirror {
|
|||||||
onEvaluate: () => this.evaluate(),
|
onEvaluate: () => this.evaluate(),
|
||||||
onStop: () => this.stop(),
|
onStop: () => this.stop(),
|
||||||
mondo: replOptions.mondo,
|
mondo: replOptions.mondo,
|
||||||
|
strudelMirror: this,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cmEditor = this.root.querySelector('.cm-editor');
|
const cmEditor = this.root.querySelector('.cm-editor');
|
||||||
if (cmEditor) {
|
if (cmEditor) {
|
||||||
this.root.style.display = 'block';
|
this.root.style.display = 'block';
|
||||||
@@ -306,8 +339,9 @@ export class StrudelMirror {
|
|||||||
this.flash();
|
this.flash();
|
||||||
await this.repl.evaluate(this.code, autostart);
|
await this.repl.evaluate(this.code, autostart);
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
this.repl.scheduler.stop();
|
this.repl.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for global stop requests (e.g., from Vim :q)
|
// Listen for global stop requests (e.g., from Vim :q)
|
||||||
@@ -326,8 +360,8 @@ export class StrudelMirror {
|
|||||||
this.evaluate();
|
this.evaluate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flash(ms) {
|
flash(ms, range) {
|
||||||
flash(this.editor, ms);
|
flash(this.editor, ms, range);
|
||||||
}
|
}
|
||||||
highlight(haps, time) {
|
highlight(haps, time) {
|
||||||
highlightMiniLocations(this.editor, time, haps);
|
highlightMiniLocations(this.editor, time, haps);
|
||||||
@@ -359,6 +393,10 @@ export class StrudelMirror {
|
|||||||
setLineWrappingEnabled(enabled) {
|
setLineWrappingEnabled(enabled) {
|
||||||
this.reconfigureExtension('isLineWrappingEnabled', enabled);
|
this.reconfigureExtension('isLineWrappingEnabled', enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setBlockBasedEvalEnabled(enabled) {
|
||||||
|
this.reconfigureExtension('isBlockBasedEvalEnabled', enabled);
|
||||||
|
}
|
||||||
setBracketMatchingEnabled(enabled) {
|
setBracketMatchingEnabled(enabled) {
|
||||||
this.reconfigureExtension('isBracketMatchingEnabled', enabled);
|
this.reconfigureExtension('isBracketMatchingEnabled', enabled);
|
||||||
}
|
}
|
||||||
@@ -380,6 +418,10 @@ export class StrudelMirror {
|
|||||||
for (let key in extensions) {
|
for (let key in extensions) {
|
||||||
this.reconfigureExtension(key, settings[key]);
|
this.reconfigureExtension(key, settings[key]);
|
||||||
}
|
}
|
||||||
|
// Update block-based eval setting on the instance
|
||||||
|
if (settings.isBlockBasedEvalEnabled !== undefined) {
|
||||||
|
this.isBlockBasedEvalEnabled = parseBooleans(settings.isBlockBasedEvalEnabled);
|
||||||
|
}
|
||||||
const updated = { ...codemirrorSettings.get(), ...settings };
|
const updated = { ...codemirrorSettings.get(), ...settings };
|
||||||
codemirrorSettings.set(updated);
|
codemirrorSettings.set(updated);
|
||||||
}
|
}
|
||||||
@@ -401,6 +443,16 @@ export class StrudelMirror {
|
|||||||
};
|
};
|
||||||
this.editor.dispatch({ changes });
|
this.editor.dispatch({ changes });
|
||||||
}
|
}
|
||||||
|
// used for debugging but could serve other purposes
|
||||||
|
getActiveWidgets() {
|
||||||
|
return getActiveWidgets(this.editor);
|
||||||
|
}
|
||||||
|
getSliderWidgets() {
|
||||||
|
return getSliderWidgets(this.editor);
|
||||||
|
}
|
||||||
|
getMiniLocations() {
|
||||||
|
return this.miniLocations;
|
||||||
|
}
|
||||||
clear() {
|
clear() {
|
||||||
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
|
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
|
||||||
this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest);
|
this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest);
|
||||||
@@ -434,6 +486,7 @@ function s4() {
|
|||||||
/**
|
/**
|
||||||
* Overrides the css of highlighted events. Make sure to use single quotes!
|
* Overrides the css of highlighted events. Make sure to use single quotes!
|
||||||
* @name markcss
|
* @name markcss
|
||||||
|
* @tag visualization
|
||||||
* @example
|
* @example
|
||||||
* note("c a f e")
|
* note("c a f e")
|
||||||
* .markcss('text-decoration:underline')
|
* .markcss('text-decoration:underline')
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export const flashField = StateField.define({
|
|||||||
const mark = Decoration.mark({
|
const mark = Decoration.mark({
|
||||||
attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` },
|
attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` },
|
||||||
});
|
});
|
||||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
const range = e.value.range || { from: 0, to: tr.newDoc.length };
|
||||||
|
flash = Decoration.set([mark.range(range.from, range.to)]);
|
||||||
} else {
|
} else {
|
||||||
flash = Decoration.set([]);
|
flash = Decoration.set([]);
|
||||||
}
|
}
|
||||||
@@ -29,8 +30,9 @@ export const flashField = StateField.define({
|
|||||||
provide: (f) => EditorView.decorations.from(f),
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const flash = (view, ms = 200) => {
|
export const flash = (view, ms = 200, range) => {
|
||||||
view.dispatch({ effects: setFlash.of(true) });
|
const flashData = range ? { range } : true;
|
||||||
|
view.dispatch({ effects: setFlash.of(flashData) });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
view.dispatch({ effects: setFlash.of(false) });
|
view.dispatch({ effects: setFlash.of(false) });
|
||||||
}, ms);
|
}, ms);
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { Decoration, EditorView } from '@codemirror/view';
|
|||||||
|
|
||||||
export const setMiniLocations = StateEffect.define();
|
export const setMiniLocations = StateEffect.define();
|
||||||
export const showMiniLocations = StateEffect.define();
|
export const showMiniLocations = StateEffect.define();
|
||||||
export const updateMiniLocations = (view, locations) => {
|
export const displayMiniLocations = StateEffect.define();
|
||||||
view.dispatch({ effects: setMiniLocations.of(locations) });
|
export const updateMiniLocations = (view, locations, range = null) => {
|
||||||
|
view.dispatch({ effects: setMiniLocations.of({ locations, range }) });
|
||||||
};
|
};
|
||||||
export const highlightMiniLocations = (view, atTime, haps) => {
|
export const highlightMiniLocations = (view, atTime, haps) => {
|
||||||
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
|
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
|
||||||
@@ -21,23 +22,54 @@ const miniLocations = StateField.define({
|
|||||||
|
|
||||||
for (let e of tr.effects) {
|
for (let e of tr.effects) {
|
||||||
if (e.is(setMiniLocations)) {
|
if (e.is(setMiniLocations)) {
|
||||||
// this is called on eval, with the mini locations obtained from the transpiler
|
//block-based eval case
|
||||||
// codemirror will automatically remap the marks when the document is edited
|
if (e.value.range) {
|
||||||
// create a mark for each mini location, adding the range to the spec to find it later
|
const stateMiniLocations = getMiniLocationsFromDecorations(locations);
|
||||||
const marks = e.value
|
|
||||||
.filter(([from]) => from < tr.newDoc.length)
|
|
||||||
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
|
|
||||||
.map(
|
|
||||||
(range) =>
|
|
||||||
Decoration.mark({
|
|
||||||
id: range.join(':'),
|
|
||||||
// this green is only to verify that the decoration moves when the document is edited
|
|
||||||
// it will be removed later, so the mark is not visible by default
|
|
||||||
attributes: { style: `background-color: #00CA2880` },
|
|
||||||
}).range(...range), // -> Decoration
|
|
||||||
);
|
|
||||||
|
|
||||||
locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<Decoration>
|
const normalized = e.value.locations
|
||||||
|
.filter(([from]) => from < tr.newDoc.length)
|
||||||
|
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)]);
|
||||||
|
|
||||||
|
const newIds = new Set(normalized.map((r) => r.join(':')));
|
||||||
|
|
||||||
|
const marks = normalized.map((range) => {
|
||||||
|
const id = range.join(':');
|
||||||
|
return Decoration.mark({
|
||||||
|
id,
|
||||||
|
// this green is only to verify that the decoration moves when the document is edited
|
||||||
|
// it will be removed later, so the mark is not visible by default
|
||||||
|
attributes: { style: `background-color: #00CA2880` },
|
||||||
|
}).range(...range); // -> Decoration
|
||||||
|
});
|
||||||
|
|
||||||
|
const previousMarks = stateMiniLocations
|
||||||
|
.filter(({ id }) => !newIds.has(id))
|
||||||
|
.map(({ from, to, id }) =>
|
||||||
|
Decoration.mark({
|
||||||
|
id,
|
||||||
|
attributes: { style: `background-color: #00CA2880` },
|
||||||
|
}).range(from, to),
|
||||||
|
);
|
||||||
|
|
||||||
|
locations = Decoration.set(previousMarks.concat(marks), true); // -> DecorationSet === RangeSet<Decoration>
|
||||||
|
} else {
|
||||||
|
// this is called on eval, with the mini locations obtained from the transpiler
|
||||||
|
// codemirror will automatically remap the marks when the document is edited
|
||||||
|
// create a mark for each mini location, adding the range to the spec to find it later
|
||||||
|
const marks = e.value.locations
|
||||||
|
.filter(([from]) => from < tr.newDoc.length)
|
||||||
|
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
|
||||||
|
.map(
|
||||||
|
(range) =>
|
||||||
|
Decoration.mark({
|
||||||
|
id: range.join(':'),
|
||||||
|
// this green is only to verify that the decoration moves when the document is edited
|
||||||
|
// it will be removed later, so the mark is not visible by default
|
||||||
|
attributes: { style: `background-color: #00CA2880` },
|
||||||
|
}).range(...range), // -> Decoration
|
||||||
|
);
|
||||||
|
locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<Decoration>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,12 +107,83 @@ const visibleMiniLocations = StateField.define({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// // Derive the set of decorations from the miniLocations and visibleLocations
|
const displayMiniLocationsState = StateField.define({
|
||||||
const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
|
create() {
|
||||||
const iterator = state.field(miniLocations).iter();
|
return true; // default to showing miniLocations
|
||||||
const { haps } = state.field(visibleMiniLocations);
|
},
|
||||||
const builder = new RangeSetBuilder();
|
update(display, tr) {
|
||||||
|
for (let e of tr.effects) {
|
||||||
|
if (e.is(displayMiniLocations)) {
|
||||||
|
display = e.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return display;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// // Derive the set of decorations from the miniLocations and visibleLocations
|
||||||
|
const miniLocationHighlights = EditorView.decorations.compute(
|
||||||
|
[miniLocations, visibleMiniLocations, displayMiniLocationsState],
|
||||||
|
(state) => {
|
||||||
|
// Check if miniLocations display is disabled
|
||||||
|
const shouldDisplay = state.field(displayMiniLocationsState);
|
||||||
|
if (!shouldDisplay) {
|
||||||
|
return Decoration.none; // Return empty decorations if display is disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
const iterator = state.field(miniLocations).iter();
|
||||||
|
const { haps } = state.field(visibleMiniLocations);
|
||||||
|
const builder = new RangeSetBuilder();
|
||||||
|
|
||||||
|
while (iterator.value) {
|
||||||
|
const {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
value: {
|
||||||
|
spec: { id },
|
||||||
|
},
|
||||||
|
} = iterator;
|
||||||
|
|
||||||
|
if (haps.has(id)) {
|
||||||
|
const hap = haps.get(id);
|
||||||
|
const color = hap.value?.color ?? 'var(--foreground)';
|
||||||
|
const style = hap.value?.markcss || `outline: solid 2px ${color}`;
|
||||||
|
// Get explicit channels for color values
|
||||||
|
/*
|
||||||
|
const swatch = document.createElement('div');
|
||||||
|
swatch.style.color = color;
|
||||||
|
document.body.appendChild(swatch);
|
||||||
|
let channels = getComputedStyle(swatch)
|
||||||
|
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
|
||||||
|
.slice(1)
|
||||||
|
.map((c) => parseFloat(c || 1));
|
||||||
|
document.body.removeChild(swatch);
|
||||||
|
|
||||||
|
// Get percentage of event
|
||||||
|
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
|
||||||
|
channels[3] *= percent;
|
||||||
|
*/
|
||||||
|
|
||||||
|
builder.add(
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
Decoration.mark({
|
||||||
|
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
|
||||||
|
attributes: { style },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
iterator.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.finish();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const getMiniLocationsFromDecorations = (decorations) => {
|
||||||
|
const iterator = decorations.iter();
|
||||||
|
const miniLocationsArray = [];
|
||||||
while (iterator.value) {
|
while (iterator.value) {
|
||||||
const {
|
const {
|
||||||
from,
|
from,
|
||||||
@@ -89,50 +192,48 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
|
|||||||
spec: { id },
|
spec: { id },
|
||||||
},
|
},
|
||||||
} = iterator;
|
} = iterator;
|
||||||
|
miniLocationsArray.push({
|
||||||
if (haps.has(id)) {
|
from,
|
||||||
const hap = haps.get(id);
|
to,
|
||||||
const color = hap.value?.color ?? 'var(--foreground)';
|
id,
|
||||||
const style = hap.value?.markcss || `outline: solid 2px ${color}`;
|
});
|
||||||
// Get explicit channels for color values
|
|
||||||
/*
|
|
||||||
const swatch = document.createElement('div');
|
|
||||||
swatch.style.color = color;
|
|
||||||
document.body.appendChild(swatch);
|
|
||||||
let channels = getComputedStyle(swatch)
|
|
||||||
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
|
|
||||||
.slice(1)
|
|
||||||
.map((c) => parseFloat(c || 1));
|
|
||||||
document.body.removeChild(swatch);
|
|
||||||
|
|
||||||
// Get percentage of event
|
|
||||||
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
|
|
||||||
channels[3] *= percent;
|
|
||||||
*/
|
|
||||||
|
|
||||||
builder.add(
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
Decoration.mark({
|
|
||||||
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
|
|
||||||
attributes: { style },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
iterator.next();
|
iterator.next();
|
||||||
}
|
}
|
||||||
|
return miniLocationsArray;
|
||||||
|
};
|
||||||
|
|
||||||
return builder.finish();
|
export const getMiniLocations = (state) => {
|
||||||
});
|
const decorations = state.field(miniLocations);
|
||||||
|
return getMiniLocationsFromDecorations(decorations);
|
||||||
|
};
|
||||||
|
|
||||||
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
|
export const getActiveMiniLocations = (state) => {
|
||||||
|
const miniLocations = getMiniLocations(state);
|
||||||
|
const { haps } = state.field(visibleMiniLocations);
|
||||||
|
|
||||||
|
const activeMiniLocations = miniLocations.filter((location) => haps.has(location.id));
|
||||||
|
return activeMiniLocations;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const highlightExtension = [
|
||||||
|
miniLocations,
|
||||||
|
visibleMiniLocations,
|
||||||
|
displayMiniLocationsState,
|
||||||
|
miniLocationHighlights,
|
||||||
|
];
|
||||||
|
|
||||||
export const isPatternHighlightingEnabled = (on, config) => {
|
export const isPatternHighlightingEnabled = (on, config) => {
|
||||||
on &&
|
// NOTE:
|
||||||
config &&
|
// Modified this function to always return the highlightExtension, and instead just toggle whether or not the miniLocations are displayed.
|
||||||
setTimeout(() => {
|
// This is because block based evaluation only updates regions of miniLocations, and those updates need to be kept track of constantly.
|
||||||
updateMiniLocations(config.editor, config.miniLocations);
|
// The setTimeout was also removed because it conflicted with the range-specific updates required by
|
||||||
}, 100);
|
// block based evaluation.
|
||||||
return on ? Prec.highest(highlightExtension) : [];
|
// Not sure if this is the best approach, but for block based eval I can't think of a better way to do it.
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
// Toggle the display state for miniLocations
|
||||||
|
config.editor.dispatch({ effects: displayMiniLocations.of(on) });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Prec.highest(highlightExtension);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { emacs } from '@replit/codemirror-emacs';
|
|||||||
import { vim, Vim } from '@replit/codemirror-vim';
|
import { vim, Vim } from '@replit/codemirror-vim';
|
||||||
// import { vim } from './vim_test.mjs';
|
// import { vim } from './vim_test.mjs';
|
||||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||||
|
import { helix, commands } from 'codemirror-helix';
|
||||||
import { logger } from '@strudel/core';
|
import { logger } from '@strudel/core';
|
||||||
|
|
||||||
const vscodePlugin = ViewPlugin.fromClass(
|
const vscodePlugin = ViewPlugin.fromClass(
|
||||||
@@ -20,6 +21,70 @@ const vscodePlugin = ViewPlugin.fromClass(
|
|||||||
);
|
);
|
||||||
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
||||||
|
|
||||||
|
function replEval(view) {
|
||||||
|
try {
|
||||||
|
// Dispatch a dedicated evaluate event first
|
||||||
|
let handled = false;
|
||||||
|
try {
|
||||||
|
const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true });
|
||||||
|
handled = document.dispatchEvent(ev) === false; // false means preventDefault was called
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error dispatching repl-evaluate event', e);
|
||||||
|
}
|
||||||
|
if (handled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Try Ctrl+Enter first if not handled by custom event
|
||||||
|
const ctrlEnter = new KeyboardEvent('keydown', {
|
||||||
|
key: 'Enter',
|
||||||
|
code: 'Enter',
|
||||||
|
ctrlKey: true,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
view?.dom?.dispatchEvent?.(ctrlEnter);
|
||||||
|
// If not handled (no handler called preventDefault), try Alt+Enter as
|
||||||
|
// fallback
|
||||||
|
if (!ctrlEnter.defaultPrevented) {
|
||||||
|
const altEnter = new KeyboardEvent('keydown', {
|
||||||
|
key: 'Enter',
|
||||||
|
code: 'Enter',
|
||||||
|
altKey: true,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
view?.dom?.dispatchEvent?.(altEnter);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error dispatching repl evaluation event', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function replStop(view) {
|
||||||
|
try {
|
||||||
|
// First try dispatching our custom stop event, then fallback to Alt+.
|
||||||
|
let handled = false;
|
||||||
|
try {
|
||||||
|
const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true });
|
||||||
|
handled = document.dispatchEvent(ev) === false;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error dispatching repl-stop event', e);
|
||||||
|
}
|
||||||
|
if (!handled) {
|
||||||
|
const altDot = new KeyboardEvent('keydown', {
|
||||||
|
key: '.',
|
||||||
|
code: 'Period',
|
||||||
|
altKey: true,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
view?.dom?.dispatchEvent?.(altDot);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error dispatching repl stop event', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Map Vim :w to trigger the same action as evaluation. We dispatch a custom
|
// Map Vim :w to trigger the same action as evaluation. We dispatch a custom
|
||||||
// event 'repl-evaluate' that the editor listens for, and also simulate
|
// event 'repl-evaluate' that the editor listens for, and also simulate
|
||||||
// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
|
// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
|
||||||
@@ -47,29 +112,8 @@ try {
|
|||||||
|
|
||||||
// :q to pause/stop
|
// :q to pause/stop
|
||||||
Vim.defineEx('quit', 'q', (cm) => {
|
Vim.defineEx('quit', 'q', (cm) => {
|
||||||
try {
|
const view = cm?.view || cm;
|
||||||
const view = cm?.view || cm;
|
replStop(view);
|
||||||
// First try dispatching our custom stop event, then fallback to Alt+.
|
|
||||||
let handled = false;
|
|
||||||
try {
|
|
||||||
const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true });
|
|
||||||
handled = document.dispatchEvent(ev) === false;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error dispatching repl-stop event', e);
|
|
||||||
}
|
|
||||||
if (!handled) {
|
|
||||||
const altDot = new KeyboardEvent('keydown', {
|
|
||||||
key: '.',
|
|
||||||
code: 'Period',
|
|
||||||
altKey: true,
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
});
|
|
||||||
view?.dom?.dispatchEvent?.(altDot);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error dispatching :q stop event', e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// :w to evaluate
|
// :w to evaluate
|
||||||
@@ -83,38 +127,7 @@ try {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error logging Vim :w evaluation', e);
|
console.error('Error logging Vim :w evaluation', e);
|
||||||
}
|
}
|
||||||
// Dispatch a dedicated evaluate event first
|
replEval(view);
|
||||||
let handled = false;
|
|
||||||
try {
|
|
||||||
const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true });
|
|
||||||
handled = document.dispatchEvent(ev) === false; // false means preventDefault was called
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error dispatching repl-evaluate event', e);
|
|
||||||
}
|
|
||||||
if (handled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Try Ctrl+Enter first if not handled by custom event
|
|
||||||
const ctrlEnter = new KeyboardEvent('keydown', {
|
|
||||||
key: 'Enter',
|
|
||||||
code: 'Enter',
|
|
||||||
ctrlKey: true,
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
});
|
|
||||||
view?.dom?.dispatchEvent?.(ctrlEnter);
|
|
||||||
// If not handled (no handler called preventDefault), try Alt+Enter as
|
|
||||||
// fallback
|
|
||||||
if (!ctrlEnter.defaultPrevented) {
|
|
||||||
const altEnter = new KeyboardEvent('keydown', {
|
|
||||||
key: 'Enter',
|
|
||||||
code: 'Enter',
|
|
||||||
altKey: true,
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
});
|
|
||||||
view?.dom?.dispatchEvent?.(altEnter);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error dispatching :w evaluation event', e);
|
console.error('Error dispatching :w evaluation event', e);
|
||||||
}
|
}
|
||||||
@@ -124,11 +137,49 @@ try {
|
|||||||
console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e);
|
console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map Helix :w to trigger the same action as evaluation. We dispatch a custom
|
||||||
|
// event 'repl-evaluate' that the editor listens for, and also simulate
|
||||||
|
// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
|
||||||
|
// appears in the Console panel.
|
||||||
|
const helixCommands = commands.of([
|
||||||
|
{
|
||||||
|
// :w to evaluate
|
||||||
|
name: 'write',
|
||||||
|
aliases: ['w'],
|
||||||
|
help: 'Repl-eval',
|
||||||
|
handler(view, args) {
|
||||||
|
try {
|
||||||
|
view?.focus?.(); // Let the app know this came from Helix :w
|
||||||
|
logger('[helix] :w — evaluating code');
|
||||||
|
replEval(view);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error dispatching helix :w evaluation event', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// :q to pause/stop
|
||||||
|
name: 'quit',
|
||||||
|
aliases: ['q'],
|
||||||
|
help: 'Repl-stop',
|
||||||
|
handler(view, args) {
|
||||||
|
try {
|
||||||
|
view?.focus?.(); // Let the app know this came from Helix :q
|
||||||
|
logger('[helix] :q — stopping repl');
|
||||||
|
replStop(view);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error dispatching helix :q stop event', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const keymaps = {
|
const keymaps = {
|
||||||
vim,
|
vim,
|
||||||
emacs,
|
emacs,
|
||||||
codemirror: () => keymap.of(defaultKeymap),
|
codemirror: () => keymap.of(defaultKeymap),
|
||||||
vscode: vscodeExtension,
|
vscode: vscodeExtension,
|
||||||
|
helix: () => [helix(), helixCommands],
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Vim } from '@replit/codemirror-vim';
|
export { Vim } from '@replit/codemirror-vim';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/codemirror",
|
"name": "@strudel/codemirror",
|
||||||
"version": "1.2.7",
|
"version": "1.3.0",
|
||||||
"description": "Codemirror Extensions for Strudel",
|
"description": "Codemirror Extensions for Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
"@replit/codemirror-emacs": "^6.1.0",
|
"@replit/codemirror-emacs": "^6.1.0",
|
||||||
"@replit/codemirror-vim": "^6.3.0",
|
"@replit/codemirror-vim": "^6.3.0",
|
||||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||||
|
"codemirror-helix": "^0.5.0",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
|
|||||||
+107
-17
@@ -2,11 +2,11 @@ import { ref, pure } from '@strudel/core';
|
|||||||
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
|
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
|
||||||
import { StateEffect } from '@codemirror/state';
|
import { StateEffect } from '@codemirror/state';
|
||||||
|
|
||||||
|
// Global state storage for all widget types
|
||||||
export let sliderValues = {};
|
export let sliderValues = {};
|
||||||
const getSliderID = (from) => `slider_${from}`;
|
|
||||||
|
|
||||||
export class SliderWidget extends WidgetType {
|
export class SliderWidget extends WidgetType {
|
||||||
constructor(value, min, max, from, to, step, view) {
|
constructor(value, min, max, from, to, step, view, id) {
|
||||||
super();
|
super();
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.min = min;
|
this.min = min;
|
||||||
@@ -16,10 +16,21 @@ export class SliderWidget extends WidgetType {
|
|||||||
this.to = to;
|
this.to = to;
|
||||||
this.step = step;
|
this.step = step;
|
||||||
this.view = view;
|
this.view = view;
|
||||||
|
this.id = id || `${from}:${to}`; // Range-based ID for stability
|
||||||
}
|
}
|
||||||
|
|
||||||
eq() {
|
eq(other) {
|
||||||
return false;
|
if (!(other instanceof SliderWidget)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
this.id === other.id &&
|
||||||
|
this.from === other.from &&
|
||||||
|
this.to === other.to &&
|
||||||
|
this.value === other.value &&
|
||||||
|
this.min === other.min &&
|
||||||
|
this.max === other.max
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toDOM() {
|
toDOM() {
|
||||||
@@ -38,6 +49,7 @@ export class SliderWidget extends WidgetType {
|
|||||||
slider.from = this.from;
|
slider.from = this.from;
|
||||||
slider.originalFrom = this.originalFrom;
|
slider.originalFrom = this.originalFrom;
|
||||||
slider.to = this.to;
|
slider.to = this.to;
|
||||||
|
slider.id = this.id; // Store range-based ID in DOM element
|
||||||
slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
|
slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
|
||||||
this.slider = slider;
|
this.slider = slider;
|
||||||
slider.addEventListener('input', (e) => {
|
slider.addEventListener('input', (e) => {
|
||||||
@@ -49,7 +61,7 @@ export class SliderWidget extends WidgetType {
|
|||||||
slider.originalValue = insert;
|
slider.originalValue = insert;
|
||||||
slider.value = insert;
|
slider.value = insert;
|
||||||
this.view.dispatch({ changes: change });
|
this.view.dispatch({ changes: change });
|
||||||
const id = getSliderID(slider.originalFrom); // matches id generated in transpiler
|
const id = slider.id; // Use range-based ID
|
||||||
window.postMessage({ type: 'cm-slider', value: Number(next), id });
|
window.postMessage({ type: 'cm-slider', value: Number(next), id });
|
||||||
});
|
});
|
||||||
return wrap;
|
return wrap;
|
||||||
@@ -62,19 +74,60 @@ export class SliderWidget extends WidgetType {
|
|||||||
|
|
||||||
export const setSliderWidgets = StateEffect.define();
|
export const setSliderWidgets = StateEffect.define();
|
||||||
|
|
||||||
export const updateSliderWidgets = (view, widgets) => {
|
export const setSliderWidgetsInRange = StateEffect.define();
|
||||||
view.dispatch({ effects: setSliderWidgets.of(widgets) });
|
|
||||||
|
export const updateSliderWidgets = (view, widgets, range = null) => {
|
||||||
|
if (range) {
|
||||||
|
// range argument passed for block-based evaluation
|
||||||
|
view.dispatch({ effects: setSliderWidgetsInRange.of({ widgets, range }) });
|
||||||
|
} else {
|
||||||
|
view.dispatch({ effects: setSliderWidgets.of(widgets) });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSliders(widgetConfigs, view) {
|
function getSliders(widgetConfigs, view) {
|
||||||
return widgetConfigs
|
return (
|
||||||
.filter((w) => w.type === 'slider')
|
widgetConfigs
|
||||||
.map(({ from, to, value, min, max, step }) => {
|
.filter((w) => w.type === 'slider')
|
||||||
return Decoration.widget({
|
// Deduplicate sliders that might appear multiple times (e.g., during paste operations)
|
||||||
widget: new SliderWidget(value, min, max, from, to, step, view),
|
.filter((slider, index, self) => index === self.findIndex((s) => s.from === slider.from && s.to === slider.to))
|
||||||
side: 0,
|
.sort((a, b) => a.from - b.from)
|
||||||
}).range(from /* , to */);
|
.map(({ from, to, value, min, max, step, id }) => {
|
||||||
});
|
return Decoration.widget({
|
||||||
|
widget: new SliderWidget(value, min, max, from, to, step, view, id),
|
||||||
|
side: 0,
|
||||||
|
}).range(from /* , to */);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSliderWidgets(view) {
|
||||||
|
if (!view || !view.state) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sliderPluginInstance = view.plugin(sliderPlugin);
|
||||||
|
if (!sliderPluginInstance || !sliderPluginInstance.decorations) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sliderWidgets = [];
|
||||||
|
|
||||||
|
sliderPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => {
|
||||||
|
if (decoration.widget instanceof SliderWidget) {
|
||||||
|
sliderWidgets.push({
|
||||||
|
type: 'slider',
|
||||||
|
from: decoration.widget.from,
|
||||||
|
to: decoration.widget.to,
|
||||||
|
value: decoration.widget.value,
|
||||||
|
min: decoration.widget.min,
|
||||||
|
max: decoration.widget.max,
|
||||||
|
step: decoration.widget.step,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return sliderWidgets;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sliderPlugin = ViewPlugin.fromClass(
|
export const sliderPlugin = ViewPlugin.fromClass(
|
||||||
@@ -101,7 +154,42 @@ export const sliderPlugin = ViewPlugin.fromClass(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let e of tr.effects) {
|
for (let e of tr.effects) {
|
||||||
if (e.is(setSliderWidgets)) {
|
if (e.is(setSliderWidgetsInRange)) {
|
||||||
|
// Block-aware slider update logic
|
||||||
|
const { widgets, range } = e.value;
|
||||||
|
const [rangeStart, rangeEnd] = range;
|
||||||
|
|
||||||
|
// Get existing slider widgets that should be preserved
|
||||||
|
const existingSliders = [];
|
||||||
|
this.decorations.between(0, update.view.state.doc.length, (from, to, decoration) => {
|
||||||
|
if (decoration.widget instanceof SliderWidget) {
|
||||||
|
// Preserve sliders outside the evaluation range
|
||||||
|
// Use strict > for rangeEnd because when code is deleted, slider positions
|
||||||
|
// map to the deletion boundary (rangeEnd), and those should be removed, not preserved
|
||||||
|
if (from < rangeStart || from > rangeEnd) {
|
||||||
|
existingSliders.push({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
value: decoration.widget.value,
|
||||||
|
min: decoration.widget.min,
|
||||||
|
max: decoration.widget.max,
|
||||||
|
step: decoration.widget.step,
|
||||||
|
id: decoration.widget.id || `${from}:${to}`,
|
||||||
|
type: 'slider',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge preserved sliders with new widgets
|
||||||
|
const mergedWidgets = [...existingSliders, ...widgets]
|
||||||
|
.filter(
|
||||||
|
(slider, index, self) => index === self.findIndex((s) => s.type === 'slider' && s.id === slider.id),
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.from - b.from);
|
||||||
|
|
||||||
|
this.decorations = Decoration.set(getSliders(mergedWidgets, update.view));
|
||||||
|
} else if (e.is(setSliderWidgets)) {
|
||||||
this.decorations = Decoration.set(getSliders(e.value, update.view));
|
this.decorations = Decoration.set(getSliders(e.value, update.view));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,6 +205,7 @@ export const sliderPlugin = ViewPlugin.fromClass(
|
|||||||
* Displays a slider widget to allow the user manipulate a value
|
* Displays a slider widget to allow the user manipulate a value
|
||||||
*
|
*
|
||||||
* @name slider
|
* @name slider
|
||||||
|
* @tags external_io, visualization
|
||||||
* @param {number} value Initial value
|
* @param {number} value Initial value
|
||||||
* @param {number} min Minimum value - optional, defaults to 0
|
* @param {number} min Minimum value - optional, defaults to 0
|
||||||
* @param {number} max Maximum value - optional, defaults to 1
|
* @param {number} max Maximum value - optional, defaults to 1
|
||||||
@@ -131,6 +220,7 @@ export let sliderWithID = (id, value, min, max) => {
|
|||||||
sliderValues[id] = value; // sync state at eval time (code -> state)
|
sliderValues[id] = value; // sync state at eval time (code -> state)
|
||||||
return ref(() => sliderValues[id]); // use state at query time
|
return ref(() => sliderValues[id]); // use state at query time
|
||||||
};
|
};
|
||||||
|
|
||||||
// update state when sliders are moved
|
// update state when sliders are moved
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.addEventListener('message', (e) => {
|
window.addEventListener('message', (e) => {
|
||||||
@@ -139,7 +229,7 @@ if (typeof window !== 'undefined') {
|
|||||||
// update state when slider is moved
|
// update state when slider is moved
|
||||||
sliderValues[e.data.id] = e.data.value;
|
sliderValues[e.data.id] = e.data.value;
|
||||||
} else {
|
} else {
|
||||||
console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
|
console.error(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+1
@@ -198,6 +198,7 @@ export function activateTheme(name) {
|
|||||||
const themeSettings = settings[name] || settings.strudelTheme;
|
const themeSettings = settings[name] || settings.strudelTheme;
|
||||||
// set css variables
|
// set css variables
|
||||||
themeStyle.innerHTML = `:root {
|
themeStyle.innerHTML = `:root {
|
||||||
|
color-scheme: ${themeSettings.light ? 'light' : 'dark'};
|
||||||
${Object.entries(themeSettings)
|
${Object.entries(themeSettings)
|
||||||
// important to override fallback
|
// important to override fallback
|
||||||
.map(([key, value]) => `--${key}: ${value} !important;`)
|
.map(([key, value]) => `--${key}: ${value} !important;`)
|
||||||
|
|||||||
+3
-1
@@ -17,13 +17,15 @@ export const settings = {
|
|||||||
background: 'white',
|
background: 'white',
|
||||||
lineBackground: 'transparent',
|
lineBackground: 'transparent',
|
||||||
foreground: deepPurple,
|
foreground: deepPurple,
|
||||||
|
muted: deepPurple + '50',
|
||||||
caret: '#797977',
|
caret: '#797977',
|
||||||
selection: yellowPink,
|
selection: yellowPink,
|
||||||
selectionMatch: '#2B323D',
|
selectionMatch: '#2B323D',
|
||||||
gutterBackground: grey,
|
gutterBackground: pinkAccent,
|
||||||
gutterForeground: lightGrey,
|
gutterForeground: lightGrey,
|
||||||
gutterBorder: 'transparent',
|
gutterBorder: 'transparent',
|
||||||
lineHighlight: pinkAccent,
|
lineHighlight: pinkAccent,
|
||||||
|
light: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createTheme({
|
export default createTheme({
|
||||||
|
|||||||
+1
@@ -31,6 +31,7 @@ const palette = palettes['Sour Watermelon B'];
|
|||||||
export const settings = {
|
export const settings = {
|
||||||
background: palette[3],
|
background: palette[3],
|
||||||
foreground: palette[1],
|
foreground: palette[1],
|
||||||
|
muted: palette[1] + '50',
|
||||||
caret: palette[0],
|
caret: palette[0],
|
||||||
selection: palette[0],
|
selection: palette[0],
|
||||||
selectionMatch: palette[1],
|
selectionMatch: palette[1],
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const settings = {
|
|||||||
background: '#282b2e',
|
background: '#282b2e',
|
||||||
lineBackground: '#282b2e99',
|
lineBackground: '#282b2e99',
|
||||||
foreground: '#a9b7c6',
|
foreground: '#a9b7c6',
|
||||||
|
muted: '#a9b7c650',
|
||||||
caret: '#00FF00',
|
caret: '#00FF00',
|
||||||
selection: '#343739',
|
selection: '#343739',
|
||||||
selectionMatch: '#343739',
|
selectionMatch: '#343739',
|
||||||
@@ -19,6 +20,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#282b2e',
|
background: '#282b2e',
|
||||||
foreground: '#a9b7c6',
|
foreground: '#a9b7c6',
|
||||||
|
muted: '#a9b7c650',
|
||||||
caret: '#00FF00',
|
caret: '#00FF00',
|
||||||
selection: '#4e5254',
|
selection: '#4e5254',
|
||||||
selectionMatch: '#4e5254',
|
selectionMatch: '#4e5254',
|
||||||
|
|||||||
+1
@@ -12,6 +12,7 @@ export const settings = {
|
|||||||
background: hex[0],
|
background: hex[0],
|
||||||
lineBackground: 'transparent',
|
lineBackground: 'transparent',
|
||||||
foreground: hex[1],
|
foreground: hex[1],
|
||||||
|
muted: hex[2],
|
||||||
selection: hex[2],
|
selection: hex[2],
|
||||||
selectionMatch: hex[0],
|
selectionMatch: hex[0],
|
||||||
gutterBackground: hex[0],
|
gutterBackground: hex[0],
|
||||||
|
|||||||
+2
@@ -11,6 +11,7 @@ export const settings = {
|
|||||||
background: '#272C35',
|
background: '#272C35',
|
||||||
lineBackground: '#272C3599',
|
lineBackground: '#272C3599',
|
||||||
foreground: 'hsl(220, 14%, 71%)',
|
foreground: 'hsl(220, 14%, 71%)',
|
||||||
|
muted: 'hsl(220, 14%, 41%)',
|
||||||
caret: '#797977',
|
caret: '#797977',
|
||||||
selection: '#ffffff30',
|
selection: '#ffffff30',
|
||||||
selectionMatch: '#2B323D',
|
selectionMatch: '#2B323D',
|
||||||
@@ -25,6 +26,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#272C35',
|
background: '#272C35',
|
||||||
foreground: '#9d9b97',
|
foreground: '#9d9b97',
|
||||||
|
muted: 'hsl(220, 14%, 41%)',
|
||||||
caret: '#797977',
|
caret: '#797977',
|
||||||
selection: '#3d4c64',
|
selection: '#3d4c64',
|
||||||
selectionMatch: '#3d4c64',
|
selectionMatch: '#3d4c64',
|
||||||
|
|||||||
Vendored
+2
@@ -5,6 +5,7 @@ export const settings = {
|
|||||||
background: '#21202e',
|
background: '#21202e',
|
||||||
lineBackground: '#21202e99',
|
lineBackground: '#21202e99',
|
||||||
foreground: '#edecee',
|
foreground: '#edecee',
|
||||||
|
muted: '#edecee50',
|
||||||
caret: '#a277ff',
|
caret: '#a277ff',
|
||||||
selection: '#3d375e7f',
|
selection: '#3d375e7f',
|
||||||
selectionMatch: '#3d375e7f',
|
selectionMatch: '#3d375e7f',
|
||||||
@@ -18,6 +19,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#21202e',
|
background: '#21202e',
|
||||||
foreground: '#edecee',
|
foreground: '#edecee',
|
||||||
|
muted: '#edecee50',
|
||||||
caret: '#a277ff',
|
caret: '#a277ff',
|
||||||
selection: '#5a51898f',
|
selection: '#5a51898f',
|
||||||
selectionMatch: '#5a51898f',
|
selectionMatch: '#5a51898f',
|
||||||
|
|||||||
+2
@@ -6,6 +6,7 @@ export const settings = {
|
|||||||
background: '#FFFFFF',
|
background: '#FFFFFF',
|
||||||
lineBackground: '#FFFFFF99',
|
lineBackground: '#FFFFFF99',
|
||||||
foreground: '#000000',
|
foreground: '#000000',
|
||||||
|
muted: '#00000050',
|
||||||
caret: '#FBAC52',
|
caret: '#FBAC52',
|
||||||
selection: '#FFD420',
|
selection: '#FFD420',
|
||||||
selectionMatch: '#FFD420',
|
selectionMatch: '#FFD420',
|
||||||
@@ -20,6 +21,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#FFFFFF',
|
background: '#FFFFFF',
|
||||||
foreground: '#000000',
|
foreground: '#000000',
|
||||||
|
muted: '#00000050',
|
||||||
caret: '#FBAC52',
|
caret: '#FBAC52',
|
||||||
selection: '#FFD420',
|
selection: '#FFD420',
|
||||||
selectionMatch: '#FFD420',
|
selectionMatch: '#FFD420',
|
||||||
|
|||||||
+2
-1
@@ -2,7 +2,8 @@ import { tags as t } from '@lezer/highlight';
|
|||||||
import { createTheme } from './theme-helper.mjs';
|
import { createTheme } from './theme-helper.mjs';
|
||||||
export const settings = {
|
export const settings = {
|
||||||
background: 'black',
|
background: 'black',
|
||||||
foreground: 'white', // whats that?
|
foreground: 'white',
|
||||||
|
muted: '#ffffff50',
|
||||||
caret: 'white',
|
caret: 'white',
|
||||||
selection: '#ffffff20',
|
selection: '#ffffff20',
|
||||||
selectionMatch: '#036dd626',
|
selectionMatch: '#036dd626',
|
||||||
|
|||||||
+2
-1
@@ -3,7 +3,8 @@ import { createTheme } from './theme-helper.mjs';
|
|||||||
export const settings = {
|
export const settings = {
|
||||||
background: '#051DB5',
|
background: '#051DB5',
|
||||||
lineBackground: '#051DB550',
|
lineBackground: '#051DB550',
|
||||||
foreground: 'white', // whats that?
|
foreground: 'white',
|
||||||
|
muted: '#ffffff50',
|
||||||
caret: 'white',
|
caret: 'white',
|
||||||
selection: 'rgba(128, 203, 196, 0.5)',
|
selection: 'rgba(128, 203, 196, 0.5)',
|
||||||
selectionMatch: '#036dd626',
|
selectionMatch: '#036dd626',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const settings = {
|
|||||||
background: hex[0],
|
background: hex[0],
|
||||||
lineBackground: 'transparent',
|
lineBackground: 'transparent',
|
||||||
foreground: hex[2],
|
foreground: hex[2],
|
||||||
|
muted: hex[3],
|
||||||
selection: hex[3],
|
selection: hex[3],
|
||||||
selectionMatch: hex[0],
|
selectionMatch: hex[0],
|
||||||
gutterBackground: hex[0],
|
gutterBackground: hex[0],
|
||||||
|
|||||||
+2
@@ -9,6 +9,7 @@ export const settings = {
|
|||||||
background: '#242424',
|
background: '#242424',
|
||||||
lineBackground: '#24242499',
|
lineBackground: '#24242499',
|
||||||
foreground: '#f8f8f2',
|
foreground: '#f8f8f2',
|
||||||
|
muted: '#f8f8f250',
|
||||||
caret: '#FFFFFF',
|
caret: '#FFFFFF',
|
||||||
selection: 'rgba(255, 255, 255, 0.1)',
|
selection: 'rgba(255, 255, 255, 0.1)',
|
||||||
selectionMatch: 'rgba(255, 255, 255, 0.2)',
|
selectionMatch: 'rgba(255, 255, 255, 0.2)',
|
||||||
@@ -23,6 +24,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#242424',
|
background: '#242424',
|
||||||
foreground: '#f8f8f2',
|
foreground: '#f8f8f2',
|
||||||
|
muted: '#f8f8f250',
|
||||||
caret: '#FFFFFF',
|
caret: '#FFFFFF',
|
||||||
selection: 'rgba(255, 255, 255, 0.1)',
|
selection: 'rgba(255, 255, 255, 0.1)',
|
||||||
selectionMatch: 'rgba(255, 255, 255, 0.2)',
|
selectionMatch: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
|||||||
+2
@@ -11,6 +11,7 @@ export const settings = {
|
|||||||
background: '#282a36',
|
background: '#282a36',
|
||||||
lineBackground: '#282a3699',
|
lineBackground: '#282a3699',
|
||||||
foreground: '#f8f8f2',
|
foreground: '#f8f8f2',
|
||||||
|
muted: '#f8f8f250',
|
||||||
caret: '#f8f8f0',
|
caret: '#f8f8f0',
|
||||||
selection: 'rgba(255, 255, 255, 0.1)',
|
selection: 'rgba(255, 255, 255, 0.1)',
|
||||||
selectionMatch: 'rgba(255, 255, 255, 0.2)',
|
selectionMatch: 'rgba(255, 255, 255, 0.2)',
|
||||||
@@ -27,6 +28,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#282a36',
|
background: '#282a36',
|
||||||
foreground: '#f8f8f2',
|
foreground: '#f8f8f2',
|
||||||
|
muted: '#f8f8f250',
|
||||||
caret: '#f8f8f0',
|
caret: '#f8f8f0',
|
||||||
selection: 'rgba(255, 255, 255, 0.1)',
|
selection: 'rgba(255, 255, 255, 0.1)',
|
||||||
selectionMatch: 'rgba(255, 255, 255, 0.2)',
|
selectionMatch: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
|||||||
+2
@@ -10,6 +10,7 @@ export const settings = {
|
|||||||
background: '#2a2734',
|
background: '#2a2734',
|
||||||
lineBackground: '#2a273499',
|
lineBackground: '#2a273499',
|
||||||
foreground: '#eeebff',
|
foreground: '#eeebff',
|
||||||
|
muted: '#eeebff50',
|
||||||
caret: '#ffad5c',
|
caret: '#ffad5c',
|
||||||
selection: 'rgba(255, 255, 255, 0.1)',
|
selection: 'rgba(255, 255, 255, 0.1)',
|
||||||
gutterBackground: '#2a2734',
|
gutterBackground: '#2a2734',
|
||||||
@@ -22,6 +23,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#2a2734',
|
background: '#2a2734',
|
||||||
foreground: '#6c6783',
|
foreground: '#6c6783',
|
||||||
|
muted: '#eeebff50',
|
||||||
caret: '#ffad5c',
|
caret: '#ffad5c',
|
||||||
selection: '#9a86fd',
|
selection: '#9a86fd',
|
||||||
selectionMatch: '#9a86fd',
|
selectionMatch: '#9a86fd',
|
||||||
|
|||||||
+2
@@ -6,6 +6,7 @@ export const settings = {
|
|||||||
background: '#fff',
|
background: '#fff',
|
||||||
lineBackground: '#ffffff99',
|
lineBackground: '#ffffff99',
|
||||||
foreground: '#000',
|
foreground: '#000',
|
||||||
|
muted: '#00000050',
|
||||||
caret: '#FFFFFF',
|
caret: '#FFFFFF',
|
||||||
selection: '#d7d4f0',
|
selection: '#d7d4f0',
|
||||||
selectionMatch: '#d7d4f0',
|
selectionMatch: '#d7d4f0',
|
||||||
@@ -20,6 +21,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
foreground: '#000',
|
foreground: '#000',
|
||||||
|
muted: '#00000050',
|
||||||
caret: '#FFFFFF',
|
caret: '#FFFFFF',
|
||||||
selection: '#d7d4f0',
|
selection: '#d7d4f0',
|
||||||
selectionMatch: '#d7d4f0',
|
selectionMatch: '#d7d4f0',
|
||||||
|
|||||||
+1
@@ -23,6 +23,7 @@ export const settings = {
|
|||||||
background: hex[0],
|
background: hex[0],
|
||||||
lineBackground: 'transparent',
|
lineBackground: 'transparent',
|
||||||
foreground: hex[10],
|
foreground: hex[10],
|
||||||
|
muted: hex[7],
|
||||||
selection: hex[8],
|
selection: hex[8],
|
||||||
selectionMatch: hex[0],
|
selectionMatch: hex[0],
|
||||||
gutterBackground: hex[3],
|
gutterBackground: hex[3],
|
||||||
|
|||||||
+2
@@ -5,6 +5,7 @@ export const settings = {
|
|||||||
background: '#0d1117',
|
background: '#0d1117',
|
||||||
lineBackground: '#0d111799',
|
lineBackground: '#0d111799',
|
||||||
foreground: '#c9d1d9',
|
foreground: '#c9d1d9',
|
||||||
|
muted: '#c9d1d950',
|
||||||
caret: '#c9d1d9',
|
caret: '#c9d1d9',
|
||||||
selection: '#003d73',
|
selection: '#003d73',
|
||||||
selectionMatch: '#003d73',
|
selectionMatch: '#003d73',
|
||||||
@@ -16,6 +17,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#0d1117',
|
background: '#0d1117',
|
||||||
foreground: '#c9d1d9',
|
foreground: '#c9d1d9',
|
||||||
|
muted: '#c9d1d950',
|
||||||
caret: '#c9d1d9',
|
caret: '#c9d1d9',
|
||||||
selection: '#003d73',
|
selection: '#003d73',
|
||||||
selectionMatch: '#003d73',
|
selectionMatch: '#003d73',
|
||||||
|
|||||||
+2
@@ -6,6 +6,7 @@ export const settings = {
|
|||||||
background: '#fff',
|
background: '#fff',
|
||||||
lineBackground: '#ffffff99',
|
lineBackground: '#ffffff99',
|
||||||
foreground: '#24292e',
|
foreground: '#24292e',
|
||||||
|
muted: '#24292e50',
|
||||||
selection: '#BBDFFF',
|
selection: '#BBDFFF',
|
||||||
selectionMatch: '#BBDFFF',
|
selectionMatch: '#BBDFFF',
|
||||||
gutterBackground: '#fff',
|
gutterBackground: '#fff',
|
||||||
@@ -17,6 +18,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
foreground: '#24292e',
|
foreground: '#24292e',
|
||||||
|
muted: '#24292e50',
|
||||||
selection: '#BBDFFF',
|
selection: '#BBDFFF',
|
||||||
selectionMatch: '#BBDFFF',
|
selectionMatch: '#BBDFFF',
|
||||||
gutterBackground: '#fff',
|
gutterBackground: '#fff',
|
||||||
|
|||||||
+1
@@ -13,6 +13,7 @@ export const settings = {
|
|||||||
background: hex[0],
|
background: hex[0],
|
||||||
lineBackground: 'transparent',
|
lineBackground: 'transparent',
|
||||||
foreground: hex[2],
|
foreground: hex[2],
|
||||||
|
muted: hex[2] + '50',
|
||||||
selection: hex[4],
|
selection: hex[4],
|
||||||
selectionMatch: hex[0],
|
selectionMatch: hex[0],
|
||||||
gutterBackground: hex[0],
|
gutterBackground: hex[0],
|
||||||
|
|||||||
+2
@@ -10,6 +10,7 @@ export const settings = {
|
|||||||
background: '#282828',
|
background: '#282828',
|
||||||
lineBackground: '#28282899',
|
lineBackground: '#28282899',
|
||||||
foreground: '#ebdbb2',
|
foreground: '#ebdbb2',
|
||||||
|
muted: '#ebdbb250',
|
||||||
caret: '#ebdbb2',
|
caret: '#ebdbb2',
|
||||||
selection: '#bdae93',
|
selection: '#bdae93',
|
||||||
selectionMatch: '#bdae93',
|
selectionMatch: '#bdae93',
|
||||||
@@ -23,6 +24,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#282828',
|
background: '#282828',
|
||||||
foreground: '#ebdbb2',
|
foreground: '#ebdbb2',
|
||||||
|
muted: '#ebdbb250',
|
||||||
caret: '#ebdbb2',
|
caret: '#ebdbb2',
|
||||||
selection: '#b99d555c',
|
selection: '#b99d555c',
|
||||||
selectionMatch: '#b99d555c',
|
selectionMatch: '#b99d555c',
|
||||||
|
|||||||
+2
@@ -11,6 +11,7 @@ export const settings = {
|
|||||||
background: '#fbf1c7',
|
background: '#fbf1c7',
|
||||||
lineBackground: '#fbf1c799',
|
lineBackground: '#fbf1c799',
|
||||||
foreground: '#3c3836',
|
foreground: '#3c3836',
|
||||||
|
muted: '#3c383650',
|
||||||
caret: '#af3a03',
|
caret: '#af3a03',
|
||||||
selection: '#ebdbb2',
|
selection: '#ebdbb2',
|
||||||
selectionMatch: '#bdae93',
|
selectionMatch: '#bdae93',
|
||||||
@@ -25,6 +26,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#fbf1c7',
|
background: '#fbf1c7',
|
||||||
foreground: '#3c3836',
|
foreground: '#3c3836',
|
||||||
|
muted: '#3c383650',
|
||||||
caret: '#af3a03',
|
caret: '#af3a03',
|
||||||
selection: '#bdae9391',
|
selection: '#bdae9391',
|
||||||
selectionMatch: '#bdae9391',
|
selectionMatch: '#bdae9391',
|
||||||
|
|||||||
+2
@@ -5,6 +5,7 @@ export const settings = {
|
|||||||
background: '#212121',
|
background: '#212121',
|
||||||
lineBackground: '#21212199',
|
lineBackground: '#21212199',
|
||||||
foreground: '#bdbdbd',
|
foreground: '#bdbdbd',
|
||||||
|
muted: '#bdbdbd50',
|
||||||
caret: '#a0a4ae',
|
caret: '#a0a4ae',
|
||||||
selection: '#d7d4f0',
|
selection: '#d7d4f0',
|
||||||
selectionMatch: '#d7d4f0',
|
selectionMatch: '#d7d4f0',
|
||||||
@@ -19,6 +20,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#212121',
|
background: '#212121',
|
||||||
foreground: '#bdbdbd',
|
foreground: '#bdbdbd',
|
||||||
|
muted: '#bdbdbd50',
|
||||||
caret: '#a0a4ae',
|
caret: '#a0a4ae',
|
||||||
selection: '#d7d4f063',
|
selection: '#d7d4f063',
|
||||||
selectionMatch: '#d7d4f063',
|
selectionMatch: '#d7d4f063',
|
||||||
|
|||||||
+3
-1
@@ -5,7 +5,8 @@ export const settings = {
|
|||||||
light: true,
|
light: true,
|
||||||
background: '#FAFAFA',
|
background: '#FAFAFA',
|
||||||
lineBackground: '#FAFAFA99',
|
lineBackground: '#FAFAFA99',
|
||||||
foreground: '#90A4AE',
|
foreground: '#6182B8',
|
||||||
|
muted: '#6182B850',
|
||||||
caret: '#272727',
|
caret: '#272727',
|
||||||
selection: '#80CBC440',
|
selection: '#80CBC440',
|
||||||
selectionMatch: '#FAFAFA',
|
selectionMatch: '#FAFAFA',
|
||||||
@@ -19,6 +20,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#FAFAFA',
|
background: '#FAFAFA',
|
||||||
foreground: '#90A4AE',
|
foreground: '#90A4AE',
|
||||||
|
muted: '#6182B850',
|
||||||
caret: '#272727',
|
caret: '#272727',
|
||||||
selection: '#80CBC440',
|
selection: '#80CBC440',
|
||||||
selectionMatch: '#80CBC440',
|
selectionMatch: '#80CBC440',
|
||||||
|
|||||||
+2
@@ -5,6 +5,7 @@ export const settings = {
|
|||||||
background: '#272822',
|
background: '#272822',
|
||||||
lineBackground: '#27282299',
|
lineBackground: '#27282299',
|
||||||
foreground: '#FFFFFF',
|
foreground: '#FFFFFF',
|
||||||
|
muted: '#FFFFFF50',
|
||||||
caret: '#FFFFFF',
|
caret: '#FFFFFF',
|
||||||
selection: '#49483E',
|
selection: '#49483E',
|
||||||
selectionMatch: '#49483E',
|
selectionMatch: '#49483E',
|
||||||
@@ -18,6 +19,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#272822',
|
background: '#272822',
|
||||||
foreground: '#FFFFFF',
|
foreground: '#FFFFFF',
|
||||||
|
muted: '#FFFFFF50',
|
||||||
caret: '#FFFFFF',
|
caret: '#FFFFFF',
|
||||||
selection: '#49483E',
|
selection: '#49483E',
|
||||||
selectionMatch: '#49483E',
|
selectionMatch: '#49483E',
|
||||||
|
|||||||
+2
@@ -6,6 +6,7 @@ export const settings = {
|
|||||||
background: '#f2f1f8',
|
background: '#f2f1f8',
|
||||||
lineBackground: '#f2f1f899',
|
lineBackground: '#f2f1f899',
|
||||||
foreground: '#0c006b',
|
foreground: '#0c006b',
|
||||||
|
muted: '#0c006b50',
|
||||||
caret: '#5c49e9',
|
caret: '#5c49e9',
|
||||||
selection: '#d5d1f2',
|
selection: '#d5d1f2',
|
||||||
selectionMatch: '#d5d1f2',
|
selectionMatch: '#d5d1f2',
|
||||||
@@ -19,6 +20,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#f2f1f8',
|
background: '#f2f1f8',
|
||||||
foreground: '#0c006b',
|
foreground: '#0c006b',
|
||||||
|
muted: '#0c006b50',
|
||||||
caret: '#5c49e9',
|
caret: '#5c49e9',
|
||||||
selection: '#d5d1f2',
|
selection: '#d5d1f2',
|
||||||
selectionMatch: '#d5d1f2',
|
selectionMatch: '#d5d1f2',
|
||||||
|
|||||||
Vendored
+2
@@ -5,6 +5,7 @@ export const settings = {
|
|||||||
background: '#2e3440',
|
background: '#2e3440',
|
||||||
lineBackground: '#2e344099',
|
lineBackground: '#2e344099',
|
||||||
foreground: '#FFFFFF',
|
foreground: '#FFFFFF',
|
||||||
|
muted: '#FFFFFF50',
|
||||||
caret: '#FFFFFF',
|
caret: '#FFFFFF',
|
||||||
selection: '#3b4252',
|
selection: '#3b4252',
|
||||||
selectionMatch: '#e5e9f0',
|
selectionMatch: '#e5e9f0',
|
||||||
@@ -20,6 +21,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#2e3440',
|
background: '#2e3440',
|
||||||
foreground: '#FFFFFF',
|
foreground: '#FFFFFF',
|
||||||
|
muted: '#FFFFFF50',
|
||||||
caret: '#FFFFFF',
|
caret: '#FFFFFF',
|
||||||
selection: '#00000073',
|
selection: '#00000073',
|
||||||
selectionMatch: '#00000073',
|
selectionMatch: '#00000073',
|
||||||
|
|||||||
+1
@@ -13,6 +13,7 @@ export const settings = {
|
|||||||
background: hex[0],
|
background: hex[0],
|
||||||
lineBackground: 'transparent',
|
lineBackground: 'transparent',
|
||||||
foreground: hex[2],
|
foreground: hex[2],
|
||||||
|
muted: hex[2] + '50',
|
||||||
selection: hex[4],
|
selection: hex[4],
|
||||||
selectionMatch: hex[0],
|
selectionMatch: hex[0],
|
||||||
gutterBackground: hex[0],
|
gutterBackground: hex[0],
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const settings = {
|
|||||||
background: '#002b36',
|
background: '#002b36',
|
||||||
lineBackground: '#002b3699',
|
lineBackground: '#002b3699',
|
||||||
foreground: '#93a1a1',
|
foreground: '#93a1a1',
|
||||||
|
muted: '#93a1a150',
|
||||||
caret: '#839496',
|
caret: '#839496',
|
||||||
selection: '#173541',
|
selection: '#173541',
|
||||||
selectionMatch: '#aafe661a',
|
selectionMatch: '#aafe661a',
|
||||||
@@ -45,6 +46,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: c.background,
|
background: c.background,
|
||||||
foreground: c.foreground,
|
foreground: c.foreground,
|
||||||
|
muted: c.muted,
|
||||||
caret: c.cursor,
|
caret: c.cursor,
|
||||||
selection: c.selection,
|
selection: c.selection,
|
||||||
selectionMatch: c.selection,
|
selectionMatch: c.selection,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const settings = {
|
|||||||
background: '#fdf6e3',
|
background: '#fdf6e3',
|
||||||
lineBackground: '#fdf6e399',
|
lineBackground: '#fdf6e399',
|
||||||
foreground: '#657b83',
|
foreground: '#657b83',
|
||||||
|
muted: '#657b8350',
|
||||||
caret: '#586e75',
|
caret: '#586e75',
|
||||||
selection: '#dfd9c8',
|
selection: '#dfd9c8',
|
||||||
selectionMatch: '#dfd9c8',
|
selectionMatch: '#dfd9c8',
|
||||||
@@ -19,6 +20,7 @@ export const settings = {
|
|||||||
const c = {
|
const c = {
|
||||||
background: '#FDF6E3',
|
background: '#FDF6E3',
|
||||||
foreground: '#657B83',
|
foreground: '#657B83',
|
||||||
|
muted: '#657b8350',
|
||||||
selection: '#EEE8D5',
|
selection: '#EEE8D5',
|
||||||
selectionMatch: '#EEE8D5',
|
selectionMatch: '#EEE8D5',
|
||||||
cursor: '#657B83',
|
cursor: '#657B83',
|
||||||
|
|||||||
+1
@@ -13,6 +13,7 @@ export const settings = {
|
|||||||
background: '#000000',
|
background: '#000000',
|
||||||
lineBackground: 'transparent',
|
lineBackground: 'transparent',
|
||||||
foreground: hex[4],
|
foreground: hex[4],
|
||||||
|
muted: hex[6],
|
||||||
selection: hex[6],
|
selection: hex[6],
|
||||||
gutterBackground: hex[0],
|
gutterBackground: hex[0],
|
||||||
gutterForeground: hex[5],
|
gutterForeground: hex[5],
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const settings = {
|
|||||||
background: '#222',
|
background: '#222',
|
||||||
lineBackground: '#22222299',
|
lineBackground: '#22222299',
|
||||||
foreground: '#fff',
|
foreground: '#fff',
|
||||||
|
muted: '#8a919966',
|
||||||
caret: '#ffcc00',
|
caret: '#ffcc00',
|
||||||
selection: 'rgba(128, 203, 196, 0.5)',
|
selection: 'rgba(128, 203, 196, 0.5)',
|
||||||
selectionMatch: '#036dd626',
|
selectionMatch: '#036dd626',
|
||||||
|
|||||||
+2
@@ -5,6 +5,7 @@ export const settings = {
|
|||||||
background: '#303841',
|
background: '#303841',
|
||||||
lineBackground: '#30384199',
|
lineBackground: '#30384199',
|
||||||
foreground: '#FFFFFF',
|
foreground: '#FFFFFF',
|
||||||
|
muted: '#FFFFFF50',
|
||||||
caret: '#FBAC52',
|
caret: '#FBAC52',
|
||||||
selection: '#4C5964',
|
selection: '#4C5964',
|
||||||
selectionMatch: '#3A546E',
|
selectionMatch: '#3A546E',
|
||||||
@@ -18,6 +19,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#303841',
|
background: '#303841',
|
||||||
foreground: '#FFFFFF',
|
foreground: '#FFFFFF',
|
||||||
|
muted: '#FFFFFF50',
|
||||||
caret: '#FBAC52',
|
caret: '#FBAC52',
|
||||||
selection: '#4C5964',
|
selection: '#4C5964',
|
||||||
selectionMatch: '#3A546E',
|
selectionMatch: '#3A546E',
|
||||||
|
|||||||
+2
-1
@@ -9,7 +9,8 @@ let colorD = '#f8fc55';
|
|||||||
|
|
||||||
export const settings = {
|
export const settings = {
|
||||||
background: '#000000',
|
background: '#000000',
|
||||||
foreground: colorA, // whats that?
|
foreground: colorA,
|
||||||
|
muted: colorA + '50',
|
||||||
caret: colorC,
|
caret: colorC,
|
||||||
selection: colorD,
|
selection: colorD,
|
||||||
selectionMatch: colorA,
|
selectionMatch: colorA,
|
||||||
|
|||||||
+4
-2
@@ -4,7 +4,8 @@ import { createTheme } from './theme-helper.mjs';
|
|||||||
export const settings = {
|
export const settings = {
|
||||||
background: '#24283b',
|
background: '#24283b',
|
||||||
lineBackground: '#24283b99',
|
lineBackground: '#24283b99',
|
||||||
foreground: '#7982a9',
|
foreground: '#f9f2f9',
|
||||||
|
muted: '#f9f2f950',
|
||||||
caret: '#c0caf5',
|
caret: '#c0caf5',
|
||||||
selection: '#6f7bb630',
|
selection: '#6f7bb630',
|
||||||
selectionMatch: '#1f2335',
|
selectionMatch: '#1f2335',
|
||||||
@@ -18,7 +19,8 @@ export default createTheme({
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
settings: {
|
settings: {
|
||||||
background: '#24283b',
|
background: '#24283b',
|
||||||
foreground: '#7982a9',
|
foreground: '#f9f2f9',
|
||||||
|
muted: '#f9f2f950',
|
||||||
caret: '#c0caf5',
|
caret: '#c0caf5',
|
||||||
selection: '#6f7bb630',
|
selection: '#6f7bb630',
|
||||||
selectionMatch: '#343b5f',
|
selectionMatch: '#343b5f',
|
||||||
|
|||||||
+4
-2
@@ -4,7 +4,8 @@ import { createTheme } from './theme-helper.mjs';
|
|||||||
export const settings = {
|
export const settings = {
|
||||||
background: '#1a1b26',
|
background: '#1a1b26',
|
||||||
lineBackground: '#1a1b2699',
|
lineBackground: '#1a1b2699',
|
||||||
foreground: '#787c99',
|
foreground: '#f8fcf9',
|
||||||
|
muted: '#f8fcf950',
|
||||||
caret: '#c0caf5',
|
caret: '#c0caf5',
|
||||||
selection: '#515c7e40',
|
selection: '#515c7e40',
|
||||||
selectionMatch: '#16161e',
|
selectionMatch: '#16161e',
|
||||||
@@ -18,7 +19,8 @@ export default createTheme({
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
settings: {
|
settings: {
|
||||||
background: '#1a1b26',
|
background: '#1a1b26',
|
||||||
foreground: '#787c99',
|
foreground: '#f8fcf9',
|
||||||
|
muted: '#f8fcf950',
|
||||||
caret: '#c0caf5',
|
caret: '#c0caf5',
|
||||||
selection: '#515c7e40',
|
selection: '#515c7e40',
|
||||||
selectionMatch: '#16161e',
|
selectionMatch: '#16161e',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const settings = {
|
|||||||
background: '#e1e2e7',
|
background: '#e1e2e7',
|
||||||
lineBackground: '#e1e2e799',
|
lineBackground: '#e1e2e799',
|
||||||
foreground: '#3760bf',
|
foreground: '#3760bf',
|
||||||
|
muted: '#3760bf50',
|
||||||
caret: '#3760bf',
|
caret: '#3760bf',
|
||||||
selection: '#99a7df',
|
selection: '#99a7df',
|
||||||
selectionMatch: '#99a7df',
|
selectionMatch: '#99a7df',
|
||||||
@@ -20,6 +21,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#e1e2e7',
|
background: '#e1e2e7',
|
||||||
foreground: '#3760bf',
|
foreground: '#3760bf',
|
||||||
|
muted: '#3760bf50',
|
||||||
caret: '#3760bf',
|
caret: '#3760bf',
|
||||||
selection: '#99a7df',
|
selection: '#99a7df',
|
||||||
selectionMatch: '#99a7df',
|
selectionMatch: '#99a7df',
|
||||||
|
|||||||
+2
@@ -5,6 +5,7 @@ export const settings = {
|
|||||||
background: '#1e1e1e',
|
background: '#1e1e1e',
|
||||||
lineBackground: '#1e1e1e99',
|
lineBackground: '#1e1e1e99',
|
||||||
foreground: '#fff',
|
foreground: '#fff',
|
||||||
|
muted: '#ffffff50',
|
||||||
caret: '#c6c6c6',
|
caret: '#c6c6c6',
|
||||||
selection: '#6199ff2f',
|
selection: '#6199ff2f',
|
||||||
selectionMatch: '#72a1ff59',
|
selectionMatch: '#72a1ff59',
|
||||||
@@ -19,6 +20,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#1e1e1e',
|
background: '#1e1e1e',
|
||||||
foreground: '#fff',
|
foreground: '#fff',
|
||||||
|
muted: '#ffffff50',
|
||||||
caret: '#c6c6c6',
|
caret: '#c6c6c6',
|
||||||
selection: '#6199ff2f',
|
selection: '#6199ff2f',
|
||||||
selectionMatch: '#72a1ff59',
|
selectionMatch: '#72a1ff59',
|
||||||
|
|||||||
+2
@@ -5,6 +5,7 @@ export const settings = {
|
|||||||
background: '#ffffff',
|
background: '#ffffff',
|
||||||
lineBackground: '#ffffff50',
|
lineBackground: '#ffffff50',
|
||||||
foreground: '#383a42',
|
foreground: '#383a42',
|
||||||
|
muted: '#383a4250',
|
||||||
caret: '#000',
|
caret: '#000',
|
||||||
selection: '#add6ff',
|
selection: '#add6ff',
|
||||||
selectionMatch: '#a8ac94',
|
selectionMatch: '#a8ac94',
|
||||||
@@ -20,6 +21,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#ffffff',
|
background: '#ffffff',
|
||||||
foreground: '#383a42',
|
foreground: '#383a42',
|
||||||
|
muted: '#383a4250',
|
||||||
caret: '#000',
|
caret: '#000',
|
||||||
selection: '#add6ff',
|
selection: '#add6ff',
|
||||||
selectionMatch: '#a8ac94',
|
selectionMatch: '#a8ac94',
|
||||||
|
|||||||
+2
-1
@@ -2,7 +2,8 @@ import { tags as t } from '@lezer/highlight';
|
|||||||
import { createTheme } from './theme-helper.mjs';
|
import { createTheme } from './theme-helper.mjs';
|
||||||
export const settings = {
|
export const settings = {
|
||||||
background: 'white',
|
background: 'white',
|
||||||
foreground: 'black', // whats that?
|
foreground: 'black',
|
||||||
|
muted: '#00000050',
|
||||||
caret: 'black',
|
caret: 'black',
|
||||||
selection: 'rgba(128, 203, 196, 0.5)',
|
selection: 'rgba(128, 203, 196, 0.5)',
|
||||||
selectionMatch: '#ffffff26',
|
selectionMatch: '#ffffff26',
|
||||||
|
|||||||
+2
@@ -6,6 +6,7 @@ export const settings = {
|
|||||||
background: '#fff',
|
background: '#fff',
|
||||||
lineBackground: '#ffffff99',
|
lineBackground: '#ffffff99',
|
||||||
foreground: '#3D3D3D',
|
foreground: '#3D3D3D',
|
||||||
|
muted: '#3D3D3D50',
|
||||||
selection: '#BBDFFF',
|
selection: '#BBDFFF',
|
||||||
selectionMatch: '#BBDFFF',
|
selectionMatch: '#BBDFFF',
|
||||||
gutterBackground: '#fff',
|
gutterBackground: '#fff',
|
||||||
@@ -18,6 +19,7 @@ export default createTheme({
|
|||||||
settings: {
|
settings: {
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
foreground: '#3D3D3D',
|
foreground: '#3D3D3D',
|
||||||
|
muted: '#3D3D3D50',
|
||||||
selection: '#BBDFFF',
|
selection: '#BBDFFF',
|
||||||
selectionMatch: '#BBDFFF',
|
selectionMatch: '#BBDFFF',
|
||||||
gutterBackground: '#fff',
|
gutterBackground: '#fff',
|
||||||
|
|||||||
+194
-46
@@ -1,55 +1,111 @@
|
|||||||
import { StateEffect, StateField } from '@codemirror/state';
|
import { StateEffect, StateField } from '@codemirror/state';
|
||||||
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
import { Decoration, EditorView, WidgetType, ViewPlugin } from '@codemirror/view';
|
||||||
import { getWidgetID, registerWidgetType } from '@strudel/transpiler';
|
import { getWidgetID, registerWidgetType } from '@strudel/transpiler';
|
||||||
import { Pattern } from '@strudel/core';
|
import { Pattern } from '@strudel/core';
|
||||||
|
|
||||||
export const addWidget = StateEffect.define({
|
export const setWidgets = StateEffect.define();
|
||||||
map: ({ from, to }, change) => {
|
|
||||||
return { from: change.mapPos(from), to: change.mapPos(to) };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateWidgets = (view, widgets) => {
|
export const setWidgetsInRange = StateEffect.define();
|
||||||
view.dispatch({ effects: addWidget.of(widgets) });
|
|
||||||
|
export const updateWidgets = (view, widgets, range = null) => {
|
||||||
|
if (range) {
|
||||||
|
// range argument passed for block-based evaluation
|
||||||
|
view.dispatch({ effects: setWidgetsInRange.of({ widgets, range }) });
|
||||||
|
} else {
|
||||||
|
view.dispatch({ effects: setWidgets.of(widgets) });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function getWidgets(widgetConfigs) {
|
function getWidgets(widgetConfigs, view) {
|
||||||
return (
|
const filtered = widgetConfigs
|
||||||
widgetConfigs
|
// Filter to widget configs only (exclude sliders)
|
||||||
// codemirror throws an error if we don't sort
|
.filter((w) => w && w.type && w.type !== 'slider')
|
||||||
.sort((a, b) => a.to - b.to)
|
// Deduplicate widgets by ID, matching slider behavior for stable widget identity
|
||||||
.map((widgetConfig) => {
|
.filter((widget, index, self) => index === self.findIndex((w) => w.type === widget.type && w.id === widget.id));
|
||||||
|
|
||||||
|
// Filter out widgets whose range is encompassed by another widget
|
||||||
|
// const nonEncompassed = filterEncompassedWidgets(filtered);
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
.sort((a, b) => (a.to || 0) - (b.to || 0))
|
||||||
|
.map((widgetConfig) => {
|
||||||
|
try {
|
||||||
return Decoration.widget({
|
return Decoration.widget({
|
||||||
widget: new BlockWidget(widgetConfig),
|
widget: new BlockWidget(widgetConfig, view),
|
||||||
side: 0,
|
side: 0,
|
||||||
block: true,
|
}).range(widgetConfig.to || widgetConfig.from || 0);
|
||||||
}).range(widgetConfig.to);
|
} catch (error) {
|
||||||
})
|
console.error('error creating widget', error);
|
||||||
);
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean); // Remove any null results from failed creations
|
||||||
}
|
}
|
||||||
|
|
||||||
const widgetField = StateField.define(
|
export const widgetPlugin = ViewPlugin.fromClass(
|
||||||
/* <DecorationSet> */ {
|
class {
|
||||||
create() {
|
decorations; //: DecorationSet
|
||||||
return Decoration.none;
|
|
||||||
},
|
constructor(view /* : EditorView */) {
|
||||||
update(widgets, tr) {
|
this.decorations = Decoration.set([]);
|
||||||
widgets = widgets.map(tr.changes);
|
}
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(addWidget)) {
|
update(update /* : ViewUpdate */) {
|
||||||
try {
|
update.transactions.forEach((tr) => {
|
||||||
widgets = widgets.update({
|
if (tr.docChanged) {
|
||||||
filter: () => false,
|
this.decorations = this.decorations.map(tr.changes);
|
||||||
add: getWidgets(e.value),
|
const iterator = this.decorations.iter();
|
||||||
});
|
// Apply changes to iterator.from and iterator.to if docChanged
|
||||||
} catch (error) {
|
while (iterator.value) {
|
||||||
console.log('err', error);
|
// when the widgets are moved, we need to tell the dom node the current position
|
||||||
|
// this is important because the widget functions have to work with the dom node
|
||||||
|
if (iterator.value?.widget instanceof BlockWidget) {
|
||||||
|
iterator.value.widget.from = iterator.from;
|
||||||
|
iterator.value.widget.to = iterator.to;
|
||||||
|
}
|
||||||
|
iterator.next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
for (let e of tr.effects) {
|
||||||
return widgets;
|
if (e.is(setWidgetsInRange)) {
|
||||||
},
|
// Block-aware widget update logic
|
||||||
provide: (f) => EditorView.decorations.from(f),
|
const { widgets, range } = e.value;
|
||||||
|
const [rangeStart, rangeEnd] = range;
|
||||||
|
|
||||||
|
// Get existing widget widgets that should be preserved
|
||||||
|
const existingWidgets = [];
|
||||||
|
this.decorations.between(0, update.view.state.doc.length, (from, to, decoration) => {
|
||||||
|
if (decoration.widget instanceof BlockWidget) {
|
||||||
|
// Preserve widgets outside the evaluation range
|
||||||
|
// Use strict > for rangeEnd because when code is deleted, widget positions
|
||||||
|
// map to the deletion boundary (rangeEnd), and those should be removed, not preserved
|
||||||
|
if (from < rangeStart || from > rangeEnd) {
|
||||||
|
existingWidgets.push({
|
||||||
|
from: decoration.widget.from,
|
||||||
|
to: decoration.widget.to,
|
||||||
|
type: decoration.widget.type,
|
||||||
|
index: decoration.widget.index,
|
||||||
|
id: decoration.widget.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge preserved widgets with new widgets, deduplicating by ID
|
||||||
|
const mergedWidgets = [...existingWidgets, ...widgets].filter(
|
||||||
|
(widget, index, self) => index === self.findIndex((w) => w.type === widget.type && w.id === widget.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.decorations = Decoration.set(getWidgets(mergedWidgets, update.view));
|
||||||
|
} else if (e.is(setWidgets)) {
|
||||||
|
this.decorations = Decoration.set(getWidgets(e.value, update.view));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decorations: (v) => v.decorations,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -60,24 +116,116 @@ export function setWidget(id, el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class BlockWidget extends WidgetType {
|
export class BlockWidget extends WidgetType {
|
||||||
constructor(widgetConfig) {
|
constructor(widgetConfig, view) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
// Graceful handling of invalid configs like sliders
|
||||||
|
if (!widgetConfig || typeof widgetConfig !== 'object') {
|
||||||
|
widgetConfig = { type: 'unknown', from: 0, to: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.from = widgetConfig.from || 0;
|
||||||
|
this.originalFrom = widgetConfig.from || 0;
|
||||||
|
this.to = widgetConfig.to || this.from;
|
||||||
|
this.originalTo = widgetConfig.to || this.from;
|
||||||
|
this.type = widgetConfig.type || 'unknown';
|
||||||
|
this.index = widgetConfig.index || 0;
|
||||||
|
this.view = view;
|
||||||
|
|
||||||
|
// Use range-based ID for stability, similar to sliders
|
||||||
|
this.id = widgetConfig.id || getWidgetID?.(widgetConfig);
|
||||||
this.widgetConfig = widgetConfig;
|
this.widgetConfig = widgetConfig;
|
||||||
}
|
}
|
||||||
eq() {
|
|
||||||
return true;
|
eq(other) {
|
||||||
|
if (!(other instanceof BlockWidget)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
this.id === other.id &&
|
||||||
|
this.from === other.from &&
|
||||||
|
this.to === other.to &&
|
||||||
|
this.type === other.type &&
|
||||||
|
this.index === other.index
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toDOM() {
|
toDOM() {
|
||||||
const id = getWidgetID(this.widgetConfig);
|
let wrap = document.createElement('span');
|
||||||
const el = widgetElements[id];
|
wrap.setAttribute('aria-hidden', 'true');
|
||||||
return el;
|
wrap.className = 'cm-widget-container';
|
||||||
|
|
||||||
|
let el = widgetElements[this.id];
|
||||||
|
if (el) {
|
||||||
|
// Ensure the element has the correct ID
|
||||||
|
el.id = this.id;
|
||||||
|
wrap.appendChild(el);
|
||||||
|
} else {
|
||||||
|
// Create a placeholder element if the widget element doesn't exist
|
||||||
|
// This prevents CodeMirror errors when widget is missing
|
||||||
|
const placeholder = document.createElement('span');
|
||||||
|
placeholder.setAttribute('aria-hidden', 'true');
|
||||||
|
placeholder.className = 'cm-widget-placeholder';
|
||||||
|
placeholder.style.cssText = 'display: none;'; // Hide placeholder
|
||||||
|
placeholder.id = this.id;
|
||||||
|
wrap.appendChild(placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
ignoreEvent(e) {
|
ignoreEvent(e) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const widgetPlugin = [widgetField];
|
export function getActiveWidgets(view) {
|
||||||
|
if (!view || !view.state) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgetPluginInstance = view.plugin(widgetPlugin);
|
||||||
|
if (!widgetPluginInstance || !widgetPluginInstance.decorations) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgets = [];
|
||||||
|
|
||||||
|
widgetPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => {
|
||||||
|
if (decoration.widget instanceof BlockWidget) {
|
||||||
|
widgets.push({
|
||||||
|
type: decoration.widget.type,
|
||||||
|
from: decoration.widget.from,
|
||||||
|
to: decoration.widget.to,
|
||||||
|
index: decoration.widget.index,
|
||||||
|
id: decoration.widget.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return widgets;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllWidgetIds(view) {
|
||||||
|
if (!view || !view.state) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgetPluginInstance = view.plugin(widgetPlugin);
|
||||||
|
if (!widgetPluginInstance || !widgetPluginInstance.decorations) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgetIds = [];
|
||||||
|
|
||||||
|
widgetPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => {
|
||||||
|
if (decoration.widget instanceof BlockWidget) {
|
||||||
|
widgetIds.push(decoration.widget.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return widgetIds;
|
||||||
|
}
|
||||||
|
|
||||||
// widget implementer API to create a new widget type
|
// widget implementer API to create a new widget type
|
||||||
export function registerWidget(type, fn) {
|
export function registerWidget(type, fn) {
|
||||||
|
|||||||
+265
-38
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ import Fraction, { gcd } from './fraction.mjs';
|
|||||||
* - "-" hold previous value
|
* - "-" hold previous value
|
||||||
* - "." silence
|
* - "." silence
|
||||||
*
|
*
|
||||||
|
* @tags visualization
|
||||||
* @param {Pattern} pattern the pattern to use
|
* @param {Pattern} pattern the pattern to use
|
||||||
* @param {number} chars max number of characters (approximately)
|
* @param {number} chars max number of characters (approximately)
|
||||||
* @returns string
|
* @returns string
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export const bjorklund = function (ons, steps) {
|
|||||||
*
|
*
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @name euclid
|
* @name euclid
|
||||||
|
* @tags temporal
|
||||||
* @param {number} pulses the number of onsets/beats
|
* @param {number} pulses the number of onsets/beats
|
||||||
* @param {number} steps the number of steps to fill
|
* @param {number} steps the number of steps to fill
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
@@ -73,6 +74,7 @@ export const bjorklund = function (ons, steps) {
|
|||||||
* Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence.
|
* Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence.
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @name euclidRot
|
* @name euclidRot
|
||||||
|
* @tags temporal
|
||||||
* @param {number} pulses the number of onsets/beats
|
* @param {number} pulses the number of onsets/beats
|
||||||
* @param {number} steps the number of steps to fill
|
* @param {number} steps the number of steps to fill
|
||||||
* @param {number} rotation offset in steps
|
* @param {number} rotation offset in steps
|
||||||
@@ -156,6 +158,7 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
|
|||||||
* so there will be no gaps.
|
* so there will be no gaps.
|
||||||
* @name euclidLegato
|
* @name euclidLegato
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
|
* @tags temporal
|
||||||
* @param {number} pulses the number of onsets/beats
|
* @param {number} pulses the number of onsets/beats
|
||||||
* @param {number} steps the number of steps to fill
|
* @param {number} steps the number of steps to fill
|
||||||
* @param rotation offset in steps
|
* @param rotation offset in steps
|
||||||
@@ -187,6 +190,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
|
|||||||
* the resulting sequence
|
* the resulting sequence
|
||||||
* @name euclidLegatoRot
|
* @name euclidLegatoRot
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
|
* @tags temporal
|
||||||
* @param {number} pulses the number of onsets/beats
|
* @param {number} pulses the number of onsets/beats
|
||||||
* @param {number} steps the number of steps to fill
|
* @param {number} steps the number of steps to fill
|
||||||
* @param {number} rotation offset in steps
|
* @param {number} rotation offset in steps
|
||||||
@@ -208,6 +212,7 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s
|
|||||||
* @name euclidish
|
* @name euclidish
|
||||||
* @synonyms eish
|
* @synonyms eish
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
|
* @tags temporal
|
||||||
* @param {number} pulses the number of onsets
|
* @param {number} pulses the number of onsets
|
||||||
* @param {number} steps the number of steps to fill
|
* @param {number} steps the number of steps to fill
|
||||||
* @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse)
|
* @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse)
|
||||||
|
|||||||
@@ -5,6 +5,34 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const strudelScope = {};
|
export const strudelScope = {};
|
||||||
|
// Make strudelScope available globally so transpiled code can access it
|
||||||
|
globalThis.strudelScope = strudelScope;
|
||||||
|
|
||||||
|
// Track user-defined keys (from block-based eval) so we can clear them without removing strudel functions
|
||||||
|
export const userDefinedKeys = new Set();
|
||||||
|
globalThis.userDefinedKeys = userDefinedKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all user-defined variables and functions from the scope.
|
||||||
|
* This removes variables created during block-based evaluation.
|
||||||
|
* @name clearScope
|
||||||
|
* @example
|
||||||
|
* // After defining variables in blocks:
|
||||||
|
* // let myVar = 5
|
||||||
|
* // function myFunc() { return 10; }
|
||||||
|
* clearScope() // removes myVar and myFunc from scope
|
||||||
|
*/
|
||||||
|
export const clearScope = () => {
|
||||||
|
for (const key of userDefinedKeys) {
|
||||||
|
delete strudelScope[key];
|
||||||
|
delete globalThis[key];
|
||||||
|
}
|
||||||
|
userDefinedKeys.clear();
|
||||||
|
// Return silence if available (for use in pattern expressions), otherwise undefined
|
||||||
|
return globalThis.silence;
|
||||||
|
};
|
||||||
|
// Make clearScope available globally
|
||||||
|
globalThis.clearScope = clearScope;
|
||||||
|
|
||||||
export const evalScope = async (...args) => {
|
export const evalScope = async (...args) => {
|
||||||
const results = await Promise.allSettled(args);
|
const results = await Promise.allSettled(args);
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://strudel.cc",
|
"homepage": "https://strudel.cc",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kabelsalat/web": "^0.4.1",
|
|
||||||
"fraction.js": "^5.2.1"
|
"fraction.js": "^5.2.1"
|
||||||
},
|
},
|
||||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||||
|
|||||||
+295
-109
File diff suppressed because it is too large
Load Diff
+40
-22
@@ -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).
|
/** * 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.
|
* Similar to `inhabit`, but maintains the structure of the original patterns.
|
||||||
|
* @tags combiners
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
@@ -57,6 +58,7 @@ const __pick = register('pick', function (lookup, pat) {
|
|||||||
* it wraps around, rather than sticking at the maximum value.
|
* 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
|
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||||
* second one.
|
* second one.
|
||||||
|
* @tags combiners
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
@@ -67,32 +69,38 @@ 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.
|
/** * 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} pat
|
||||||
* @param {Pattern} lookup a pattern of indices
|
* @param {Pattern} lookup a pattern of indices or names
|
||||||
* @param {function[]} funcs the array of functions from which to pull
|
* @param {function[] | object} lookup the array or lookup object of functions from which to pull
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
|
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
|
||||||
* @example
|
* @example
|
||||||
* note("<c2 d2>(3,8)").s("square")
|
* note("<c2 d2>(3,8)").s("square")
|
||||||
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
|
* .pickF("<0 2> 1", [jux(rev), fast(2), x=>x.lpf(800)])
|
||||||
|
* @example
|
||||||
|
* note("<c2 d2>(3,8)").s("square")
|
||||||
|
* .pickF("<jr l> f", { jr:jux(rev), f:fast(2), l:x=>x.lpf(800) })
|
||||||
*/
|
*/
|
||||||
export const pickF = register('pickF', function (lookup, funcs, pat) {
|
export const pickF = register('pickF', function (pickPattern, lookup, pat) {
|
||||||
return pat.apply(pick(lookup, funcs));
|
return pat.apply(pick(lookup, pickPattern));
|
||||||
});
|
});
|
||||||
|
|
||||||
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
|
/** * 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.
|
* it wraps around, rather than sticking at the maximum value.
|
||||||
|
* @tags combiners
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {Pattern} lookup a pattern of indices
|
* @param {Pattern} lookup a pattern of indices or names
|
||||||
* @param {function[]} funcs the array of functions from which to pull
|
* @param {function[] | object} lookup the array or lookup object of functions from which to pull
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
*/
|
*/
|
||||||
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
|
export const pickmodF = register('pickmodF', function (pickPattern, lookup, pat) {
|
||||||
return pat.apply(pickmod(lookup, funcs));
|
return pat.apply(pickmod(lookup, pickPattern));
|
||||||
});
|
});
|
||||||
|
|
||||||
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
|
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
|
||||||
|
* @tags combiners
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
@@ -103,6 +111,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,
|
/** * 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.
|
* it wraps around, rather than sticking at the maximum value.
|
||||||
|
* @tags combiners
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
@@ -112,6 +121,7 @@ export const pickmodOut = register('pickmodOut', function (lookup, pat) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
|
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
|
||||||
|
* @tags combiners
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
@@ -122,6 +132,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,
|
/** * 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.
|
* it wraps around, rather than sticking at the maximum value.
|
||||||
|
* @tags combiners
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
@@ -138,6 +149,7 @@ export const pickmodRestart = register('pickmodRestart', function (lookup, pat)
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
|
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
|
||||||
|
* @tags combiners
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
@@ -148,6 +160,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,
|
/** * 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.
|
* it wraps around, rather than sticking at the maximum value.
|
||||||
|
* @tags combiners
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
@@ -157,19 +170,22 @@ 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).
|
/** 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.
|
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
|
||||||
* @name inhabit
|
* @name inhabit
|
||||||
* @synonyms pickSqueeze
|
* @tags combiners
|
||||||
* @param {Pattern} pat
|
* @synonyms pickSqueeze
|
||||||
* @param {*} xs
|
* @param {Pattern} pat
|
||||||
* @returns {Pattern}
|
* @param {*} xs
|
||||||
* @example
|
* @returns {Pattern}
|
||||||
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
|
* @example
|
||||||
b: s("cp sd")
|
* let a = s("bd(3,8)")
|
||||||
})
|
* let b = s("cp sd")
|
||||||
* @example
|
* "<a b [a,b]>".inhabit({ a, b })
|
||||||
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
|
* @example
|
||||||
*/
|
* s("a@2 [a b] a"
|
||||||
|
* .inhabit({a: "bd(3,8)", b: "sd sd"}))
|
||||||
|
* .slow(4)
|
||||||
|
*/
|
||||||
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
|
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
|
||||||
return _pick(lookup, pat, false).squeezeJoin();
|
return _pick(lookup, pat, false).squeezeJoin();
|
||||||
});
|
});
|
||||||
@@ -180,6 +196,7 @@ export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], fun
|
|||||||
* second one.
|
* second one.
|
||||||
* @name inhabitmod
|
* @name inhabitmod
|
||||||
* @synonyms pickmodSqueeze
|
* @synonyms pickmodSqueeze
|
||||||
|
* @tags combiners
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
@@ -192,6 +209,7 @@ export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSq
|
|||||||
/**
|
/**
|
||||||
* Pick from the list of values (or patterns of values) via the index using the given
|
* 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
|
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
|
||||||
|
* @tags combiners
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
|
|||||||
+310
-49
@@ -12,7 +12,6 @@ import {
|
|||||||
import { evalScope } from './evaluate.mjs';
|
import { evalScope } from './evaluate.mjs';
|
||||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||||
import { reset_state } from './impure.mjs';
|
import { reset_state } from './impure.mjs';
|
||||||
import { SalatRepl } from '@kabelsalat/web';
|
|
||||||
|
|
||||||
export function repl({
|
export function repl({
|
||||||
defaultOutput,
|
defaultOutput,
|
||||||
@@ -31,7 +30,6 @@ export function repl({
|
|||||||
id,
|
id,
|
||||||
mondo = false,
|
mondo = false,
|
||||||
}) {
|
}) {
|
||||||
const kabel = new SalatRepl({ localScope: true });
|
|
||||||
const state = {
|
const state = {
|
||||||
schedulerError: undefined,
|
schedulerError: undefined,
|
||||||
evalError: undefined,
|
evalError: undefined,
|
||||||
@@ -40,6 +38,7 @@ export function repl({
|
|||||||
pattern: undefined,
|
pattern: undefined,
|
||||||
miniLocations: [],
|
miniLocations: [],
|
||||||
widgets: [],
|
widgets: [],
|
||||||
|
sliders: [],
|
||||||
pending: false,
|
pending: false,
|
||||||
started: false,
|
started: false,
|
||||||
};
|
};
|
||||||
@@ -81,19 +80,135 @@ export function repl({
|
|||||||
let allTransform;
|
let allTransform;
|
||||||
let eachTransform;
|
let eachTransform;
|
||||||
|
|
||||||
|
// Block-based evaluation state
|
||||||
|
let codeBlocks = {};
|
||||||
|
let lastActiveVisualizerLabel = null;
|
||||||
|
// Track which patterns belong to which blocks: { blockRange: [patternKeys] }
|
||||||
|
let blockPatterns = new Map();
|
||||||
|
|
||||||
|
// Helper function to collect properties from all code blocks (handles both labeled and anonymous blocks)
|
||||||
|
function collectFromBlocks(property) {
|
||||||
|
return Object.entries(codeBlocks).flatMap(([key, block]) => {
|
||||||
|
if (key === '$') {
|
||||||
|
// Anonymous blocks are stored as an array of block objects
|
||||||
|
return Array.isArray(block) ? block.flatMap((b) => b[property] || []) : [];
|
||||||
|
}
|
||||||
|
// Labeled blocks are stored as single block objects
|
||||||
|
return block[property] || [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to process a single labeled block
|
||||||
|
function processLabeledBlock(labels, i, code, options, meta) {
|
||||||
|
const label = labels[i];
|
||||||
|
const nextLabel = labels[i + 1] || { index: code.length, end: code.length };
|
||||||
|
|
||||||
|
const labelCode = code.slice(label.index, nextLabel.index);
|
||||||
|
const labelRange = [label.index + options.range[0], label.end + options.range[0]];
|
||||||
|
|
||||||
|
// Calculate the full block range (from label start to next label start)
|
||||||
|
const blockStart = label.index + options.range[0];
|
||||||
|
const blockEnd = nextLabel.index + options.range[0];
|
||||||
|
|
||||||
|
const blockWidgets = (meta?.widgets || []).filter((widget) => {
|
||||||
|
const widgetPos = widget.from ?? widget.index ?? 0;
|
||||||
|
return widgetPos >= blockStart && widgetPos < blockEnd;
|
||||||
|
});
|
||||||
|
|
||||||
|
const blockSliders = (meta?.sliders || []).filter((slider) => {
|
||||||
|
const sliderPos = slider.from ?? slider.index ?? 0;
|
||||||
|
return sliderPos >= blockStart && sliderPos < blockEnd;
|
||||||
|
});
|
||||||
|
|
||||||
|
const blockMiniLocations = (meta?.miniLocations || []).filter((loc) => {
|
||||||
|
// const locStart = loc.start ?? loc.from ?? 0;
|
||||||
|
// mini locations can be either [start, end] arrays or objects with start/from
|
||||||
|
const locStart = Array.isArray(loc) ? loc[0] : (loc.start ?? loc.from ?? 0);
|
||||||
|
return locStart >= blockStart && locStart < blockEnd;
|
||||||
|
});
|
||||||
|
|
||||||
|
handleSingleLabelBlock(
|
||||||
|
label,
|
||||||
|
labelCode,
|
||||||
|
{ ...options, range: labelRange },
|
||||||
|
{ widgets: blockWidgets, sliders: blockSliders, miniLocations: blockMiniLocations },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper
|
||||||
|
function cleanupConflictingRanges(codeBlocks, currentKey, newRange) {
|
||||||
|
for (const [existingKey, existingBlock] of Object.entries(codeBlocks)) {
|
||||||
|
if (existingKey === currentKey) continue;
|
||||||
|
if (!existingBlock.range) continue;
|
||||||
|
|
||||||
|
const [existingStart, existingEnd] = existingBlock.range;
|
||||||
|
const [newStart, newEnd] = newRange;
|
||||||
|
|
||||||
|
// If ranges overlap (not just touch), remove the stale block
|
||||||
|
if (!(newEnd <= existingStart || newStart >= existingEnd)) {
|
||||||
|
delete codeBlocks[existingKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper
|
||||||
|
function handleSingleLabelBlock(label, code, options, meta) {
|
||||||
|
// Detect if this block contains a non-inline widget
|
||||||
|
// The activeVisualizer is now provided by the transpiler for all labels
|
||||||
|
const activeVisualizer = label.activeVisualizer || null;
|
||||||
|
|
||||||
|
if (activeVisualizer !== null) {
|
||||||
|
lastActiveVisualizerLabel = label.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the entire code block under the label name
|
||||||
|
codeBlocks[label.name] = {
|
||||||
|
code: code,
|
||||||
|
range: options.range,
|
||||||
|
labels: [label.name],
|
||||||
|
miniLocations: meta?.miniLocations || [],
|
||||||
|
widgets: meta?.widgets || [],
|
||||||
|
sliders: meta?.sliders || [],
|
||||||
|
activeVisualizer: activeVisualizer, // Store the widget type if present, null otherwise
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clean up any blocks with conflicting ranges (including declaration blocks)
|
||||||
|
cleanupConflictingRanges(codeBlocks, label.name, options.range);
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper
|
||||||
|
// These blocks return silence but may contain mini notation strings that need highlighting
|
||||||
|
function handleDeclarationBlock(code, options, meta) {
|
||||||
|
const range = options.range || [];
|
||||||
|
if (range.length < 2) return;
|
||||||
|
|
||||||
|
const blockKey = `_decl:${range[0]}:${range[1]}`;
|
||||||
|
|
||||||
|
codeBlocks[blockKey] = {
|
||||||
|
code: code,
|
||||||
|
range: range,
|
||||||
|
labels: [],
|
||||||
|
miniLocations: meta?.miniLocations || [],
|
||||||
|
widgets: meta?.widgets || [],
|
||||||
|
sliders: meta?.sliders || [],
|
||||||
|
activeVisualizer: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clean up any overlapping declaration blocks
|
||||||
|
cleanupConflictingRanges(codeBlocks, blockKey, range);
|
||||||
|
}
|
||||||
|
|
||||||
const hush = function () {
|
const hush = function () {
|
||||||
pPatterns = {};
|
pPatterns = {};
|
||||||
anonymousIndex = 0;
|
anonymousIndex = 0;
|
||||||
allTransform = undefined;
|
allTransform = undefined;
|
||||||
eachTransform = undefined;
|
eachTransform = undefined;
|
||||||
|
codeBlocks = {};
|
||||||
|
blockPatterns.clear();
|
||||||
|
lastActiveVisualizerLabel = null; // Reset 'all' visualizer tracking
|
||||||
return silence;
|
return silence;
|
||||||
};
|
};
|
||||||
|
|
||||||
const compileKabel = (code) => {
|
|
||||||
const node = kabel.evaluate(code);
|
|
||||||
return node.compile({ log: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
// helper to get a patternified pure value out
|
// helper to get a patternified pure value out
|
||||||
function unpure(pat) {
|
function unpure(pat) {
|
||||||
if (pat._Pattern) {
|
if (pat._Pattern) {
|
||||||
@@ -110,7 +225,60 @@ export function repl({
|
|||||||
};
|
};
|
||||||
setTime(() => scheduler.now()); // TODO: refactor?
|
setTime(() => scheduler.now()); // TODO: refactor?
|
||||||
|
|
||||||
const stop = () => scheduler.stop();
|
// Helper function to apply pattern transformations (solo, each, all)
|
||||||
|
// this should be abstracted more
|
||||||
|
function applyPatternTransforms(pattern) {
|
||||||
|
const allPatterns = Object.values(pPatterns);
|
||||||
|
|
||||||
|
if (allPatterns.length) {
|
||||||
|
let patterns = [];
|
||||||
|
let soloActive = false;
|
||||||
|
for (const [key, value] of Object.entries(pPatterns)) {
|
||||||
|
// handle soloed patterns ex: S$: s("bd!4")
|
||||||
|
const isSolod = key.length > 1 && key.startsWith('S');
|
||||||
|
if (isSolod && soloActive === false) {
|
||||||
|
// first time we see a soloed pattern, clear existing patterns
|
||||||
|
patterns = [];
|
||||||
|
soloActive = true;
|
||||||
|
}
|
||||||
|
if (!soloActive || (soloActive && isSolod)) {
|
||||||
|
const valWithState = value.withState((state) => state.setControls({ id: key }));
|
||||||
|
patterns.push(valWithState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (eachTransform) {
|
||||||
|
// Explicit lambda so only element (not index and array) are passed
|
||||||
|
patterns = patterns.map((x) => eachTransform(x));
|
||||||
|
}
|
||||||
|
pattern = stack(...patterns);
|
||||||
|
} else if (eachTransform) {
|
||||||
|
pattern = eachTransform(pattern);
|
||||||
|
}
|
||||||
|
if (allTransforms.length) {
|
||||||
|
for (const transform of allTransforms) {
|
||||||
|
pattern = transform(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPattern(pattern)) {
|
||||||
|
pattern = silence;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
codeBlocks = {};
|
||||||
|
blockPatterns.clear();
|
||||||
|
pPatterns = {};
|
||||||
|
lastActiveVisualizerLabel = null; // Reset 'all' visualizer tracking
|
||||||
|
updateState({
|
||||||
|
miniLocations: [],
|
||||||
|
widgets: [],
|
||||||
|
sliders: [],
|
||||||
|
});
|
||||||
|
scheduler.stop();
|
||||||
|
};
|
||||||
const start = () => scheduler.start();
|
const start = () => scheduler.start();
|
||||||
const pause = () => scheduler.pause();
|
const pause = () => scheduler.pause();
|
||||||
const toggle = () => scheduler.toggle();
|
const toggle = () => scheduler.toggle();
|
||||||
@@ -123,6 +291,7 @@ export function repl({
|
|||||||
* Changes the global tempo to the given cycles per minute
|
* Changes the global tempo to the given cycles per minute
|
||||||
*
|
*
|
||||||
* @name setcpm
|
* @name setcpm
|
||||||
|
* @tags temporal
|
||||||
* @alias setCpm
|
* @alias setCpm
|
||||||
* @param {number} cpm cycles per minute
|
* @param {number} cpm cycles per minute
|
||||||
* @example
|
* @example
|
||||||
@@ -136,7 +305,9 @@ export function repl({
|
|||||||
|
|
||||||
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
|
// 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.
|
* a version that applies the function to each pattern separately.
|
||||||
* ```
|
* ```
|
||||||
* $: sound("bd - cp sd")
|
* $: sound("bd - cp sd")
|
||||||
@@ -148,18 +319,21 @@ export function repl({
|
|||||||
* $: sound("hh*8")
|
* $: sound("hh*8")
|
||||||
* all(x => x.pianoroll())
|
* all(x => x.pianoroll())
|
||||||
* ```
|
* ```
|
||||||
|
*
|
||||||
|
* @tags combiners
|
||||||
*/
|
*/
|
||||||
let allTransforms = [];
|
|
||||||
const all = function (transform) {
|
const all = function (transform) {
|
||||||
allTransforms.push(transform);
|
allTransforms.push(transform);
|
||||||
return silence;
|
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.
|
/** 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("bd - cp sd")
|
||||||
* $: sound("hh*8")
|
* $: sound("hh*8")
|
||||||
* each(fast("<2 3>"))
|
* each(fast("<2 3>"))
|
||||||
* ```
|
* ```
|
||||||
|
* @tags combiners
|
||||||
*/
|
*/
|
||||||
const each = function (transform) {
|
const each = function (transform) {
|
||||||
eachTransform = transform;
|
eachTransform = transform;
|
||||||
@@ -215,11 +389,10 @@ export function repl({
|
|||||||
setcps: setCps,
|
setcps: setCps,
|
||||||
setCpm,
|
setCpm,
|
||||||
setcpm: setCpm,
|
setcpm: setCpm,
|
||||||
compileKabel,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const evaluate = async (code, autostart = true, shouldHush = true) => {
|
const evaluate = async (code, autostart = true) => {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error('no code to evaluate');
|
throw new Error('no code to evaluate');
|
||||||
}
|
}
|
||||||
@@ -227,59 +400,34 @@ export function repl({
|
|||||||
updateState({ code, pending: true });
|
updateState({ code, pending: true });
|
||||||
await injectPatternMethods();
|
await injectPatternMethods();
|
||||||
setTime(() => scheduler.now()); // TODO: refactor?
|
setTime(() => scheduler.now()); // TODO: refactor?
|
||||||
await beforeEval?.({ code });
|
await beforeEval?.({ code, blockBased: false });
|
||||||
allTransforms = []; // reset all transforms
|
allTransforms = []; // reset all transforms
|
||||||
shouldHush && hush();
|
|
||||||
|
codeBlocks = {};
|
||||||
|
hush();
|
||||||
|
|
||||||
if (mondo) {
|
if (mondo) {
|
||||||
code = `mondolang\`${code}\``;
|
code = `mondolang\`${code}\``;
|
||||||
}
|
}
|
||||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
|
||||||
if (Object.keys(pPatterns).length) {
|
|
||||||
let patterns = [];
|
|
||||||
let soloActive = false;
|
|
||||||
for (const [key, value] of Object.entries(pPatterns)) {
|
|
||||||
// handle soloed patterns ex: S$: s("bd!4")
|
|
||||||
const isSolod = key.length > 1 && key.startsWith('S');
|
|
||||||
if (isSolod && soloActive === false) {
|
|
||||||
// first time we see a soloed pattern, clear existing patterns
|
|
||||||
patterns = [];
|
|
||||||
soloActive = true;
|
|
||||||
}
|
|
||||||
if (!soloActive || (soloActive && isSolod)) {
|
|
||||||
const valWithState = value.withState((state) => state.setControls({ id: key }));
|
|
||||||
patterns.push(valWithState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (eachTransform) {
|
|
||||||
// Explicit lambda so only element (not index and array) are passed
|
|
||||||
patterns = patterns.map((x) => eachTransform(x));
|
|
||||||
}
|
|
||||||
pattern = stack(...patterns);
|
|
||||||
} else if (eachTransform) {
|
|
||||||
pattern = eachTransform(pattern);
|
|
||||||
}
|
|
||||||
if (allTransforms.length) {
|
|
||||||
for (const transform of allTransforms) {
|
|
||||||
pattern = transform(pattern);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isPattern(pattern)) {
|
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||||
pattern = silence;
|
|
||||||
}
|
pattern = applyPatternTransforms(pattern);
|
||||||
|
|
||||||
logger(`[eval] code updated`);
|
logger(`[eval] code updated`);
|
||||||
pattern = await setPattern(pattern, autostart);
|
pattern = await setPattern(pattern, autostart);
|
||||||
updateState({
|
updateState({
|
||||||
miniLocations: meta?.miniLocations || [],
|
miniLocations: meta?.miniLocations || [],
|
||||||
widgets: meta?.widgets || [],
|
widgets: meta?.widgets || [],
|
||||||
|
sliders: meta?.sliders || [],
|
||||||
activeCode: code,
|
activeCode: code,
|
||||||
pattern,
|
pattern,
|
||||||
evalError: undefined,
|
evalError: undefined,
|
||||||
schedulerError: undefined,
|
schedulerError: undefined,
|
||||||
pending: false,
|
pending: false,
|
||||||
});
|
});
|
||||||
afterEval?.({ code, pattern, meta });
|
|
||||||
|
afterEval?.({ code, pattern, meta, range: undefined, widgetRemoved: false });
|
||||||
return pattern;
|
return pattern;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger(`[eval] error: ${err.message}`, 'error');
|
logger(`[eval] error: ${err.message}`, 'error');
|
||||||
@@ -288,8 +436,121 @@ export function repl({
|
|||||||
onEvalError?.(err);
|
onEvalError?.(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const evaluateBlock = async (code, autostart = true, options = {}) => {
|
||||||
|
if (!code) {
|
||||||
|
throw new Error('no code to evaluate');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
updateState({ code, pending: true });
|
||||||
|
await injectPatternMethods();
|
||||||
|
setTime(() => scheduler.now()); // TODO: refactor?
|
||||||
|
await beforeEval?.({ code, blockBased: true });
|
||||||
|
allTransforms = []; // reset all transforms
|
||||||
|
|
||||||
|
const transpilerOptionsWithBlock = {
|
||||||
|
...transpilerOptions,
|
||||||
|
blockBased: true,
|
||||||
|
range: options.range || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mondo) {
|
||||||
|
code = `mondolang\`${code}\``;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptionsWithBlock);
|
||||||
|
|
||||||
|
// Track activeVisualizer cleanup: check if any block's visualizer was removed
|
||||||
|
let widgetRemoved = false;
|
||||||
|
|
||||||
|
const labels = meta.labels || [];
|
||||||
|
|
||||||
|
// Check for anonymous labels (labels starting with '$')
|
||||||
|
const hasAnonymousLabel = labels.some((label) => label.name.startsWith('$'));
|
||||||
|
|
||||||
|
// Store code blocks in dictionary using labels as keys
|
||||||
|
if (hasAnonymousLabel) {
|
||||||
|
// variable/function declarations that don't return patterns are allowed,
|
||||||
|
// but anonymous pattern blocks pose an issue for block-based evaluation
|
||||||
|
// if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder
|
||||||
|
|
||||||
|
// it's very common for users to write code prefixed with '$'
|
||||||
|
// but to modify and override existing patterns, the patterns must be labeled,
|
||||||
|
// otherwise we'll have no idea of which pattern is being overridden
|
||||||
|
|
||||||
|
// (we probably need to update the docs on this)
|
||||||
|
// we could easily enable it, but it would confuse a lot of people
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)',
|
||||||
|
);
|
||||||
|
} else if (labels.length > 0) {
|
||||||
|
for (let i = 0; i < labels.length; i++) {
|
||||||
|
// processing transpiler output instead of code is simply to avoid
|
||||||
|
// extra regex in detecting whether or not an inline widget has been commented out
|
||||||
|
processLabeledBlock(labels, i, meta.output, options, meta);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Declaration block (variable/function that returns silence)
|
||||||
|
// Store it so its miniLocations are preserved for highlighting patterns stored in variables
|
||||||
|
handleDeclarationBlock(code, options, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.miniLocations = collectFromBlocks('miniLocations');
|
||||||
|
meta.widgets = collectFromBlocks('widgets');
|
||||||
|
meta.sliders = collectFromBlocks('sliders');
|
||||||
|
|
||||||
|
// Track activeVisualizer cleanup: check if any block's visualizer was removed
|
||||||
|
const blocksToUpdate = labels.map((label) => label.name);
|
||||||
|
|
||||||
|
// this is the hackiest bit
|
||||||
|
for (const [key, block] of Object.entries(codeBlocks)) {
|
||||||
|
if (blocksToUpdate.includes(key)) {
|
||||||
|
// This block was just updated
|
||||||
|
if (block.activeVisualizer !== null) {
|
||||||
|
// Block now has a visualizer, update tracking
|
||||||
|
lastActiveVisualizerLabel = key;
|
||||||
|
} else if (lastActiveVisualizerLabel === key) {
|
||||||
|
// This block lost its visualizer, trigger cleanup
|
||||||
|
widgetRemoved = true;
|
||||||
|
lastActiveVisualizerLabel = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pPatterns = Object.fromEntries(
|
||||||
|
Object.entries(pPatterns).filter(([key]) => {
|
||||||
|
return Object.keys(codeBlocks).includes(key);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
pattern = applyPatternTransforms(pattern);
|
||||||
|
|
||||||
|
logger(`[eval] code updated`);
|
||||||
|
pattern = await setPattern(pattern, autostart);
|
||||||
|
updateState({
|
||||||
|
miniLocations: meta?.miniLocations || [],
|
||||||
|
widgets: meta?.widgets || [],
|
||||||
|
sliders: meta?.sliders || [],
|
||||||
|
activeCode: code,
|
||||||
|
pattern,
|
||||||
|
evalError: undefined,
|
||||||
|
schedulerError: undefined,
|
||||||
|
pending: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEval?.({ code, pattern, meta, range: options.range, widgetRemoved });
|
||||||
|
return pattern;
|
||||||
|
} catch (err) {
|
||||||
|
logger(`[eval] error: ${err.message}`, 'error');
|
||||||
|
console.error(err);
|
||||||
|
updateState({ evalError: err, pending: false });
|
||||||
|
onEvalError?.(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const setCode = (code) => updateState({ code });
|
const setCode = (code) => updateState({ code });
|
||||||
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTrigger =
|
export const getTrigger =
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const signal = (func) => {
|
|||||||
* A sawtooth signal between 0 and 1.
|
* A sawtooth signal between 0 and 1.
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
* @example
|
* @example
|
||||||
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
|
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
|
||||||
* .clip(saw.slow(2))
|
* .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).
|
* A sawtooth signal between -1 and 1 (like `saw`, but bipolar).
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
*/
|
*/
|
||||||
export const saw2 = saw.toBipolar();
|
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).
|
* A sawtooth signal between 1 and 0 (like `saw`, but flipped).
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
* @example
|
* @example
|
||||||
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
|
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
|
||||||
* .clip(isaw.slow(2))
|
* .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).
|
* A sawtooth signal between 1 and -1 (like `saw2`, but flipped).
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
*/
|
*/
|
||||||
export const isaw2 = isaw.toBipolar();
|
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).
|
* A sine signal between -1 and 1 (like `sine`, but bipolar).
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
*/
|
*/
|
||||||
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sine signal between 0 and 1.
|
* A sine signal between 0 and 1.
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
* @example
|
* @example
|
||||||
* n(sine.segment(16).range(0,15))
|
* n(sine.segment(16).range(0,15))
|
||||||
* .scale("C:minor")
|
* .scale("C:minor")
|
||||||
@@ -83,6 +89,7 @@ export const sine = sine2.fromBipolar();
|
|||||||
* A cosine signal between 0 and 1.
|
* A cosine signal between 0 and 1.
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
* @example
|
* @example
|
||||||
* n(stack(sine,cosine).segment(16).range(0,15))
|
* n(stack(sine,cosine).segment(16).range(0,15))
|
||||||
* .scale("C:minor")
|
* .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).
|
* A cosine signal between -1 and 1 (like `cosine`, but bipolar).
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
*/
|
*/
|
||||||
export const cosine2 = sine2._early(Fraction(1).div(4));
|
export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A square signal between 0 and 1.
|
* A square signal between 0 and 1.
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
* @example
|
* @example
|
||||||
* n(square.segment(4).range(0,7)).scale("C:minor")
|
* 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).
|
* A square signal between -1 and 1 (like `square`, but bipolar).
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
*/
|
*/
|
||||||
export const square2 = square.toBipolar();
|
export const square2 = square.toBipolar();
|
||||||
|
|
||||||
@@ -117,6 +127,7 @@ export const square2 = square.toBipolar();
|
|||||||
* A triangle signal between 0 and 1.
|
* A triangle signal between 0 and 1.
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
* @example
|
* @example
|
||||||
* n(tri.segment(8).range(0,7)).scale("C:minor")
|
* 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).
|
* A triangle signal between -1 and 1 (like `tri`, but bipolar).
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
*/
|
*/
|
||||||
export const tri2 = fastcat(saw2, isaw2);
|
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).
|
* An inverted triangle signal between 1 and 0 (like `tri`, but flipped).
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
* @example
|
* @example
|
||||||
* n(itri.segment(8).range(0,7)).scale("C:minor")
|
* 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).
|
* An inverted triangle signal between -1 and 1 (like `itri`, but bipolar).
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
*/
|
*/
|
||||||
export const itri2 = fastcat(isaw2, saw2);
|
export const itri2 = fastcat(isaw2, saw2);
|
||||||
|
|
||||||
@@ -151,6 +165,7 @@ export const itri2 = fastcat(isaw2, saw2);
|
|||||||
* A signal representing the cycle time.
|
* A signal representing the cycle time.
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
*/
|
*/
|
||||||
export const time = signal(id);
|
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.
|
* The mouse's x position value ranges from 0 to 1.
|
||||||
* @name mousex
|
* @name mousex
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(mousex.segment(4).range(0,7)).scale("C:minor")
|
* 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.
|
* The mouse's y position value ranges from 0 to 1.
|
||||||
* @name mousey
|
* @name mousey
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(mousey.segment(4).range(0,7)).scale("C:minor")
|
* 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')`.
|
* precise RNG, try `useRNG('precise')`.
|
||||||
*
|
*
|
||||||
* @name useRNG
|
* @name useRNG
|
||||||
|
* @tags generators, math
|
||||||
* @param {string} mod - Mode. One of 'legacy', 'precise'
|
* @param {string} mod - Mode. One of 'legacy', 'precise'
|
||||||
* @example
|
* @example
|
||||||
* useRNG('legacy')
|
* useRNG('legacy')
|
||||||
@@ -280,6 +298,7 @@ export const useRNG = (mode = 'legacy') => (RNG_MODE = mode);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A discrete pattern of numbers from 0 to n-1
|
* A discrete pattern of numbers from 0 to n-1
|
||||||
|
* @tags generators
|
||||||
* @example
|
* @example
|
||||||
* n(run(4)).scale("C4:pentatonic")
|
* n(run(4)).scale("C4:pentatonic")
|
||||||
* // n("0 1 2 3").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.
|
* Creates a binary pattern from a number.
|
||||||
*
|
*
|
||||||
* @name binary
|
* @name binary
|
||||||
|
* @tags generators
|
||||||
* @param {number} n - input number to convert to binary
|
* @param {number} n - input number to convert to binary
|
||||||
* @example
|
* @example
|
||||||
* "hh".s().struct(binary(5))
|
* "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.
|
* Creates a binary pattern from a number, padded to n bits long.
|
||||||
*
|
*
|
||||||
* @name binaryN
|
* @name binaryN
|
||||||
|
* @tags generators
|
||||||
* @param {number} n - input number to convert to binary
|
* @param {number} n - input number to convert to binary
|
||||||
* @param {number} nBits - pattern length, defaults to 16
|
* @param {number} nBits - pattern length, defaults to 16
|
||||||
* @example
|
* @example
|
||||||
@@ -321,6 +342,7 @@ export const binaryN = (n, nBits = 16) => {
|
|||||||
* Creates a binary list pattern from a number.
|
* Creates a binary list pattern from a number.
|
||||||
*
|
*
|
||||||
* @name binaryL
|
* @name binaryL
|
||||||
|
* @tags generators
|
||||||
* @param {number} n - input number to convert to binary
|
* @param {number} n - input number to convert to binary
|
||||||
* s("saw").seg(8)
|
* s("saw").seg(8)
|
||||||
* .partials(binaryL(irand(4096).add(1)))
|
* .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.
|
* Creates a binary list pattern from a number, padded to n bits long.
|
||||||
*
|
*
|
||||||
* @name binaryNL
|
* @name binaryNL
|
||||||
|
* @tags generators
|
||||||
* @param {number} n - input number to convert to binary
|
* @param {number} n - input number to convert to binary
|
||||||
* @param {number} nBits - pattern length, defaults to 16
|
* @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
|
* Creates a list of random numbers of the given length
|
||||||
*
|
*
|
||||||
* @name randL
|
* @name randL
|
||||||
|
* @tags generators
|
||||||
* @param {number} n Number of random numbers to sample
|
* @param {number} n Number of random numbers to sample
|
||||||
* @example
|
* @example
|
||||||
* s("saw").seg(16).n(irand(12)).scale("F1:minor")
|
* 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.
|
* 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.
|
* Each part will be played exactly once per cycle.
|
||||||
* @name shuffle
|
* @name shuffle
|
||||||
|
* @tags temporal
|
||||||
* @example
|
* @example
|
||||||
* note("c d e f").sound("piano").shuffle(4)
|
* note("c d e f").sound("piano").shuffle(4)
|
||||||
* @example
|
* @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`,
|
* 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.
|
* but parts might be played more than once, or not at all, per cycle.
|
||||||
* @name scramble
|
* @name scramble
|
||||||
|
* @tags temporal
|
||||||
* @example
|
* @example
|
||||||
* note("c d e f").sound("piano").scramble(4)
|
* note("c d e f").sound("piano").scramble(4)
|
||||||
* @example
|
* @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
|
* 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 {Function} func Function from seed (or undefined) to seed (or undefined)
|
||||||
* @param {Pattern} pat Pattern to update
|
* @param {Pattern} pat Pattern to update
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
@@ -428,6 +455,7 @@ export const withSeed = (func, pat) => {
|
|||||||
* that use randomness, like `shuffle` and `sometimes`.
|
* that use randomness, like `shuffle` and `sometimes`.
|
||||||
*
|
*
|
||||||
* @name seed
|
* @name seed
|
||||||
|
* @tags math
|
||||||
* @param {number} n A new seed. Can be any number.
|
* @param {number} n A new seed. Can be any number.
|
||||||
* @example
|
* @example
|
||||||
* $: s("hh*4").degrade();
|
* $: 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.
|
* A continuous pattern of random numbers, between 0 and 1.
|
||||||
*
|
*
|
||||||
* @name rand
|
* @name rand
|
||||||
|
* @tags generators
|
||||||
* @example
|
* @example
|
||||||
* // randomly change the cutoff
|
* // randomly change the cutoff
|
||||||
* s("bd*4,hh*8").cutoff(rand.range(500,8000))
|
* 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));
|
export const rand = signal((t, controls) => getRandsAtTime(t, 1, controls.randSeed));
|
||||||
/**
|
/**
|
||||||
* A continuous pattern of random numbers, between -1 and 1
|
* A continuous pattern of random numbers, between -1 and 1
|
||||||
|
* @tags generators
|
||||||
*/
|
*/
|
||||||
export const rand2 = rand.toBipolar();
|
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
|
* A continuous pattern of 0 or 1 (binary random), with a probability for the value being 1
|
||||||
*
|
*
|
||||||
* @name brandBy
|
* @name brandBy
|
||||||
|
* @tags generators
|
||||||
* @param {number} probability - a number between 0 and 1
|
* @param {number} probability - a number between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* s("hh*10").pan(brandBy(0.2))
|
* 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)
|
* A continuous pattern of 0 or 1 (binary random)
|
||||||
*
|
*
|
||||||
* @name brand
|
* @name brand
|
||||||
|
* @tags generators
|
||||||
* @example
|
* @example
|
||||||
* s("hh*10").pan(brand)
|
* 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.
|
* A continuous pattern of random integers, between 0 and n-1.
|
||||||
*
|
*
|
||||||
* @name irand
|
* @name irand
|
||||||
|
* @tags generators
|
||||||
* @param {number} n max value (exclusive)
|
* @param {number} n max value (exclusive)
|
||||||
* @example
|
* @example
|
||||||
* // randomly select scale notes from 0 - 7 (= C to C)
|
* // 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
|
* 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
|
* pattern of numbers, which should be in the range of 0..1
|
||||||
|
* @tags temporal
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
@@ -514,6 +548,7 @@ export const chooseWith = (pat, xs) => {
|
|||||||
/**
|
/**
|
||||||
* As with {chooseWith}, but the structure comes from the chosen values, rather
|
* As with {chooseWith}, but the structure comes from the chosen values, rather
|
||||||
* than the pattern you're using to choose with.
|
* than the pattern you're using to choose with.
|
||||||
|
* @tags temporal
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
@@ -524,6 +559,7 @@ export const chooseInWith = (pat, xs) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Chooses randomly from the given list of elements.
|
* Chooses randomly from the given list of elements.
|
||||||
|
* @tags temporal
|
||||||
* @param {...any} xs values / patterns to choose from.
|
* @param {...any} xs values / patterns to choose from.
|
||||||
* @returns {Pattern} - a continuous pattern.
|
* @returns {Pattern} - a continuous pattern.
|
||||||
* @example
|
* @example
|
||||||
@@ -539,6 +575,7 @@ export const chooseOut = choose;
|
|||||||
* Chooses from the given list of values (or patterns of values), according
|
* 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
|
* to the pattern that the method is called on. The pattern should be in
|
||||||
* the range 0 .. 1.
|
* the range 0 .. 1.
|
||||||
|
* @tags temporal
|
||||||
* @param {...any} xs
|
* @param {...any} xs
|
||||||
* @returns {Pattern}
|
* @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
|
* As with choose, but the pattern that this method is called on should be
|
||||||
* in the range -1 .. 1
|
* in the range -1 .. 1
|
||||||
|
* @tags temporal
|
||||||
* @param {...any} xs
|
* @param {...any} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
*/
|
*/
|
||||||
@@ -558,6 +596,7 @@ Pattern.prototype.choose2 = function (...xs) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Picks one of the elements at random each cycle.
|
* Picks one of the elements at random each cycle.
|
||||||
|
* @tags temporal
|
||||||
* @synonyms randcat
|
* @synonyms randcat
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
* @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
|
* 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
|
* @param {...any} pairs arrays of value and weight
|
||||||
* @returns {Pattern} - a continuous pattern.
|
* @returns {Pattern} - a continuous pattern.
|
||||||
* @example
|
* @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
|
* Picks one of the elements at random each cycle by giving a probability to each element
|
||||||
|
* @tags temporal
|
||||||
* @synonyms wrandcat
|
* @synonyms wrandcat
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
* @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.
|
* Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1.
|
||||||
*
|
*
|
||||||
|
* @tags generators
|
||||||
* @name perlin
|
* @name perlin
|
||||||
* @example
|
* @example
|
||||||
* // randomly change the cutoff
|
* // 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,
|
* 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.
|
* like perlin noise but with sawtooth waves), in the range 0..1.
|
||||||
*
|
*
|
||||||
|
* @tags generators
|
||||||
* @name berlin
|
* @name berlin
|
||||||
* @example
|
* @example
|
||||||
* // ascending arpeggios
|
* // ascending arpeggios
|
||||||
@@ -684,6 +727,7 @@ export const degradeByWith = register(
|
|||||||
* 0 = 0% chance of removal
|
* 0 = 0% chance of removal
|
||||||
* 1 = 100% chance of removal
|
* 1 = 100% chance of removal
|
||||||
*
|
*
|
||||||
|
* @tags temporal
|
||||||
* @name degradeBy
|
* @name degradeBy
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @param {number} amount - a number between 0 and 1
|
* @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)`
|
* Randomly removes 50% of events from the pattern. Shorthand for `.degradeBy(0.5)`
|
||||||
*
|
*
|
||||||
|
* @tags temporal
|
||||||
* @name degrade
|
* @name degrade
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
@@ -725,6 +770,7 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, t
|
|||||||
* 1 = 0% chance of removal
|
* 1 = 0% chance of removal
|
||||||
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
|
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
|
||||||
*
|
*
|
||||||
|
* @tags temporal
|
||||||
* @name undegradeBy
|
* @name undegradeBy
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @param {number} amount - a number between 0 and 1
|
* @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)`
|
* 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).
|
* Events that would be removed by degrade are let through by undegrade and vice versa (see second example).
|
||||||
*
|
*
|
||||||
|
* @tags temporal
|
||||||
* @name undegrade
|
* @name undegrade
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns 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.
|
* Randomly applies the given function by the given probability.
|
||||||
* Similar to `someCyclesBy`
|
* Similar to `someCyclesBy`
|
||||||
*
|
*
|
||||||
|
* @tags temporal
|
||||||
* @name sometimesBy
|
* @name sometimesBy
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @param {number | Pattern} probability - a number between 0 and 1
|
* @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
|
* Applies the given function with a 50% chance
|
||||||
*
|
*
|
||||||
|
* @tags temporal
|
||||||
* @name sometimes
|
* @name sometimes
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @param {function} function - the transformation to apply
|
* @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 {number | Pattern} probability - a number between 0 and 1
|
||||||
* @param {function} function - the transformation to apply
|
* @param {function} function - the transformation to apply
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @tags temporal
|
||||||
* @example
|
* @example
|
||||||
* s("bd,hh*8").someCyclesBy(.3, x=>x.speed("0.5"))
|
* 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
|
* @name someCycles
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @tags temporal
|
||||||
* @example
|
* @example
|
||||||
* s("bd,hh*8").someCycles(x=>x.speed("0.5"))
|
* s("bd,hh*8").someCycles(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
@@ -847,6 +898,7 @@ export const someCycles = register('someCycles', function (func, pat) {
|
|||||||
* @name often
|
* @name often
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @tags temporal
|
||||||
* @example
|
* @example
|
||||||
* s("hh*8").often(x=>x.speed("0.5"))
|
* s("hh*8").often(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
@@ -861,6 +913,7 @@ export const often = register('often', function (func, pat) {
|
|||||||
* @name rarely
|
* @name rarely
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @tags temporal
|
||||||
* @example
|
* @example
|
||||||
* s("hh*8").rarely(x=>x.speed("0.5"))
|
* 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)`
|
* Shorthand for `.sometimesBy(0.1, fn)`
|
||||||
*
|
*
|
||||||
|
* @tags temporal
|
||||||
* @name almostNever
|
* @name almostNever
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
@@ -886,6 +940,7 @@ export const almostNever = register('almostNever', function (func, pat) {
|
|||||||
*
|
*
|
||||||
* Shorthand for `.sometimesBy(0.9, fn)`
|
* Shorthand for `.sometimesBy(0.9, fn)`
|
||||||
*
|
*
|
||||||
|
* @tags temporal
|
||||||
* @name almostAlways
|
* @name almostAlways
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
@@ -900,6 +955,7 @@ export const almostAlways = register('almostAlways', function (func, pat) {
|
|||||||
*
|
*
|
||||||
* Shorthand for `.sometimesBy(0, fn)` (never calls fn)
|
* Shorthand for `.sometimesBy(0, fn)` (never calls fn)
|
||||||
*
|
*
|
||||||
|
* @tags temporal
|
||||||
* @name never
|
* @name never
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
@@ -914,6 +970,7 @@ export const never = register('never', function (_, pat) {
|
|||||||
*
|
*
|
||||||
* Shorthand for `.sometimesBy(1, fn)` (always calls fn)
|
* Shorthand for `.sometimesBy(1, fn)` (always calls fn)
|
||||||
*
|
*
|
||||||
|
* @tags temporal
|
||||||
* @name always
|
* @name always
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
@@ -942,6 +999,7 @@ export function _keyDown(keyname) {
|
|||||||
* Do something on a keypress, or array of keypresses
|
* 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)
|
* [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
|
||||||
*
|
*
|
||||||
|
* @tags external_io
|
||||||
* @name whenKey
|
* @name whenKey
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns 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
|
* 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)
|
* [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
|
||||||
*
|
*
|
||||||
|
* @tags external_io
|
||||||
* @name keyDown
|
* @name keyDown
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
@@ -975,6 +1034,8 @@ export const keyDown = register('keyDown', function (pat) {
|
|||||||
* event durations, from the pattern that it is combined with.
|
* 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"`.
|
* 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`.
|
* See also its reciprocal, `per`, also known as `perCycle`.
|
||||||
|
*
|
||||||
|
* @tags temporal
|
||||||
* @example
|
* @example
|
||||||
* // Shorter events are lower in pitch
|
* // Shorter events are lower in pitch
|
||||||
* sound("saw saw [saw saw] saw")
|
* 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.
|
* 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"`.
|
* For example `per.struct("1 1 [1 1] 1")` would give the same as `"4 4 [8 8] 4"`.
|
||||||
* See also its reciprocal, `cyclesPer`.
|
* See also its reciprocal, `cyclesPer`.
|
||||||
|
* @tags temporal
|
||||||
* @synonyms perCycle
|
* @synonyms perCycle
|
||||||
* @example
|
* @example
|
||||||
* // Shorter events are more distorted
|
* // Shorter events are more distorted
|
||||||
@@ -1010,6 +1072,7 @@ export const perCycle = per;
|
|||||||
* particular, where the event duration halves, the
|
* particular, where the event duration halves, the
|
||||||
* returned value increases by one. `perx.struct("1 1 [1 [1 1]] 1")` would therefore be
|
* 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"`.
|
* the same as `"3 3 [4 [5 5]] 3"`.
|
||||||
|
* @tags temporal
|
||||||
*/
|
*/
|
||||||
export const perx = new Pattern(function (state) {
|
export const perx = new Pattern(function (state) {
|
||||||
const n = Fraction(1).div(state.span.duration);
|
const n = Fraction(1).div(state.span.duration);
|
||||||
|
|||||||
@@ -339,8 +339,13 @@ export function uniqsortr(a) {
|
|||||||
|
|
||||||
export function unicodeToBase64(text) {
|
export function unicodeToBase64(text) {
|
||||||
const utf8Bytes = new TextEncoder().encode(text);
|
const utf8Bytes = new TextEncoder().encode(text);
|
||||||
const base64String = btoa(String.fromCharCode(...utf8Bytes));
|
let binaryString = '';
|
||||||
return base64String;
|
const chunkSize = 0x8000;
|
||||||
|
for (let i = 0; i < utf8Bytes.length; i += chunkSize) {
|
||||||
|
const chunk = utf8Bytes.subarray(i, i + chunkSize);
|
||||||
|
binaryString += String.fromCharCode.apply(null, chunk);
|
||||||
|
}
|
||||||
|
return btoa(binaryString);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function base64ToUnicode(base64String) {
|
export function base64ToUnicode(base64String) {
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ export async function loadOrc(url) {
|
|||||||
* p4 -- MIDI key number (as a real number, not an integer but in [0, 127].
|
* 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].
|
* p5 -- MIDI velocity (as a real number, not an integer but in [0, 127].
|
||||||
* p6 -- Strudel controls, as a string.
|
* p6 -- Strudel controls, as a string.
|
||||||
|
*
|
||||||
|
* @tags external_io
|
||||||
*/
|
*/
|
||||||
export const csoundm = register('csoundm', (instrument, pat) => {
|
export const csoundm = register('csoundm', (instrument, pat) => {
|
||||||
let p1 = instrument;
|
let p1 = instrument;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/csound",
|
"name": "@strudel/csound",
|
||||||
"version": "1.2.7",
|
"version": "1.3.0",
|
||||||
"description": "csound bindings for strudel",
|
"description": "csound bindings for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -78,6 +78,16 @@ export const cleanupDraw = (clearScreen = true, id) => {
|
|||||||
stopAllAnimations(id);
|
stopAllAnimations(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const cleanupDrawContext = (replID) => {
|
||||||
|
const ctx = getDrawContext();
|
||||||
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
|
|
||||||
|
// clear the big canvas context, ignore inline widgets
|
||||||
|
Object.keys(animationFrames).forEach(
|
||||||
|
(id) => (!replID || id.startsWith(replID)) && !id.startsWith('_') && stopAnimationFrame(id),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
Pattern.prototype.onPaint = function (painter) {
|
Pattern.prototype.onPaint = function (painter) {
|
||||||
return this.withState((state) => {
|
return this.withState((state) => {
|
||||||
if (!state.controls.painters) {
|
if (!state.controls.painters) {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const getValue = (e) => {
|
|||||||
*
|
*
|
||||||
* @name pianoroll
|
* @name pianoroll
|
||||||
* @synonyms punchcard
|
* @synonyms punchcard
|
||||||
|
* @tags visualization
|
||||||
* @param {Object} options Object containing all the optional following parameters as key value pairs:
|
* @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 {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
|
* @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.
|
* Supports all the same options as pianoroll.
|
||||||
*
|
*
|
||||||
* @name wordfall
|
* @name wordfall
|
||||||
|
* @tags visualization
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.wordfall = function (options) {
|
Pattern.prototype.wordfall = function (options) {
|
||||||
return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...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
|
* Renders a pitch circle to visualize frequencies within one octave
|
||||||
* @name pitchwheel
|
* @name pitchwheel
|
||||||
|
* @tags visualization
|
||||||
* @param {number} hapcircles
|
* @param {number} hapcircles
|
||||||
* @param {number} circle
|
* @param {number} circle
|
||||||
* @param {number} edo
|
* @param {number} edo
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ function drawSpiral(options) {
|
|||||||
* Displays a spiral visual.
|
* Displays a spiral visual.
|
||||||
*
|
*
|
||||||
* @name spiral
|
* @name spiral
|
||||||
|
* @tags visualization
|
||||||
* @param {Object} options Object containing all the optional following parameters as key value pairs:
|
* @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} stretch controls the rotations per cycle ratio, where 1 = 1 cycle / 360 degrees
|
||||||
* @param {number} size the diameter of the spiral
|
* @param {number} size the diameter of the spiral
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
/*
|
||||||
|
input.mjs - MIDI input wrapper
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/midi/midi.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WebMidi } from 'webmidi';
|
||||||
|
import { logger, ref } from '@strudel/core';
|
||||||
|
import { getDevice } from './util.mjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI input device wrapper that manages connection and reconnection, tracks
|
||||||
|
* persisted CC states, etc. These instances are long-lived and are maintained as singletons
|
||||||
|
* (keyed globally by input string/number).
|
||||||
|
*/
|
||||||
|
export class MidiInput {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string | number} input MIDI device name or index defaulting to 0
|
||||||
|
*/
|
||||||
|
constructor(input) {
|
||||||
|
this.input = input;
|
||||||
|
this.stateKey = typeof input === 'string' ? input : undefined; // Saved state is not tracked for numeric index inputs
|
||||||
|
|
||||||
|
this._refs = {};
|
||||||
|
this._refsByChan = {};
|
||||||
|
|
||||||
|
this._loadAllStates();
|
||||||
|
|
||||||
|
this.initialDevice = this._startDeviceListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation for the cc() factory function tied to this specific input.
|
||||||
|
* @param {number} cc MIDI CC number
|
||||||
|
* @param {number | undefined} chan MIDI channel (1-16) or undefined for all channels
|
||||||
|
*/
|
||||||
|
createCC(cc, chan) {
|
||||||
|
const lookupMap = chan === undefined ? this._refs : this._refsByChan[chan];
|
||||||
|
if (!(cc in lookupMap)) {
|
||||||
|
const initialState = this._loadState(chan);
|
||||||
|
lookupMap[cc] = initialState[cc] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref(() => lookupMap[cc]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_startDeviceListener() {
|
||||||
|
const initialDevice = getDevice(this.input, WebMidi.inputs);
|
||||||
|
|
||||||
|
// Background connection loop
|
||||||
|
(async () => {
|
||||||
|
const midiListener = this._onMidiMessage.bind(this);
|
||||||
|
let device = initialDevice;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (!device) {
|
||||||
|
device = await this._waitForDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a bit for device to be ready to receive last state
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Still continue if sending did not work
|
||||||
|
this._sendAllStates(device);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('midiin: failed to send last state on connect:', device.name, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for incoming MIDI messages and for disconnection
|
||||||
|
device.addListener('midimessage', midiListener);
|
||||||
|
|
||||||
|
await this._waitForDeviceDisconnect(device);
|
||||||
|
|
||||||
|
device.removeListener('midimessage', midiListener);
|
||||||
|
device = null; // Clear var to trigger wait for connection
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return initialDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that resolves when the specified device is connected
|
||||||
|
_waitForDevice() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const connListener = () => {
|
||||||
|
const device = getDevice(this.input, WebMidi.inputs);
|
||||||
|
if (device) {
|
||||||
|
logger(`[midi] device reconnected: ${device.name}`);
|
||||||
|
|
||||||
|
WebMidi.removeListener('connected', connListener);
|
||||||
|
resolve(device);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
WebMidi.addListener('connected', connListener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that resolves when the specified device is disconnected
|
||||||
|
_waitForDeviceDisconnect(device) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const disconnListener = (e) => {
|
||||||
|
if (e.port.name === device.name) {
|
||||||
|
logger(`[midi] device disconnected: ${device.name}`);
|
||||||
|
|
||||||
|
WebMidi.removeListener('disconnected', disconnListener);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
WebMidi.addListener('disconnected', disconnListener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMidiMessage(e) {
|
||||||
|
const ccNum = e.dataBytes[0];
|
||||||
|
const v = e.dataBytes[1];
|
||||||
|
const chan = e.message.channel;
|
||||||
|
const scaled = v / 127;
|
||||||
|
|
||||||
|
this._refs[ccNum] = scaled;
|
||||||
|
this._refsByChan[chan] ??= {};
|
||||||
|
this._refsByChan[chan][ccNum] = scaled;
|
||||||
|
|
||||||
|
this._saveState(undefined, ccNum, scaled);
|
||||||
|
this._saveState(chan, ccNum, scaled);
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadAllStates() {
|
||||||
|
Object.assign(this._refs, this._loadState(undefined));
|
||||||
|
|
||||||
|
for (let chan = 1; chan <= 16; chan++) {
|
||||||
|
this._refsByChan[chan] ??= {};
|
||||||
|
Object.assign(this._refsByChan[chan], this._loadState(chan));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadState(chan) {
|
||||||
|
if (!this.stateKey) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialDataRaw = localStorage.getItem(
|
||||||
|
`strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`,
|
||||||
|
);
|
||||||
|
if (!initialDataRaw) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(initialDataRaw);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
`Failed to parse MIDI state from localStorage for input "${this.stateKey}" and channel "${chan}"`,
|
||||||
|
initialDataRaw,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_saveState(chan, cc, value) {
|
||||||
|
if (!this.stateKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = this._loadState(chan);
|
||||||
|
state[cc] = value;
|
||||||
|
localStorage.setItem(
|
||||||
|
`strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`,
|
||||||
|
JSON.stringify(state),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send CC values back to device to restore encoders and motorized sliders
|
||||||
|
_sendAllStates(device) {
|
||||||
|
const output = WebMidi.outputs.find((o) => o.name === device.name);
|
||||||
|
if (!output) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [chan, refs] of Object.entries(this._refsByChan)) {
|
||||||
|
const channel = Number(chan);
|
||||||
|
for (const [cc, value] of Object.entries(refs)) {
|
||||||
|
const ccn = Number(cc);
|
||||||
|
const scaled = Math.round(value * 127);
|
||||||
|
output.sendControlChange(ccn, scaled, channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
-63
@@ -23,6 +23,8 @@ import { noteToMidi, getControlName } from '@strudel/core';
|
|||||||
import { Note } from 'webmidi';
|
import { Note } from 'webmidi';
|
||||||
import { getAudioContext } from '@strudel/webaudio';
|
import { getAudioContext } from '@strudel/webaudio';
|
||||||
import { scheduleAtTime } from '../superdough/helpers.mjs';
|
import { scheduleAtTime } from '../superdough/helpers.mjs';
|
||||||
|
import { getMidiDeviceNamesString, getDevice } from './util.mjs';
|
||||||
|
import { MidiInput } from './input.mjs';
|
||||||
|
|
||||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
export const { WebMidi } = _WebMidi;
|
export const { WebMidi } = _WebMidi;
|
||||||
@@ -31,10 +33,6 @@ function supportsMidi() {
|
|||||||
return typeof navigator.requestMIDIAccess === 'function';
|
return typeof navigator.requestMIDIAccess === 'function';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMidiDeviceNamesString(devices) {
|
|
||||||
return devices.map((o) => `'${o.name}'`).join(' | ');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function enableWebMidi(options = {}) {
|
export function enableWebMidi(options = {}) {
|
||||||
const { onReady, onConnected, onDisconnected, onEnabled } = options;
|
const { onReady, onConnected, onDisconnected, onEnabled } = options;
|
||||||
if (WebMidi.enabled) {
|
if (WebMidi.enabled) {
|
||||||
@@ -72,29 +70,6 @@ export function enableWebMidi(options = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDevice(indexOrName, devices) {
|
|
||||||
if (!devices.length) {
|
|
||||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
|
||||||
}
|
|
||||||
if (typeof indexOrName === 'number') {
|
|
||||||
return devices[indexOrName];
|
|
||||||
}
|
|
||||||
const byName = (name) => devices.find((output) => output.name.includes(name));
|
|
||||||
if (typeof indexOrName === 'string') {
|
|
||||||
return byName(indexOrName);
|
|
||||||
}
|
|
||||||
// attempt to default to first IAC device if none is specified
|
|
||||||
const IACOutput = byName('IAC');
|
|
||||||
const device = IACOutput ?? devices[0];
|
|
||||||
if (!device) {
|
|
||||||
throw new Error(
|
|
||||||
`🔌 MIDI device '${device ? device : ''}' not found. Use one of ${getMidiDeviceNamesString(devices)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return IACOutput ?? devices[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// send start/stop messages to outputs when repl starts/stops
|
// send start/stop messages to outputs when repl starts/stops
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.addEventListener('message', (e) => {
|
window.addEventListener('message', (e) => {
|
||||||
@@ -138,6 +113,7 @@ function githubPath(base, subpath = '') {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* configures the default midimap, which is used when no "midimap" port is set
|
* configures the default midimap, which is used when no "midimap" port is set
|
||||||
|
* @tags external_io, midi
|
||||||
* @example
|
* @example
|
||||||
* defaultmidimap({ lpf: 74 })
|
* defaultmidimap({ lpf: 74 })
|
||||||
* $: note("c a f e").midi();
|
* $: note("c a f e").midi();
|
||||||
@@ -151,6 +127,7 @@ let loadCache = {};
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds midimaps to the registry. Inside each midimap, control names (e.g. lpf) are mapped to cc numbers.
|
* Adds midimaps to the registry. Inside each midimap, control names (e.g. lpf) are mapped to cc numbers.
|
||||||
|
* @tags external_io, midi
|
||||||
* @example
|
* @example
|
||||||
* midimaps({ mymap: { lpf: 74 } })
|
* midimaps({ mymap: { lpf: 74 } })
|
||||||
* $: note("c a f e")
|
* $: note("c a f e")
|
||||||
@@ -305,6 +282,7 @@ function sendNote(note, velocity, duration, device, midichan, targetTime) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* MIDI output: Opens a MIDI output port.
|
* MIDI output: Opens a MIDI output port.
|
||||||
|
* @tags external_io
|
||||||
* @param {string | number} midiport MIDI device name or index defaulting to 0
|
* @param {string | number} midiport MIDI device name or index defaulting to 0
|
||||||
* @param {object} options Additional MIDI configuration options
|
* @param {object} options Additional MIDI configuration options
|
||||||
* @example
|
* @example
|
||||||
@@ -492,9 +470,9 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a midi device
|
* Initialize a midi input device
|
||||||
*/
|
*/
|
||||||
async function _initialize(input) {
|
async function _initializeInput(input) {
|
||||||
if (isPattern(input)) {
|
if (isPattern(input)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[midi] Midi input cannot be a pattern. Make sure to pass device name with single quotes. Example: midin('${
|
`[midi] Midi input cannot be a pattern. Make sure to pass device name with single quotes. Example: midin('${
|
||||||
@@ -502,30 +480,38 @@ async function _initialize(input) {
|
|||||||
}')`,
|
}')`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const initial = await enableWebMidi(); // only returns on first init
|
const initial = await enableWebMidi(); // only returns on first init
|
||||||
const device = getDevice(input, WebMidi.inputs);
|
|
||||||
if (!device) {
|
const instance = midiInputs[input] || new MidiInput(input);
|
||||||
throw new Error(
|
midiInputs[input] = instance;
|
||||||
`[midi] Midi device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (initial) {
|
if (initial) {
|
||||||
|
const device = instance.initialDevice;
|
||||||
|
|
||||||
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
||||||
logger(
|
logger(
|
||||||
`[midi] Midi enabled! Using "${device.name}". ${
|
device
|
||||||
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
|
? `[midi] Midi enabled! Using "${device.name}". ${
|
||||||
}`,
|
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
|
||||||
|
}`
|
||||||
|
: `[midi] Midi enabled! Waiting for device "${input}"... Currently connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return device;
|
|
||||||
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MIDI input wrappers, by specified input string/index
|
||||||
|
const midiInputs = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MIDI input: Opens a MIDI input port to receive MIDI control change messages.
|
* 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
|
* The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel
|
||||||
*
|
*
|
||||||
* @name midin
|
* @name midin
|
||||||
|
* @tags external_io, midi
|
||||||
* @param {string | number} input MIDI device name or index defaulting to 0
|
* @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.
|
* @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)
|
* When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1)
|
||||||
@@ -539,31 +525,10 @@ async function _initialize(input) {
|
|||||||
* note("c a f e").s("saw")
|
* note("c a f e").s("saw")
|
||||||
* .when(cc(0).gt(0), x => x.postgain(0))
|
* .when(cc(0).gt(0), x => x.postgain(0))
|
||||||
*/
|
*/
|
||||||
let listeners = {};
|
|
||||||
const refs = {};
|
|
||||||
const refsByChan = {};
|
|
||||||
export async function midin(input) {
|
export async function midin(input) {
|
||||||
const device = await _initialize(input);
|
const instance = await _initializeInput(input);
|
||||||
refs[input] ??= {};
|
|
||||||
refsByChan[input] ??= {};
|
|
||||||
const cc = (cc, chan) => {
|
|
||||||
if (chan !== undefined) {
|
|
||||||
return ref(() => refsByChan[input][cc]?.[chan] || 0);
|
|
||||||
}
|
|
||||||
return ref(() => refs[input][cc] || 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
listeners[input] && device.removeListener('midimessage', listeners[input]);
|
return instance.createCC.bind(instance);
|
||||||
listeners[input] = (e) => {
|
|
||||||
const [ccNum, v] = e.dataBytes;
|
|
||||||
const chan = e.message.channel;
|
|
||||||
const scaled = v / 127;
|
|
||||||
refsByChan[input][ccNum] ??= {};
|
|
||||||
refsByChan[input][ccNum][chan] = scaled;
|
|
||||||
refs[input][ccNum] = scaled;
|
|
||||||
};
|
|
||||||
device.addListener('midimessage', listeners[input]);
|
|
||||||
return cc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -573,6 +538,7 @@ export async function midin(input) {
|
|||||||
* note durations
|
* note durations
|
||||||
*
|
*
|
||||||
* @name midikeys
|
* @name midikeys
|
||||||
|
* @tags external_io, midi
|
||||||
* @param {string | number} input MIDI device name or index defaulting to 0
|
* @param {string | number} input MIDI device name or index defaulting to 0
|
||||||
* @returns {function((number | Pattern)=): Pattern} A function that produces a pattern.
|
* @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,
|
* When queried, the pattern will produces the most recently played midi notes and velocities,
|
||||||
@@ -617,7 +583,16 @@ function _triggerKeyboard(input, cps, now, latencyCycles) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
export async function midikeys(input) {
|
export async function midikeys(input) {
|
||||||
const device = await _initialize(input);
|
const instance = await _initializeInput(input);
|
||||||
|
|
||||||
|
// TODO: support unpluggable device usage
|
||||||
|
const device = instance.initialDevice;
|
||||||
|
if (!device) {
|
||||||
|
throw new Error(
|
||||||
|
`[midi] Midi device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!kHaps[input]) {
|
if (!kHaps[input]) {
|
||||||
kHaps[input] = [];
|
kHaps[input] = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/midi",
|
"name": "@strudel/midi",
|
||||||
"version": "1.2.7",
|
"version": "1.3.0",
|
||||||
"description": "Midi API for strudel",
|
"description": "Midi API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
util.mjs - MIDI utility functions
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/midi/midi.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Input, Output } from 'webmidi';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string listing device names for error messages.
|
||||||
|
* @param {Input[] | Output[]} devices
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getMidiDeviceNamesString(devices) {
|
||||||
|
return devices.map((o) => `'${o.name}'`).join(' | ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up a device by index or name. Otherwise return a default device, or fail if none are connected.
|
||||||
|
*
|
||||||
|
* @param {string | number} indexOrName
|
||||||
|
* @param {Input[] | Output[]} devices
|
||||||
|
* @returns {Input | Output | undefined}
|
||||||
|
*/
|
||||||
|
export function getDevice(indexOrName, devices) {
|
||||||
|
if (typeof indexOrName === 'number') {
|
||||||
|
return devices[indexOrName];
|
||||||
|
}
|
||||||
|
const byName = (name) => devices.find((output) => output.name.includes(name));
|
||||||
|
if (typeof indexOrName === 'string') {
|
||||||
|
return byName(indexOrName);
|
||||||
|
}
|
||||||
|
// attempt to default to first IAC device if none is specified
|
||||||
|
const IACOutput = byName('IAC');
|
||||||
|
const device = IACOutput ?? devices[0];
|
||||||
|
if (!device) {
|
||||||
|
if (!devices.length) {
|
||||||
|
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||||
|
}
|
||||||
|
throw new Error(`🔌 Default MIDI device not found. Use one of ${getMidiDeviceNamesString(devices)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return device;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name accelerationX
|
* @name accelerationX
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms accX
|
* @synonyms accX
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(accelerationX.segment(4).range(0,7)).scale("C:minor")
|
* n(accelerationX.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
@@ -17,6 +18,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name accelerationY
|
* @name accelerationY
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms accY
|
* @synonyms accY
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(accelerationY.segment(4).range(0,7)).scale("C:minor")
|
* n(accelerationY.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
@@ -27,6 +29,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name accelerationZ
|
* @name accelerationZ
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms accZ
|
* @synonyms accZ
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(accelerationZ.segment(4).range(0,7)).scale("C:minor")
|
* n(accelerationZ.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
@@ -37,6 +40,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name gravityX
|
* @name gravityX
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms gravX
|
* @synonyms gravX
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(gravityX.segment(4).range(0,7)).scale("C:minor")
|
* n(gravityX.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
@@ -47,6 +51,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name gravityY
|
* @name gravityY
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms gravY
|
* @synonyms gravY
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(gravityY.segment(4).range(0,7)).scale("C:minor")
|
* n(gravityY.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
@@ -57,6 +62,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name gravityZ
|
* @name gravityZ
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms gravZ
|
* @synonyms gravZ
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(gravityZ.segment(4).range(0,7)).scale("C:minor")
|
* n(gravityZ.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
@@ -67,6 +73,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name rotationAlpha
|
* @name rotationAlpha
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms rotA, rotZ, rotationZ
|
* @synonyms rotA, rotZ, rotationZ
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(rotationAlpha.segment(4).range(0,7)).scale("C:minor")
|
* n(rotationAlpha.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
@@ -77,6 +84,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name rotationBeta
|
* @name rotationBeta
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms rotB, rotX, rotationX
|
* @synonyms rotB, rotX, rotationX
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(rotationBeta.segment(4).range(0,7)).scale("C:minor")
|
* n(rotationBeta.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
@@ -87,6 +95,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name rotationGamma
|
* @name rotationGamma
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms rotG, rotY, rotationY
|
* @synonyms rotG, rotY, rotationY
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(rotationGamma.segment(4).range(0,7)).scale("C:minor")
|
* n(rotationGamma.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
@@ -97,6 +106,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name orientationAlpha
|
* @name orientationAlpha
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms oriA, oriZ, orientationZ
|
* @synonyms oriA, oriZ, orientationZ
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(orientationAlpha.segment(4).range(0,7)).scale("C:minor")
|
* n(orientationAlpha.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
@@ -107,6 +117,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name orientationBeta
|
* @name orientationBeta
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms oriB, oriX, orientationX
|
* @synonyms oriB, oriX, orientationX
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(orientationBeta.segment(4).range(0,7)).scale("C:minor")
|
* n(orientationBeta.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
@@ -117,6 +128,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name orientationGamma
|
* @name orientationGamma
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms oriG, oriY, orientationY
|
* @synonyms oriG, oriY, orientationY
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(orientationGamma.segment(4).range(0,7)).scale("C:minor")
|
* n(orientationGamma.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
@@ -127,6 +139,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name absoluteOrientationAlpha
|
* @name absoluteOrientationAlpha
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms absOriA, absOriZ, absoluteOrientationZ
|
* @synonyms absOriA, absOriZ, absoluteOrientationZ
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(absoluteOrientationAlpha.segment(4).range(0,7)).scale("C:minor")
|
* n(absoluteOrientationAlpha.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
@@ -137,6 +150,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name absoluteOrientationBeta
|
* @name absoluteOrientationBeta
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms absOriB, absOriX, absoluteOrientationX
|
* @synonyms absOriB, absOriX, absoluteOrientationX
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(absoluteOrientationBeta.segment(4).range(0,7)).scale("C:minor")
|
* n(absoluteOrientationBeta.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
@@ -147,6 +161,7 @@ import { signal } from '../core/signal.mjs';
|
|||||||
* @name absoluteOrientationGamma
|
* @name absoluteOrientationGamma
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms absOriG, absOriY, absoluteOrientationY
|
* @synonyms absOriG, absOriY, absoluteOrientationY
|
||||||
|
* @tags external_io
|
||||||
* @example
|
* @example
|
||||||
* n(absoluteOrientationGamma.segment(4).range(0,7)).scale("C:minor")
|
* n(absoluteOrientationGamma.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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/)
|
* For more info, read [MIDI & OSC in the docs](https://strudel.cc/learn/input-output/)
|
||||||
*
|
*
|
||||||
* @name osc
|
* @name osc
|
||||||
|
* @tags external_io
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/osc",
|
"name": "@strudel/osc",
|
||||||
"version": "1.3.1",
|
"version": "1.3.2",
|
||||||
"description": "OSC messaging for strudel",
|
"description": "OSC messaging for strudel",
|
||||||
"main": "osc.mjs",
|
"main": "osc.mjs",
|
||||||
"bin": "./server.js",
|
"bin": "./server.js",
|
||||||
|
|||||||
Regular → Executable
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/repl",
|
"name": "@strudel/repl",
|
||||||
"version": "1.2.8",
|
"version": "1.3.0",
|
||||||
"description": "Strudel REPL as a Web Component",
|
"description": "Strudel REPL as a Web Component",
|
||||||
"module": "index.mjs",
|
"module": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/soundfonts",
|
"name": "@strudel/soundfonts",
|
||||||
"version": "1.2.7",
|
"version": "1.3.0",
|
||||||
"description": "Soundsfont support for strudel",
|
"description": "Soundsfont support for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
|
|||||||
if (a == null && d == null && s == null && r == null) {
|
if (a == null && d == null && s == null && r == null) {
|
||||||
return defaultValues ?? [envmin, envmin, envmax, releaseMin];
|
return defaultValues ?? [envmin, envmin, envmax, releaseMin];
|
||||||
}
|
}
|
||||||
|
|
||||||
const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin;
|
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)];
|
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export const connectLFO = (id, params, nodeTracker) => {
|
|||||||
fxi = 'main',
|
fxi = 'main',
|
||||||
depth = 1,
|
depth = 1,
|
||||||
depthabs,
|
depthabs,
|
||||||
|
retrig = 0,
|
||||||
...filteredParams
|
...filteredParams
|
||||||
} = params;
|
} = params;
|
||||||
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
|
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
|
||||||
@@ -109,7 +110,7 @@ export const connectLFO = (id, params, nodeTracker) => {
|
|||||||
const modParams = {
|
const modParams = {
|
||||||
...filteredParams,
|
...filteredParams,
|
||||||
frequency: sync !== undefined ? sync * cps : rate,
|
frequency: sync !== undefined ? sync * cps : rate,
|
||||||
time: cycle / cps,
|
time: retrig > 0.5 ? 0 : cycle / cps,
|
||||||
depth: depthValue,
|
depth: depthValue,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ class OLAProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Handles dynamic reallocation of input/output channels buffer
|
/** 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) {
|
reallocateChannelsIfNeeded(inputs, outputs) {
|
||||||
for (let i = 0; i < this.nbInputs; i++) {
|
for (let i = 0; i < this.nbInputs; i++) {
|
||||||
let nbChannels = inputs[i].length;
|
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) {
|
readInputs(inputs) {
|
||||||
// when playback is paused, we may stop receiving new samples
|
// when playback is paused, we may stop receiving new samples
|
||||||
if (inputs[0].length && inputs[0][0].length == 0) {
|
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) {
|
writeOutputs(outputs) {
|
||||||
for (let i = 0; i < this.nbInputs; i++) {
|
for (let i = 0; i < this.nbInputs; i++) {
|
||||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
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() {
|
shiftInputBuffers() {
|
||||||
for (let i = 0; i < this.nbInputs; i++) {
|
for (let i = 0; i < this.nbInputs; i++) {
|
||||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
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() {
|
shiftOutputBuffers() {
|
||||||
for (let i = 0; i < this.nbOutputs; i++) {
|
for (let i = 0; i < this.nbOutputs; i++) {
|
||||||
for (let j = 0; j < this.outputBuffers[i].length; j++) {
|
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() {
|
prepareInputBuffersToSend() {
|
||||||
for (let i = 0; i < this.nbInputs; i++) {
|
for (let i = 0; i < this.nbInputs; i++) {
|
||||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
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() {
|
handleOutputBuffersToRetrieve() {
|
||||||
for (let i = 0; i < this.nbOutputs; i++) {
|
for (let i = 0; i < this.nbOutputs; i++) {
|
||||||
for (let j = 0; j < this.outputBuffers[i].length; j++) {
|
for (let j = 0; j < this.outputBuffers[i].length; j++) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "superdough",
|
"name": "superdough",
|
||||||
"version": "1.2.7",
|
"version": "1.3.0",
|
||||||
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kabelsalat/lib": "^0.4.1",
|
"@kabelsalat/lib": "^0.4.1",
|
||||||
|
"@kabelsalat/web": "^0.4.1",
|
||||||
"nanostores": "^0.11.3"
|
"nanostores": "^0.11.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ var reverbGen = {};
|
|||||||
|
|
||||||
/** Generates a reverb impulse response.
|
/** Generates a reverb impulse response.
|
||||||
|
|
||||||
|
@tags internals
|
||||||
@param {!Object} params TODO: Document the properties.
|
@param {!Object} params TODO: Document the properties.
|
||||||
@param {!function(!AudioBuffer)} callback Function to call when
|
@param {!function(!AudioBuffer)} callback Function to call when
|
||||||
the impulse response has been generated. The impulse response
|
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.
|
/** Creates a canvas element showing a graph of the given data.
|
||||||
|
|
||||||
|
@tags internals
|
||||||
@param {!Float32Array} data An array of numbers, or a Float32Array.
|
@param {!Float32Array} data An array of numbers, or a Float32Array.
|
||||||
@param {number} width Width in pixels of the canvas.
|
@param {number} width Width in pixels of the canvas.
|
||||||
@param {number} height Height 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} lpFreqEnd
|
||||||
@param {number} lpFreqEndAt
|
@param {number} lpFreqEndAt
|
||||||
@param {!function(!AudioBuffer)} callback May be called
|
@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) {
|
var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt, callback) {
|
||||||
if (lpFreqStart == 0) {
|
if (lpFreqStart == 0) {
|
||||||
callback(input);
|
callback(input);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
import { BASE_MIDI_NOTE, getBaseURL, getCommonSampleInfo, noteToFreq, noteToMidi } from './util.mjs';
|
||||||
import { registerSound, registerWaveTable } from './index.mjs';
|
import { registerSound, registerWaveTable } from './index.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +14,13 @@ import { logger } from './logger.mjs';
|
|||||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @typedef {Object} SampleMetaData
|
||||||
|
* @property {string} url
|
||||||
|
* @property {midi} number
|
||||||
|
*/
|
||||||
|
|
||||||
export const getCachedBuffer = (url) => bufferCache[url];
|
export const getCachedBuffer = (url) => bufferCache[url];
|
||||||
|
|
||||||
function humanFileSize(bytes, si) {
|
function humanFileSize(bytes, si) {
|
||||||
@@ -33,7 +40,7 @@ function humanFileSize(bytes, si) {
|
|||||||
export function getSampleInfo(hapValue, bank) {
|
export function getSampleInfo(hapValue, bank) {
|
||||||
const { speed = 1.0 } = hapValue;
|
const { speed = 1.0 } = hapValue;
|
||||||
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
||||||
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
const playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||||
return { transpose, url, index, midi, label, playbackRate };
|
return { transpose, url, index, midi, label, playbackRate };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,15 +163,20 @@ export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '')
|
|||||||
if (baseUrl.startsWith('github:')) {
|
if (baseUrl.startsWith('github:')) {
|
||||||
baseUrl = githubPath(baseUrl, '');
|
baseUrl = githubPath(baseUrl, '');
|
||||||
}
|
}
|
||||||
const fullUrl = (v) => baseUrl + v;
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} v
|
||||||
|
* @returns {SampleMetaData}
|
||||||
|
*/
|
||||||
|
const getMetaData = (v) => ({ url: baseUrl + v, midi: extractMidiNoteFromString(v) });
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
//return [key, value.map(replaceUrl)];
|
//return [key, value.map(replaceUrl)];
|
||||||
value = value.map(fullUrl);
|
value = value.map(getMetaData);
|
||||||
} else {
|
} else {
|
||||||
// must be object
|
// must be object
|
||||||
value = Object.fromEntries(
|
value = Object.fromEntries(
|
||||||
Object.entries(value).map(([note, samples]) => {
|
Object.entries(value).map(([note, samples]) => {
|
||||||
return [note, (typeof samples === 'string' ? [samples] : samples).map(fullUrl)];
|
return [note, (typeof samples === 'string' ? [samples] : samples).map(getMetaData)];
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -235,6 +247,7 @@ export async function fetchSampleMap(url) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a collection of samples to use with `s`
|
* Loads a collection of samples to use with `s`
|
||||||
|
* @tags samples
|
||||||
* @example
|
* @example
|
||||||
* samples('github:tidalcycles/dirt-samples');
|
* samples('github:tidalcycles/dirt-samples');
|
||||||
* s("[bd ~]*2, [~ hh]*2, ~ sd")
|
* s("[bd ~]*2, [~ hh]*2, ~ sd")
|
||||||
@@ -366,3 +379,16 @@ export function registerSampleSource(key, bank, params) {
|
|||||||
registerSample(key, bank, params);
|
registerSample(key, bank, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractMidiNoteFromString(str) {
|
||||||
|
const regex = /_([a-gA-G])([#b])?([1-9])?\b/;
|
||||||
|
const match = str.match(regex);
|
||||||
|
if (match == null) {
|
||||||
|
return BASE_MIDI_NOTE;
|
||||||
|
}
|
||||||
|
const base = match[1].toUpperCase();
|
||||||
|
const accidental = match[2] ?? '';
|
||||||
|
const octave_str = match[3] ?? '';
|
||||||
|
const parsedVal = base + accidental + octave_str;
|
||||||
|
return noteToMidi(parsedVal);
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,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
|
* start to die out in first-in-first-out order once the max polyphony has been hit
|
||||||
*
|
*
|
||||||
* @name setMaxPolyphony
|
* @name setMaxPolyphony
|
||||||
|
* @tags superdough
|
||||||
* @param {number} Max polyphony. Defaults to 128
|
* @param {number} Max polyphony. Defaults to 128
|
||||||
* @example
|
* @example
|
||||||
* setMaxPolyphony(4)
|
* setMaxPolyphony(4)
|
||||||
@@ -73,6 +74,7 @@ export function applyGainCurve(val) {
|
|||||||
* quadratic, exponential, etc. rather than linear
|
* quadratic, exponential, etc. rather than linear
|
||||||
*
|
*
|
||||||
* @name setGainCurve
|
* @name setGainCurve
|
||||||
|
* @tags amplitude, superdough
|
||||||
* @param {Function} function to apply to all gain values
|
* @param {Function} function to apply to all gain values
|
||||||
* @example
|
* @example
|
||||||
* setGainCurve((x) => x * x) // quadratic gain
|
* setGainCurve((x) => x * x) // quadratic gain
|
||||||
@@ -128,6 +130,8 @@ async function aliasBankPath(path) {
|
|||||||
* Optionally accepts a single argument string of a path to a JSON file containing bank aliases.
|
* 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} bank - The bank to alias
|
||||||
* @param {string} alias - The alias to use for the bank
|
* @param {string} alias - The alias to use for the bank
|
||||||
|
*
|
||||||
|
* @tags samples
|
||||||
*/
|
*/
|
||||||
export async function aliasBank(...args) {
|
export async function aliasBank(...args) {
|
||||||
switch (args.length) {
|
switch (args.length) {
|
||||||
@@ -146,6 +150,7 @@ export async function aliasBank(...args) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Register an alias for a sound.
|
* Register an alias for a sound.
|
||||||
|
* @tags samples
|
||||||
* @param {string} original - The original sound name
|
* @param {string} original - The original sound name
|
||||||
* @param {string} alias - The alias to use for the sound
|
* @param {string} alias - The alias to use for the sound
|
||||||
*/
|
*/
|
||||||
@@ -255,6 +260,14 @@ export function loadWorklets() {
|
|||||||
return workletsLoading;
|
return workletsLoading;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
// this function should be called on first user interaction (to avoid console warning)
|
||||||
export async function initAudio(options = {}) {
|
export async function initAudio(options = {}) {
|
||||||
const {
|
const {
|
||||||
@@ -301,6 +314,7 @@ export async function initAudio(options = {}) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('could not load AudioWorklet effects', err);
|
console.warn('could not load AudioWorklet effects', err);
|
||||||
}
|
}
|
||||||
|
await initKabelsalat();
|
||||||
logger('[superdough] ready');
|
logger('[superdough] ready');
|
||||||
}
|
}
|
||||||
let audioReady;
|
let audioReady;
|
||||||
@@ -436,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) => {
|
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
|
||||||
// mapping from main FX and numbered FX chains to nodes
|
// mapping from main FX and numbered FX chains to nodes
|
||||||
const nodes = { main: {} };
|
const nodes = { main: {} };
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
|||||||
export const getAccidentalsOffset = (accidentals) => {
|
export const getAccidentalsOffset = (accidentals) => {
|
||||||
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} note
|
||||||
|
* @param {number} defaultOctave
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
export const noteToMidi = (note, defaultOctave = 3) => {
|
export const noteToMidi = (note, defaultOctave = 3) => {
|
||||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
@@ -29,6 +34,11 @@ export const noteToMidi = (note, defaultOctave = 3) => {
|
|||||||
const offset = getAccidentalsOffset(acc);
|
const offset = getAccidentalsOffset(acc);
|
||||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} n
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
export const midiToFreq = (n) => {
|
export const midiToFreq = (n) => {
|
||||||
return Math.pow(2, (n - 69) / 12) * 440;
|
return Math.pow(2, (n - 69) / 12) * 440;
|
||||||
};
|
};
|
||||||
@@ -38,7 +48,17 @@ export const freqToMidi = (freq) => {
|
|||||||
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const valueToMidi = (value, fallbackValue) => {
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} note
|
||||||
|
* @param {number} defaultOctave
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const noteToFreq = (note, defaultOctave = 3) => {
|
||||||
|
return midiToFreq(noteToMidi(note, defaultOctave));
|
||||||
|
};
|
||||||
|
function __valueToMidi(value) {
|
||||||
if (typeof value !== 'object') {
|
if (typeof value !== 'object') {
|
||||||
throw new Error('valueToMidi: expected object value');
|
throw new Error('valueToMidi: expected object value');
|
||||||
}
|
}
|
||||||
@@ -52,10 +72,15 @@ export const valueToMidi = (value, fallbackValue) => {
|
|||||||
if (typeof note === 'number') {
|
if (typeof note === 'number') {
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
if (!fallbackValue) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const valueToMidi = (value, fallbackValue) => {
|
||||||
|
const parsedValue = __valueToMidi(value) ?? fallbackValue;
|
||||||
|
if (parsedValue == null) {
|
||||||
throw new Error('valueToMidi: expected freq or note to be set');
|
throw new Error('valueToMidi: expected freq or note to be set');
|
||||||
}
|
}
|
||||||
return fallbackValue;
|
return parsedValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function nanFallback(value, fallback = 0, silent) {
|
export function nanFallback(value, fallback = 0, silent) {
|
||||||
@@ -84,15 +109,18 @@ export function secondsToCycle(t, cps) {
|
|||||||
// deduces relevant info for sample loading from hap.value and sample definition
|
// deduces relevant info for sample loading from hap.value and sample definition
|
||||||
// it encapsulates the core sampler logic into a pure and synchronous function
|
// it encapsulates the core sampler logic into a pure and synchronous function
|
||||||
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
||||||
|
export const BASE_MIDI_NOTE = 36;
|
||||||
|
|
||||||
export function getCommonSampleInfo(hapValue, bank) {
|
export function getCommonSampleInfo(hapValue, bank) {
|
||||||
const { s, n = 0 } = hapValue;
|
const { s, n = 0 } = hapValue;
|
||||||
let midi = valueToMidi(hapValue, 36);
|
const maybeMidiNote = __valueToMidi(hapValue);
|
||||||
let transpose = midi - 36; // C3 is middle C;
|
const midi = maybeMidiNote ?? BASE_MIDI_NOTE;
|
||||||
let url;
|
let transpose = midi - BASE_MIDI_NOTE; // C3 is middle C;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
let samplemeta;
|
||||||
if (Array.isArray(bank)) {
|
if (Array.isArray(bank)) {
|
||||||
index = getSoundIndex(n, bank.length);
|
index = getSoundIndex(n, bank.length);
|
||||||
url = bank[index];
|
samplemeta = bank[index];
|
||||||
} else {
|
} else {
|
||||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||||
// object format will expect keys as notes
|
// object format will expect keys as notes
|
||||||
@@ -104,13 +132,19 @@ export function getCommonSampleInfo(hapValue, bank) {
|
|||||||
);
|
);
|
||||||
transpose = -midiDiff(closest); // semitones to repitch
|
transpose = -midiDiff(closest); // semitones to repitch
|
||||||
index = getSoundIndex(n, bank[closest].length);
|
index = getSoundIndex(n, bank[closest].length);
|
||||||
url = bank[closest][index];
|
samplemeta = bank[closest][index];
|
||||||
}
|
}
|
||||||
const label = `${s}:${index}`;
|
const label = `${s}:${index}`;
|
||||||
return { transpose, url, index, midi, label };
|
if (maybeMidiNote != null) {
|
||||||
|
transpose = transpose + (BASE_MIDI_NOTE - samplemeta.midi);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { transpose, index, midi, label, url: samplemeta.url };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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) => {
|
export const pickAndRename = (source, map) => {
|
||||||
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
|
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ export function registerWaveTable(key, tables, params) {
|
|||||||
* Loads a collection of wavetables to use with `s`
|
* Loads a collection of wavetables to use with `s`
|
||||||
*
|
*
|
||||||
* @name tables
|
* @name tables
|
||||||
|
* @tags wavetable
|
||||||
*/
|
*/
|
||||||
export const tables = async (url, frameLen, json, options = {}) => {
|
export const tables = async (url, frameLen, json, options = {}) => {
|
||||||
if (json !== undefined) return _processTables(json, url, frameLen);
|
if (json !== undefined) return _processTables(json, url, frameLen);
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ class CoarseProcessor extends AudioWorkletProcessor {
|
|||||||
coarse = Math.max(1, coarse);
|
coarse = Math.max(1, coarse);
|
||||||
for (let n = 0; n < blockSize; n++) {
|
for (let n = 0; n < blockSize; n++) {
|
||||||
for (let i = 0; i < input.length; i++) {
|
for (let i = 0; i < input.length; i++) {
|
||||||
output[i][n] = n % coarse === 0 ? input[i][n] : output[i][n - 1];
|
output[i][n] = n % coarse < 1 ? input[i][n] : output[i][n - 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -646,14 +646,18 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
|||||||
this.timeCursor += this.hopSize;
|
this.timeCursor += this.hopSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apply Hann window in-place */
|
/** Apply Hann window in-place
|
||||||
|
* @tags internals
|
||||||
|
*/
|
||||||
applyHannWindow(input) {
|
applyHannWindow(input) {
|
||||||
for (let i = 0; i < this.blockSize; i++) {
|
for (let i = 0; i < this.blockSize; i++) {
|
||||||
input[i] *= this.hannWindow[i] * 1.62;
|
input[i] *= this.hannWindow[i] * 1.62;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Compute squared magnitudes for peak finding **/
|
/** Compute squared magnitudes for peak finding
|
||||||
|
* @tags internals
|
||||||
|
**/
|
||||||
computeMagnitudes() {
|
computeMagnitudes() {
|
||||||
let i = 0,
|
let i = 0,
|
||||||
j = 0;
|
j = 0;
|
||||||
@@ -667,7 +671,9 @@ class PhaseVocoderProcessor extends OLAProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Find peaks in spectrum magnitudes **/
|
/** Find peaks in spectrum magnitudes
|
||||||
|
* @tags internals
|
||||||
|
**/
|
||||||
findPeaks() {
|
findPeaks() {
|
||||||
this.nbPeaks = 0;
|
this.nbPeaks = 0;
|
||||||
let i = 2;
|
let i = 2;
|
||||||
@@ -688,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) {
|
shiftPeaks(pitchFactor) {
|
||||||
// zero-fill new spectrum
|
// zero-fill new spectrum
|
||||||
this.freqComplexBufferShifted.fill(0);
|
this.freqComplexBufferShifted.fill(0);
|
||||||
@@ -841,7 +849,9 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
|
registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
|
||||||
|
|
||||||
/** BYTE BEATS */
|
/** BYTE BEATS
|
||||||
|
* @tags internals
|
||||||
|
*/
|
||||||
const chyx = {
|
const chyx = {
|
||||||
/*bit*/ bitC: function (x, y, z) {
|
/*bit*/ bitC: function (x, y, z) {
|
||||||
return x & y ? z : 0;
|
return x & y ? z : 0;
|
||||||
|
|||||||
@@ -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} 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} 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).
|
* @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix).
|
||||||
|
* @tags internals
|
||||||
* @returns {number} Crossfaded output value.
|
* @returns {number} Crossfaded output value.
|
||||||
*/
|
*/
|
||||||
function crossfade(a, b, m) {
|
function crossfade(a, b, m) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "supradough",
|
"name": "supradough",
|
||||||
"version": "1.2.5",
|
"version": "1.2.4",
|
||||||
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
|
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ function scaleOffset(scale, offset, note) {
|
|||||||
* - 5P = perfect fifth
|
* - 5P = perfect fifth
|
||||||
* - 5d = diminished fifth
|
* - 5d = diminished fifth
|
||||||
*
|
*
|
||||||
|
* @tags tonal
|
||||||
* @param {string | number} amount Either number of semitones or interval string.
|
* @param {string | number} amount Either number of semitones or interval string.
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
@@ -154,6 +155,7 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
|
|||||||
*
|
*
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @name scaleTranspose
|
* @name scaleTranspose
|
||||||
|
* @tags tonal
|
||||||
* @param {offset} offset number of steps inside the scale
|
* @param {offset} offset number of steps inside the scale
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @synonyms scaleTrans, strans
|
* @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.
|
* The root note defaults to octave 3, if no octave number is given.
|
||||||
*
|
*
|
||||||
* @name scale
|
* @name scale
|
||||||
|
* @tags tonal
|
||||||
* @param {string} scale Name of scale
|
* @param {string} scale Name of scale
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistr
|
|||||||
* Adds a new custom voicing dictionary.
|
* Adds a new custom voicing dictionary.
|
||||||
*
|
*
|
||||||
* @name addVoicings
|
* @name addVoicings
|
||||||
|
* @tags tonal
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @param {string} name identifier for the voicing dictionary
|
* @param {string} name identifier for the voicing dictionary
|
||||||
* @param {Object} dictionary maps chord symbol to possible voicings
|
* @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).
|
* Uses [chord-voicings package](https://github.com/felixroos/chord-voicings#chord-voicings).
|
||||||
*
|
*
|
||||||
* @name voicings
|
* @name voicings
|
||||||
|
* @tags tonal
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @param {string} dictionary which voicing dictionary to use.
|
* @param {string} dictionary which voicing dictionary to use.
|
||||||
* @returns Pattern
|
* @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.
|
* Maps the chords of the incoming pattern to root notes in the given octave.
|
||||||
*
|
*
|
||||||
* @name rootNotes
|
* @name rootNotes
|
||||||
|
* @tags tonal
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @param {octave} octave octave to use
|
* @param {octave} octave octave to use
|
||||||
* @returns Pattern
|
* @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.
|
* If you pass a pattern of strings to voicing, they will be interpreted as chords.
|
||||||
*
|
*
|
||||||
* @name voicing
|
* @name voicing
|
||||||
|
* @tags tonal
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* n("0 1 2 3").chord("<C Am F G>").voicing()
|
* n("0 1 2 3").chord("<C Am F G>").voicing()
|
||||||
|
|||||||
@@ -19,7 +19,14 @@ export function registerLanguage(type, config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function transpiler(input, options = {}) {
|
export function transpiler(input, options = {}) {
|
||||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
const {
|
||||||
|
wrapAsync = false,
|
||||||
|
addReturn = true,
|
||||||
|
emitMiniLocations = true,
|
||||||
|
emitWidgets = true,
|
||||||
|
blockBased = false,
|
||||||
|
range = [],
|
||||||
|
} = options;
|
||||||
|
|
||||||
const comments = [];
|
const comments = [];
|
||||||
let ast = parse(input, {
|
let ast = parse(input, {
|
||||||
@@ -31,6 +38,13 @@ export function transpiler(input, options = {}) {
|
|||||||
|
|
||||||
const miniDisableRanges = findMiniDisableRanges(comments, input.length);
|
const miniDisableRanges = findMiniDisableRanges(comments, input.length);
|
||||||
let miniLocations = [];
|
let miniLocations = [];
|
||||||
|
|
||||||
|
// Position offset for block-based evaluation
|
||||||
|
let nodeOffset = range && range.length > 0 ? range[0] : 0;
|
||||||
|
|
||||||
|
// Track declarations to add to strudelScope for block-based eval
|
||||||
|
let scopeDeclarations = [];
|
||||||
|
|
||||||
const collectMiniLocations = (value, node) => {
|
const collectMiniLocations = (value, node) => {
|
||||||
const minilang = languages.get('minilang');
|
const minilang = languages.get('minilang');
|
||||||
if (minilang) {
|
if (minilang) {
|
||||||
@@ -43,9 +57,28 @@ export function transpiler(input, options = {}) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let widgets = [];
|
let widgets = [];
|
||||||
|
let sliders = [];
|
||||||
|
let labels = [];
|
||||||
|
|
||||||
walk(ast, {
|
walk(ast, {
|
||||||
enter(node, parent /* , prop, index */) {
|
enter(node, parent /* , prop, index */) {
|
||||||
|
// Apply position offset for block-based evaluation
|
||||||
|
if (blockBased && node.start !== undefined) {
|
||||||
|
node.start = node.start + nodeOffset;
|
||||||
|
node.end = node.end + nodeOffset;
|
||||||
|
}
|
||||||
|
// Collect variable and function declarations for strudelScope (block-based eval)
|
||||||
|
if (blockBased && parent?.type === 'Program') {
|
||||||
|
if (node.type === 'VariableDeclaration') {
|
||||||
|
for (const declarator of node.declarations) {
|
||||||
|
if (declarator.id?.name) {
|
||||||
|
scopeDeclarations.push(declarator.id.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (node.type === 'FunctionDeclaration' && node.id?.name) {
|
||||||
|
scopeDeclarations.push(node.id.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isLanguageLiteral(node)) {
|
if (isLanguageLiteral(node)) {
|
||||||
const { name } = node.tag;
|
const { name } = node.tag;
|
||||||
const language = languages.get(name);
|
const language = languages.get(name);
|
||||||
@@ -88,22 +121,29 @@ export function transpiler(input, options = {}) {
|
|||||||
return this.replace(miniWithLocation(value, node));
|
return this.replace(miniWithLocation(value, node));
|
||||||
}
|
}
|
||||||
if (isSliderFunction(node)) {
|
if (isSliderFunction(node)) {
|
||||||
emitWidgets &&
|
const from = node.arguments[0].start + nodeOffset;
|
||||||
widgets.push({
|
const to = node.arguments[0].end + nodeOffset;
|
||||||
from: node.arguments[0].start,
|
const id = `${from}:${to}`; // Range-based ID for stability
|
||||||
to: node.arguments[0].end,
|
|
||||||
value: node.arguments[0].raw, // don't use value!
|
const sliderConfig = {
|
||||||
min: node.arguments[1]?.value ?? 0,
|
from,
|
||||||
max: node.arguments[2]?.value ?? 1,
|
to,
|
||||||
step: node.arguments[3]?.value,
|
id,
|
||||||
type: 'slider',
|
value: node.arguments[0].raw, // don't use value!
|
||||||
});
|
min: node.arguments[1]?.value ?? 0,
|
||||||
return this.replace(sliderWithLocation(node));
|
max: node.arguments[2]?.value ?? 1,
|
||||||
|
step: node.arguments[3]?.value,
|
||||||
|
type: 'slider',
|
||||||
|
};
|
||||||
|
emitWidgets && widgets.push(sliderConfig);
|
||||||
|
sliders.push(sliderConfig);
|
||||||
|
return this.replace(sliderWithLocation(node, nodeOffset));
|
||||||
}
|
}
|
||||||
if (isWidgetMethod(node)) {
|
if (isWidgetMethod(node)) {
|
||||||
const type = node.callee.property.name;
|
const type = node.callee.property.name;
|
||||||
const index = widgets.filter((w) => w.type === type).length;
|
const index = widgets.filter((w) => w.type === type).length;
|
||||||
const widgetConfig = {
|
const widgetConfig = {
|
||||||
|
from: node.start,
|
||||||
to: node.end,
|
to: node.end,
|
||||||
index,
|
index,
|
||||||
type,
|
type,
|
||||||
@@ -116,8 +156,30 @@ export function transpiler(input, options = {}) {
|
|||||||
return this.replace(withAwait(node));
|
return this.replace(withAwait(node));
|
||||||
}
|
}
|
||||||
if (isLabelStatement(node)) {
|
if (isLabelStatement(node)) {
|
||||||
|
// Collect label info for block-based evaluation
|
||||||
|
// Store positions WITHOUT offset so repl can slice the transpiler output correctly
|
||||||
|
if (blockBased) {
|
||||||
|
labels.push({
|
||||||
|
name: node.label.name,
|
||||||
|
index: node.start - nodeOffset,
|
||||||
|
end: node.label.end - nodeOffset,
|
||||||
|
fullMatch: input.slice(node.start - nodeOffset, node.label.end - nodeOffset),
|
||||||
|
activeVisualizer: findVisualizerInSubtree(node.body),
|
||||||
|
});
|
||||||
|
}
|
||||||
return this.replace(labelToP(node));
|
return this.replace(labelToP(node));
|
||||||
}
|
}
|
||||||
|
// Detect all() calls as special labels for block management
|
||||||
|
// Store positions WITHOUT offset so repl can slice the transpiler output correctly
|
||||||
|
if (blockBased && isAllCall(node)) {
|
||||||
|
labels.push({
|
||||||
|
name: 'all',
|
||||||
|
index: node.start - nodeOffset,
|
||||||
|
end: node.end - nodeOffset,
|
||||||
|
fullMatch: input.slice(node.start - nodeOffset, node.end - nodeOffset),
|
||||||
|
activeVisualizer: node.arguments[0] ? findVisualizerInSubtree(node.arguments[0]) : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
leave(node, parent, prop, index) {
|
leave(node, parent, prop, index) {
|
||||||
@@ -181,17 +243,33 @@ export function transpiler(input, options = {}) {
|
|||||||
|
|
||||||
let { body } = ast;
|
let { body } = ast;
|
||||||
|
|
||||||
|
const silenceExpression = {
|
||||||
|
type: 'ExpressionStatement',
|
||||||
|
expression: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'silence',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if (!body.length) {
|
if (!body.length) {
|
||||||
console.warn('empty body -> fallback to silence');
|
console.warn('empty body -> fallback to silence');
|
||||||
body.push({
|
body.push(silenceExpression);
|
||||||
type: 'ExpressionStatement',
|
|
||||||
expression: {
|
|
||||||
type: 'Identifier',
|
|
||||||
name: 'silence',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (!body?.[body.length - 1]?.expression) {
|
} else if (!body?.[body.length - 1]?.expression) {
|
||||||
throw new Error('unexpected ast format without body expression');
|
// Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration)
|
||||||
|
if (blockBased) {
|
||||||
|
// For block-based eval, add silence as the return value when block ends with declaration
|
||||||
|
body.push(silenceExpression);
|
||||||
|
} else {
|
||||||
|
throw new Error('unexpected ast format without body expression');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For block-based eval, add scope assignments before the return statement
|
||||||
|
// This allows variables/functions defined in one block to be used in other blocks
|
||||||
|
if (blockBased && scopeDeclarations.length > 0) {
|
||||||
|
const scopeAssignments = scopeDeclarations.flatMap((name) => createScopeAssignment(name));
|
||||||
|
// Insert scope assignments before the last statement (which will become the return)
|
||||||
|
body.splice(body.length - 1, 0, ...scopeAssignments);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add return to last statement
|
// add return to last statement
|
||||||
@@ -209,7 +287,7 @@ export function transpiler(input, options = {}) {
|
|||||||
if (!emitMiniLocations) {
|
if (!emitMiniLocations) {
|
||||||
return { output };
|
return { output };
|
||||||
}
|
}
|
||||||
return { output, miniLocations, widgets };
|
return { output, miniLocations, widgets, sliders, labels };
|
||||||
}
|
}
|
||||||
|
|
||||||
function isKabelCall(node) {
|
function isKabelCall(node) {
|
||||||
@@ -416,8 +494,14 @@ function isWidgetMethod(node) {
|
|||||||
return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name);
|
return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sliderWithLocation(node) {
|
function sliderWithLocation(node, nodeOffset = 0) {
|
||||||
const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id
|
// Apply nodeOffset for block-based evaluation to generate correct range
|
||||||
|
const from = node.arguments[0].start + nodeOffset;
|
||||||
|
const to = node.arguments[0].end + nodeOffset;
|
||||||
|
|
||||||
|
// Use range-based ID for stability during block evaluation
|
||||||
|
const id = `${from}:${to}`;
|
||||||
|
|
||||||
// add loc as identifier to first argument
|
// add loc as identifier to first argument
|
||||||
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
||||||
node.arguments.unshift({
|
node.arguments.unshift({
|
||||||
@@ -432,14 +516,26 @@ function sliderWithLocation(node) {
|
|||||||
export function getWidgetID(widgetConfig) {
|
export function getWidgetID(widgetConfig) {
|
||||||
// the widget id is used as id for the dom element + as key for eventual resources
|
// the widget id is used as id for the dom element + as key for eventual resources
|
||||||
// for example, for each scope widget, a new analyser + buffer (large) is created
|
// for example, for each scope widget, a new analyser + buffer (large) is created
|
||||||
// that means, if we use the index index of line position as id, less garbage is generated
|
// Update: use range-based ID generation for better stability during block evaluation
|
||||||
// return `widget_${widgetConfig.to}`; // more gargabe
|
// When we have both from and to, use them together for stability
|
||||||
//return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage
|
// Otherwise fall back to position-based ID for backward compatibility
|
||||||
return `${widgetConfig.id || ''}_widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage
|
let uniqueIdentifier;
|
||||||
|
if (widgetConfig.from !== undefined && widgetConfig.to !== undefined) {
|
||||||
|
// Use range for more stable identification
|
||||||
|
uniqueIdentifier = `${widgetConfig.from}-${widgetConfig.to}`;
|
||||||
|
} else {
|
||||||
|
// Fallback to single position (for backward compatibility)
|
||||||
|
uniqueIdentifier = widgetConfig.to || widgetConfig.from || 0;
|
||||||
|
}
|
||||||
|
const baseId = `${widgetConfig.id || ''}_widget_${widgetConfig.type}`;
|
||||||
|
return `${baseId}_${widgetConfig.index}_${uniqueIdentifier}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function widgetWithLocation(node, widgetConfig) {
|
function widgetWithLocation(node, widgetConfig) {
|
||||||
const id = getWidgetID(widgetConfig);
|
const id = getWidgetID(widgetConfig);
|
||||||
|
// Store the unique ID back into the config so it's available for widget management
|
||||||
|
// This is critical for block-based evaluation to match existing widgets with new ones
|
||||||
|
widgetConfig.id = id;
|
||||||
// add loc as identifier to first argument
|
// add loc as identifier to first argument
|
||||||
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
||||||
node.arguments.unshift({
|
node.arguments.unshift({
|
||||||
@@ -454,6 +550,10 @@ function isBareSamplesCall(node, parent) {
|
|||||||
return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression';
|
return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAllCall(node) {
|
||||||
|
return node.type === 'CallExpression' && node.callee.name === 'all';
|
||||||
|
}
|
||||||
|
|
||||||
function withAwait(node) {
|
function withAwait(node) {
|
||||||
return {
|
return {
|
||||||
type: 'AwaitExpression',
|
type: 'AwaitExpression',
|
||||||
@@ -554,6 +654,128 @@ function languageWithLocation(name, value, offset) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List of non-inline widgets that need cleanup
|
||||||
|
// These are Pattern.prototype methods that create persistent visualizations
|
||||||
|
// (should be repalced by a function call producing an actual list of registered widgets)
|
||||||
|
const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall'];
|
||||||
|
|
||||||
|
function isVisualizerCall(node) {
|
||||||
|
if (
|
||||||
|
node.type === 'CallExpression' &&
|
||||||
|
node.callee.type === 'MemberExpression' &&
|
||||||
|
nonInlineWidgets.includes(node.callee.property?.name)
|
||||||
|
) {
|
||||||
|
return node.callee.property.name;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findVisualizerInSubtree(node) {
|
||||||
|
if (!node || typeof node !== 'object') return null;
|
||||||
|
|
||||||
|
// Check if this node is a visualizer call
|
||||||
|
const viz = isVisualizerCall(node);
|
||||||
|
if (viz) return viz;
|
||||||
|
|
||||||
|
// Recursively search children
|
||||||
|
for (const key of Object.keys(node)) {
|
||||||
|
if (key === 'parent') continue; // Skip parent references to avoid cycles
|
||||||
|
const child = node[key];
|
||||||
|
if (Array.isArray(child)) {
|
||||||
|
for (const item of child) {
|
||||||
|
const found = findVisualizerInSubtree(item);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
} else if (child && typeof child === 'object' && child.type) {
|
||||||
|
const found = findVisualizerInSubtree(child);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates AST nodes for: userDefinedKeys.add('name'); strudelScope.name = name; globalThis.name = name;
|
||||||
|
// Used in block-based evaluation to persist variables/functions across blocks
|
||||||
|
// We add to both strudelScope (for internal lookups) and globalThis (for direct access)
|
||||||
|
// We also track the key in userDefinedKeys so clearScope() can remove it later
|
||||||
|
function createScopeAssignment(name) {
|
||||||
|
return [
|
||||||
|
// userDefinedKeys.add('name');
|
||||||
|
{
|
||||||
|
type: 'ExpressionStatement',
|
||||||
|
expression: {
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: {
|
||||||
|
type: 'MemberExpression',
|
||||||
|
object: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'userDefinedKeys',
|
||||||
|
},
|
||||||
|
property: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'add',
|
||||||
|
},
|
||||||
|
computed: false,
|
||||||
|
},
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
type: 'Literal',
|
||||||
|
value: name,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// strudelScope.name = name;
|
||||||
|
{
|
||||||
|
type: 'ExpressionStatement',
|
||||||
|
expression: {
|
||||||
|
type: 'AssignmentExpression',
|
||||||
|
operator: '=',
|
||||||
|
left: {
|
||||||
|
type: 'MemberExpression',
|
||||||
|
object: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'strudelScope',
|
||||||
|
},
|
||||||
|
property: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: name,
|
||||||
|
},
|
||||||
|
computed: false,
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// globalThis.name = name;
|
||||||
|
{
|
||||||
|
type: 'ExpressionStatement',
|
||||||
|
expression: {
|
||||||
|
type: 'AssignmentExpression',
|
||||||
|
operator: '=',
|
||||||
|
left: {
|
||||||
|
type: 'MemberExpression',
|
||||||
|
object: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'globalThis',
|
||||||
|
},
|
||||||
|
property: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: name,
|
||||||
|
},
|
||||||
|
computed: false,
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function findMiniDisableRanges(comments, codeEnd) {
|
function findMiniDisableRanges(comments, codeEnd) {
|
||||||
const ranges = [];
|
const ranges = [];
|
||||||
const stack = []; // used to track on/off pairs
|
const stack = []; // used to track on/off pairs
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/web",
|
"name": "@strudel/web",
|
||||||
"version": "1.2.7",
|
"version": "1.3.0",
|
||||||
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
||||||
"module": "web.mjs",
|
"module": "web.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/webaudio",
|
"name": "@strudel/webaudio",
|
||||||
"version": "1.2.7",
|
"version": "1.3.0",
|
||||||
"description": "Web Audio helpers for Strudel",
|
"description": "Web Audio helpers for Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"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.
|
* Renders an oscilloscope for the frequency domain of the audio signal.
|
||||||
* @name fscope
|
* @name fscope
|
||||||
|
* @tags visualization
|
||||||
* @param {string} color line color as hex or color name. defaults to white.
|
* @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} scale scales the y-axis. Defaults to 0.25
|
||||||
* @param {number} pos y-position relative to screen height. 0 = top, 1 = bottom of screen
|
* @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.
|
* Renders an oscilloscope for the time domain of the audio signal.
|
||||||
* @name scope
|
* @name scope
|
||||||
* @synonyms tscope
|
* @synonyms tscope
|
||||||
|
* @tags visualization
|
||||||
* @param {object} config optional config with options:
|
* @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 {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.
|
* @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.
|
* Renders a spectrum analyzer for the incoming audio signal.
|
||||||
* @name spectrum
|
* @name spectrum
|
||||||
|
* @tags visualization
|
||||||
* @param {object} config optional config with options:
|
* @param {object} config optional config with options:
|
||||||
* @param {integer} thickness line thickness in px (default 3)
|
* @param {integer} thickness line thickness in px (default 3)
|
||||||
* @param {integer} speed scroll speed (default 1)
|
* @param {integer} speed scroll speed (default 1)
|
||||||
|
|||||||
+38
-1
@@ -7,14 +7,51 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import Tune from './tunejs.js';
|
import Tune from './tunejs.js';
|
||||||
import { register } from '@strudel/core';
|
import { register } from '@strudel/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assumes pattern contains numerical scale degrees on the `i` control (see examples below). Accepts a scale name or list of frequencies (see all available names at the link on the reference). Returns a new pattern with all values mapped to a frequency ratio. Similar to `xen`.
|
||||||
|
* @name tune
|
||||||
|
* @returns Pattern
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {(string | number[] )} scale
|
||||||
|
* @example
|
||||||
|
* i("0 1 2 3 4 5").tune("hexany15").mul("220").freq()
|
||||||
|
* @example
|
||||||
|
* // You can set your root to be a
|
||||||
|
* // particular note with getFreq:
|
||||||
|
* i("4 8 9 10 - - 5 7 9 11 - -").tune("tranh3")
|
||||||
|
* .mul(getFreq('c3'))
|
||||||
|
* .freq().clip(.5).room(1)
|
||||||
|
* @example
|
||||||
|
* // You can also give tune a list of
|
||||||
|
* // frequencies to use as the scale:
|
||||||
|
* i("0 1 2 3 4").tune([
|
||||||
|
* 261.6255653006,
|
||||||
|
* 302.72962012827,
|
||||||
|
* 350.29154279212,
|
||||||
|
* 405.32593044476,
|
||||||
|
* 469.00678383895,
|
||||||
|
* 523.2511306012
|
||||||
|
* ]).mul(220).freq();
|
||||||
|
*
|
||||||
|
* @tags tonal
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Tune.scale seems to be in ratio format
|
||||||
export const tune = register('tune', (scale, pat) => {
|
export const tune = register('tune', (scale, pat) => {
|
||||||
const tune = new Tune();
|
const tune = new Tune();
|
||||||
if (!tune.isValidScale(scale)) {
|
if (!tune.isValidScale(scale)) {
|
||||||
throw new Error('not a valid tune.js scale name: "' + scale + '". See http://abbernie.github.io/tune/scales.html');
|
throw new Error('not a valid tune.js scale name: "' + scale + '". See http://abbernie.github.io/tune/scales.html');
|
||||||
}
|
}
|
||||||
tune.loadScale(scale);
|
tune.loadScale(scale);
|
||||||
|
// if the tonic is a frequency, why are we putting in "1"
|
||||||
tune.tonicize(1);
|
tune.tonicize(1);
|
||||||
return pat.withHap((hap) => {
|
return pat.withHap((hap) => {
|
||||||
return hap.withValue(() => tune.note(hap.value));
|
if (typeof hap.value !== 'object') {
|
||||||
|
throw new Error(`Expected hap to have control 'i' set, but received ${hap.value.i}, try wrapping input in i()`);
|
||||||
|
}
|
||||||
|
// const { i, ...otherValues } = hap.value;
|
||||||
|
// hap.value = { ...otherValues, freq: tune.note(i)}
|
||||||
|
// return hap
|
||||||
|
return hap.withValue(() => tune.note(hap.value.i));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+14
-4
@@ -78,14 +78,21 @@ Tune.prototype.frequency = function(stepIn, octaveIn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// which scale degree (0 - scale length) is our input
|
// which scale degree (0 - scale length) is our input
|
||||||
|
// 60 % 12 = 0
|
||||||
var scaleDegree = stepIn % this.scale.length
|
var scaleDegree = stepIn % this.scale.length
|
||||||
|
|
||||||
|
// what's this doing
|
||||||
|
// 0 scaleDegree = 12
|
||||||
|
// seems to simply be for a negative result from above, maybe another way to do it, but ok for now
|
||||||
while (scaleDegree < 0) {
|
while (scaleDegree < 0) {
|
||||||
scaleDegree += this.scale.length
|
scaleDegree += this.scale.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tonic is currently always 1
|
||||||
|
// so this is 1*scale[scaledegree]
|
||||||
var freq = this.tonic*this.scale[scaleDegree]
|
var freq = this.tonic*this.scale[scaleDegree]
|
||||||
|
|
||||||
|
// map it to octave
|
||||||
freq = freq*(Math.pow(2,octave))
|
freq = freq*(Math.pow(2,octave))
|
||||||
|
|
||||||
// truncate irrational numbers
|
// truncate irrational numbers
|
||||||
@@ -137,10 +144,13 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Load a new scale */
|
/* Load a new scale */
|
||||||
|
// sets .scale to ratios
|
||||||
|
|
||||||
Tune.prototype.loadScale = function(scale){
|
Tune.prototype.loadScale = function(scale){
|
||||||
|
|
||||||
/* load the scale */
|
/* load the scale */
|
||||||
|
let name
|
||||||
|
if (typeof scale === 'string') name = scale;
|
||||||
var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies
|
var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies
|
||||||
this.scale = []
|
this.scale = []
|
||||||
for (var i=0;i<freqs.length-1;i++) {
|
for (var i=0;i<freqs.length-1;i++) {
|
||||||
@@ -148,10 +158,10 @@ Tune.prototype.loadScale = function(scale){
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* visualize in console */
|
/* visualize in console */
|
||||||
/* console.log(" ");
|
// console.log(" ");
|
||||||
console.log("LOADED "+name);
|
// console.log("LOADED "+name);
|
||||||
console.log(TuningList[name].description);
|
// console.log(TuningList[name].description);
|
||||||
console.log(this.scale); */
|
// console.log(this.scale);
|
||||||
var vis = [];
|
var vis = [];
|
||||||
for (var i=0;i<100;i++) {
|
for (var i=0;i<100;i++) {
|
||||||
vis[i] = " ";
|
vis[i] = " ";
|
||||||
|
|||||||
+176
-9
@@ -4,8 +4,10 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { register, _mod, parseNumeral } from '@strudel/core';
|
import { register, _mod, parseNumeral, removeUndefineds } from '@strudel/core';
|
||||||
|
import Tune from './tunejs.js';
|
||||||
|
|
||||||
|
// returns a list of frequency ratios for given edo scale
|
||||||
export function edo(name) {
|
export function edo(name) {
|
||||||
if (!/^[1-9]+[0-9]*edo$/.test(name)) {
|
if (!/^[1-9]+[0-9]*edo$/.test(name)) {
|
||||||
throw new Error('not an edo scale: "' + name + '"');
|
throw new Error('not an edo scale: "' + name + '"');
|
||||||
@@ -18,23 +20,33 @@ const presets = {
|
|||||||
'12ji': [1 / 1, 16 / 15, 9 / 8, 6 / 5, 5 / 4, 4 / 3, 45 / 32, 3 / 2, 8 / 5, 5 / 3, 16 / 9, 15 / 8],
|
'12ji': [1 / 1, 16 / 15, 9 / 8, 6 / 5, 5 / 4, 4 / 3, 45 / 32, 3 / 2, 8 / 5, 5 / 3, 16 / 9, 15 / 8],
|
||||||
};
|
};
|
||||||
|
|
||||||
function withBase(freq, scale) {
|
// Given a base frequency such as 220 and an edo scale, returns
|
||||||
|
// an array of frequencies representing the given edo scale in that base
|
||||||
|
function _withBase(freq, scale) {
|
||||||
return scale.map((r) => r * freq);
|
return scale.map((r) => r * freq);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultBase = 220;
|
const defaultBase = 220;
|
||||||
|
|
||||||
|
const isEdo = (scale) => /^[1-9]+[0-9]*edo$/.test(scale);
|
||||||
|
|
||||||
|
// Assumes a base of 220. Returns a filtered scale based on 'indices'
|
||||||
|
// NOTE: indices functionality is unused
|
||||||
function getXenScale(scale, indices) {
|
function getXenScale(scale, indices) {
|
||||||
|
let tune = new Tune();
|
||||||
if (typeof scale === 'string') {
|
if (typeof scale === 'string') {
|
||||||
if (/^[1-9]+[0-9]*edo$/.test(scale)) {
|
if (isEdo(scale)) {
|
||||||
scale = edo(scale);
|
scale = edo(scale);
|
||||||
} else if (presets[scale]) {
|
} else if (presets[scale]) {
|
||||||
scale = presets[scale];
|
scale = presets[scale];
|
||||||
|
} else if (tune.isValidScale(scale)) {
|
||||||
|
tune.loadScale(scale);
|
||||||
|
scale = tune.scale;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('unknown scale name: "' + scale + '"');
|
throw new Error('unknown scale name: "' + scale + '"');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scale = withBase(defaultBase, scale);
|
scale = _withBase(defaultBase, scale);
|
||||||
if (!indices) {
|
if (!indices) {
|
||||||
return scale;
|
return scale;
|
||||||
}
|
}
|
||||||
@@ -47,16 +59,171 @@ function xenOffset(xenScale, offset, index = 0) {
|
|||||||
return xenScale[i] * Math.pow(2, oct);
|
return xenScale[i] * Math.pow(2, oct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trimFreq = (freq) => parseFloat(freq.toPrecision(10));
|
||||||
|
|
||||||
|
// accepts a scale name such as 31edo, and a pattern
|
||||||
|
// pattern expected to follow format such that a value can be mapped
|
||||||
|
// to an edostep within the scale. Returns the pattern with
|
||||||
|
// values mapped to the frequencies associated with the given edosteps
|
||||||
// scaleNameOrRatios: string || number[], steps?: number
|
// scaleNameOrRatios: string || number[], steps?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assumes a numerical pattern of scale steps, and a scale. Scales accepted are all preset scale names of `tune`, arbitrary edos such as 31edo, or an array of frequency ratios. Assumes scales repeat at octave (2/1). Returns a new pattern with all values mapped to their associated frequency, assuming a base frequency of 220hz.
|
||||||
|
*
|
||||||
|
* @name xen
|
||||||
|
* @returns Pattern
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {(string | number[] )} scaleNameOrRatios
|
||||||
|
* @tags tonal
|
||||||
|
* @example
|
||||||
|
* // A minor triad in 31edo:
|
||||||
|
* i("0 8 18").xen("31edo").piano()
|
||||||
|
* @example
|
||||||
|
* // You can also use xen with frequency ratios.
|
||||||
|
* // This is equivalent to the above:
|
||||||
|
* i("0 1 2").xen([
|
||||||
|
* Math.pow(2, 0/31),
|
||||||
|
* Math.pow(2, 8/31),
|
||||||
|
* Math.pow(2, 18/31),
|
||||||
|
* ]).piano()
|
||||||
|
* @example
|
||||||
|
* // xen also supports all scale names that
|
||||||
|
* // tune does:
|
||||||
|
* i("0 1 2 3 4 5").xen("hexany15")
|
||||||
|
* // equiv to:
|
||||||
|
* // "0 1 2 3 4 5".tune("hexany15").mul("220").freq()
|
||||||
|
* @example
|
||||||
|
* i("0 1 2 3 4 5 6 7").xen("<5edo 10edo 15edo hexany15>")
|
||||||
|
*/
|
||||||
|
|
||||||
export const xen = register('xen', function (scaleNameOrRatios, pat) {
|
export const xen = register('xen', function (scaleNameOrRatios, pat) {
|
||||||
return pat.withHap((hap) => {
|
return pat.withHaps((haps) => {
|
||||||
const scale = getXenScale(scaleNameOrRatios);
|
haps = haps.map((hap) => {
|
||||||
const frequency = xenOffset(scale, parseNumeral(hap.value));
|
let hVal = hap.value;
|
||||||
return hap.withValue(() => frequency);
|
const isObject = typeof hVal === 'object';
|
||||||
|
if (!isObject) {
|
||||||
|
throw new Error(`Expected hap to have control 'i' set, but received ${hap.value.i}, try wrapping input in i()`);
|
||||||
|
}
|
||||||
|
const { i, ...otherValues } = hVal;
|
||||||
|
const scale = getXenScale(scaleNameOrRatios);
|
||||||
|
let freq = xenOffset(scale, parseNumeral(hVal.i));
|
||||||
|
// 10 is somewhat arbitrary
|
||||||
|
freq = trimFreq(freq);
|
||||||
|
hap.value = { ...otherValues, freq };
|
||||||
|
return isEdo(scaleNameOrRatios)
|
||||||
|
? hap.setContext({ ...hap.context, edoSize: scaleNameOrRatios.match(/^([1-9]+[0-9]*)edo$/)[1] })
|
||||||
|
: hap;
|
||||||
|
});
|
||||||
|
return removeUndefineds(haps);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export const tuning = register('tuning', function (ratios, pat) {
|
/**
|
||||||
|
* Assumes pattern of frequencies tuned to some `base` frequency, such as the output of `xen`
|
||||||
|
* Because `xen` defaults to `220Hz`, so will `withBase`.
|
||||||
|
* but you can specify a different original base with the standard optional array syntax '`:`'
|
||||||
|
* @name withBase
|
||||||
|
* @param {number} base
|
||||||
|
* @param {number} (optional) originalBase
|
||||||
|
* @tags tonal
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* i("[0 1 2 3] [3 4] [4 3 2 1]").xen("hexany23").withBase("<220 [300 200]>")
|
||||||
|
* @example
|
||||||
|
* mini([1 / 1, 16 / 15, 9 / 8, 6 / 5, 5 / 4].join(' ')).withBase("220:1")
|
||||||
|
* // mini([1 / 1, 16 / 15, 9 / 8, 6 / 5, 5 / 4].join(' ')).mul(220).freq()
|
||||||
|
*
|
||||||
|
* @returns Pattern
|
||||||
|
*/
|
||||||
|
export const withBase = register('withBase', (b, pat) => {
|
||||||
|
let base;
|
||||||
|
let originalBase = 220;
|
||||||
|
if (Array.isArray(b)) {
|
||||||
|
base = b[0];
|
||||||
|
originalBase = b[1];
|
||||||
|
} else {
|
||||||
|
base = b;
|
||||||
|
}
|
||||||
|
return pat.withHaps((haps) => {
|
||||||
|
haps = haps.map((hap) => {
|
||||||
|
let hVal = hap.value;
|
||||||
|
const isObject = typeof hVal === 'object';
|
||||||
|
let freq = isObject ? hVal.freq : hVal;
|
||||||
|
freq = (freq * base) / originalBase;
|
||||||
|
hap.value = isObject ? { ...hap.value, freq } : { freq };
|
||||||
|
return hap;
|
||||||
|
});
|
||||||
|
return removeUndefineds(haps);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frequency transpose. Assumes pattern either has `freq` set, or has values that can be interpreted as frequencies
|
||||||
|
* amt has optional `edoSize` param, defaults to 12.
|
||||||
|
* If haps have edoSize param set, such as from the output of `xen("31edo")`,
|
||||||
|
* `ftrans` will fallback to that instead of 12 as the default.
|
||||||
|
*
|
||||||
|
* Transposes the frequency by `amt` edoSteps
|
||||||
|
* @name ftranspose
|
||||||
|
* @synonyms ftrans, fTrans, ftranspose, fTranspose
|
||||||
|
* @tags tonal
|
||||||
|
* @param {number} amt
|
||||||
|
* @param {number} edoSize (optional)
|
||||||
|
* @returns {Pattern}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* i("0 1 2").xen("12edo").ftrans("7")
|
||||||
|
* // n("0 1 2").scale("A:chromatic").trans("7")
|
||||||
|
* @example
|
||||||
|
* i("0 8 18").xen("31edo").ftrans("<8 -8>")
|
||||||
|
* @example
|
||||||
|
* // to transpose by steps of an edo, use "step:edo" :
|
||||||
|
* i("0 7 8 18").xen("31edo").ftrans("<0 1:31 1:12>")
|
||||||
|
* @example
|
||||||
|
* // it can also work with frequency values directly
|
||||||
|
* freq("200 300 400").ftrans("<0 7:31 7>")
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* f = frequency (Hz)
|
||||||
|
n = edo (steps per octave)
|
||||||
|
x = number of steps
|
||||||
|
if 0\n = f, then x\n = f * 2^(x/n)
|
||||||
|
example: 5edo, 0\5 = 220 Hz, then 2\5 = 220*2^(2/5) = 290.29 Hz */
|
||||||
|
|
||||||
|
export const { ftrans, fTrans, ftranspose, fTranspose } = register(
|
||||||
|
['ftrans', 'fTrans', 'ftranspose', 'fTranspose'],
|
||||||
|
(amt, pat) => {
|
||||||
|
let edoSize;
|
||||||
|
let numSteps;
|
||||||
|
if (Array.isArray(amt)) {
|
||||||
|
edoSize = amt[1];
|
||||||
|
numSteps = amt[0];
|
||||||
|
} else {
|
||||||
|
numSteps = amt;
|
||||||
|
}
|
||||||
|
return pat.withHaps((haps) => {
|
||||||
|
haps = haps.map((hap) => {
|
||||||
|
let hVal = hap.value;
|
||||||
|
const isObject = typeof hVal === 'object';
|
||||||
|
hVal = isObject ? hVal : { freq: hVal };
|
||||||
|
let { freq, ...otherValues } = hVal;
|
||||||
|
if (edoSize == undefined && hap.context.edoSize != undefined) {
|
||||||
|
edoSize = hap.context.edoSize;
|
||||||
|
} else if (edoSize == undefined) {
|
||||||
|
edoSize = 12;
|
||||||
|
}
|
||||||
|
freq = freq * Math.pow(2, numSteps / edoSize);
|
||||||
|
freq = trimFreq(freq);
|
||||||
|
hap.value = isObject ? { ...otherValues, freq } : freq;
|
||||||
|
return hap.setContext({ ...hap.context, edoSize });
|
||||||
|
});
|
||||||
|
return removeUndefineds(haps);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// not sure there's a point to having this and the above, seems like a proto version of the above.
|
||||||
|
const tuning = register('tuning', function (ratios, pat) {
|
||||||
return pat.withHap((hap) => {
|
return pat.withHap((hap) => {
|
||||||
const frequency = xenOffset(ratios, parseNumeral(hap.value));
|
const frequency = xenOffset(ratios, parseNumeral(hap.value));
|
||||||
return hap.withValue(() => frequency);
|
return hap.withValue(() => frequency);
|
||||||
|
|||||||
Generated
+23
-3
@@ -221,6 +221,9 @@ importers:
|
|||||||
'@tonaljs/tonal':
|
'@tonaljs/tonal':
|
||||||
specifier: ^4.10.0
|
specifier: ^4.10.0
|
||||||
version: 4.10.0
|
version: 4.10.0
|
||||||
|
codemirror-helix:
|
||||||
|
specifier: ^0.5.0
|
||||||
|
version: 0.5.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||||
nanostores:
|
nanostores:
|
||||||
specifier: ^0.11.3
|
specifier: ^0.11.3
|
||||||
version: 0.11.3
|
version: 0.11.3
|
||||||
@@ -234,9 +237,6 @@ importers:
|
|||||||
|
|
||||||
packages/core:
|
packages/core:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@kabelsalat/web':
|
|
||||||
specifier: ^0.4.1
|
|
||||||
version: 0.4.1
|
|
||||||
fraction.js:
|
fraction.js:
|
||||||
specifier: ^5.2.1
|
specifier: ^5.2.1
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
@@ -524,6 +524,9 @@ importers:
|
|||||||
'@kabelsalat/lib':
|
'@kabelsalat/lib':
|
||||||
specifier: ^0.4.1
|
specifier: ^0.4.1
|
||||||
version: 0.4.1
|
version: 0.4.1
|
||||||
|
'@kabelsalat/web':
|
||||||
|
specifier: ^0.4.1
|
||||||
|
version: 0.4.1
|
||||||
nanostores:
|
nanostores:
|
||||||
specifier: ^0.11.3
|
specifier: ^0.11.3
|
||||||
version: 0.11.3
|
version: 0.11.3
|
||||||
@@ -3551,6 +3554,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==}
|
resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
|
codemirror-helix@0.5.0:
|
||||||
|
resolution: {integrity: sha512-hI56hf9VGz53H1YvL6H1GC7HtP6te8vX+MsIHaE9J7Q3PQ6KFapKtIRg6lqSH898ikHWpMCPu42r6HJN0IfVLA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@codemirror/commands': ^6.0.0
|
||||||
|
'@codemirror/language': ^6.0.0
|
||||||
|
'@codemirror/search': ^6.0.0
|
||||||
|
'@codemirror/state': ^6.0.0
|
||||||
|
'@codemirror/view': ^6.0.0
|
||||||
|
|
||||||
collapse-white-space@2.1.0:
|
collapse-white-space@2.1.0:
|
||||||
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
|
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
|
||||||
|
|
||||||
@@ -11280,6 +11292,14 @@ snapshots:
|
|||||||
|
|
||||||
cmd-shim@6.0.3: {}
|
cmd-shim@6.0.3: {}
|
||||||
|
|
||||||
|
codemirror-helix@0.5.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2):
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/commands': 6.8.0
|
||||||
|
'@codemirror/language': 6.10.8
|
||||||
|
'@codemirror/search': 6.5.8
|
||||||
|
'@codemirror/state': 6.5.1
|
||||||
|
'@codemirror/view': 6.36.2
|
||||||
|
|
||||||
collapse-white-space@2.1.0: {}
|
collapse-white-space@2.1.0: {}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user