Compare commits

...

323 Commits

Author SHA1 Message Date
yaxu c150752372 revert 85e6d436ef
revert motors workshop page
(committed to main by mistake!)
2026-06-21 20:47:04 +02:00
Alex McLean e248bf85f3 Merge pull request 'Creates a pattern of numbers in base x from a number or pattern of numbers @stretchyboy' (#2075) from stretchyboy-base-function into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2075
2026-06-21 20:45:28 +02:00
Alex McLean 6c51c0261a Merge branch 'main' into stretchyboy-base-function 2026-06-21 20:45:05 +02:00
Felix Roos deefde7b50 hotfix: codeformat 2026-06-21 15:41:50 +02:00
froos d4f63e8de3 Merge pull request 'fix: cache bunny cdn urls #2057' (#2076) from cache-bunny into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2076
2026-06-21 15:29:56 +02:00
alex 19cb3dedc2 snapshot 2026-06-21 14:02:34 +01:00
alex a7c3407da7 codeformat 2026-06-21 13:54:38 +01:00
Martyn Eggleton 6870b04fb2 Pretty code now 2026-06-21 13:49:11 +01:00
Alex McLean 2b2646a768 Merge branch 'main' into base-function 2026-06-21 14:34:18 +02:00
alex 97ef5bc335 Merge branch 'main' of ssh://codeberg.org/uzu/strudel 2026-06-19 21:30:17 +01:00
alex 85e6d436ef motors workshop page 2026-06-19 21:29:58 +01:00
alex d3e2b7c7b4 motors workshop page 2026-06-19 21:29:49 +01:00
alex def1738259 shortcuts for workshop 2026-06-19 21:29:25 +01:00
Alex McLean 0feeb1e701 Merge pull request 'Add growlist' (#2073) from growlist into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2073
2026-06-18 13:25:38 +02:00
alex 813cc6c190 format 2026-06-18 12:04:03 +01:00
alex 62d4f84e69 add growlist 2026-06-18 10:55:34 +01:00
stretchyboy 49a1e11cd8 Merge branch 'main' into base-function 2026-06-17 13:58:30 +02:00
Felix Roos 941c97da0d fix: cache bunny cdn urls #2057 2026-06-15 09:15:06 +02:00
Alex McLean 95a9d301a1 Merge pull request 'midikeys-with-channel' (#2063) from eefano/strudel:midikeys-with-channel into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2063
2026-06-10 13:38:02 +02:00
Martyn Eggleton 475f17ddfd base now accepts Arrays which improve compatibility with rockstar-strudel 2026-06-03 21:56:26 +01:00
Martyn Eggleton bd68c6a0a7 Documentation tweak 2026-06-03 12:36:41 +01:00
eefano 2635716c87 added midikeys third test 2026-06-02 14:56:55 +02:00
eefano 58f956b57e prettier fix 2026-06-02 14:29:28 +02:00
eefano 0ae2c120b3 updated documentation for midikeys 2026-06-02 13:28:23 +02:00
eefano 05a43ef687 included midichan value in midikeys haps 2026-06-02 13:14:30 +02:00
Martyn Eggleton f21aeb55bd The base param can be a pattern. We also add a max number of digits to produce for each n 2026-06-01 14:02:20 +01:00
Martyn Eggleton d83139980b Creates a pattern of numbers in base x from a number or pattern of numbers 2026-05-27 21:23:55 +01:00
Alex McLean 0c61cd7670 Merge pull request 'Don't force SSL for mqtt websocket connections' (#2058) from mqtt-ws into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2058
2026-05-27 13:35:28 +02:00
alex 13b11efa15 don't force SSL for mqtt websocket connections - fall back on url instead (wss: for SSL, ws: for unencrypted) 2026-05-26 16:11:33 +01:00
froos f73b395648 Merge pull request 'Add error for when name arg in register is a pattern' (#2020) from tyow/localstrudel:registerErrorFix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2020
2026-05-07 11:46:59 +02:00
froos 9c47efd3cf Merge pull request 'docs: note that all/each require labeled patterns' (#2015) from bwagner/strudel:fix/all-unlabeled-pattern-docs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2015
2026-05-07 11:28:31 +02:00
froos f4066089f4 Merge pull request 'Add flux function' (#1528) from TodePond/strudel:lu/flux into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1528
2026-05-07 10:51:57 +02:00
Felix Roos 0e81987c4e Merge remote-tracking branch 'origin/main' into lu/flux 2026-05-07 10:36:11 +02:00
Felix Roos 23dbe53013 add snapshots 2026-05-07 10:32:58 +02:00
froos 0e4d0484a3 Merge pull request 'Fix the documentation of stretch' (#1670) from vvolhejn/strudel:vv/fix-stretch-docs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1670
2026-05-07 09:32:37 +02:00
froos 37bf2e4dc7 Merge pull request 'Added .dockerignore' (#2005) from KuboOrk/strudel:docker/add-dockerignore into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2005
2026-05-07 00:45:30 +02:00
froos e69761397e Merge pull request 'fix midi numbers as notes for osc backend' (#2001) from jagen31/strudel:jagen-midinote-osc into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2001
2026-05-07 00:20:41 +02:00
froos 10c33f24f5 Merge pull request 'LFO documentation' (#2000) from scrappy_fiddler/strudel:lfo_docs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2000
2026-05-07 00:11:54 +02:00
Felix Roos ea0ec7edb9 fix format 2026-05-06 22:14:11 +02:00
froos f900642359 cross-reference signals page 2026-05-06 22:01:58 +02:00
froos 4cc63b8b01 Merge pull request 'fixed bug where withBase would silence a pattern if freq was unset' (#1994) from tyow/localstrudel:withbase-fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1994
2026-05-06 21:29:34 +02:00
Alex McLean 26832c0fe7 Merge pull request 'send manual deploy to cold.strudel.cc, and autodeploy to strudel.cc' (#2037) from move-domains into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2037
2026-04-05 12:42:49 +02:00
alex 01abc8ae79 send manual deploy to cold.strudel.cc, and autodeploy to strudel.cc 2026-04-05 11:37:40 +01:00
Alex McLean 84df683794 Merge pull request 'colorless version display implementation' (#2029) from tyow/localstrudel:versionIndicator into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2029
2026-04-05 12:12:57 +02:00
Alex McLean ca1aca0c00 Merge branch 'main' into versionIndicator 2026-04-05 12:12:20 +02:00
tyow e8668a1552 improved unofficial detection 2026-03-30 14:51:22 -04:00
tyow 86d65c72f0 colorless version display implementation 2026-03-30 14:39:14 -04:00
Aria 9f19e4616a Merge pull request 'Feature: Add replace and insert code helpers to codemirror' (#2021) from glossing/insert-code-helper into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2021
2026-03-15 19:25:56 +01:00
Aria e15ce7d402 Add replace and insert code helpers to codemirror 2026-03-14 14:48:57 -05:00
Aria ef5ddc90c2 Merge pull request 'Bug Fix: Fix typo in phase computation' (#1974) from glossing/phases-bugfix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1974
2026-03-14 20:44:37 +01:00
tyow ae32743f12 format 2026-03-13 14:11:38 -04:00
tyow 7294cd5443 Add error for when name arg in register is a pattern 2026-03-13 14:11:19 -04:00
Bernhard Wagner cdae661b24 docs: note that all/each require labeled patterns 2026-03-09 11:15:14 +01:00
Aria 28af99914f Merge branch 'main' into glossing/phases-bugfix 2026-03-03 18:42:33 +01:00
TodePond 63f42faca8 Update packages/core/pattern.mjs 2026-03-03 11:27:05 +01:00
Václav Volhejn 2624b97500 Merge branch 'main' of ssh://codeberg.org/vvolhejn/strudel into vv/fix-stretch-docs 2026-03-01 11:15:28 +01:00
Václav Volhejn 633b7287c0 Merge branch 'main' of ssh://codeberg.org/vvolhejn/strudel into vv/fix-stretch-docs 2026-03-01 11:13:14 +01:00
KuboOrk 3805ba8f76 Added .dockerignore 2026-02-28 13:39:07 +03:00
jagen31 82e88d4a28 fix midi numbers as notes for osc backend
note -> midinote
2026-02-23 01:24:53 +01:00
scrappy_fiddler 5cd45d5ea1 typo in lfo docs 2026-02-23 00:22:30 +01:00
scrappy_fiddler d930057000 lfo documentation 2026-02-23 00:18:42 +01:00
scrappy_fiddler daf27ed971 added lfo documentation 2026-02-23 00:11:17 +01:00
tyow 8520044f3e fixed bug where withBase would silence a pattern if freq was unset 2026-02-20 13:10:16 -05:00
froos 4a801ab668 Merge pull request 'fix bug where delaytime didn't appear in reference' (#1984) from tyow/localstrudel:delayt into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1984
2026-02-19 23:27:24 +01:00
froos 8f61a0aa90 Merge pull request 'getDuration' (#1980) from tyow/localstrudel:sampleDur into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1980
2026-02-19 23:26:21 +01:00
tyow 1e2705c07f format 2026-02-19 16:01:19 -05:00
tyow 3cefbf80bf moved to sampler 2026-02-19 13:59:30 -05:00
jeromew 81342fb469 Merge pull request '@strudel/transpiler plugin system' (#1985) from jeromew/strudel:transpiler-plugin into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1985
2026-02-19 17:43:31 +01:00
Felix Roos 761e2e347e refactor: localize, remove helpers file when there's no reuse 2026-02-18 23:47:47 +01:00
Felix Roos 3911142443 fix: remove outdated feature note 2026-02-18 20:56:41 +01:00
jeromew bc8517ade7 @strudel/transpiler plugin system 2026-02-18 10:57:25 +00:00
tyow a926346d89 fix bug where delaytime didn't appear in reference 2026-02-17 18:14:16 -05:00
tyow 0f4e6af49d fixed failing test 2026-02-16 17:16:00 -05:00
tyow c20a6673af Added getDuration, need to fix failing test 2026-02-15 16:58:35 +01:00
froos 6b00837a4d Merge pull request 'fix delaytime typo' (#1979) from tyow/localstrudel:delayt into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1979
2026-02-15 15:54:54 +01:00
tyow 807cce521c fix delaytime typo 2026-02-15 08:45:51 -05:00
froos aa49d9dcc2 Merge pull request 'fix: shuffle(1) returns source pattern' (#1968) from tyow/localstrudel:shuffleFix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1968
2026-02-15 00:24:30 +01:00
froos 60da901952 Merge pull request 'move log2 from composer to registered function' (#1977) from tyow/localstrudel:log2 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1977
2026-02-14 23:34:53 +01:00
froos 30cc92e863 Merge pull request 'Add packages/edo for edoScale() function' (#1430) from robmckinnon/strudel-edo-fork:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1430
2026-02-14 23:19:16 +01:00
froos 91203233c9 Merge pull request 'set and keep documentation' (#1975) from tyow/localstrudel:composerDocs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1975
2026-02-14 22:49:53 +01:00
tyow c52e8f3b38 move log2 from composer to registered function 2026-02-14 16:40:02 -05:00
tyow d5dbc5c428 typo 2026-02-14 16:16:16 -05:00
tyow 161d2ce056 Added note about pattern alignment 2026-02-14 16:13:37 -05:00
tyow 7cb8a151a4 slight language change 2026-02-13 14:36:22 -05:00
froos 6f108f7f28 Merge pull request 'Edit prebake in settings' (#1958) from JohnBjrk/strudel:edit-prebake-in-settings into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1958
2026-02-13 17:13:37 +01:00
John Björk 7c607c8d06 fix: vim :q command 2026-02-13 16:21:50 +01:00
Felix Roos be0b3c0d8b fix: change default prebake to not use global effect (to prevent glitches) 2026-02-13 13:46:35 +01:00
Felix Roos d3e9b9c020 chore: localize 2026-02-13 13:45:52 +01:00
Felix Roos c16b300144 fix: shove prebake eval out of react 2026-02-13 13:40:51 +01:00
Felix Roos 29a3543918 chore: dedupe cx + cleanup 2026-02-13 13:40:13 +01:00
Felix Roos 1e502c7e8c chore: simplify 2026-02-13 13:21:50 +01:00
Felix Roos 27a22d7609 fix: shove logic out of react 2026-02-13 10:02:58 +01:00
Felix Roos c515ba89a0 fix: don't use id attribute in reusable component.. 2026-02-13 10:02:44 +01:00
Felix Roos 7e3063038e chore: minimize changes 2026-02-13 10:02:14 +01:00
Felix Roos 9bc560b17f fix: cleanup keybinding events 2026-02-13 09:37:34 +01:00
Felix Roos bdeafbaf75 fix: remove prebake specific logic in transpiler 2026-02-13 08:42:20 +01:00
Felix Roos 8984055ea6 fix: remove empty body error -> fall back to silence 2026-02-13 08:40:27 +01:00
Felix Roos 04dcbda9a3 fix: cleanup 2026-02-13 08:36:46 +01:00
tyow d7067f3562 typo 2026-02-12 22:55:30 -05:00
tyow a39c9e6096 Attempt at keep documentation 2026-02-12 21:52:54 -05:00
tyow 9690f78c30 Initial set documentation 2026-02-12 21:13:17 -05:00
Aria cf845ddbab Fix typo in phase computation 2026-02-12 18:30:50 -06:00
froos 3f72b9b97a Merge pull request 'fix: delaytime jsdoc' (#1973) from delaytime-fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1973
2026-02-12 00:35:15 +01:00
Felix Roos a347bd607f fix: delaytime jsdoc 2026-02-12 00:12:04 +01:00
yaxu 3966659f18 Update .forgejo/workflows/deploy-hot.yml 2026-02-09 21:25:21 +01:00
yaxu 6d9b899ae4 Update .forgejo/workflows/deploy-hot.yml 2026-02-09 20:56:48 +01:00
yaxu d802907dac Update .forgejo/workflows/deploy-hot.yml 2026-02-09 20:49:38 +01:00
yaxu 2bea102dc1 Update .forgejo/workflows/deploy-hot.yml 2026-02-09 16:04:11 +01:00
yaxu d9e5c5b0c8 See if it runs without this test.. 2026-02-09 15:59:47 +01:00
yaxu 7c1304690f Update .forgejo/workflows/deploy-hot.yml 2026-02-09 15:58:14 +01:00
yaxu 3c366b37ae Update .forgejo/workflows/deploy-hot.yml 2026-02-09 15:57:36 +01:00
Alex McLean 52a206ceca hotfix: try to serve PRs hot 2026-02-09 14:44:50 +00:00
John Björk 23f6ff9bc4 Moved evaluateUserPrebake 2026-02-08 22:47:56 +01:00
John Björk 3e1d02cb3d Added codemirrors deps to website
Moved codemirrors deps to catalog
Added deps to website
Changed deps in codemirror package to use catalog deps
2026-02-08 22:34:33 +01:00
tyow 8a72f21257 add test 2026-02-08 12:45:47 -05:00
froos 2755a7eebf Merge pull request 'Feature: reconnectable MIDI inputs that keep track of last known CC state' (#1852) from unframework/strudel:nm/midi-input-state into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1852
2026-02-08 11:22:27 +01:00
unframework deb69ced9f Fix setting that uses a specific MIDI channel 2026-02-08 06:26:44 +01:00
tyow d5a9019220 shuffleFix 2026-02-07 21:17:49 -05:00
John Björk d0d2459e14 Missed one import - thank you eslint 2026-02-07 23:30:35 +01:00
John Björk a60a54147c Moved PrebakeCodeMirror to its own file 2026-02-07 23:15:12 +01:00
Felix Roos 6707c7893b move prebake nav down 2026-02-07 21:51:00 +01:00
froos 1d0e2bd0c7 Merge pull request 'Added host as prefix to pattern export' (#1962) from JohnBjrk/strudel:add-host-prefix-to-pattern-export into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1962
2026-02-07 21:40:02 +01:00
froos b5ef12a2b1 Merge pull request 'Bug: Set active pattern to viewing on init' (#1961) from JohnBjrk/strudel:fix-active-viewing-pattern-out-of-sync into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1961
2026-02-07 21:38:37 +01:00
froos e3dd4ab126 Merge pull request 'coarse: handle non-integer parameters more smoothly' (#1964) from pulu/strudel:non-integer-coarse into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1964
2026-02-07 16:16:09 +01:00
miranda 751f924a4d coarse: handle non-integer parameters more smoothly 2026-02-07 16:53:06 +02:00
John Björk 7b58bbf12b Remove icon buttons 2026-02-05 16:49:23 +01:00
John Björk b2cfd3dc3d Use ctrl-enter also on mac 2026-02-05 16:49:23 +01:00
John Björk 9f2237e11d Prebake in settings
Update prebake without reload
2026-02-05 16:49:23 +01:00
John Björk 6d99a7f399 Added host as prefix to pattern export 2026-02-05 16:46:18 +01:00
John Björk 5921bec5f4 Set active pattern to viewing on init 2026-02-05 16:35:36 +01:00
froos b4e171f5b4 Merge pull request 'fix 'Maximum call stack size exceeded'' (#1951) from Dsm0/strudel:overflow_fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1951
2026-02-05 10:26:30 +01:00
froos 90ad12300a Merge pull request 'feat: Add Helix keybindings' (#1821) from space-shell/strudel:space-shell/helix-keybindings into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1821
2026-02-05 10:12:58 +01:00
froos ffc6d12d87 Merge pull request 'scale & xen parity' (#1955) from tyow/localstrudel:xen-feats into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1955
2026-02-05 09:47:21 +01:00
tyow 1334276ec0 format 2026-02-04 20:29:44 -05:00
tyow cbf472fe0a creation of i control for tune and xen functions 2026-02-04 20:28:19 -05:00
tyow 4439599117 Merge branch 'main' into xen-feats 2026-02-04 15:31:33 -05:00
tyow 32754ef97a formatting uzu/strudel#1944 2026-02-04 12:44:49 -05:00
tyow 67bce82a62 added jsdoc documentation for tune re uzu/strudel#1944 2026-02-04 12:44:35 -05:00
tyow 5da3ae8df4 prelim work on uzu/strudel#1944 2026-02-04 12:42:59 -05:00
tyow eb3818eebb formatting uzu/strudel#1944 2026-02-04 12:34:51 -05:00
tyow e6b824ebff added jsdoc documentation for tune re uzu/strudel#1944 2026-02-04 12:34:28 -05:00
tyow c74dd95bd0 prelim work on uzu/strudel#1944 2026-02-04 12:31:57 -05:00
tyow 897a750f70 tagging 2026-02-02 16:10:47 -05:00
tyow 81d02cf5ae format 2026-02-02 16:08:01 -05:00
tyow a6dc6bb831 withBase 2026-02-02 16:07:40 -05:00
tyow 00a3c1acc0 clarified an example 2026-02-02 13:40:48 -05:00
tyow 8ff51d4468 small fixes 2026-02-01 18:16:19 -05:00
tyow 2e041e8647 added xen example 2026-02-01 13:53:50 -05:00
tyow 55095ba4e8 added examples to ftrans 2026-02-01 13:34:22 -05:00
tyow 530f45a7bb format 2026-02-01 00:39:42 -05:00
tyow 99295275ec ftrans draft 2026-02-01 00:39:22 -05:00
tyow c2e45eb36c code format 2026-01-31 14:07:26 -05:00
tyow 35e32a32e8 added frequency rounding 2026-01-31 14:07:13 -05:00
tyow 8def3f6af6 add note about octave assumption uzu/strudel#1944 2026-01-31 14:06:50 -05:00
tyow aa3698c4f4 Small wording changes, bring up to date with main, uzu/strudel#1944 2026-01-31 14:06:50 -05:00
tyow 6b525b4819 formatting uzu/strudel#1944 2026-01-31 14:06:50 -05:00
tyow 77ced19144 added jsdoc documentation for tune re uzu/strudel#1944 2026-01-31 14:06:28 -05:00
tyow 14cc5d989b feat: added Tune scales to Xen, improved documentation re uzu/strudel#1944 2026-01-31 14:06:28 -05:00
tyow 47ad4bb171 minor: remove space between Xen and Harmonic re uzu/strudel#1944 2026-01-31 14:05:36 -05:00
tyow e3ad6b1f0a added documentation for xen() re: uzu/strudel#1944 2026-01-31 14:05:36 -05:00
tyow 1e771d6150 prelim work on uzu/strudel#1944 2026-01-31 14:05:36 -05:00
froos 3ac4649536 Merge pull request 'improve xen documentation' (#1946) from tyow/localstrudel:xen-docs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1946
2026-01-31 10:02:54 +01:00
tyow 14be1ddf7f code format 2026-01-30 17:22:52 -05:00
tyow 207ce9cbec added frequency rounding 2026-01-30 16:59:33 -05:00
tyow 3abcc59874 adjust wording 2026-01-30 14:13:36 -05:00
tyow 7fc8f1c5c9 xen/scale parity improvements 2026-01-30 14:12:31 -05:00
Dsm0 016315afbc fix 'Maximum call stack size exceeded' 2026-01-28 17:06:16 -08:00
space-shell b3c0d9179f Merge branch 'main' into space-shell/helix-keybindings 2026-01-28 09:59:38 +01:00
froos 993dc53e92 Merge pull request 'fix: pickF / pickmodF with object lookup' (#1924) from robojumper/strudel:fix/pickF-object-name-lookup into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1924
2026-01-28 00:03:57 +01:00
robojumper 60345db1bd Fix pickF / pickmodF with object lookup 2026-01-27 23:16:29 +01:00
tyow a09349e457 add note about octave assumption uzu/strudel#1944 2026-01-26 17:52:25 -05:00
tyow 71f538b115 Small wording changes, bring up to date with main, uzu/strudel#1944 2026-01-26 17:50:38 -05:00
Alex McLean 70be560a7a Merge pull request 'block based eval' (#1714) from Dsm0/strudel:block-based-eval-squashed into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1714
2026-01-25 23:57:37 +01:00
Alex McLean 269758ebdb Merge branch 'main' into block-based-eval-squashed 2026-01-25 23:49:44 +01:00
froos 98d613fbdf Merge pull request 'add lots of synonyms' (#1801) from TodePond/strudel:lu/synonyms into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1801
2026-01-25 20:30:51 +01:00
Felix Roos f5e21a220b Merge remote-tracking branch 'origin/main' into lu/synonyms 2026-01-25 20:27:52 +01:00
froos f6e6df78a8 Merge pull request 'improve reference tags' (#1945) from reference-tagging into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1945
2026-01-25 14:14:36 +01:00
tyow 05ae80b0ca formatting uzu/strudel#1944 2026-01-24 14:30:15 -05:00
tyow 3bbaf77b20 added jsdoc documentation for tune re uzu/strudel#1944 2026-01-24 14:18:51 -05:00
Felix Roos a9975a2b41 reference: show synonyms in the same row, grayed out, instead of adding a row for each synonym 2026-01-24 02:01:36 +01:00
tyow 2b3f719484 feat: added Tune scales to Xen, improved documentation re uzu/strudel#1944 2026-01-23 18:09:03 -05:00
Felix Roos 4051fd5241 oh snap 2026-01-23 23:52:59 +01:00
Felix Roos 55ffb59b9c tagging a lot of superdough functions 2026-01-23 23:40:51 +01:00
tyow 93cf9fcb6a minor: remove space between Xen and Harmonic re uzu/strudel#1944 2026-01-23 17:18:38 -05:00
tyow a5b867f1b1 added documentation for xen() re: uzu/strudel#1944 2026-01-23 16:32:41 -05:00
tyow d8645e9a71 prelim work on uzu/strudel#1944 2026-01-23 13:36:20 -05:00
Felix Roos b824ad16ae Merge remote-tracking branch 'origin/main' into reference-tagging 2026-01-23 10:00:17 +01:00
space-shell 03b221a2a2 Merge branch 'main' into space-shell/helix-keybindings 2026-01-23 09:17:09 +01:00
froos 28db6d1fd4 Merge pull request 'clean up crumbs of crunchy design' (#1942) from stylez into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1942
2026-01-22 22:15:01 +01:00
Aria dc9cadc979 Merge pull request 'Feat: Make default alignment configurable' (#1941) from glossing/alignment into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1941
Reviewed-by: Switch Angel AKA Jade Rose <daslyfe@noreply.codeberg.org>
2026-01-22 21:50:28 +01:00
Felix Roos f99b13efc2 fix: zen mode lost border 2026-01-22 21:46:48 +01:00
Aria 0001d9c27b Change name to setDefaultJoin 2026-01-22 14:24:45 -06:00
Aria d79d122f68 Add docstring and example 2026-01-22 14:08:26 -06:00
Aria 6e61f3cdb6 Make default alignment configurable 2026-01-22 13:52:57 -06:00
Felix Roos 4993ec4cbf fix: console scroll to bottom on change + more logs + dont wrap 2026-01-22 20:40:35 +01:00
Felix Roos 8e5ca57edd cleanup + pending state 2026-01-22 20:29:03 +01:00
Felix Roos 107a3acd3a rename panel components + fix borders 2026-01-22 20:24:04 +01:00
Felix Roos 1d887e81ef fix: h-full 2026-01-22 20:18:59 +01:00
froos 7f2860eb14 Merge pull request 'crunch up layout' (#1938) from stylez into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1938
2026-01-22 10:28:08 +01:00
Felix Roos 3364b9f05f keep it simple stupid 2026-01-22 10:12:16 +01:00
Felix Roos f9ed71fbf8 fix: import sounds font size 2026-01-22 01:35:19 +01:00
Felix Roos e9ea0bc7cc fix: typo 2026-01-22 01:32:04 +01:00
Felix Roos a87daf3d21 fix: patterns tab performance 2026-01-22 01:31:23 +01:00
Felix Roos 784b9c08ec copmuted settings 2026-01-22 01:16:57 +01:00
Felix Roos 139b5376fc rebrand hide button row to hide footer 2026-01-22 01:16:22 +01:00
Felix Roos a052def314 piping hot (main menu in footer) + border around panel toggle 2026-01-22 00:35:04 +01:00
Felix Roos bcce1fa68a make move sidepanel one layer up, so tabs are at the top and the toggle doesn't float 2026-01-21 23:38:25 +01:00
Felix Roos 9a10cac8e9 fix bottom panel 2026-01-21 23:07:49 +01:00
Felix Roos cf999462b7 fix: export tab placeholder color 2026-01-21 23:02:14 +01:00
Felix Roos 9fc084b291 tweaks: consistent font sizes and row heights, persist selected reference tag, add wrap prop to ButtonGroup 2026-01-21 22:47:15 +01:00
Felix Roos cb46dd7840 make it crunchier: border borders, less padding 2026-01-21 01:31:25 +01:00
froos b684e183a0 Merge pull request 'add muted theme color + tweak pane styles' (#1936) from stylez into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1936
2026-01-21 00:27:43 +01:00
Felix Roos 84e4dde3d7 mini tweaks 2026-01-21 00:15:00 +01:00
Felix Roos 5f8d028d4f cleanup 2026-01-21 00:04:18 +01:00
Felix Roos e3abdd41c0 make panel toggle floating 2026-01-21 00:03:19 +01:00
Felix Roos bce6ffde08 cleanup + tweak bottom panel style 2026-01-20 23:36:09 +01:00
Felix Roos 35a3e32238 no bg on button group 2026-01-20 23:08:31 +01:00
Felix Roos 60b5024f63 remove panel hover + optimize bottom panel 2026-01-20 23:06:04 +01:00
Felix Roos 4f14ff00d5 responsiveness 2026-01-20 22:29:16 +01:00
Felix Roos 3d3130e880 fine tune 2026-01-20 22:19:32 +01:00
Felix Roos 83d542d06a proper strudel icon + toggle menu from top bar 2026-01-20 22:13:03 +01:00
Aria 6105d5027d Merge pull request 'Feat: Add retrig option for LFOs' (#1934) from glossing/lfo-retrig into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1934
2026-01-20 04:37:19 +01:00
Felix Roos 7e34069021 fix more font sizes 2026-01-20 01:06:07 +01:00
Felix Roos b75ca193a2 fix: font sizes 2026-01-20 01:01:20 +01:00
Felix Roos afac1f62ca fix: adapt color-theme attribute on root based on theme 2026-01-20 00:59:43 +01:00
Felix Roos de83805fd9 add muted theme color + use it for inputs and tags 2026-01-20 00:06:44 +01:00
Felix Roos 38b7bcb3d2 style tweaks 2026-01-19 23:00:14 +01:00
Dsm0 c96b1c2faf fixed anonymous label casing 2026-01-19 11:53:08 -08:00
Dsm0 87e723649e fixed highlighting issue 2026-01-19 11:53:08 -08:00
Aria 62bf9abed3 Add retrig option for LFOs 2026-01-19 09:52:49 -08:00
space-shell e35a2000de reverts changelog changes because human... 2026-01-19 10:15:10 +01:00
space-shell 4b8d80016b Remove trailin whitespace from CHANGELOG.md 2026-01-19 10:13:48 +01:00
space-shell d2f2c6df02 Merge branch 'main' into space-shell/helix-keybindings 2026-01-19 10:05:14 +01:00
froos 8ec1d4d034 Merge pull request 'fix: move kabelsalat web to superdough' (#1932) from fix-kabelsalat-dependency into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1932
2026-01-18 22:27:45 +01:00
Felix Roos 6981638f70 fix: move kabelsalat web to superdough 2026-01-18 22:04:49 +01:00
Felix Roos 9bcaaa3ac4 Merge branch 'main' into block-based-eval-squashed 2026-01-18 13:09:00 +01:00
froos f5823ce981 Merge pull request 'Add tags to reference' (#1645) from vvolhejn/strudel:radical-new-docs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1645
2026-01-18 12:36:49 +01:00
Václav Volhejn f8750901f0 Tag the remaining functions 2026-01-18 12:16:50 +01:00
Václav Volhejn c1fd8c82c6 Merge branch 'radical-new-docs' of ssh://codeberg.org/vvolhejn/strudel into radical-new-docs 2026-01-18 12:08:17 +01:00
Václav Volhejn 6e36bdfff7 Add tags for some of the new functions 2026-01-18 12:08:12 +01:00
Felix Roos 8dbdd0a9b8 Merge branch 'main' into radical-new-docs 2026-01-18 10:39:53 +01:00
froos f4c0281613 Merge pull request 'fix: performance issues when reference is open' (#1926) from robojumper/strudel:fix/reference-performance-issue into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1926
2026-01-18 10:36:00 +01:00
Jade (Rose) Rowland f610965f43 Publish
- @strudel/codemirror@1.3.0
 - @strudel/csound@1.3.0
 - @strudel/midi@1.3.0
 - @strudel/repl@1.3.0
 - @strudel/soundfonts@1.3.0
 - superdough@1.3.0
 - @strudel/web@1.3.0
 - @strudel/webaudio@1.3.0
2026-01-17 19:12:20 -08:00
Jade (Rose) Rowland 17ef84ed70 sp 2026-01-17 19:11:08 -08:00
Jade (Rose) Rowland f0cb96dcad patch 2026-01-17 19:02:25 -08:00
Jade (Rose) Rowland f4f04a7d57 Publish
- @strudel/codemirror@1.2.8
 - @strudel/csound@1.2.8
 - @strudel/midi@1.2.8
 - @strudel/osc@1.3.2
 - @strudel/repl@1.2.9
 - @strudel/soundfonts@1.2.8
 - superdough@1.2.5
 - supradough@1.2.4
 - @strudel/web@1.2.8
 - @strudel/webaudio@1.2.9
2026-01-17 19:01:10 -08:00
Jade (Rose) Rowland a3ac9646f5 inc webaudio 2026-01-17 18:54:39 -08:00
Jade (Rose) Rowland 12b89f9f88 rollback supradough 2026-01-17 16:12:51 -08:00
Jade (Rose) Rowland 166985a055 spr 2026-01-17 16:00:34 -08:00
Jade (Rose) Rowland 85e787214b rollback_supradough 2026-01-17 15:59:38 -08:00
Václav Volhejn 99cf256ee2 Clean up 2 2026-01-17 20:11:26 +01:00
Václav Volhejn 42d0bfadce Clean up 2026-01-17 20:08:23 +01:00
Václav Volhejn 22f78fbb2f Merge branch 'radical-new-docs' of ssh://codeberg.org/vvolhejn/strudel into radical-new-docs 2026-01-17 19:24:41 +01:00
Václav Volhejn 77076a1c3e Improve reference search/filtering semantics 2026-01-17 19:12:23 +01:00
Václav Volhejn a1d31df556 Merge branch 'main' of ssh://codeberg.org/vvolhejn/strudel into radical-new-docs 2026-01-17 18:22:56 +01:00
robojumper f9d0e154e8 fix: performance issues when reference is open 2026-01-17 17:51:32 +01:00
Dsm0 2e7ec9ea27 variable declarations with active patterns now have highlighting 2026-01-15 19:27:54 -08:00
Nick Matantsev bc9e97b9ac Add comments 2026-01-15 00:13:49 -05:00
Nick Matantsev 00f0ef56bc Merge remote-tracking branch 'origin/main' into nm/midi-input-state 2026-01-14 23:55:02 -05:00
Nick Matantsev 44ad05fb20 Whitespace fixes 2026-01-14 23:37:26 -05:00
space-shell 18cc1e4466 Merge branch 'space-shell/helix-keybindings' of codeberg.org:space-shell/strudel into space-shell/helix-keybindings 2026-01-13 19:49:49 +01:00
space-shell 055f009357 feat: Refactor Vim/Helix keybindings with shared helper functions
Factor out the repl-eval and repl-stop event dispatching code into
replEval() and replStop() helper functions that are shared between
Vim and Helix keybindings.

This refactoring:
- Reduces code duplication between Vim :w/:q and Helix :w/:q commands
- Makes the keybinding handlers more maintainable
- Properly implements Helix commands using the commands.of() API
- Ensures consistent behavior across both keybinding modes

The Helix keybindings now properly integrate with the REPL using the
same event dispatch mechanism as Vim, dispatching custom repl-evaluate
and repl-stop events with fallbacks to keyboard events.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-13 19:47:12 +01:00
Felix Roos 9ffd0e496f fix: all doc 2026-01-11 12:33:03 +01:00
Felix Roos 550d7ff6cb fix: react error 2026-01-11 12:32:54 +01:00
Nick Matantsev 2fe1712dae Merge remote-tracking branch 'upstream/main' into nm/midi-input-state 2026-01-10 12:33:23 -05:00
Václav Volhejn 726e5ef14a Fix formatting 2026-01-10 07:18:29 -08:00
Václav Volhejn cc15e3cd36 Merge branch 'main' of ssh://codeberg.org/vvolhejn/strudel into radical-new-docs 2026-01-10 06:58:39 -08:00
Václav Volhejn 7c22b29493 Merge branch 'main' of ssh://codeberg.org/vvolhejn/strudel into vv/fix-stretch-docs 2026-01-10 06:34:34 -08:00
space-shell 0ef30d5e8d Merge branch 'main' into space-shell/helix-keybindings 2026-01-05 10:49:16 +01:00
Dsm0 160ef84b47 move evaluate/evaluateBlock to different functions 2025-12-30 20:14:32 -06:00
Dsm0 1a7f464998 requested revisions 1 (and fix failing test) 2025-12-30 18:34:26 -06:00
Nick Matantsev ee326c709e Fix MIDI input logging 2025-12-25 21:43:06 -05:00
Nick Matantsev a0291e8a42 Fix comments 2025-12-25 21:07:39 -05:00
Nick Matantsev ecada58edc Carry out MIDI input wrapper into dedicated file 2025-12-25 20:59:50 -05:00
Nick Matantsev bb7f587025 Encapsulate more logic 2025-12-25 20:42:11 -05:00
Nick Matantsev dab1a018e4 Support delayed connection and reconnection of MIDI inputs 2025-12-25 20:42:11 -05:00
Nick Matantsev 0c01a997ac Avoid referencing found MIDI device name 2025-12-25 20:42:11 -05:00
Nick Matantsev b8dc50267e Clean up unused state fields 2025-12-25 20:42:11 -05:00
Nick Matantsev 1aaa04ab50 Send stored CC values to controller 2025-12-25 20:42:11 -05:00
Nick Matantsev 1860d31624 Tweak cc init 2025-12-25 20:42:11 -05:00
Nick Matantsev 47557cf06f Move device lookup inside input wrapper 2025-12-25 20:42:11 -05:00
Nick Matantsev 213edfb211 Roll up save/load state into MIDI wrapper 2025-12-25 20:42:11 -05:00
Nick Matantsev 84e59645fd Encapsulate MIDI input handling 2025-12-25 20:42:11 -05:00
Nick Matantsev 4df19f9ff8 Save MIDI-in CC state 2025-12-25 20:42:11 -05:00
Dsm0 ba7721f53c code formatting 2025-12-23 02:25:18 -08:00
Dsm0 fd1e3d248a block based eval 2025-12-23 01:47:06 -08:00
space-shell bad81e3850 Merge branch 'main' into space-shell/helix-keybindings 2025-12-22 13:01:22 +01:00
space-shell a409149dae Merge branch 'main' into space-shell/helix-keybindings 2025-12-15 13:48:44 +01:00
space-shell a6b3b9ef5e updates formatting 2025-12-15 13:22:13 +01:00
space-shell ec73c48e2e feat: Add Helix keybindings and Nix development environment
Add support for Helix editor keybindings in the REPL, providing users
with a modern, selection-first modal editing experience as an alternative
to Vim, Emacs, and VSCode keybindings.

Helix Keybindings Integration:

- Add codemirror-helix@^0.5.0 dependency to @strudel/codemirror
- Import and register helix keybindings in keybindings.mjs
- Add 'Helix' option to keybindings selector in Settings UI
- Create comprehensive user documentation at technical-manual/helix.mdx
- Update developer documentation with keybinding integration pattern

The Helix mode provides selection-first editing (select → action) which
differs from Vim's action-motion paradigm, offering an alternative
workflow for modal editing enthusiasts.

Documentation:

- Update CLAUDE.md with Helix support and keybinding addition process
- Create helix.mdx with user-facing documentation and command reference
- Update CHANGELOG.md with December 2025 entry

Testing:

Run `pnpm i` to install new dependencies, then `pnpm dev` to test.
Verify Helix mode works in Settings → Keybindings → Helix.
2025-12-08 09:44:02 +01:00
Lu[ke] Wilson c77675c492 add another synonym 2025-11-29 18:01:27 +00:00
Lu[ke] Wilson 5197af1aa4 Merge remote-tracking branch 'origin/main' into lu/flux 2025-11-29 17:58:41 +00:00
Lu[ke] Wilson 65dd79e374 add lots of synonyms 2025-11-29 17:44:33 +00:00
Václav Volhejn 2fa8ed1424 Revert seq synonym refactor 2025-11-09 20:21:19 +01:00
Václav Volhejn b7827cb89f Tag new functions 2025-11-01 21:11:57 +01:00
Václav Volhejn 29fd541f1f Merge branch 'radical-new-docs' of ssh://codeberg.org/vvolhejn/strudel into radical-new-docs 2025-11-01 20:20:07 +01:00
Václav Volhejn 0830a9df69 Add superdirt tags 2025-11-01 20:19:57 +01:00
froos 85f6472894 Merge branch 'main' into lu/flux 2025-10-28 23:53:44 +01:00
froos 4159c8394d Merge branch 'main' into radical-new-docs 2025-10-28 22:02:10 +01:00
Václav Volhejn 466bf819d8 Fix formatting (trailing space) 2025-10-19 21:38:46 +02:00
Václav Volhejn 7e4cbefc8f Tag crossfade as internals 2025-10-19 21:22:57 +02:00
Václav Volhejn 2483c673a4 Effects -> fx, untag "crossfade" 2025-10-19 21:19:42 +02:00
Václav Volhejn 69360b3ec8 Also tag "noise" 2025-10-19 21:17:25 +02:00
Václav Volhejn 96d0f2053e Add supradough controls 2025-10-19 21:13:14 +02:00
Václav Volhejn b952e4c735 Add superdough tags 2025-10-19 20:54:22 +02:00
Václav Volhejn 54fd877be1 Update snapshot 2025-10-18 13:19:18 +02:00
Václav Volhejn 7d948fd1a7 Fix example snapshot 2025-10-18 12:25:15 +02:00
Václav Volhejn 3373fa39a6 Fix the documentation of stretch 2025-10-18 12:14:39 +02:00
Václav Volhejn a4212589e5 Recategorize 2025-10-12 16:33:07 +02:00
Václav Volhejn ee1a894f3b Show tags in reference, more compact functions list 2025-10-12 16:28:05 +02:00
Václav Volhejn c62428ca27 Document remaining functions 2025-10-12 16:07:11 +02:00
Václav Volhejn d516d765d5 Redo frontend for tags 2025-10-11 14:12:45 +02:00
Václav Volhejn 5032b4e966 Rename group -> tags 2025-10-11 14:11:50 +02:00
Václav Volhejn a4d7803d43 Annotate controls.mjs 2025-10-02 21:59:45 +02:00
Václav Volhejn 4a395f372d Annote pattern.mjs and signal.mjs 2025-10-01 23:45:48 +02:00
Václav Volhejn c754dab1ef Order groups 2025-10-01 22:31:03 +02:00
Václav Volhejn df54404733 Group by category 2025-10-01 22:21:03 +02:00
froos 1de8123437 Merge branch 'main' into lu/flux 2025-09-14 01:12:28 +02:00
Lu[ke] Wilson 781c477078 rename to juxFlip 2025-08-29 17:20:18 +01:00
Lu[ke] Wilson e2cdce5ac9 add flux function 2025-08-29 15:35:41 +01:00
robmckinnon ad331c8c2e Show EDO intervals in _pitchwheel() display 2025-06-29 10:49:04 +02:00
robmckinnon 43293dc80f Import packages/edo into strudel 2025-06-29 10:49:04 +02:00
robmckinnon 73965a3a39 Add packages/edo for Equal Division of the Octave scales 2025-06-29 10:49:04 +02:00
179 changed files with 7215 additions and 1706 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"devToolbar": {
"enabled": false
}
}
+11
View File
@@ -0,0 +1,11 @@
**/node_modules
**/.angular
**/dist
.astro/
.forgejo/
.idea/
.vscode/
test/
.gitignore
Dockerfile
.dockerignore
+44
View File
@@ -0,0 +1,44 @@
name: Build and Deploy hot PRs
on:
pull_request_target:
types: [labeled]
# Allow one concurrent deployment
concurrency:
group: "warm-pages"
cancel-in-progress: false
jobs:
build:
if: ${{ github.event.label.name == 'serve-hot' }}
runs-on: docker
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: pnpm/action-setup@v4
with:
version: 9.12.2
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Deploy
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
eval $(ssh-agent -s)
echo "$SSH_PRIVATE_KEY" | ssh-add -
apt update && apt install -y rsync
mkdir -p ~/.ssh
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
rsync -atv --delete --delete-after --progress \
./website/dist/ \
strudel@matrix.toplap.org:/home/strudel/deploy/pr-${{ github.event.pull_request.number }}.hot.strudel.cc
+2 -2
View File
@@ -1,4 +1,4 @@
name: Build and Deploy to beta (warm.strudel.cc)
name: Build and Deploy to warm (strudel.cc)
on:
push:
@@ -37,4 +37,4 @@ jobs:
apt update && apt install -y rsync
mkdir ~/.ssh
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/warm.strudel.cc
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
+2 -2
View File
@@ -1,4 +1,4 @@
name: Build and Deploy to live (strudel.cc)
name: Build and Deploy to cold (cold.strudel.cc)
on: [workflow_dispatch]
@@ -34,4 +34,4 @@ jobs:
apt update && apt install -y rsync
mkdir ~/.ssh
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/cold.strudel.cc
+1
View File
@@ -29,6 +29,7 @@ const editor = new StrudelMirror({
import('@strudel/core'),
import('@strudel/draw'),
import('@strudel/mini'),
import('@strudel/edo'),
import('@strudel/tonal'),
import('@strudel/webaudio'),
);
+1
View File
@@ -15,6 +15,7 @@
"@strudel/codemirror": "workspace:*",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/edo": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/soundfonts": "workspace:*",
"@strudel/tonal": "workspace:*",
+1
View File
@@ -4,6 +4,7 @@ export * from './packages/core/index.mjs';
export * from './packages/csound/index.mjs';
export * from './packages/desktopbridge/index.mjs';
export * from './packages/draw/index.mjs';
export * from './packages/edo/index.mjs';
export * from './packages/embed/index.mjs';
export * from './packages/hydra/index.mjs';
export * from './packages/midi/index.mjs';
+7
View File
@@ -12,6 +12,13 @@ function defineTags(dictionary) {
doclet.synonyms = doclet.synonyms_text.split(/[ ,]+/);
},
});
dictionary.defineTag('tags', {
mustHaveValue: true,
onTagged: function (doclet, tag) {
doclet.tags = tag.value.split(/[ ,]+/);
},
});
}
module.exports = { defineTags: defineTags };
+211
View File
@@ -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)
+1
View File
@@ -48,6 +48,7 @@
"homepage": "https://strudel.cc",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/edo": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
+51
View File
@@ -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;
};
+88 -24
View File
@@ -13,18 +13,18 @@ import {
} from '@codemirror/view';
import { persistentAtom } from '@nanostores/persistent';
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 { basicSetup } from './basicSetup.mjs';
import { evalBlock } from './block_utilities.mjs';
import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.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 { isTooltipEnabled } from './tooltip.mjs';
import { updateWidgets, widgetPlugin } from './widget.mjs';
import { jumpToCharacter } from './labelJump.mjs';
import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs';
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
@@ -64,6 +64,7 @@ export const defaultSettings = {
isLineWrappingEnabled: false,
isTabIndentationEnabled: false,
isMultiCursorEnabled: false,
isBlockBasedEvalEnabled: false,
theme: 'strudelTheme',
fontFamily: 'monospace',
fontSize: 18,
@@ -75,7 +76,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
});
// 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 initialSettings = Object.keys(compartments).map((key) =>
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
@@ -105,11 +106,26 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
keymap.of([
{
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',
run: () => onEvaluate?.(),
run: () => {
if (strudelMirror?.isBlockBasedEvalEnabled) {
evalBlock(strudelMirror);
return true;
} else {
return onEvaluate?.();
}
},
},
{
key: 'Ctrl-.',
@@ -171,6 +187,7 @@ export class StrudelMirror {
this.onDraw = onDraw || this.draw;
this.id = id || s4();
this.solo = solo;
this.isBlockBasedEvalEnabled = false; // Will be updated via updateSettings()
this.drawer = new Drawer((haps, time, _, painters) => {
const currentFrame = haps.filter((hap) => hap.isActive(time));
@@ -201,20 +218,28 @@ export class StrudelMirror {
cleanupDraw(true, id);
}
},
beforeEval: async () => {
cleanupDraw(true, id);
beforeEval: async ({ blockBased } = {}) => {
// 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 replOptions?.beforeEval?.();
},
afterEval: (options) => {
// remember for when highlighting is toggled on
this.miniLocations = options.meta?.miniLocations;
this.widgets = options.meta?.widgets;
this.miniLocations = options.meta?.miniLocations || [];
this.widgets = options.meta?.widgets || [];
const sliders = this.widgets.filter((w) => w.type === 'slider');
updateSliderWidgets(this.editor, sliders);
const widgets = this.widgets.filter((w) => w.type !== 'slider');
updateWidgets(this.editor, widgets);
updateMiniLocations(this.editor, this.miniLocations);
// range-aware update for block-based evaluation
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);
// if no painters are set (.onPaint was not called), then we only need
// the present moment (for highlighting)
@@ -222,8 +247,14 @@ export class StrudelMirror {
this.drawer.setDrawTime(drawTime);
// invalidate drawer after we've set the appropriate drawTime
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({
root,
initialCode,
@@ -236,7 +267,9 @@ export class StrudelMirror {
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
mondo: replOptions.mondo,
strudelMirror: this,
});
const cmEditor = this.root.querySelector('.cm-editor');
if (cmEditor) {
this.root.style.display = 'block';
@@ -260,6 +293,9 @@ export class StrudelMirror {
// Handle global evaluation requests (e.g., from Vim :w)
this.onEvaluateRequest = (e) => {
try {
if (e.detail.view !== this.editor) {
return; // ignore events from other editors
}
// Evaluate current editor on repl-evaluate
logger('[repl] evaluate via event');
this.evaluate();
@@ -274,6 +310,9 @@ export class StrudelMirror {
// Toggle comments requested from Vim (gc)
this.onToggleComment = (e) => {
try {
if (e.detail.view !== this.editor) {
return; // ignore events from other editors
}
// Honor selections; toggleLineComment handles both selections and
// single line
toggleLineComment(this.editor);
@@ -306,13 +345,17 @@ export class StrudelMirror {
this.flash();
await this.repl.evaluate(this.code, autostart);
}
async stop() {
this.repl.scheduler.stop();
this.repl.stop();
}
// Listen for global stop requests (e.g., from Vim :q)
onStopRequest = (e) => {
try {
if (e.detail.view !== this.editor) {
return; // ignore events from other editors
}
this.stop();
e?.cancelable && e.preventDefault?.();
} catch (err) {
@@ -326,8 +369,8 @@ export class StrudelMirror {
this.evaluate();
}
}
flash(ms) {
flash(this.editor, ms);
flash(ms, range) {
flash(this.editor, ms, range);
}
highlight(haps, time) {
highlightMiniLocations(this.editor, time, haps);
@@ -359,6 +402,10 @@ export class StrudelMirror {
setLineWrappingEnabled(enabled) {
this.reconfigureExtension('isLineWrappingEnabled', enabled);
}
setBlockBasedEvalEnabled(enabled) {
this.reconfigureExtension('isBlockBasedEvalEnabled', enabled);
}
setBracketMatchingEnabled(enabled) {
this.reconfigureExtension('isBracketMatchingEnabled', enabled);
}
@@ -380,6 +427,10 @@ export class StrudelMirror {
for (let key in extensions) {
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 };
codemirrorSettings.set(updated);
}
@@ -393,14 +444,26 @@ export class StrudelMirror {
this.setFontSize(value);
}
}
setCode(code) {
const changes = {
from: 0,
to: this.editor.state.doc.length,
insert: code,
};
replaceCode(code, from, to) {
const changes = { from, to, insert: code };
this.editor.dispatch({ changes });
}
insertCode(code, position) {
this.replaceCode(code, position, position);
}
setCode(code) {
this.replaceCode(code, 0, this.editor.state.doc.length);
}
// used for debugging but could serve other purposes
getActiveWidgets() {
return getActiveWidgets(this.editor);
}
getSliderWidgets() {
return getSliderWidgets(this.editor);
}
getMiniLocations() {
return this.miniLocations;
}
clear() {
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest);
@@ -434,6 +497,7 @@ function s4() {
/**
* Overrides the css of highlighted events. Make sure to use single quotes!
* @name markcss
* @tag visualization
* @example
* note("c a f e")
* .markcss('text-decoration:underline')
+5 -3
View File
@@ -14,7 +14,8 @@ export const flashField = StateField.define({
const mark = Decoration.mark({
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 {
flash = Decoration.set([]);
}
@@ -29,8 +30,9 @@ export const flashField = StateField.define({
provide: (f) => EditorView.decorations.from(f),
});
export const flash = (view, ms = 200) => {
view.dispatch({ effects: setFlash.of(true) });
export const flash = (view, ms = 200, range) => {
const flashData = range ? { range } : true;
view.dispatch({ effects: setFlash.of(flashData) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, ms);
+164 -63
View File
@@ -3,8 +3,9 @@ import { Decoration, EditorView } from '@codemirror/view';
export const setMiniLocations = StateEffect.define();
export const showMiniLocations = StateEffect.define();
export const updateMiniLocations = (view, locations) => {
view.dispatch({ effects: setMiniLocations.of(locations) });
export const displayMiniLocations = StateEffect.define();
export const updateMiniLocations = (view, locations, range = null) => {
view.dispatch({ effects: setMiniLocations.of({ locations, range }) });
};
export const highlightMiniLocations = (view, atTime, haps) => {
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
@@ -21,23 +22,54 @@ const miniLocations = StateField.define({
for (let e of tr.effects) {
if (e.is(setMiniLocations)) {
// 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
.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
);
//block-based eval case
if (e.value.range) {
const stateMiniLocations = getMiniLocationsFromDecorations(locations);
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 miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
const iterator = state.field(miniLocations).iter();
const { haps } = state.field(visibleMiniLocations);
const builder = new RangeSetBuilder();
const displayMiniLocationsState = StateField.define({
create() {
return true; // default to showing miniLocations
},
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) {
const {
from,
@@ -89,50 +192,48 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
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 },
}),
);
}
miniLocationsArray.push({
from,
to,
id,
});
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) => {
on &&
config &&
setTimeout(() => {
updateMiniLocations(config.editor, config.miniLocations);
}, 100);
return on ? Prec.highest(highlightExtension) : [];
// NOTE:
// Modified this function to always return the highlightExtension, and instead just toggle whether or not the miniLocations are displayed.
// This is because block based evaluation only updates regions of miniLocations, and those updates need to be kept track of constantly.
// The setTimeout was also removed because it conflicted with the range-specific updates required by
// block based evaluation.
// 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);
};
+1
View File
@@ -5,3 +5,4 @@ export * from './slider.mjs';
export * from './themes.mjs';
export * from './widget.mjs';
export { Vim } from './keybindings.mjs';
export * from './basicSetup.mjs';
+110 -58
View File
@@ -6,6 +6,7 @@ import { emacs } from '@replit/codemirror-emacs';
import { vim, Vim } from '@replit/codemirror-vim';
// import { vim } from './vim_test.mjs';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import { helix, commands } from 'codemirror-helix';
import { logger } from '@strudel/core';
const vscodePlugin = ViewPlugin.fromClass(
@@ -20,6 +21,70 @@ const vscodePlugin = ViewPlugin.fromClass(
);
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
// 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
@@ -31,7 +96,7 @@ try {
// internal actions and works with current selections/visual mode.
try {
Vim.defineAction('strudelToggleComment', (cm) => {
const view = cm?.view || cm;
const view = cm.cm6;
try {
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
document.dispatchEvent(ev);
@@ -47,74 +112,23 @@ try {
// :q to pause/stop
Vim.defineEx('quit', 'q', (cm) => {
try {
const view = cm?.view || cm;
// 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);
}
const view = cm.cm6;
logger('[vim] :q — stopping repl');
replStop(view);
});
// :w to evaluate
Vim.defineEx('write', 'w', (cm) => {
const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself
const view = cm.cm6;
try {
view?.focus?.();
view.focus?.();
// Let the app know this came from Vim :w
try {
logger('[vim] :w — evaluating code');
} catch (e) {
console.error('Error logging Vim :w evaluation', e);
}
// 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);
}
replEval(view);
} catch (e) {
console.error('Error dispatching :w evaluation event', e);
}
@@ -124,11 +138,49 @@ try {
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 = {
vim,
emacs,
codemirror: () => keymap.of(defaultKeymap),
vscode: vscodeExtension,
helix: () => [helix(), helixCommands],
};
export { Vim } from '@replit/codemirror-vim';
+9 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "1.2.7",
"version": "1.3.0",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
@@ -32,18 +32,19 @@
},
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.18.4",
"@codemirror/commands": "^6.8.0",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/language": "^6.10.8",
"@codemirror/search": "^6.5.8",
"@codemirror/state": "^6.5.1",
"@codemirror/view": "^6.36.2",
"@codemirror/autocomplete": "catalog:",
"@codemirror/commands": "catalog:",
"@codemirror/lang-javascript": "catalog:",
"@codemirror/language": "catalog:",
"@codemirror/search": "catalog:",
"@codemirror/state": "catalog:",
"@codemirror/view": "catalog:",
"@lezer/highlight": "^1.2.1",
"@nanostores/persistent": "^0.10.2",
"@replit/codemirror-emacs": "^6.1.0",
"@replit/codemirror-vim": "^6.3.0",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"codemirror-helix": "^0.5.0",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/tonal": "workspace:*",
+107 -17
View File
@@ -2,11 +2,11 @@ import { ref, pure } from '@strudel/core';
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
import { StateEffect } from '@codemirror/state';
// Global state storage for all widget types
export let sliderValues = {};
const getSliderID = (from) => `slider_${from}`;
export class SliderWidget extends WidgetType {
constructor(value, min, max, from, to, step, view) {
constructor(value, min, max, from, to, step, view, id) {
super();
this.value = value;
this.min = min;
@@ -16,10 +16,21 @@ export class SliderWidget extends WidgetType {
this.to = to;
this.step = step;
this.view = view;
this.id = id || `${from}:${to}`; // Range-based ID for stability
}
eq() {
return false;
eq(other) {
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() {
@@ -38,6 +49,7 @@ export class SliderWidget extends WidgetType {
slider.from = this.from;
slider.originalFrom = this.originalFrom;
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)';
this.slider = slider;
slider.addEventListener('input', (e) => {
@@ -49,7 +61,7 @@ export class SliderWidget extends WidgetType {
slider.originalValue = insert;
slider.value = insert;
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 });
});
return wrap;
@@ -62,19 +74,60 @@ export class SliderWidget extends WidgetType {
export const setSliderWidgets = StateEffect.define();
export const updateSliderWidgets = (view, widgets) => {
view.dispatch({ effects: setSliderWidgets.of(widgets) });
export const setSliderWidgetsInRange = StateEffect.define();
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) {
return widgetConfigs
.filter((w) => w.type === 'slider')
.map(({ from, to, value, min, max, step }) => {
return Decoration.widget({
widget: new SliderWidget(value, min, max, from, to, step, view),
side: 0,
}).range(from /* , to */);
});
return (
widgetConfigs
.filter((w) => w.type === 'slider')
// Deduplicate sliders that might appear multiple times (e.g., during paste operations)
.filter((slider, index, self) => index === self.findIndex((s) => s.from === slider.from && s.to === slider.to))
.sort((a, b) => a.from - b.from)
.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(
@@ -101,7 +154,42 @@ export const sliderPlugin = ViewPlugin.fromClass(
}
}
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));
}
}
@@ -117,6 +205,7 @@ export const sliderPlugin = ViewPlugin.fromClass(
* Displays a slider widget to allow the user manipulate a value
*
* @name slider
* @tags external_io, visualization
* @param {number} value Initial value
* @param {number} min Minimum value - optional, defaults to 0
* @param {number} max Maximum value - optional, defaults to 1
@@ -131,6 +220,7 @@ export let sliderWithID = (id, value, min, max) => {
sliderValues[id] = value; // sync state at eval time (code -> state)
return ref(() => sliderValues[id]); // use state at query time
};
// update state when sliders are moved
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
@@ -139,7 +229,7 @@ if (typeof window !== 'undefined') {
// update state when slider is moved
sliderValues[e.data.id] = e.data.value;
} 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)}`);
}
}
});
+1
View File
@@ -198,6 +198,7 @@ export function activateTheme(name) {
const themeSettings = settings[name] || settings.strudelTheme;
// set css variables
themeStyle.innerHTML = `:root {
color-scheme: ${themeSettings.light ? 'light' : 'dark'};
${Object.entries(themeSettings)
// important to override fallback
.map(([key, value]) => `--${key}: ${value} !important;`)
+3 -1
View File
@@ -17,13 +17,15 @@ export const settings = {
background: 'white',
lineBackground: 'transparent',
foreground: deepPurple,
muted: deepPurple + '50',
caret: '#797977',
selection: yellowPink,
selectionMatch: '#2B323D',
gutterBackground: grey,
gutterBackground: pinkAccent,
gutterForeground: lightGrey,
gutterBorder: 'transparent',
lineHighlight: pinkAccent,
light: true,
};
export default createTheme({
+1
View File
@@ -31,6 +31,7 @@ const palette = palettes['Sour Watermelon B'];
export const settings = {
background: palette[3],
foreground: palette[1],
muted: palette[1] + '50',
caret: palette[0],
selection: palette[0],
selectionMatch: palette[1],
+2
View File
@@ -8,6 +8,7 @@ export const settings = {
background: '#282b2e',
lineBackground: '#282b2e99',
foreground: '#a9b7c6',
muted: '#a9b7c650',
caret: '#00FF00',
selection: '#343739',
selectionMatch: '#343739',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#282b2e',
foreground: '#a9b7c6',
muted: '#a9b7c650',
caret: '#00FF00',
selection: '#4e5254',
selectionMatch: '#4e5254',
+1
View File
@@ -12,6 +12,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[1],
muted: hex[2],
selection: hex[2],
selectionMatch: hex[0],
gutterBackground: hex[0],
+2
View File
@@ -11,6 +11,7 @@ export const settings = {
background: '#272C35',
lineBackground: '#272C3599',
foreground: 'hsl(220, 14%, 71%)',
muted: 'hsl(220, 14%, 41%)',
caret: '#797977',
selection: '#ffffff30',
selectionMatch: '#2B323D',
@@ -25,6 +26,7 @@ export default createTheme({
settings: {
background: '#272C35',
foreground: '#9d9b97',
muted: 'hsl(220, 14%, 41%)',
caret: '#797977',
selection: '#3d4c64',
selectionMatch: '#3d4c64',
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#21202e',
lineBackground: '#21202e99',
foreground: '#edecee',
muted: '#edecee50',
caret: '#a277ff',
selection: '#3d375e7f',
selectionMatch: '#3d375e7f',
@@ -18,6 +19,7 @@ export default createTheme({
settings: {
background: '#21202e',
foreground: '#edecee',
muted: '#edecee50',
caret: '#a277ff',
selection: '#5a51898f',
selectionMatch: '#5a51898f',
+2
View File
@@ -6,6 +6,7 @@ export const settings = {
background: '#FFFFFF',
lineBackground: '#FFFFFF99',
foreground: '#000000',
muted: '#00000050',
caret: '#FBAC52',
selection: '#FFD420',
selectionMatch: '#FFD420',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#FFFFFF',
foreground: '#000000',
muted: '#00000050',
caret: '#FBAC52',
selection: '#FFD420',
selectionMatch: '#FFD420',
+2 -1
View File
@@ -2,7 +2,8 @@ import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: 'black',
foreground: 'white', // whats that?
foreground: 'white',
muted: '#ffffff50',
caret: 'white',
selection: '#ffffff20',
selectionMatch: '#036dd626',
+2 -1
View File
@@ -3,7 +3,8 @@ import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#051DB5',
lineBackground: '#051DB550',
foreground: 'white', // whats that?
foreground: 'white',
muted: '#ffffff50',
caret: 'white',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
+1
View File
@@ -11,6 +11,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
muted: hex[3],
selection: hex[3],
selectionMatch: hex[0],
gutterBackground: hex[0],
+2
View File
@@ -9,6 +9,7 @@ export const settings = {
background: '#242424',
lineBackground: '#24242499',
foreground: '#f8f8f2',
muted: '#f8f8f250',
caret: '#FFFFFF',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
@@ -23,6 +24,7 @@ export default createTheme({
settings: {
background: '#242424',
foreground: '#f8f8f2',
muted: '#f8f8f250',
caret: '#FFFFFF',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
+2
View File
@@ -11,6 +11,7 @@ export const settings = {
background: '#282a36',
lineBackground: '#282a3699',
foreground: '#f8f8f2',
muted: '#f8f8f250',
caret: '#f8f8f0',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
@@ -27,6 +28,7 @@ export default createTheme({
settings: {
background: '#282a36',
foreground: '#f8f8f2',
muted: '#f8f8f250',
caret: '#f8f8f0',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
+2
View File
@@ -10,6 +10,7 @@ export const settings = {
background: '#2a2734',
lineBackground: '#2a273499',
foreground: '#eeebff',
muted: '#eeebff50',
caret: '#ffad5c',
selection: 'rgba(255, 255, 255, 0.1)',
gutterBackground: '#2a2734',
@@ -22,6 +23,7 @@ export default createTheme({
settings: {
background: '#2a2734',
foreground: '#6c6783',
muted: '#eeebff50',
caret: '#ffad5c',
selection: '#9a86fd',
selectionMatch: '#9a86fd',
+2
View File
@@ -6,6 +6,7 @@ export const settings = {
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#000',
muted: '#00000050',
caret: '#FFFFFF',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#fff',
foreground: '#000',
muted: '#00000050',
caret: '#FFFFFF',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
+1
View File
@@ -23,6 +23,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[10],
muted: hex[7],
selection: hex[8],
selectionMatch: hex[0],
gutterBackground: hex[3],
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#0d1117',
lineBackground: '#0d111799',
foreground: '#c9d1d9',
muted: '#c9d1d950',
caret: '#c9d1d9',
selection: '#003d73',
selectionMatch: '#003d73',
@@ -16,6 +17,7 @@ export default createTheme({
settings: {
background: '#0d1117',
foreground: '#c9d1d9',
muted: '#c9d1d950',
caret: '#c9d1d9',
selection: '#003d73',
selectionMatch: '#003d73',
+2
View File
@@ -6,6 +6,7 @@ export const settings = {
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#24292e',
muted: '#24292e50',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
@@ -17,6 +18,7 @@ export default createTheme({
settings: {
background: '#fff',
foreground: '#24292e',
muted: '#24292e50',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
+1
View File
@@ -13,6 +13,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
muted: hex[2] + '50',
selection: hex[4],
selectionMatch: hex[0],
gutterBackground: hex[0],
+2
View File
@@ -10,6 +10,7 @@ export const settings = {
background: '#282828',
lineBackground: '#28282899',
foreground: '#ebdbb2',
muted: '#ebdbb250',
caret: '#ebdbb2',
selection: '#bdae93',
selectionMatch: '#bdae93',
@@ -23,6 +24,7 @@ export default createTheme({
settings: {
background: '#282828',
foreground: '#ebdbb2',
muted: '#ebdbb250',
caret: '#ebdbb2',
selection: '#b99d555c',
selectionMatch: '#b99d555c',
+2
View File
@@ -11,6 +11,7 @@ export const settings = {
background: '#fbf1c7',
lineBackground: '#fbf1c799',
foreground: '#3c3836',
muted: '#3c383650',
caret: '#af3a03',
selection: '#ebdbb2',
selectionMatch: '#bdae93',
@@ -25,6 +26,7 @@ export default createTheme({
settings: {
background: '#fbf1c7',
foreground: '#3c3836',
muted: '#3c383650',
caret: '#af3a03',
selection: '#bdae9391',
selectionMatch: '#bdae9391',
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#212121',
lineBackground: '#21212199',
foreground: '#bdbdbd',
muted: '#bdbdbd50',
caret: '#a0a4ae',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#212121',
foreground: '#bdbdbd',
muted: '#bdbdbd50',
caret: '#a0a4ae',
selection: '#d7d4f063',
selectionMatch: '#d7d4f063',
+3 -1
View File
@@ -5,7 +5,8 @@ export const settings = {
light: true,
background: '#FAFAFA',
lineBackground: '#FAFAFA99',
foreground: '#90A4AE',
foreground: '#6182B8',
muted: '#6182B850',
caret: '#272727',
selection: '#80CBC440',
selectionMatch: '#FAFAFA',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#FAFAFA',
foreground: '#90A4AE',
muted: '#6182B850',
caret: '#272727',
selection: '#80CBC440',
selectionMatch: '#80CBC440',
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#272822',
lineBackground: '#27282299',
foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FFFFFF',
selection: '#49483E',
selectionMatch: '#49483E',
@@ -18,6 +19,7 @@ export default createTheme({
settings: {
background: '#272822',
foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FFFFFF',
selection: '#49483E',
selectionMatch: '#49483E',
+2
View File
@@ -6,6 +6,7 @@ export const settings = {
background: '#f2f1f8',
lineBackground: '#f2f1f899',
foreground: '#0c006b',
muted: '#0c006b50',
caret: '#5c49e9',
selection: '#d5d1f2',
selectionMatch: '#d5d1f2',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#f2f1f8',
foreground: '#0c006b',
muted: '#0c006b50',
caret: '#5c49e9',
selection: '#d5d1f2',
selectionMatch: '#d5d1f2',
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#2e3440',
lineBackground: '#2e344099',
foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FFFFFF',
selection: '#3b4252',
selectionMatch: '#e5e9f0',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#2e3440',
foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FFFFFF',
selection: '#00000073',
selectionMatch: '#00000073',
+1
View File
@@ -13,6 +13,7 @@ export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
muted: hex[2] + '50',
selection: hex[4],
selectionMatch: hex[0],
gutterBackground: hex[0],
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#002b36',
lineBackground: '#002b3699',
foreground: '#93a1a1',
muted: '#93a1a150',
caret: '#839496',
selection: '#173541',
selectionMatch: '#aafe661a',
@@ -45,6 +46,7 @@ export default createTheme({
settings: {
background: c.background,
foreground: c.foreground,
muted: c.muted,
caret: c.cursor,
selection: c.selection,
selectionMatch: c.selection,
+2
View File
@@ -8,6 +8,7 @@ export const settings = {
background: '#fdf6e3',
lineBackground: '#fdf6e399',
foreground: '#657b83',
muted: '#657b8350',
caret: '#586e75',
selection: '#dfd9c8',
selectionMatch: '#dfd9c8',
@@ -19,6 +20,7 @@ export const settings = {
const c = {
background: '#FDF6E3',
foreground: '#657B83',
muted: '#657b8350',
selection: '#EEE8D5',
selectionMatch: '#EEE8D5',
cursor: '#657B83',
+1
View File
@@ -13,6 +13,7 @@ export const settings = {
background: '#000000',
lineBackground: 'transparent',
foreground: hex[4],
muted: hex[6],
selection: hex[6],
gutterBackground: hex[0],
gutterForeground: hex[5],
+1
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#222',
lineBackground: '#22222299',
foreground: '#fff',
muted: '#8a919966',
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#303841',
lineBackground: '#30384199',
foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FBAC52',
selection: '#4C5964',
selectionMatch: '#3A546E',
@@ -18,6 +19,7 @@ export default createTheme({
settings: {
background: '#303841',
foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FBAC52',
selection: '#4C5964',
selectionMatch: '#3A546E',
+2 -1
View File
@@ -9,7 +9,8 @@ let colorD = '#f8fc55';
export const settings = {
background: '#000000',
foreground: colorA, // whats that?
foreground: colorA,
muted: colorA + '50',
caret: colorC,
selection: colorD,
selectionMatch: colorA,
+4 -2
View File
@@ -4,7 +4,8 @@ import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#24283b',
lineBackground: '#24283b99',
foreground: '#7982a9',
foreground: '#f9f2f9',
muted: '#f9f2f950',
caret: '#c0caf5',
selection: '#6f7bb630',
selectionMatch: '#1f2335',
@@ -18,7 +19,8 @@ export default createTheme({
theme: 'dark',
settings: {
background: '#24283b',
foreground: '#7982a9',
foreground: '#f9f2f9',
muted: '#f9f2f950',
caret: '#c0caf5',
selection: '#6f7bb630',
selectionMatch: '#343b5f',
+4 -2
View File
@@ -4,7 +4,8 @@ import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#1a1b26',
lineBackground: '#1a1b2699',
foreground: '#787c99',
foreground: '#f8fcf9',
muted: '#f8fcf950',
caret: '#c0caf5',
selection: '#515c7e40',
selectionMatch: '#16161e',
@@ -18,7 +19,8 @@ export default createTheme({
theme: 'dark',
settings: {
background: '#1a1b26',
foreground: '#787c99',
foreground: '#f8fcf9',
muted: '#f8fcf950',
caret: '#c0caf5',
selection: '#515c7e40',
selectionMatch: '#16161e',
+2
View File
@@ -6,6 +6,7 @@ export const settings = {
background: '#e1e2e7',
lineBackground: '#e1e2e799',
foreground: '#3760bf',
muted: '#3760bf50',
caret: '#3760bf',
selection: '#99a7df',
selectionMatch: '#99a7df',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#e1e2e7',
foreground: '#3760bf',
muted: '#3760bf50',
caret: '#3760bf',
selection: '#99a7df',
selectionMatch: '#99a7df',
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#1e1e1e',
lineBackground: '#1e1e1e99',
foreground: '#fff',
muted: '#ffffff50',
caret: '#c6c6c6',
selection: '#6199ff2f',
selectionMatch: '#72a1ff59',
@@ -19,6 +20,7 @@ export default createTheme({
settings: {
background: '#1e1e1e',
foreground: '#fff',
muted: '#ffffff50',
caret: '#c6c6c6',
selection: '#6199ff2f',
selectionMatch: '#72a1ff59',
+2
View File
@@ -5,6 +5,7 @@ export const settings = {
background: '#ffffff',
lineBackground: '#ffffff50',
foreground: '#383a42',
muted: '#383a4250',
caret: '#000',
selection: '#add6ff',
selectionMatch: '#a8ac94',
@@ -20,6 +21,7 @@ export default createTheme({
settings: {
background: '#ffffff',
foreground: '#383a42',
muted: '#383a4250',
caret: '#000',
selection: '#add6ff',
selectionMatch: '#a8ac94',
+2 -1
View File
@@ -2,7 +2,8 @@ import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: 'white',
foreground: 'black', // whats that?
foreground: 'black',
muted: '#00000050',
caret: 'black',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#ffffff26',
+2
View File
@@ -6,6 +6,7 @@ export const settings = {
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#3D3D3D',
muted: '#3D3D3D50',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
@@ -18,6 +19,7 @@ export default createTheme({
settings: {
background: '#fff',
foreground: '#3D3D3D',
muted: '#3D3D3D50',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
+194 -46
View File
@@ -1,55 +1,111 @@
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 { Pattern } from '@strudel/core';
export const addWidget = StateEffect.define({
map: ({ from, to }, change) => {
return { from: change.mapPos(from), to: change.mapPos(to) };
},
});
export const setWidgets = StateEffect.define();
export const updateWidgets = (view, widgets) => {
view.dispatch({ effects: addWidget.of(widgets) });
export const setWidgetsInRange = StateEffect.define();
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) {
return (
widgetConfigs
// codemirror throws an error if we don't sort
.sort((a, b) => a.to - b.to)
.map((widgetConfig) => {
function getWidgets(widgetConfigs, view) {
const filtered = widgetConfigs
// Filter to widget configs only (exclude sliders)
.filter((w) => w && w.type && w.type !== 'slider')
// Deduplicate widgets by ID, matching slider behavior for stable widget identity
.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({
widget: new BlockWidget(widgetConfig),
widget: new BlockWidget(widgetConfig, view),
side: 0,
block: true,
}).range(widgetConfig.to);
})
);
}).range(widgetConfig.to || widgetConfig.from || 0);
} catch (error) {
console.error('error creating widget', error);
return null;
}
})
.filter(Boolean); // Remove any null results from failed creations
}
const widgetField = StateField.define(
/* <DecorationSet> */ {
create() {
return Decoration.none;
},
update(widgets, tr) {
widgets = widgets.map(tr.changes);
for (let e of tr.effects) {
if (e.is(addWidget)) {
try {
widgets = widgets.update({
filter: () => false,
add: getWidgets(e.value),
});
} catch (error) {
console.log('err', error);
export const widgetPlugin = ViewPlugin.fromClass(
class {
decorations; //: DecorationSet
constructor(view /* : EditorView */) {
this.decorations = Decoration.set([]);
}
update(update /* : ViewUpdate */) {
update.transactions.forEach((tr) => {
if (tr.docChanged) {
this.decorations = this.decorations.map(tr.changes);
const iterator = this.decorations.iter();
// Apply changes to iterator.from and iterator.to if docChanged
while (iterator.value) {
// 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();
}
}
}
return widgets;
},
provide: (f) => EditorView.decorations.from(f),
for (let e of tr.effects) {
if (e.is(setWidgetsInRange)) {
// Block-aware widget update logic
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 {
constructor(widgetConfig) {
constructor(widgetConfig, view) {
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;
}
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() {
const id = getWidgetID(this.widgetConfig);
const el = widgetElements[id];
return el;
let wrap = document.createElement('span');
wrap.setAttribute('aria-hidden', 'true');
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) {
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
export function registerWidget(type, fn) {
File diff suppressed because it is too large Load Diff
+1
View File
@@ -15,6 +15,7 @@ import Fraction, { gcd } from './fraction.mjs';
* - "-" hold previous value
* - "." silence
*
* @tags visualization
* @param {Pattern} pattern the pattern to use
* @param {number} chars max number of characters (approximately)
* @returns string
+5
View File
@@ -61,6 +61,7 @@ export const bjorklund = function (ons, steps) {
*
* @memberof Pattern
* @name euclid
* @tags temporal
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @returns Pattern
@@ -73,6 +74,7 @@ export const bjorklund = function (ons, steps) {
* Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence.
* @memberof Pattern
* @name euclidRot
* @tags temporal
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @param {number} rotation offset in steps
@@ -156,6 +158,7 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
* so there will be no gaps.
* @name euclidLegato
* @memberof Pattern
* @tags temporal
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @param rotation offset in steps
@@ -187,6 +190,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
* the resulting sequence
* @name euclidLegatoRot
* @memberof Pattern
* @tags temporal
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @param {number} rotation offset in steps
@@ -208,6 +212,7 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s
* @name euclidish
* @synonyms eish
* @memberof Pattern
* @tags temporal
* @param {number} pulses the number of onsets
* @param {number} steps the number of steps to fill
* @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse)
+28
View File
@@ -5,6 +5,34 @@ This program is free software: you can redistribute it and/or modify it under th
*/
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) => {
const results = await Promise.allSettled(args);
-1
View File
@@ -31,7 +31,6 @@
},
"homepage": "https://strudel.cc",
"dependencies": {
"@kabelsalat/web": "^0.4.1",
"fraction.js": "^5.2.1"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
+440 -109
View File
File diff suppressed because it is too large Load Diff
+40 -22
View File
@@ -28,6 +28,7 @@ const _pick = function (lookup, pat, modulo = true) {
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `inhabit`, but maintains the structure of the original patterns.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -57,6 +58,7 @@ const __pick = register('pick', function (lookup, pat) {
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -67,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.
* @tags combiners, functional
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @param {Pattern} lookup a pattern of indices or names
* @param {function[] | object} lookup the array or lookup object of functions from which to pull
* @returns {Pattern}
* @example
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
* @example
* 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) {
return pat.apply(pick(lookup, funcs));
export const pickF = register('pickF', function (pickPattern, lookup, pat) {
return pat.apply(pick(lookup, pickPattern));
});
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
* it wraps around, rather than sticking at the maximum value.
* @tags combiners
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @param {Pattern} lookup a pattern of indices or names
* @param {function[] | object} lookup the array or lookup object of functions from which to pull
* @returns {Pattern}
*/
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
return pat.apply(pickmod(lookup, funcs));
export const pickmodF = register('pickmodF', function (pickPattern, lookup, pat) {
return pat.apply(pickmod(lookup, pickPattern));
});
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @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,
* it wraps around, rather than sticking at the maximum value.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @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.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @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,
* it wraps around, rather than sticking at the maximum value.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @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.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @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,
* it wraps around, rather than sticking at the maximum value.
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @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).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @synonyms pickSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
b: s("cp sd")
})
* @example
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
*/
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @tags combiners
* @synonyms pickSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* let a = s("bd(3,8)")
* let b = s("cp sd")
* "<a b [a,b]>".inhabit({ a, b })
* @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) {
return _pick(lookup, pat, false).squeezeJoin();
});
@@ -180,6 +196,7 @@ export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], fun
* second one.
* @name inhabitmod
* @synonyms pickmodSqueeze
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -192,6 +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
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
* @tags combiners
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
+317 -49
View File
@@ -12,7 +12,6 @@ import {
import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
import { reset_state } from './impure.mjs';
import { SalatRepl } from '@kabelsalat/web';
export function repl({
defaultOutput,
@@ -31,7 +30,6 @@ export function repl({
id,
mondo = false,
}) {
const kabel = new SalatRepl({ localScope: true });
const state = {
schedulerError: undefined,
evalError: undefined,
@@ -40,6 +38,7 @@ export function repl({
pattern: undefined,
miniLocations: [],
widgets: [],
sliders: [],
pending: false,
started: false,
};
@@ -81,19 +80,135 @@ export function repl({
let allTransform;
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 () {
pPatterns = {};
anonymousIndex = 0;
allTransform = undefined;
eachTransform = undefined;
codeBlocks = {};
blockPatterns.clear();
lastActiveVisualizerLabel = null; // Reset 'all' visualizer tracking
return silence;
};
const compileKabel = (code) => {
const node = kabel.evaluate(code);
return node.compile({ log: false });
};
// helper to get a patternified pure value out
function unpure(pat) {
if (pat._Pattern) {
@@ -110,7 +225,60 @@ export function repl({
};
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 pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
@@ -123,6 +291,7 @@ export function repl({
* Changes the global tempo to the given cycles per minute
*
* @name setcpm
* @tags temporal
* @alias setCpm
* @param {number} cpm cycles per minute
* @example
@@ -136,8 +305,14 @@ export function repl({
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
/** Applies a function to all the running patterns. Note that the patterns are groups together into a single `stack` before the function is applied. This is probably what you want, but see `each` for
let allTransforms = [];
/**
* Applies a function to all the running patterns. Note that the patterns are grouped 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.
*
* **Note:** Patterns must be labeled (e.g. with `$:`) to be picked up by `all`. An unlabeled
* pattern such as `note("c4")` is not registered and will produce no audio when `all` is present.
* Use `$: note("c4")` instead.
* ```
* $: sound("bd - cp sd")
* $: sound("hh*8")
@@ -148,18 +323,24 @@ export function repl({
* $: sound("hh*8")
* all(x => x.pianoroll())
* ```
*
* @tags combiners
*/
let allTransforms = [];
const all = function (transform) {
allTransforms.push(transform);
return silence;
};
/** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern.
*
* **Note:** Patterns must be labeled (e.g. with `$:`) to be picked up by `each`. An unlabeled
* pattern such as `note("c4")` is not registered and will produce no audio when `each` is present.
* Use `$: note("c4")` instead.
* ```
* $: sound("bd - cp sd")
* $: sound("hh*8")
* each(fast("<2 3>"))
* ```
* @tags combiners
*/
const each = function (transform) {
eachTransform = transform;
@@ -215,11 +396,10 @@ export function repl({
setcps: setCps,
setCpm,
setcpm: setCpm,
compileKabel,
});
};
const evaluate = async (code, autostart = true, shouldHush = true) => {
const evaluate = async (code, autostart = true) => {
if (!code) {
throw new Error('no code to evaluate');
}
@@ -227,59 +407,34 @@ export function repl({
updateState({ code, pending: true });
await injectPatternMethods();
setTime(() => scheduler.now()); // TODO: refactor?
await beforeEval?.({ code });
await beforeEval?.({ code, blockBased: false });
allTransforms = []; // reset all transforms
shouldHush && hush();
codeBlocks = {};
hush();
if (mondo) {
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)) {
pattern = silence;
}
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
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 });
afterEval?.({ code, pattern, meta, range: undefined, widgetRemoved: false });
return pattern;
} catch (err) {
logger(`[eval] error: ${err.message}`, 'error');
@@ -288,8 +443,121 @@ export function repl({
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 });
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 =
+68 -3
View File
@@ -24,6 +24,7 @@ export const signal = (func) => {
* A sawtooth signal between 0 and 1.
*
* @return {Pattern}
* @tags generators
* @example
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
* .clip(saw.slow(2))
@@ -38,6 +39,7 @@ export const saw = signal((t) => t % 1);
* A sawtooth signal between -1 and 1 (like `saw`, but bipolar).
*
* @return {Pattern}
* @tags generators
*/
export const saw2 = saw.toBipolar();
@@ -45,6 +47,7 @@ export const saw2 = saw.toBipolar();
* A sawtooth signal between 1 and 0 (like `saw`, but flipped).
*
* @return {Pattern}
* @tags generators
* @example
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
* .clip(isaw.slow(2))
@@ -59,6 +62,7 @@ export const isaw = signal((t) => 1 - (t % 1));
* A sawtooth signal between 1 and -1 (like `saw2`, but flipped).
*
* @return {Pattern}
* @tags generators
*/
export const isaw2 = isaw.toBipolar();
@@ -66,12 +70,14 @@ export const isaw2 = isaw.toBipolar();
* A sine signal between -1 and 1 (like `sine`, but bipolar).
*
* @return {Pattern}
* @tags generators
*/
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
/**
* A sine signal between 0 and 1.
* @return {Pattern}
* @tags generators
* @example
* n(sine.segment(16).range(0,15))
* .scale("C:minor")
@@ -83,6 +89,7 @@ export const sine = sine2.fromBipolar();
* A cosine signal between 0 and 1.
*
* @return {Pattern}
* @tags generators
* @example
* n(stack(sine,cosine).segment(16).range(0,15))
* .scale("C:minor")
@@ -94,12 +101,14 @@ export const cosine = sine._early(Fraction(1).div(4));
* A cosine signal between -1 and 1 (like `cosine`, but bipolar).
*
* @return {Pattern}
* @tags generators
*/
export const cosine2 = sine2._early(Fraction(1).div(4));
/**
* A square signal between 0 and 1.
* @return {Pattern}
* @tags generators
* @example
* n(square.segment(4).range(0,7)).scale("C:minor")
*
@@ -110,6 +119,7 @@ export const square = signal((t) => Math.floor((t * 2) % 2));
* A square signal between -1 and 1 (like `square`, but bipolar).
*
* @return {Pattern}
* @tags generators
*/
export const square2 = square.toBipolar();
@@ -117,6 +127,7 @@ export const square2 = square.toBipolar();
* A triangle signal between 0 and 1.
*
* @return {Pattern}
* @tags generators
* @example
* n(tri.segment(8).range(0,7)).scale("C:minor")
*
@@ -127,6 +138,7 @@ export const tri = fastcat(saw, isaw);
* A triangle signal between -1 and 1 (like `tri`, but bipolar).
*
* @return {Pattern}
* @tags generators
*/
export const tri2 = fastcat(saw2, isaw2);
@@ -134,6 +146,7 @@ export const tri2 = fastcat(saw2, isaw2);
* An inverted triangle signal between 1 and 0 (like `tri`, but flipped).
*
* @return {Pattern}
* @tags generators
* @example
* n(itri.segment(8).range(0,7)).scale("C:minor")
*
@@ -144,6 +157,7 @@ export const itri = fastcat(isaw, saw);
* An inverted triangle signal between -1 and 1 (like `itri`, but bipolar).
*
* @return {Pattern}
* @tags generators
*/
export const itri2 = fastcat(isaw2, saw2);
@@ -151,6 +165,7 @@ export const itri2 = fastcat(isaw2, saw2);
* A signal representing the cycle time.
*
* @return {Pattern}
* @tags generators
*/
export const time = signal(id);
@@ -158,6 +173,7 @@ export const time = signal(id);
* The mouse's x position value ranges from 0 to 1.
* @name mousex
* @return {Pattern}
* @tags external_io
* @example
* n(mousex.segment(4).range(0,7)).scale("C:minor")
*
@@ -167,6 +183,7 @@ export const time = signal(id);
* The mouse's y position value ranges from 0 to 1.
* @name mousey
* @return {Pattern}
* @tags external_io
* @example
* n(mousey.segment(4).range(0,7)).scale("C:minor")
*
@@ -269,6 +286,7 @@ export const getRandsAtTime = (t, n = 1, seed = 0) => {
* precise RNG, try `useRNG('precise')`.
*
* @name useRNG
* @tags generators, math
* @param {string} mod - Mode. One of 'legacy', 'precise'
* @example
* useRNG('legacy')
@@ -280,6 +298,7 @@ export const useRNG = (mode = 'legacy') => (RNG_MODE = mode);
/**
* A discrete pattern of numbers from 0 to n-1
* @tags generators
* @example
* n(run(4)).scale("C4:pentatonic")
* // n("0 1 2 3").scale("C4:pentatonic")
@@ -290,13 +309,14 @@ export const run = (n) => saw.range(0, n).round().segment(n);
* Creates a binary pattern from a number.
*
* @name binary
* @tags generators
* @param {number} n - input number to convert to binary
* @example
* "hh".s().struct(binary(5))
* // "hh".s().struct("1 0 1")
*/
export const binary = (n) => {
const nBits = reify(n).log2(0).floor().add(1);
const nBits = reify(n).log2().floor().add(1);
return binaryN(n, nBits);
};
@@ -304,6 +324,7 @@ export const binary = (n) => {
* Creates a binary pattern from a number, padded to n bits long.
*
* @name binaryN
* @tags generators
* @param {number} n - input number to convert to binary
* @param {number} nBits - pattern length, defaults to 16
* @example
@@ -321,12 +342,13 @@ export const binaryN = (n, nBits = 16) => {
* Creates a binary list pattern from a number.
*
* @name binaryL
* @tags generators
* @param {number} n - input number to convert to binary
* s("saw").seg(8)
* .partials(binaryL(irand(4096).add(1)))
*/
export const binaryL = (n) => {
const nBits = reify(n).log2(0).floor().add(1);
const nBits = reify(n).log2().floor().add(1);
return binaryNL(n, nBits);
};
@@ -334,6 +356,7 @@ export const binaryL = (n) => {
* Creates a binary list pattern from a number, padded to n bits long.
*
* @name binaryNL
* @tags generators
* @param {number} n - input number to convert to binary
* @param {number} nBits - pattern length, defaults to 16
*/
@@ -353,6 +376,7 @@ export const binaryNL = (n, nBits = 16) => {
* Creates a list of random numbers of the given length
*
* @name randL
* @tags generators
* @param {number} n Number of random numbers to sample
* @example
* s("saw").seg(16).n(irand(12)).scale("F1:minor")
@@ -365,7 +389,9 @@ export const randL = (n) => {
export const randrun = (n) => {
return signal((t, controls) => {
// Without adding 0.5, the first cycle is always 0,1,2,3,...
const rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed);
let rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed);
// Support n = 1
if (!Array.isArray(rands)) rands = [rands];
const nums = rands
.map((n, i) => [n, i])
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
@@ -384,6 +410,7 @@ const _rearrangeWith = (ipat, n, pat) => {
* Slices a pattern into the given number of parts, then plays those parts in random order.
* Each part will be played exactly once per cycle.
* @name shuffle
* @tags temporal
* @example
* note("c d e f").sound("piano").shuffle(4)
* @example
@@ -397,6 +424,7 @@ export const shuffle = register('shuffle', (n, pat) => {
* Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`,
* but parts might be played more than once, or not at all, per cycle.
* @name scramble
* @tags temporal
* @example
* note("c d e f").sound("piano").scramble(4)
* @example
@@ -409,6 +437,7 @@ export const scramble = register('scramble', (n, pat) => {
/**
* Modify a pattern by applying a function to the `randomSeed` control if present
*
* @tags math
* @param {Function} func Function from seed (or undefined) to seed (or undefined)
* @param {Pattern} pat Pattern to update
* @returns Pattern
@@ -428,6 +457,7 @@ export const withSeed = (func, pat) => {
* that use randomness, like `shuffle` and `sometimes`.
*
* @name seed
* @tags math
* @param {number} n A new seed. Can be any number.
* @example
* $: s("hh*4").degrade();
@@ -441,6 +471,7 @@ export const seed = register('seed', (n, pat) => {
* A continuous pattern of random numbers, between 0 and 1.
*
* @name rand
* @tags generators
* @example
* // randomly change the cutoff
* s("bd*4,hh*8").cutoff(rand.range(500,8000))
@@ -449,6 +480,7 @@ export const seed = register('seed', (n, pat) => {
export const rand = signal((t, controls) => getRandsAtTime(t, 1, controls.randSeed));
/**
* A continuous pattern of random numbers, between -1 and 1
* @tags generators
*/
export const rand2 = rand.toBipolar();
@@ -458,6 +490,7 @@ export const _brandBy = (p) => rand.fmap((x) => x < p);
* A continuous pattern of 0 or 1 (binary random), with a probability for the value being 1
*
* @name brandBy
* @tags generators
* @param {number} probability - a number between 0 and 1
* @example
* s("hh*10").pan(brandBy(0.2))
@@ -468,6 +501,7 @@ export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
* A continuous pattern of 0 or 1 (binary random)
*
* @name brand
* @tags generators
* @example
* s("hh*10").pan(brand)
*/
@@ -479,6 +513,7 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
* A continuous pattern of random integers, between 0 and n-1.
*
* @name irand
* @tags generators
* @param {number} n max value (exclusive)
* @example
* // randomly select scale notes from 0 - 7 (= C to C)
@@ -501,6 +536,7 @@ export const __chooseWith = (pat, xs) => {
/**
* Choose from the list of values (or patterns of values) using the given
* pattern of numbers, which should be in the range of 0..1
* @tags temporal
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -514,6 +550,7 @@ export const chooseWith = (pat, xs) => {
/**
* As with {chooseWith}, but the structure comes from the chosen values, rather
* than the pattern you're using to choose with.
* @tags temporal
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
@@ -524,6 +561,7 @@ export const chooseInWith = (pat, xs) => {
/**
* Chooses randomly from the given list of elements.
* @tags temporal
* @param {...any} xs values / patterns to choose from.
* @returns {Pattern} - a continuous pattern.
* @example
@@ -539,6 +577,7 @@ export const chooseOut = choose;
* Chooses from the given list of values (or patterns of values), according
* to the pattern that the method is called on. The pattern should be in
* the range 0 .. 1.
* @tags temporal
* @param {...any} xs
* @returns {Pattern}
*/
@@ -549,6 +588,7 @@ Pattern.prototype.choose = function (...xs) {
/**
* As with choose, but the pattern that this method is called on should be
* in the range -1 .. 1
* @tags temporal
* @param {...any} xs
* @returns {Pattern}
*/
@@ -558,6 +598,7 @@ Pattern.prototype.choose2 = function (...xs) {
/**
* Picks one of the elements at random each cycle.
* @tags temporal
* @synonyms randcat
* @returns {Pattern}
* @example
@@ -600,6 +641,7 @@ const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
/**
* Chooses randomly from the given list of elements by giving a probability to each element
* @tags temporal
* @param {...any} pairs arrays of value and weight
* @returns {Pattern} - a continuous pattern.
* @example
@@ -609,6 +651,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
/**
* Picks one of the elements at random each cycle by giving a probability to each element
* @tags temporal
* @synonyms wrandcat
* @returns {Pattern}
* @example
@@ -652,6 +695,7 @@ function _berlin(t, seed = 0) {
/**
* Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1.
*
* @tags generators
* @name perlin
* @example
* // randomly change the cutoff
@@ -664,6 +708,7 @@ export const perlin = signal((t, controls) => _perlin(t, controls.randSeed));
* Generates a continuous pattern of [berlin noise](conceived by Jame Coyne and Jade Rowland as a joke but turned out to be surprisingly cool and useful,
* like perlin noise but with sawtooth waves), in the range 0..1.
*
* @tags generators
* @name berlin
* @example
* // ascending arpeggios
@@ -684,6 +729,7 @@ export const degradeByWith = register(
* 0 = 0% chance of removal
* 1 = 100% chance of removal
*
* @tags temporal
* @name degradeBy
* @memberof Pattern
* @param {number} amount - a number between 0 and 1
@@ -709,6 +755,7 @@ export const degradeBy = register(
*
* Randomly removes 50% of events from the pattern. Shorthand for `.degradeBy(0.5)`
*
* @tags temporal
* @name degrade
* @memberof Pattern
* @returns Pattern
@@ -725,6 +772,7 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, t
* 1 = 0% chance of removal
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
*
* @tags temporal
* @name undegradeBy
* @memberof Pattern
* @param {number} amount - a number between 0 and 1
@@ -753,6 +801,7 @@ export const undegradeBy = register(
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
* Events that would be removed by degrade are let through by undegrade and vice versa (see second example).
*
* @tags temporal
* @name undegrade
* @memberof Pattern
* @returns Pattern
@@ -771,6 +820,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5), t
* Randomly applies the given function by the given probability.
* Similar to `someCyclesBy`
*
* @tags temporal
* @name sometimesBy
* @memberof Pattern
* @param {number | Pattern} probability - a number between 0 and 1
@@ -790,6 +840,7 @@ export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
*
* Applies the given function with a 50% chance
*
* @tags temporal
* @name sometimes
* @memberof Pattern
* @param {function} function - the transformation to apply
@@ -811,6 +862,7 @@ export const sometimes = register('sometimes', function (func, pat) {
* @param {number | Pattern} probability - a number between 0 and 1
* @param {function} function - the transformation to apply
* @returns Pattern
* @tags temporal
* @example
* s("bd,hh*8").someCyclesBy(.3, x=>x.speed("0.5"))
*/
@@ -833,6 +885,7 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat)
* @name someCycles
* @memberof Pattern
* @returns Pattern
* @tags temporal
* @example
* s("bd,hh*8").someCycles(x=>x.speed("0.5"))
*/
@@ -847,6 +900,7 @@ export const someCycles = register('someCycles', function (func, pat) {
* @name often
* @memberof Pattern
* @returns Pattern
* @tags temporal
* @example
* s("hh*8").often(x=>x.speed("0.5"))
*/
@@ -861,6 +915,7 @@ export const often = register('often', function (func, pat) {
* @name rarely
* @memberof Pattern
* @returns Pattern
* @tags temporal
* @example
* s("hh*8").rarely(x=>x.speed("0.5"))
*/
@@ -872,6 +927,7 @@ export const rarely = register('rarely', function (func, pat) {
*
* Shorthand for `.sometimesBy(0.1, fn)`
*
* @tags temporal
* @name almostNever
* @memberof Pattern
* @returns Pattern
@@ -886,6 +942,7 @@ export const almostNever = register('almostNever', function (func, pat) {
*
* Shorthand for `.sometimesBy(0.9, fn)`
*
* @tags temporal
* @name almostAlways
* @memberof Pattern
* @returns Pattern
@@ -900,6 +957,7 @@ export const almostAlways = register('almostAlways', function (func, pat) {
*
* Shorthand for `.sometimesBy(0, fn)` (never calls fn)
*
* @tags temporal
* @name never
* @memberof Pattern
* @returns Pattern
@@ -914,6 +972,7 @@ export const never = register('never', function (_, pat) {
*
* Shorthand for `.sometimesBy(1, fn)` (always calls fn)
*
* @tags temporal
* @name always
* @memberof Pattern
* @returns Pattern
@@ -942,6 +1001,7 @@ export function _keyDown(keyname) {
* Do something on a keypress, or array of keypresses
* [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
*
* @tags external_io
* @name whenKey
* @memberof Pattern
* @returns Pattern
@@ -958,6 +1018,7 @@ export const whenKey = register('whenKey', function (input, func, pat) {
* returns true when a key or array of keys is held
* [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
*
* @tags external_io
* @name keyDown
* @memberof Pattern
* @returns Pattern
@@ -975,6 +1036,8 @@ export const keyDown = register('keyDown', function (pat) {
* event durations, from the pattern that it is combined with.
* For example `cyclesPer.struct("1 1 [1 1] 1")` would give the same as `"0.25 0.25 [0.125 0.125] 0.25"`.
* See also its reciprocal, `per`, also known as `perCycle`.
*
* @tags temporal
* @example
* // Shorter events are lower in pitch
* sound("saw saw [saw saw] saw")
@@ -993,6 +1056,7 @@ export const cyclesPer = new Pattern(function (state) {
* event durations, from the pattern that it is combined with.
* For example `per.struct("1 1 [1 1] 1")` would give the same as `"4 4 [8 8] 4"`.
* See also its reciprocal, `cyclesPer`.
* @tags temporal
* @synonyms perCycle
* @example
* // Shorter events are more distorted
@@ -1010,6 +1074,7 @@ export const perCycle = per;
* particular, where the event duration halves, the
* returned value increases by one. `perx.struct("1 1 [1 [1 1]] 1")` would therefore be
* the same as `"3 3 [4 [5 5]] 3"`.
* @tags temporal
*/
export const perx = new Pattern(function (state) {
const n = Fraction(1).div(state.span.duration);
+7 -1
View File
@@ -9,7 +9,7 @@ import Fraction from 'fraction.js';
import { describe, it, expect, vi } from 'vitest';
import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs';
import { fastcat, sequence, State, TimeSpan, Hap } from '../index.mjs';
import { fastcat, sequence, State, TimeSpan, Hap, note } from '../index.mjs';
const st = (begin, end) => new State(ts(begin, end));
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
@@ -59,3 +59,9 @@ describe('perx', () => {
);
});
});
describe('shuffle', () => {
it('returns original pattern if input is 1', () => {
expect(note('c d e f').sound('piano').shuffle(1).firstCycle()).toEqual(note('c d e f').sound('piano').firstCycle());
});
});
+7 -2
View File
@@ -339,8 +339,13 @@ export function uniqsortr(a) {
export function unicodeToBase64(text) {
const utf8Bytes = new TextEncoder().encode(text);
const base64String = btoa(String.fromCharCode(...utf8Bytes));
return base64String;
let binaryString = '';
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) {
+2
View File
@@ -135,6 +135,8 @@ export async function loadOrc(url) {
* p4 -- MIDI key number (as a real number, not an integer but in [0, 127].
* p5 -- MIDI velocity (as a real number, not an integer but in [0, 127].
* p6 -- Strudel controls, as a string.
*
* @tags external_io
*/
export const csoundm = register('csoundm', (instrument, pat) => {
let p1 = instrument;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "1.2.7",
"version": "1.3.0",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
+10
View File
@@ -78,6 +78,16 @@ export const cleanupDraw = (clearScreen = true, 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) {
return this.withState((state) => {
if (!state.controls.painters) {
+2
View File
@@ -42,6 +42,7 @@ const getValue = (e) => {
*
* @name pianoroll
* @synonyms punchcard
* @tags visualization
* @param {Object} options Object containing all the optional following parameters as key value pairs:
* @param {integer} cycles number of cycles to be displayed at the same time - defaults to 4
* @param {number} playhead location of the active notes on the time axis - 0 to 1, defaults to 0.5
@@ -299,6 +300,7 @@ Pattern.prototype.punchcard = function (options) {
* Supports all the same options as pianoroll.
*
* @name wordfall
* @tags visualization
*/
Pattern.prototype.wordfall = function (options) {
return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options });
+33 -1
View File
@@ -54,11 +54,42 @@ export function pitchwheel({
}
if (edo) {
edo = haps.length >= 1 && haps[0].value && haps[0].value.edo ? haps[0].value.edo : edo;
root = haps.length >= 1 && haps[0].value && haps[0].value.root ? haps[0].value.root : root;
const degreeIndexes =
haps.length >= 1 && haps[0].value && haps[0].value.degreeIndexes ? haps[0].value.degreeIndexes : null;
const intLabels = haps.length >= 1 && haps[0].value && haps[0].value.intLabels ? haps[0].value.intLabels : null;
ctx.font = '20px sans-serif';
const label = `${edo} EDO`;
// Draw EDO label:
ctx.fillText(label, centerX + radius - ctx.measureText(label).width + 15, centerY + radius);
Array.from({ length: edo }, (_, i) => {
const angle = freq2angle(root * Math.pow(2, i / edo), root);
const [x, y] = circlePos(centerX, centerY, radius, angle);
ctx.beginPath();
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
// Draw interval label for degree i when it exists:
if (degreeIndexes === null || degreeIndexes.includes(i)) {
ctx.globalAlpha = 1;
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
if (intLabels !== null) {
const degree = degreeIndexes.indexOf(i);
if (intLabels[degree]) {
if (angle < 0.32 && angle > 0.125) {
ctx.fillText(intLabels[degree], x - 34, y);
} else {
if (angle < 0.1 && angle > -1.125) {
ctx.fillText(intLabels[degree], x - 7, y - 12);
} else {
ctx.fillText(intLabels[degree], x + 9, y);
}
}
}
}
} else {
ctx.globalAlpha = 0.15;
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
}
ctx.fill();
});
ctx.stroke();
@@ -116,6 +147,7 @@ export function pitchwheel({
/**
* Renders a pitch circle to visualize frequencies within one octave
* @name pitchwheel
* @tags visualization
* @param {number} hapcircles
* @param {number} circle
* @param {number} edo
+1
View File
@@ -129,6 +129,7 @@ function drawSpiral(options) {
* Displays a spiral visual.
*
* @name spiral
* @tags visualization
* @param {Object} options Object containing all the optional following parameters as key value pairs:
* @param {number} stretch controls the rotations per cycle ratio, where 1 = 1 cycle / 360 degrees
* @param {number} size the diameter of the spiral
+41
View File
@@ -0,0 +1,41 @@
# @strudel/edo
This package adds EDO scale functions to strudel Patterns.
## Install
```sh
npm i @strudel/edo --save
```
## Example
```js
import { n } from '@strudel/core';
import '@strudel/edo';
// E.g. edoScale for Gorgo-6 scale, 16 EDO, LLsLLLs
// base note C3, large step size 3, small step size 1:
// C3:LLsLLL:3:1
const [baseNote, sequence, largeStep, smallStep] = ['C3', 'LLsLLL', 3, 1]
const pattern = n("0 2 4 6 4 2").edoScale([baseNote, sequence, largeStep, smallStep]);
const events = pattern.firstCycle().map((e) => e.show());
console.log(events);
```
yields:
```
[
"[ 0/1 → 1/1 |
{
\"degree\":1,
\"degreeIndexes\":[0,3,6,7,10,13],
\"intLabels\":[null,\"S2\",\"d4\",\"N4\",\"s6\",\"s7\",\"P8\"],
\"root\":\"130.8128\",
\"freq\":130.813,
\"edo\":16
}
]"
]
```
+114
View File
@@ -0,0 +1,114 @@
/*
edo.mjs - Equal division of the octave (EDO) scale functions for strudel
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/edo.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 { register, pure, noteToMidi, isNote, tokenizeNote } from '@strudel/core';
import { EdoScale } from './edoscale.mjs';
import { Intervals } from './intervals.mjs';
import { Pitches } from './pitches.mjs';
const pitchesCache = new Map();
/**
* Turns numbers into notes in the given EDO scale (zero indexed).
*
* An EDO scale definition looks like this:
*
* e.g. C:LLsLLLs:2:1 <- this is the C major scale, 12 EDO
*
* e.g. C:LLsLLL:3:1 <- this is the Gorgo 6 note scale, 16 EDO
*
* An EDO scale, e.g. C:LLsLLLs:2:1, consists of a root note (e.g. C)
* followed by semicolon (':')
* and then a [Large/small step notation sequence](https://en.xen.wiki/w/MOS_scale)
* (e.g. LLsLLLs)
* followed by semicolon, then the large step size (e.g. 2)
* followed by semicolon, then the small step size (e.g. 1).
*
* The number of divisions of the octave is calculated as the sum
* of the steps in the EDO scale definition.
*
* e.g. C:LLsLLLs:2:1 is 2+2+1+2+2+2+1 = 12 EDO, 7 note scale
*
* e.g. C:LLsLLL:3:1 is 3+3+1+3+3+3 = 16 EDO, 6 note scale
*
* The root note defaults to octave 3, if no octave number is given.
*
* @name edoScale
* @param {string} scale Definition of EDO scale.
* @returns Pattern
* @example
* n("0 2 4 6 4 2").edoScale("C:LLsLLLs:2:1")
* @example
* n("[0,7] 4 [2,7] 4")
* .edoScale("G2:<LLsLLL LLLLsL>:3:1")
* .s("piano")._pitchwheel()
* @example
* n(rand.range(0,5).segment(6))
* .edoScale("<G2 C3>:LLsLL:3:1")
* .s("piano")._pitchwheel()
*/
export const edoScale = register(
'edoScale',
function (scaleDefinition, pat) {
// console.log(scaleDefinition);
// if (Array.isArray(scale)) {
const key = scaleDefinition.flat().join(':');
// }
// console.log(scaleDefinition);
let pitches;
if (pitchesCache.has(key)) {
pitches = pitchesCache.get(key);
} else {
// console.log({ key });
const [base_note, sequence, large, small] = scaleDefinition;
const root_octave = tokenizeNote(base_note)[2] || 3;
// console.log({ root_octave });
const scale = new EdoScale(large, small, sequence);
const intervals = new Intervals(scale);
// console.log({ intervals });
pitches = new Pitches(scale, intervals, 440, noteToMidi(base_note), root_octave);
pitchesCache.set(key, pitches);
// console.log({ base_note: noteToMidi(base_note) });
// console.log({ sequence });
// console.log({ edivisions: scale.edivisions });
// console.log({ intervals });
// console.log({ pat });
// console.log({ pitches: pitches });
}
return pat
.fmap((value) => {
const isObject = typeof value === 'object';
const n = isObject ? value.n : value;
if (isObject) {
delete value.n; // remove n so it won't cause trouble
}
if (isNote(n)) {
// legacy..
return pure(n);
}
const deg = (typeof n === 'string' ? parseInt(n, 10) : Number.isInteger(n) ? n : Math.round(n)) + 1;
const [oct, degree] = pitches.octdeg(deg);
const freq = pitches.octdegfreq(oct, degree);
const note = pitches.octdegmidi(oct, degree);
const edo = pitches.scale.edivisions;
const root = pitches.base_freq;
const degreeIndexes = pitches.scale.divisions;
const intLabels = pitches.intervals.intLabels;
// const color = 'red';
value = pure(isObject ? { ...value, degree, degreeIndexes, intLabels, root, freq, edo } : note);
// value = pure(isObject ? { ...value, key } : note);
// value = pure(isObject ? { ...value, edo } : note);
// console.log({ value });
return value;
})
.outerJoin()
.withHap((hap) => hap.setContext({ ...hap.context, scaleDefinition }));
},
true,
true, // preserve tactus
);
+213
View File
@@ -0,0 +1,213 @@
/*
edoscale.mjs - EdoScale defines equal division of the octave (EDO) scale in Ls notation
- Port of pitfalls/lib/Scale.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/Scale.lua>
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/edoscale.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const M = 2;
const L = 1;
const S = 0;
const LABELS = ['s', 'L', 'M'];
export class EdoScale {
constructor(large, small, sequence, medium) {
this.stepbackup = [L, L, S, L, L, L, S, L, L, L, S, L, L, L, S, L];
this.large = large;
this.medium = medium || large;
this.small = small;
this.divisions = [];
this.edivisions = null;
this.sequence = null;
this.tonic = 1;
this.mode = 1;
this.max_steps = 12;
this.min_steps = 3;
this.setSequence(sequence);
}
hasMedium() {
return this.step.some((_, i) => this.step[this.offset(i)] === M);
}
stepSize(i) {
return LABELS[this.step[this.offset(i)]];
}
sequence() {
return this.step.map((_, i) => this.stepSize(i)).join('');
}
stepValue(i) {
const step = this.step[this.offset(i)];
return step === L ? this.large : step === M ? this.medium : this.small;
}
offset(i) {
if (this.mode === 1) {
return i;
} else {
const offset = (this.mode - 1 + i) % this.length;
return offset === 0 ? this.length : offset;
}
}
static setMaxSteps(max) {
this.max_steps = max;
}
static setMinSteps(min) {
this.min_steps = min;
}
setSequence(sequence) {
if (this.sequence !== sequence) {
this.sequence = sequence;
this.length = sequence.length;
this.step = [];
for (let i = 0; i < sequence.length; i++) {
const char = sequence[i];
this.step[i] = char === 'L' ? L : char === 'M' ? M : S;
}
this.updateEdo();
} else {
return false;
}
}
setLarge(l) {
if (this.large !== l) {
this.large = l;
this.updateEdo();
} else {
return false;
}
}
setMedium(m) {
if (this.medium !== m) {
this.medium = m;
this.updateEdo();
} else {
return false;
}
}
setSmall(s) {
if (this.small !== s) {
this.small = s;
this.updateEdo();
} else {
return false;
}
}
setMode(mode) {
this.mode = mode;
}
setTonic(tonic) {
this.tonic = tonic;
}
changeMode(d) {
const orig = this.mode;
this.mode = Math.max(1, Math.min(this.mode + d, this.length));
return orig !== this.mode;
}
changeTonic(d) {
const orig = this.tonic;
this.tonic = Math.max(1, Math.min(this.tonic + d, this.edivisions));
return orig !== this.tonic;
}
updateEdo() {
const orig = this.edivisions;
this.edivisions = this.step.reduce((sum, _, i) => {
this.divisions[i] = sum;
return sum + this.stepValue(i);
}, 0);
// console.log(this.divisions);
const changed = orig !== this.edivisions;
if (changed) {
this.tonic = Math.max(1, Math.min(this.tonic, this.edivisions));
}
return changed;
}
changeStep(d, i) {
const index = this.offset(i);
const orig = this.step[index];
this.step[index] = Math.max(S, Math.min(this.step[index] + d, M));
this.stepbackup[index] = this.step[index];
const changed = orig !== this.step[index];
if (changed) {
this.updateEdo();
}
return changed;
}
changeLarge(d) {
const orig = this.large;
this.setLarge(Math.max(this.small + 1, Math.min(this.large + d, this.large + 1)));
const changed = this.large !== orig;
if (changed) {
if (this.large <= this.medium) {
this.setMedium(Math.max(this.small + 1, Math.min(this.large - 1, this.large)));
}
this.updateEdo();
}
return changed;
}
changeMedium(d) {
const orig = this.medium;
this.setMedium(Math.max(this.small + 1, Math.min(this.medium + d, this.large - 1)));
const changed = this.medium !== orig;
if (changed) {
this.updateEdo();
}
return changed;
}
changeSmall(d) {
const orig = this.small;
const value = this.small + d;
if (this.hasMedium()) {
this.setSmall(Math.max(1, Math.min(value, this.medium - 1)));
} else {
this.setSmall(Math.max(1, Math.min(value, this.large - 1)));
}
const changed = this.small !== orig;
if (changed) {
if (this.small >= this.medium) {
this.setMedium(Math.max(this.small + 1, Math.min(this.large)));
}
this.updateEdo();
}
return changed;
}
changeLength(d) {
const orig = this.length;
if (d === 1) {
this.length = Math.min(this.length + 1, this.max_steps);
} else if (d === -1) {
this.length = Math.max(this.length - 1, this.min_steps);
}
const changed = this.length !== orig;
if (changed) {
this.mode = 1;
if (d === 1) {
this.step[this.length] = this.stepbackup[this.length] || L;
} else if (d === -1 && this.length >= this.min_steps) {
this.step.pop();
}
this.updateEdo();
}
return changed;
}
}
+5
View File
@@ -0,0 +1,5 @@
import './edo.mjs';
export * from './edo.mjs';
export const packageName = '@strudel/edo';
+103
View File
@@ -0,0 +1,103 @@
/*
intervals.mjs - defines Intervals for equal division of the octave (EDO) scale
- Port of pitfalls/lib/Intervals.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/Intervals.lua>
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/intervals.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 ratiointervals from './ratios.mjs';
function ratio(division, edivisions) {
return division === 0 ? 1 : Math.pow(2, division / edivisions);
}
export class Intervals {
constructor(scale) {
this.scale = scale;
this.intLabels = [];
this.intNoms = [];
this.intRatios = [];
this.uniqLabels = [];
this.intErrors = [];
this.ratios = [];
const BLANK = '';
let division = 0;
const labToErr = {};
const labToInd = {};
this.ratios[0] = 1;
for (let i = 0; i < scale.length; i++) {
division += scale.stepValue(i);
this.ratios[i + 1] = ratio(division, scale.edivisions);
if (i < scale.length) {
const nearest = ratiointervals.nearestInterval(this.ratios[i + 1]);
const closeness = nearest[0];
const ratio = nearest[1];
const intLabel = ratiointervals.key(ratio);
this.intLabels[i + 1] = intLabel;
this.intErrors[i + 1] = closeness;
this.intNoms[i + 1] = ratio ? ratiointervals.nom(ratio) : 0;
this.intRatios[i + 1] = ratio ? `${ratiointervals.nom(ratio)}/${ratiointervals.denom(ratio)}` : '';
this.uniqLabels[i + 1] = BLANK;
if (intLabel && intLabel !== 'P1' && intLabel !== 'P8') {
if (!labToErr[intLabel]) {
this.uniqLabels[i + 1] = intLabel;
labToInd[intLabel] = i + 1;
labToErr[intLabel] = closeness;
} else if (closeness < labToErr[intLabel]) {
this.uniqLabels[labToInd[intLabel]] = BLANK;
this.uniqLabels[i + 1] = intLabel;
labToInd[intLabel] = i + 1;
labToErr[intLabel] = closeness;
}
}
}
}
}
ratio(i) {
return this.ratios[i];
}
intervalLabel(i) {
return this.intLabels[i];
}
intervalNominator(i) {
return this.intNoms[i];
}
intervalRatio(i) {
return this.intRatios[i];
}
uniqIntervalLabel(i) {
return this.uniqLabels[i];
}
intervalError(i) {
return this.intErrors[i];
}
nearestDegreeTo(r, threshold) {
let min = 1;
let degree = null;
for (const [i, v] of Object.entries(this.ratios)) {
const diff = Math.abs((r - v) / r);
if (diff < min) {
min = diff;
degree = parseInt(i, 10);
}
}
if (threshold == null) {
return degree;
} else {
return min < threshold ? degree : 1;
}
}
}
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@strudel/edo",
"version": "0.1.0",
"description": "Equal division of the octave (EDO) scale functions for strudel",
"main": "index.mjs",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"test": "vitest run",
"prepublishOnly": "npm run build"
},
"type": "module",
"repository": {
"type": "git",
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Rob McKinnon <rob@movingflow>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@tonaljs/tonal": "^4.10.0",
"chord-voicings": "^0.0.1",
"webmidi": "^3.1.12"
},
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
}
}
+98
View File
@@ -0,0 +1,98 @@
/*
pitches.mjs - defines Pitches for equal division of the octave (EDO) scale
- Port of pitfalls/lib/Pitches.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/Pitches.lua>
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/pitches.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/>.
*/
function ratio(division, edivisions) {
return division === 0 ? 1 : Math.pow(2, division / edivisions);
}
// This function gets frequency based on index, edo, octave, and base frequency
function get_freq(base_freq, edo, index, oct, base_octave) {
let f = base_freq * ratio(index - 1, edo);
if (oct < base_octave) {
f /= Math.pow(2, base_octave - oct);
} else if (oct > base_octave) {
f *= Math.pow(2, oct - base_octave);
}
return f;
}
// Convert MIDI number to Hz using a given tuning multiplier
function midi_to_hz(n, tuning) {
return tuning * Math.pow(2, (n - 69) / 12.0);
}
const denom = Math.log(2);
function hz_to_midi(freq, tuning) {
return 12.0 * (Math.log(freq / tuning) / denom) + 69;
}
export class Pitches {
constructor(scale, intervals, tuning, midi_start, root_octave) {
this.scale = scale;
this.intervals = intervals;
this.base_freq = midi_to_hz(midi_start, tuning);
this.root_octave = root_octave;
this.freqs = {};
this.midis = {};
this.degrees = {};
this.octdegfreqs = {};
this.octdegmidis = {};
let index = 0;
let f = null;
for (let oct = 0; oct <= 8; oct++) {
this.octdegfreqs[oct] = {};
this.octdegmidis[oct] = {};
f = get_freq(this.base_freq, scale.edivisions, scale.tonic, oct, this.root_octave);
for (let deg = 0; deg < scale.length; deg++) {
index = index + 1;
this.freqs[index] = parseFloat((f * intervals.ratio(deg)).toFixed(3));
this.midis[index] = parseFloat(hz_to_midi(f * intervals.ratio(deg), tuning).toFixed(4));
this.degrees[index] = deg;
this.octdegfreqs[oct][deg + 1] = this.freqs[index];
this.octdegmidis[oct][deg + 1] = this.midis[index];
}
}
this.base_freq = parseFloat(this.base_freq).toFixed(4);
}
base_freq() {
return this.base_freq;
}
degree(index) {
return this.degrees[index];
}
freq(index) {
return this.freqs[index];
}
octdeg(deg) {
const higherOcatve = deg > this.scale.length;
const octave = this.root_octave + (higherOcatve ? Math.floor((deg - 1) / this.scale.length) : 0);
const degree = higherOcatve ? (deg % this.scale.length === 0 ? this.scale.length : deg % this.scale.length) : deg;
// console.log([octave, degree]);
return [octave, degree];
}
octdegfreq(oct, deg) {
if (this.octdegfreqs[oct]) {
return this.octdegfreqs[oct][deg];
} else {
return null;
}
}
octdegmidi(oct, deg) {
if (this.octdegmidis[oct]) {
return this.octdegmidis[oct][deg];
} else {
return null;
}
}
}
+95
View File
@@ -0,0 +1,95 @@
/*
ratios.mjs - lists whole number ratios for pitch intervals
- Port of pitfalls/lib/ratios.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/ratios.lua>
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/ratios.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const ratiointervals = {};
// FJS Calculators - https://misotanni.github.io/fjs/en/calc.html
// List of pitch intervals - https://en.wikipedia.org/wiki/List_of_pitch_intervals
// Gallery of just intervals - https://en.xen.wiki/w/Gallery_of_just_intervals
// Two letter codes changed to make different interval labels more unique.
ratiointervals.list = new Map([
[1, ['P1', 'P1', 'P1', 1, 1]], // unison P1
[16 / 15, ['m2', 'm2', 'm2_5', 16, 15]], // minor second m2
[15 / 14, ['A1', 'A1', 'A1^5_7', 15, 14]], // augmented unison
[13 / 12, ['t2', 'm2', 'm2^13', 13, 12]], // tridecimal neutral second
[12 / 11, ['N2', 'M2', 'M2_11', 12, 11]], // undecimal neutral second
[11 / 10, ['n2', 'm2', 'm2^11_5', 11, 10]], // undecimal submajor second
[10 / 9, ['T2', 'M2', 'M2^5', 10, 9]], // classic (whole) tone
[9 / 8, ['M2', 'M2', 'M2', 9, 8]], // major second M2
[8 / 7, ['S2', 'M2', 'M2_7', 8, 7]], // septimal major second
[7 / 6, ['s3', 'm3', 'm3^7', 7, 6]], // septimal minor third
[19 / 16, ['o3', 'm3', 'm3^19', 19, 16]], // otonal minor third
[6 / 5, ['m3', 'm3', 'm3_5', 6, 5]], // minor third m3
[17 / 14, ['t3', 'm3', 'm3^17_7', 17, 14]], // septendecimal supraminor third
[11 / 9, ['n3', 'm3', 'm3^11', 11, 9]], // undecimal neutral third
[5 / 4, ['M3', 'M3', 'M3^5', 5, 4]], // major third M3
[9 / 7, ['S3', 'M3', 'M3_7', 9, 7]], // septimal major third SM3
[13 / 10, ['d4', 'd4', 'd4^13_5', 13, 10]], // Barbados third
[4 / 3, ['P4', 'P4', 'P4', 4, 3]], // perfect fourth P4
[19 / 14, ['N4', 'P4', 'P4^19_7', 19, 14]], // undevicesimal wide fourth
[11 / 8, ['n4', 'P4', 'P4^11', 11, 8]], // super-fourth
[25 / 18, ['a4', 'A4', 'A4^5,5', 25, 18]], // classic augmented fourth
[7 / 5, ['sT', 'd5', 'd5^7_5', 7, 5]], // lesser septimal tritone
[45 / 32, ['A4', 'A4', 'A4^5', 45, 32]], // just augmented fourth
[17 / 12, ['d5', 'd5', 'd5^17', 17, 12]], // larger septendecimal tritone
[10 / 7, ['ST', 'A4', 'A4^5_7', 10, 7]], // greater septimal tritone
[13 / 9, ['t5', 'd5', 'd5^13', 13, 9]], // tridecimal diminished fifth
[3 / 2, ['P5', 'P5', 'P5', 3, 2]], // perfect fifth P5
[14 / 9, ['s6', 'M6', 'm6^7', 14, 9]], // subminor sixth or septimal sixth
[25 / 16, ['a5', 'A5', 'A5^5,5', 25, 16]], // classic augmented fifth
[11 / 7, ['A5', 'P5', 'P5^11_7', 11, 7]], // undecimal minor sixth
[8 / 5, ['m6', 'm6', 'm6_5', 8, 5]], // minor sixth m6
[13 / 8, ['N6', 'm6', 'm6^13', 13, 8]], // tridecimal neutral sixth
[18 / 11, ['n6', 'M6', 'M6_11', 18, 11]], // undecimal neutral sixth
[5 / 3, ['M6', 'M6', 'M6^5', 5, 3]], // just major sixth M6
[128 / 75, ['d7', 'd7', 'd7_5,5', 128, 75]], // diminished seventh
[17 / 10, ['T6', 'd7', 'd7^17_5', 17, 10]], // septendecimal diminished seventh
[12 / 7, ['S6', 'M6', 'M6_7', 12, 7]], // septimal major sixth
[7 / 4, ['s7', 'm7', 'm7^7', 7, 4]], // septimal minor seventh
[16 / 9, ['m7', 'm7', 'm7', 16, 9]], // lesser minor seventh
[9 / 5, ['g7', 'm7', 'm7_5', 9, 5]], // greater just minor seventh
[11 / 6, ['n7', 'm7', 'm7^11', 11, 6]], // undecimal neutral seventh
[13 / 7, ['N7', 'm7', 'm7^13_7', 13, 7]], // tridecimal neutral seventh
[15 / 8, ['M7', 'M7', 'M7^5', 15, 8]], // major seventh
[17 / 9, ['T7', 'd8', 'd8^17', 17, 9]], // large septendecimal major seventh
[19 / 10, ['d8', 'd8', 'd8^19_5', 19, 10]], // large undevicesimal major seventh
[2, ['P8', 'P8', 'P8', 2, 1]], // octave P8
]);
ratiointervals.key = function (ratio) {
return ratio == null ? '' : ratiointervals.list.get(ratio)?.[0] || '';
};
ratiointervals.label = function (ratio) {
return ratio == null ? '' : ratiointervals.list.get(ratio)?.[1] || '';
};
ratiointervals.fjs = function (ratio) {
return ratio == null ? '' : ratiointervals.list.get(ratio)?.[2] || '';
};
ratiointervals.nom = function (ratio) {
return ratio == null ? null : ratiointervals.list.get(ratio)?.[3] || null;
};
ratiointervals.denom = function (ratio) {
return ratio == null ? null : ratiointervals.list.get(ratio)?.[4] || null;
};
ratiointervals.nearestInterval = function (v) {
let min = 1;
let match = null;
for (const [ratio, _labels] of ratiointervals.list) {
const diff = Math.abs((ratio - v) / ratio);
if (diff < min) {
min = diff;
match = ratio;
}
}
return min < 0.01 ? [min, match] : [null, null];
};
export default ratiointervals;
+34
View File
@@ -0,0 +1,34 @@
/*
edo.test.mjs - tests of edo.mjs
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tonal/test/tonal.test.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 '../edo.mjs';
import { n } from '@strudel/core';
import { describe, it, expect } from 'vitest';
describe('edoScale', () => {
it('Should run tonal functions ', () => {
// base note A3 = 220Hz
let baseNote = 'A3';
let root = 220;
let freq = 220;
let pattern = n('0').edoScale([baseNote, 'LLsLLLs', 2, 1]);
let cycle = pattern.firstCycleValues[0];
expect(cycle.freq).toEqual(freq);
expect(cycle.edo).toEqual(12);
expect(cycle.degree).toEqual(1);
expect(parseFloat(cycle.root).toFixed(0)).toEqual(root.toFixed(0));
// base note A4 = 440Hz
baseNote = 'A4';
root = 440;
freq = 880;
pattern = n('7').edoScale([baseNote, 'LLsLLLs', 2, 1]);
cycle = pattern.firstCycleValues[0];
expect(cycle.freq).toEqual(freq);
expect(cycle.edo).toEqual(12);
expect(cycle.degree).toEqual(1);
expect(parseFloat(cycle.root).toFixed(0)).toEqual(root.toFixed(0));
});
});
+21
View File
@@ -0,0 +1,21 @@
/*
edoscale.test.mjs - tests of EdoScale from edoscale.mjs
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/test/edoscale.test.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 '../edoscale.mjs'; // need to import this to add prototypes
import { EdoScale } from '../edoscale.mjs';
import { describe, it, expect } from 'vitest';
describe('EdoScale', () => {
it('updates edivisions', () => {
let scale = new EdoScale(2, 1, ['L', 'L', 's', 'L', 'L', 'L', 's']);
expect(scale.stepSize(0)).toEqual('L');
expect(scale.stepValue(0)).toEqual(2);
expect(scale.stepSize(2)).toEqual('s');
expect(scale.stepValue(2)).toEqual(1);
expect(scale.edivisions).toEqual(12);
});
});
+18
View File
@@ -0,0 +1,18 @@
/*
ratios.test.mjs - tests of ratios.mjs
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/test/ratios.test.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 ratiointervals from '../ratios.mjs';
import { describe, it, expect } from 'vitest';
describe('ratiointervals', () => {
it('updates edivisions', () => {
let ratio = 9 / 7;
expect(ratiointervals.key(ratio)).toEqual('S3');
expect(ratiointervals.label(ratio)).toEqual('M3');
expect(ratiointervals.fjs(ratio)).toEqual('M3_7');
expect(ratiointervals.nom(ratio)).toEqual(9);
expect(ratiointervals.denom(ratio)).toEqual(7);
});
});
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+193
View File
@@ -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);
}
}
}
}
+46 -64
View File
@@ -23,6 +23,8 @@ import { noteToMidi, getControlName } from '@strudel/core';
import { Note } from 'webmidi';
import { getAudioContext } from '@strudel/webaudio';
import { scheduleAtTime } from '../superdough/helpers.mjs';
import { getMidiDeviceNamesString, getDevice } from './util.mjs';
import { MidiInput } from './input.mjs';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -31,10 +33,6 @@ function supportsMidi() {
return typeof navigator.requestMIDIAccess === 'function';
}
function getMidiDeviceNamesString(devices) {
return devices.map((o) => `'${o.name}'`).join(' | ');
}
export function enableWebMidi(options = {}) {
const { onReady, onConnected, onDisconnected, onEnabled } = options;
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
if (typeof window !== 'undefined') {
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
* @tags external_io, midi
* @example
* defaultmidimap({ lpf: 74 })
* $: 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.
* @tags external_io, midi
* @example
* midimaps({ mymap: { lpf: 74 } })
* $: note("c a f e")
@@ -305,6 +282,7 @@ function sendNote(note, velocity, duration, device, midichan, targetTime) {
/**
* MIDI output: Opens a MIDI output port.
* @tags external_io
* @param {string | number} midiport MIDI device name or index defaulting to 0
* @param {object} options Additional MIDI configuration options
* @example
@@ -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)) {
throw new Error(
`[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 device = getDevice(input, WebMidi.inputs);
if (!device) {
throw new Error(
`[midi] Midi device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
);
}
const instance = midiInputs[input] || new MidiInput(input);
midiInputs[input] = instance;
if (initial) {
const device = instance.initialDevice;
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
`[midi] Midi enabled! Using "${device.name}". ${
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`,
device
? `[midi] Midi enabled! Using "${device.name}". ${
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`
: `[midi] Midi enabled! Waiting for device "${input}"... Currently connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
);
}
return 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.
*
* The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel
*
* @name midin
* @tags external_io, midi
* @param {string | number} input MIDI device name or index defaulting to 0
* @returns {function(number, number=): Pattern} A function from (cc, channel?) to a pattern.
* When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1)
@@ -539,31 +525,10 @@ async function _initialize(input) {
* note("c a f e").s("saw")
* .when(cc(0).gt(0), x => x.postgain(0))
*/
let listeners = {};
const refs = {};
const refsByChan = {};
export async function midin(input) {
const device = await _initialize(input);
refs[input] ??= {};
refsByChan[input] ??= {};
const cc = (cc, chan) => {
if (chan !== undefined) {
return ref(() => refsByChan[input][cc]?.[chan] || 0);
}
return ref(() => refs[input][cc] || 0);
};
const instance = await _initializeInput(input);
listeners[input] && device.removeListener('midimessage', listeners[input]);
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;
return instance.createCC.bind(instance);
}
/**
@@ -572,7 +537,11 @@ export async function midin(input) {
* The note length is fixed as Superdough is not currently set up for undetermined
* note durations
*
* The 'midichan' control value contains the number of the channel the note is coming from
* so it could be filtered or manipulated further in the chain.
*
* @name midikeys
* @tags external_io, midi
* @param {string | number} input MIDI device name or index defaulting to 0
* @returns {function((number | Pattern)=): Pattern} A function that produces a pattern.
* When queried, the pattern will produces the most recently played midi notes and velocities,
@@ -586,6 +555,10 @@ export async function midin(input) {
* .s("saw")
* .add(note(rand.mul(0.3)))
* .lpf(1000).lpe(2).room(0.5)
* @example
* // discard all notes not coming out from midi channel 2
* const kb = await midikeys('Arturia KeyStep 32')
* kb().filterValues(v=>v.midichan==2).s("tri")
*/
const kHaps = {};
const kListeners = {};
@@ -617,7 +590,16 @@ function _triggerKeyboard(input, cps, now, latencyCycles) {
return true;
}
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]) {
kHaps[input] = [];
}
@@ -658,7 +640,7 @@ export async function midikeys(input) {
*/
return;
} else {
value = { ...value, note: Math.round(note), velocity: velocity / 127 };
value = { ...value, note: Math.round(note), velocity: velocity / 127, midichan: message.channel };
}
kHaps[input].push(new Hap(span, span, value, {}));
if (!noteoff && triggerAvailable) {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "1.2.7",
"version": "1.3.0",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
+44
View File
@@ -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;
}
+15
View File
@@ -7,6 +7,7 @@ import { signal } from '../core/signal.mjs';
* @name accelerationX
* @return {Pattern}
* @synonyms accX
* @tags external_io
* @example
* n(accelerationX.segment(4).range(0,7)).scale("C:minor")
*
@@ -17,6 +18,7 @@ import { signal } from '../core/signal.mjs';
* @name accelerationY
* @return {Pattern}
* @synonyms accY
* @tags external_io
* @example
* n(accelerationY.segment(4).range(0,7)).scale("C:minor")
*
@@ -27,6 +29,7 @@ import { signal } from '../core/signal.mjs';
* @name accelerationZ
* @return {Pattern}
* @synonyms accZ
* @tags external_io
* @example
* n(accelerationZ.segment(4).range(0,7)).scale("C:minor")
*
@@ -37,6 +40,7 @@ import { signal } from '../core/signal.mjs';
* @name gravityX
* @return {Pattern}
* @synonyms gravX
* @tags external_io
* @example
* n(gravityX.segment(4).range(0,7)).scale("C:minor")
*
@@ -47,6 +51,7 @@ import { signal } from '../core/signal.mjs';
* @name gravityY
* @return {Pattern}
* @synonyms gravY
* @tags external_io
* @example
* n(gravityY.segment(4).range(0,7)).scale("C:minor")
*
@@ -57,6 +62,7 @@ import { signal } from '../core/signal.mjs';
* @name gravityZ
* @return {Pattern}
* @synonyms gravZ
* @tags external_io
* @example
* n(gravityZ.segment(4).range(0,7)).scale("C:minor")
*
@@ -67,6 +73,7 @@ import { signal } from '../core/signal.mjs';
* @name rotationAlpha
* @return {Pattern}
* @synonyms rotA, rotZ, rotationZ
* @tags external_io
* @example
* n(rotationAlpha.segment(4).range(0,7)).scale("C:minor")
*
@@ -77,6 +84,7 @@ import { signal } from '../core/signal.mjs';
* @name rotationBeta
* @return {Pattern}
* @synonyms rotB, rotX, rotationX
* @tags external_io
* @example
* n(rotationBeta.segment(4).range(0,7)).scale("C:minor")
*
@@ -87,6 +95,7 @@ import { signal } from '../core/signal.mjs';
* @name rotationGamma
* @return {Pattern}
* @synonyms rotG, rotY, rotationY
* @tags external_io
* @example
* n(rotationGamma.segment(4).range(0,7)).scale("C:minor")
*
@@ -97,6 +106,7 @@ import { signal } from '../core/signal.mjs';
* @name orientationAlpha
* @return {Pattern}
* @synonyms oriA, oriZ, orientationZ
* @tags external_io
* @example
* n(orientationAlpha.segment(4).range(0,7)).scale("C:minor")
*
@@ -107,6 +117,7 @@ import { signal } from '../core/signal.mjs';
* @name orientationBeta
* @return {Pattern}
* @synonyms oriB, oriX, orientationX
* @tags external_io
* @example
* n(orientationBeta.segment(4).range(0,7)).scale("C:minor")
*
@@ -117,6 +128,7 @@ import { signal } from '../core/signal.mjs';
* @name orientationGamma
* @return {Pattern}
* @synonyms oriG, oriY, orientationY
* @tags external_io
* @example
* n(orientationGamma.segment(4).range(0,7)).scale("C:minor")
*
@@ -127,6 +139,7 @@ import { signal } from '../core/signal.mjs';
* @name absoluteOrientationAlpha
* @return {Pattern}
* @synonyms absOriA, absOriZ, absoluteOrientationZ
* @tags external_io
* @example
* n(absoluteOrientationAlpha.segment(4).range(0,7)).scale("C:minor")
*
@@ -137,6 +150,7 @@ import { signal } from '../core/signal.mjs';
* @name absoluteOrientationBeta
* @return {Pattern}
* @synonyms absOriB, absOriX, absoluteOrientationX
* @tags external_io
* @example
* n(absoluteOrientationBeta.segment(4).range(0,7)).scale("C:minor")
*
@@ -147,6 +161,7 @@ import { signal } from '../core/signal.mjs';
* @name absoluteOrientationGamma
* @return {Pattern}
* @synonyms absOriG, absOriY, absoluteOrientationY
* @tags external_io
* @example
* n(absoluteOrientationGamma.segment(4).range(0,7)).scale("C:minor")
*
+10 -2
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, isPattern } from '@strudel/core';
import { Pattern, isPattern, createParams } from '@strudel/core';
import Paho from 'paho-mqtt';
const connections = {};
@@ -61,7 +61,6 @@ Pattern.prototype.mqtt = function (
const props = {
onSuccess: onConnect,
onFailure: onFailure,
useSSL: true,
};
if (username) {
@@ -119,3 +118,12 @@ Pattern.prototype.mqtt = function (
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
});
};
// This adds the 'move' and 'motor' commands to strudel
export const { move, motor } = createParams('move', 'motor');
window.move = move;
window.motor = motor;
// This adds the 'robot' command
Pattern.prototype.robot = function (robot_id, address = 'ws://192.168.8.248:9001/mqtt') {
return this.mqtt(undefined, undefined, '/move/' + robot_id, address);
};
+2 -1
View File
@@ -42,7 +42,7 @@ export function parseControlsFromHap(hap, cps) {
if (isNote(controls.note)) {
controls.midinote = noteToMidi(controls.note, controls.octave || 3);
} else {
controls.note = parseNumeral(controls.note);
controls.midinote = parseNumeral(controls.note);
}
}
controls.bank && (controls.s = controls.bank + controls.s);
@@ -79,6 +79,7 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
* For more info, read [MIDI & OSC in the docs](https://strudel.cc/learn/input-output/)
*
* @name osc
* @tags external_io
* @memberof Pattern
* @returns Pattern
*/
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
"version": "1.3.1",
"version": "1.3.2",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"bin": "./server.js",
Regular → Executable
View File
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
"version": "1.2.8",
"version": "1.3.0",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
@@ -36,6 +36,7 @@
"@strudel/codemirror": "workspace:*",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/edo": "workspace:*",
"@strudel/hydra": "workspace:*",
"@strudel/midi": "workspace:*",
"@strudel/mini": "workspace:*",
+1
View File
@@ -8,6 +8,7 @@ export async function prebake() {
core,
import('@strudel/draw'),
import('@strudel/mini'),
import('@strudel/edo'),
import('@strudel/tonal'),
import('@strudel/webaudio'),
import('@strudel/codemirror'),

Some files were not shown because too many files have changed in this diff Show More