Compare commits

...

208 Commits

Author SHA1 Message Date
Alex McLean 44064fea55 render 2023-03-14 12:28:42 +00:00
Alex McLean 1a56d8a923 reorganise 2023-03-14 12:26:57 +00:00
Alex McLean a5e9cc77a5 redo citations 2023-03-14 12:26:15 +00:00
Alex McLean 9ed9ba33c5 reorganise 2023-03-14 12:26:00 +00:00
Felix Roos d046e01684 fix example explanation 2023-03-13 20:44:10 +01:00
Felix Roos b217471d2a typos 2023-03-13 20:36:20 +01:00
Alex McLean e4c249e568 render 2023-03-13 15:01:38 +00:00
Alex McLean b4ef80fcae spellcheck 2023-03-13 14:57:25 +00:00
Alex McLean 703e516458 tweaks 2023-03-13 14:52:37 +00:00
Felix Roos 220342154b Merge pull request #474 from bwagner/main
Update README.md
2023-03-06 22:55:00 +01:00
Bernhard Wagner ef7182cd76 Merge branch 'tidalcycles:main' into main 2023-03-06 20:15:16 +01:00
Felix Roos 892d8ba152 Merge pull request #508 from tidalcycles/arrange
add arrange function
2023-03-05 14:52:30 +01:00
Felix Roos da11069f75 Merge pull request #514 from tidalcycles/react-update
update react to 18
2023-03-05 14:29:08 +01:00
Alex McLean bf72908dc9 Support list syntax in mininotation (#512)
fixes #504

* support list syntax in mininotation
* support compound controls
* remove redundant : splitting of s/n and note/n from webaudio
* patternable scale names
2023-03-04 18:06:18 +00:00
Felix Roos 81ab5c78e2 update react to 18 2023-03-03 20:38:20 +01:00
Felix Roos de503c7d63 add snapshot 2023-03-03 20:30:35 +01:00
Felix Roos 487ccb90a1 add arrange function 2023-03-03 20:17:23 +01:00
Felix Roos 4bfeaa47bf Merge pull request #502 from tidalcycles/scale-space-alternative
can now use : as a replacement for space in scales
2023-03-03 12:40:23 +01:00
Felix Roos 9a4d8036a6 add test 2023-03-03 12:38:11 +01:00
Felix Roos a50bd6375d use : instead of _ 2023-03-03 12:37:25 +01:00
Felix Roos 14508e91a9 can now use _ as a replacement for space in scales 2023-03-02 20:39:06 +01:00
Alex McLean 2d5064bd5b Reinstate slice and splice (#500)
Reinstate slice/splice, fixing up "unit" param using new global cps
2023-03-02 14:44:41 +00:00
Felix Roos fdc4819e39 Merge pull request #501 from tidalcycles/nano-repl-highlighting-fix
fix: nano-repl highlighting
2023-03-02 14:49:30 +01:00
Felix Roos efddbb616b fix: nano-repl highlighting 2023-03-02 14:47:15 +01:00
Alex McLean db4d0f26a8 Add control aliases (#497)
* add and simplify control aliases, fixes #496
* roomsize now alias of room
* simplify webaudio param handling
---------

Co-authored-by: Felix Roos <flix91@gmail.com>
2023-03-02 13:17:12 +00:00
Felix Roos 06cffbc31b add readme to nano repl 2023-03-01 12:57:33 +01:00
Felix Roos 3cfb0f4c76 Merge pull request #493 from tidalcycles/cps
implement cps in scheduler
2023-03-01 09:27:26 +01:00
Felix Roos 436ba067bc reset cps before eval 2023-03-01 09:23:01 +01:00
Felix Roos f8724bf47e comment out cps ui 2023-03-01 00:05:23 +01:00
Felix Roos 1f7e293204 add setcps to global scope 2023-02-28 23:56:11 +01:00
Felix Roos 86d2652258 pass cps to hap trigger 2023-02-28 23:54:56 +01:00
Felix Roos f5c0cc2e2d no need to warn if evalScope is called more often 2023-02-28 23:54:40 +01:00
Felix Roos 4463d4a417 Update README.md 2023-02-28 23:25:40 +01:00
Felix Roos df35a5c9b1 Merge pull request #491 from tidalcycles/refactor-react
react style fixes
2023-02-28 23:06:29 +01:00
Felix Roos dda93b3c13 Update @strudel.cycles/react to 0.6.4 2023-02-28 23:02:52 +01:00
Felix Roos 5de40b6bf5 fix disabled button take 2 2023-02-28 23:02:28 +01:00
Felix Roos dcbe799d25 Merge pull request #490 from tidalcycles/refactor-react
refactor react package
2023-02-28 22:57:19 +01:00
Felix Roos a5641aef03 Update @strudel.cycles/react to 0.6.3 2023-02-28 22:52:57 +01:00
Felix Roos 668f97a4c8 refactor react package for publishing 2023-02-28 22:48:50 +01:00
Felix Roos e9d577496b fix disabled button 2023-02-28 22:48:06 +01:00
Felix Roos 66fba947ac Update @strudel.cycles/react to 0.6.2 2023-02-28 22:40:52 +01:00
Felix Roos 18c7e02609 fix: minirepl styles for package 2023-02-28 22:40:18 +01:00
Felix Roos 7e1351507a add variables to react tailwind config 2023-02-28 22:17:24 +01:00
Felix Roos 032acbd27c comment out autocomplete 2023-02-28 22:11:17 +01:00
Felix Roos 8b51f0fbd2 Update @strudel.cycles/react to 0.6.1 2023-02-28 21:57:03 +01:00
Felix Roos ead5942ef0 implement cps + add baasic tempo control ui 2023-02-28 13:00:35 +01:00
Felix Roos cfe369d9fe Merge pull request #488 from tidalcycles/algolia
add algolia creds + optimize sidebar for crawling
2023-02-27 23:47:34 +01:00
Felix Roos 2750a3c130 fix pseudo relative links 2023-02-27 22:59:50 +01:00
Felix Roos a7f6d6ecac add algolia config 2023-02-27 22:59:38 +01:00
Felix Roos 3680282450 Merge pull request #485 from tidalcycles/app-height-fix
fix app height
2023-02-27 19:28:09 +01:00
Felix Roos 3b3435c999 change 1px 2023-02-27 19:24:49 +01:00
Felix Roos e2ae6278c2 Merge pull request #484 from tidalcycles/revert-wip-390
Revert "Another attempt at composable functions - WIP (#390)"
2023-02-27 16:04:20 +01:00
Alex McLean 37d6723e3c Update mini-notation.mdx (#365)
`x!n` is no longer an alias for `[x*n]@n` and the behaviour is a bit different. Probably best to remove this line in case it confuses someone?
2023-02-27 14:45:19 +00:00
Felix Roos 961bbf6f55 Revert "Another attempt at composable functions - WIP (#390)"
This reverts commit cbae355896.
2023-02-27 15:20:49 +01:00
Felix Roos fc71d349ed Merge pull request #482 from tidalcycles/docs
docs: packages + offline
2023-02-27 12:52:09 +01:00
Felix Roos 97c8ab0f71 fix format 2023-02-27 12:49:23 +01:00
Felix Roos 28534ea547 add screenshots 2023-02-27 12:47:38 +01:00
Felix Roos 1a75b79a33 better offline docs
fixes https://github.com/tidalcycles/strudel/issues/424
2023-02-27 12:34:17 +01:00
Felix Roos 75dfb0af35 add dependencygraph 2023-02-27 11:12:21 +01:00
Felix Roos 073fa09648 packages doc 2023-02-27 11:10:47 +01:00
Felix Roos 3015e7b203 Merge pull request #480 from tidalcycles/fix-array-args
Fix array args
2023-02-25 14:26:49 +01:00
Felix Roos 7ac0cdc0f9 fix: lint error 2023-02-25 14:18:23 +01:00
Felix Roos 49234183a3 Merge remote-tracking branch 'origin/main' into fix-array-args 2023-02-25 14:13:55 +01:00
Felix Roos ed7763df92 fix: reify after sequence to preserve array
ref https://github.com/tidalcycles/strudel/issues/383#issuecomment-1399514090
2023-02-25 14:13:39 +01:00
Felix Roos b818a02f76 Merge pull request #478 from tidalcycles/midi-fleshout
midi cc support
2023-02-25 12:33:22 +01:00
Felix Roos 886f8449fd add midichan + docs 2023-02-25 12:23:01 +01:00
Felix Roos 5de6643604 midi: support ccn and ccv 2023-02-24 10:15:21 +01:00
Felix Roos 6283273d81 add helper hap.ensureObjectValue 2023-02-24 10:15:06 +01:00
Bernhard Wagner 5506d18bc5 Update README.md
npm -> pnpm
2023-02-23 18:28:11 +01:00
Felix Roos c8f8f02a46 Merge pull request #473 from tidalcycles/fix-hashes
fix: hash links
2023-02-23 00:11:04 +01:00
Felix Roos 4c5d6f1d6c fix: hash links 2023-02-23 00:04:53 +01:00
Felix Roos f349e36345 Merge pull request #467 from tidalcycles/vim-mode
settings tab with vim / emacs modes + additional themes and fonts
2023-02-22 22:54:39 +01:00
Felix Roos 1574c4ab13 Merge pull request #471 from bwagner/patch-1
Update input-output.mdx
2023-02-22 22:53:22 +01:00
Felix Roos 0dc9bc3ab6 Merge pull request #464 from bwagner/main
FIXES: freqs instead of pitches
2023-02-22 22:52:49 +01:00
Felix Roos 0006d57a4a move isZen to nanostore
+ correctly parse non strings in store
2023-02-22 22:47:33 +01:00
Felix Roos 70f7e73b9a store footer tab + latest code in nanostore 2023-02-22 22:23:54 +01:00
Felix Roos b67b049802 refactor settings to nanostores 2023-02-22 22:04:39 +01:00
Felix Roos ff99dbcd22 customizable font family 2023-02-22 20:28:22 +01:00
Felix Roos 0198697737 Merge pull request #472 from tidalcycles/osc-fix
fix: osc should not return a promise
2023-02-22 20:01:00 +01:00
Felix Roos a742a71f67 fix: osc should not return a promise 2023-02-22 19:50:37 +01:00
Felix Roos 8f6c1531fa more contrast for blackscreen theme 2023-02-22 19:45:06 +01:00
Bernhard Wagner d9f56f11cb Merge pull request #1 from tidalcycles/main
a
2023-02-22 17:15:00 +01:00
Bernhard Wagner 7ff877464f Update input-output.mdx
npm -> pnpm
2023-02-22 17:06:15 +01:00
Alex McLean 7c367eb1e8 slice and splice (#466)
Implements `slice` and `splice` from tidal, intended for beat slicing
2023-02-22 11:51:31 +00:00
Felix Roos 20848aac09 fix store import 2023-02-19 23:28:46 +01:00
Felix Roos 7716574076 use terminal font for console + samples 2023-02-19 23:28:33 +01:00
Felix Roos 89cd0c769b fix bluescreen line bg 2023-02-19 23:27:56 +01:00
Felix Roos a21b3d788f add settings reset button 2023-02-19 22:32:28 +01:00
Felix Roos 7994ba8b38 get rid of all hard coded colors 2023-02-19 22:18:32 +01:00
Felix Roos 81b4cb2f16 more themes + fonts 2023-02-19 22:18:12 +01:00
Felix Roos 14af1df6a3 define font + fix app height 2023-02-19 20:04:30 +01:00
Felix Roos ac3bd7d7db rename algoboy theme + add font 2023-02-19 20:04:19 +01:00
Felix Roos 632e8e9634 use css var for highlighting color 2023-02-19 20:03:12 +01:00
Felix Roos 1e3f09f69b add some custom themes 2023-02-19 18:53:35 +01:00
Felix Roos f3f18ffca7 fix app height for ios 2023-02-19 13:50:08 +01:00
Felix Roos ceb3aa0627 add emacs mode + fontSize slider 2023-02-19 13:49:55 +01:00
Felix Roos 014555fe5d add vim toggle to settings
+ added persistent global state store
+ refactored themes to use the new store
2023-02-19 01:51:31 +01:00
Alex McLean fdb76867a7 weave and weaveWith (#465)
Added weave and weaveWith from tidal
2023-02-18 00:00:18 +00:00
Felix Roos 57157fde9c Merge branch 'main' into docs 2023-02-17 21:53:02 +01:00
Felix Roos 4a3540cf2b encapsulate footer components 2023-02-17 21:48:45 +01:00
Felix Roos 540bd938f2 remove unused Footer
+ fix AvatarList (still unused)
2023-02-17 21:48:34 +01:00
Bernhard Wagner c6fbebd996 FIXES: freqs instead of pitches 2023-02-17 19:05:09 +01:00
Felix Roos 18d3a7e23a add vim extension (hard coded) 2023-02-17 10:41:49 +01:00
Alex McLean cbae355896 Another attempt at composable functions - WIP (#390)
Summary of changes:

- Made unary functions composable, including controls. So e.g. s("bd sd").every(3,fast(2).iter(4).n(4)) works the same as s("bd sd").every(3,x => x.fast(2).iter(4).n(4))
- Made operators/alignments composable too, so s("bd sd").every(3, set.squeeze.n(3, 4)) works
- Patterns are not treated as functions, so s("bd sd").every(3, n(5)) is an annoying runtime error. s("bd sd").every(3, set.n(5)) does work though.

Other minor changes:

- standardised alignment 'squeezeOut' as lowercase 'squeezeout'
- made firstCycleValues turn haps sorted in order of 'part'
2023-02-16 23:15:21 +00:00
Felix Roos 924e8a764a Merge pull request #461 from bwagner/main
FIXES: Warning about jsxBracketSameLine deprecation
2023-02-16 20:38:45 +01:00
Bernhard Wagner 40e629cb0e ADDS: Continuation to Testing 2023-02-16 15:24:44 +01:00
Bernhard Wagner 99ec261f79 ADDS: Continuation to Docs 2023-02-16 15:23:34 +01:00
Bernhard Wagner 9daea5bbc2 ADDS: Continuation to REPL 2023-02-16 15:22:35 +01:00
Bernhard Wagner 4704c03d48 ADDS: Continuation to Strudel vs Tidal 2023-02-16 15:21:25 +01:00
Bernhard Wagner 9dbf52bedb ADDS: Continuation to Pattern Alignment 2023-02-16 15:20:26 +01:00
Bernhard Wagner cfd04ab7fa ADDS: Continuation to Patterns 2023-02-16 15:19:14 +01:00
Bernhard Wagner 33dbf75e3b ADDS: Continuation to Offline 2023-02-16 15:18:32 +01:00
Bernhard Wagner 8ffaeeb171 ADDS: Continuation to MIDI and OSC 2023-02-16 15:16:51 +01:00
Bernhard Wagner 451828e5d4 ADDS: Continuation to Tonal Modifiers 2023-02-16 15:15:54 +01:00
Bernhard Wagner 176877f2b8 ADDS: Continuation to Accumulation Modifiers 2023-02-16 15:14:38 +01:00
Bernhard Wagner 61b0d08b5f ADDS: Continuation to Conditional Modifiers 2023-02-16 15:12:37 +01:00
Bernhard Wagner f50a6ce1ae ADDS: Continuation to Signals 2023-02-16 15:11:28 +01:00
Bernhard Wagner 8549d6ea8b ADDS: Continuation to Control Parameters 2023-02-16 15:09:37 +01:00
Bernhard Wagner 90a90a87a5 ADDS: Continuation to Time Modifiers 2023-02-16 15:08:45 +01:00
Bernhard Wagner 760bdea521 ADDS: Continuation to Pattern Constructors 2023-02-16 15:07:03 +01:00
Bernhard Wagner 0fd783e83f ADDS: Continuation to Functional JavaScript API 2023-02-16 15:03:58 +01:00
Bernhard Wagner 42caa9058b ADDS: Continuation to Synths 2023-02-16 15:00:17 +01:00
Bernhard Wagner 9ab315ac2c ADDS: Continuation to Samples 2023-02-16 14:58:02 +01:00
Bernhard Wagner a08ad07537 ADDS: Continuation to Csound 2023-02-16 14:55:01 +01:00
Bernhard Wagner c6ce763f7c FIXES: Warning about jsxBracketSameLine deprecation 2023-02-16 14:46:33 +01:00
Felix Roos 70d1eb17a7 Merge branch 'main' into vim-mode 2023-02-16 11:41:01 +01:00
Felix Roos 0393205eda Merge pull request #438 from bwagner/patch-8
Update synths.mdx
2023-02-14 22:11:02 +01:00
Bernhard Wagner 5c05a5a63e Update synths.mdx
in future -> in the future
ADDS: Continuation to Audio Effects.
2023-02-14 22:08:40 +01:00
Felix Roos b01d9f849b Merge pull request #437 from bwagner/patch-5
Update mini-notation.mdx
2023-02-14 19:54:41 +01:00
Felix Roos 0ab904275c Merge pull request #436 from bwagner/patch-4
Update code.mdx
2023-02-14 19:54:24 +01:00
Bernhard Wagner 5f82a86801 Update mini-notation.mdx
descreases -> decreases
twice a cycle -> twice per cycle
2023-02-14 19:40:03 +01:00
Bernhard Wagner 13a5d8de86 Update code.mdx
The code example says "sine", but the description says "sawtooth".
2023-02-14 19:08:47 +01:00
Felix Roos f024b71808 Update CONTRIBUTING.md
add more infos on code style, eslint and tests
2023-02-14 18:16:39 +01:00
Felix Roos cd35a17904 Merge pull request #433 from tidalcycles/fix-anchors
Fix anchors
2023-02-13 00:43:29 +01:00
Felix Roos be3f68ed06 fix comment 2023-02-13 00:42:16 +01:00
Felix Roos 64b8fd9584 fix: right sidebar anchors earlier 2023-02-13 00:41:42 +01:00
Felix Roos b001d1d0f1 update lockfile 2023-02-13 00:37:20 +01:00
Felix Roos 64ad75c79e fix: anchor links
fixes https://github.com/tidalcycles/strudel/issues/432
+ fix style regression in mini repl
2023-02-13 00:35:40 +01:00
Felix Roos 12eb361d69 remove slash before anchor link 2023-02-12 20:13:59 +01:00
Felix Roos dd51bd0a2f Merge pull request #427 from tidalcycles/docs
autocomplete preparations
2023-02-11 21:02:11 +01:00
Felix Roos 0fb478a7c8 no autocomplete until upstream merge 2023-02-11 20:55:35 +01:00
Felix Roos 9694ecf55b autocomplete dark / light 2023-02-11 13:02:04 +01:00
Felix Roos fe80478b8a Merge remote-tracking branch 'origin/main' into docs 2023-02-10 23:18:04 +01:00
Felix Roos a253c26bee Merge pull request #431 from tidalcycles/themes
Themes
2023-02-10 23:14:47 +01:00
Felix Roos a500f983f5 Merge branch 'main' into themes 2023-02-10 22:59:52 +01:00
Felix Roos 3579b6f8f3 dynamic highlight color
+ refactor hooks
2023-02-10 22:52:34 +01:00
Felix Roos 14cb954213 themed minirepl
+ add strudelTheme
+ add lineBackground with opacity
+ add some missing light flags
+ add fallback css variables for non js context
2023-02-10 21:42:27 +01:00
Felix Roos 6170eef441 format 2023-02-10 20:09:23 +01:00
Felix Roos ddb9af4d57 themify rest of layout (except minirepls) 2023-02-10 20:07:36 +01:00
Felix Roos 6db2c0bf93 only invert prose for dark themes 2023-02-10 18:34:02 +01:00
Felix Roos 46f70e6a5e persisted theme + add all settings 2023-02-10 12:54:15 +01:00
Felix Roos 0b994a1b0f switchable css variables for theming 2023-02-10 11:32:36 +01:00
Felix Roos 0326244956 update themes 2023-02-10 09:25:58 +01:00
Felix Roos ea1b2252fc remove unused descriptions
+ fix some titles
+ comment out unfinished packages page
+ start offline page
2023-02-09 21:42:48 +01:00
Felix Roos 0da25ea7cc Merge pull request #429 from tidalcycles/minirepl-keyboard-support
minirepl: add keyboard shortcuts
2023-02-09 19:22:56 +01:00
Felix Roos 3209afa4ff minirepl: add keyboard shortcuts 2023-02-09 19:20:44 +01:00
Felix Roos 33e2631e5d update snapshot 2023-02-09 18:48:34 +01:00
Felix Roos f02a41b675 fix param description 2023-02-09 18:44:41 +01:00
Felix Roos f497d7b212 reactify autocomplete info 2023-02-09 18:37:29 +01:00
Felix Roos 0f18f5eb81 remove logs 2023-02-09 14:16:37 +01:00
Felix Roos 8da81104aa doc: note 2023-02-09 12:42:37 +01:00
Felix Roos 97cbdc6b0f use jsdoc in autocomplete, still not context aware
+ add noAutocomplete flags
+ add superdirtOnly flags
2023-02-09 12:19:38 +01:00
Felix Roos 7f53f4e36b Merge branch 'main' into docs 2023-02-09 08:57:20 +01:00
Felix Roos c350f262e6 Merge pull request #425 from tidalcycles/pwa
add cdn.freesound to cache list
2023-02-09 08:56:42 +01:00
Felix Roos 3e727893bf basic theme picker 2023-02-08 23:43:02 +01:00
Felix Roos 2a2c7437e4 add cdn.freesound to cache list 2023-02-08 21:18:03 +01:00
Felix Roos 4f0885d3af Merge branch 'main' into themes 2023-02-08 20:49:20 +01:00
Felix Roos 5656cd2fc6 Merge pull request #421 from tidalcycles/pwa
add more offline caching
2023-02-08 20:36:00 +01:00
Felix Roos b6e42876ad fix cache rule + disable service worker in dev 2023-02-08 20:32:03 +01:00
Felix Roos 3671344867 add freesound + shabda to offline cache rule 2023-02-08 20:26:44 +01:00
Felix Roos 30b149053c basic theme switcher 2023-02-08 18:54:34 +01:00
Felix Roos cdd40df9d3 disable sw in dev 2023-02-08 18:54:19 +01:00
Felix Roos 9d661808ba Merge pull request #419 from tidalcycles/pwa
add caching strategy for missing file types + cache all samples loaded from github
2023-02-07 22:08:01 +01:00
Felix Roos 6a5d849b3c add caching strategy for missing file types
+ cache all samples loaded from github
2023-02-07 21:50:08 +01:00
Felix Roos 25bbd73a90 Merge pull request #417 from tidalcycles/pwa
PWA with offline support
2023-02-06 23:29:56 +01:00
Felix Roos 3d27a5f8c7 format 2023-02-06 21:06:33 +01:00
Felix Roos 44ef372399 update og meta data 2023-02-06 21:05:28 +01:00
Felix Roos fd09dff381 basic pwa setup 2023-02-06 20:57:34 +01:00
Felix Roos 10580a0bec change rocket to strudel 2023-02-06 20:21:09 +01:00
Felix Roos f3f87b0a46 dont lint json 2023-02-06 20:20:45 +01:00
Felix Roos ac606bcf71 Update README.md 2023-02-05 23:16:38 +01:00
alex bb50df07e7 iclc reviews 2023-02-05 21:01:02 +00:00
Felix Roos f7ac033e1c Merge pull request #411 from tidalcycles/docs
improve samples doc
2023-02-05 17:27:32 +01:00
Felix Roos a1043352c1 Merge pull request #413 from tidalcycles/remove-google
google gtfo
2023-02-05 17:27:09 +01:00
Felix Roos e15fecf6cf google gtfo 2023-02-05 17:19:32 +01:00
Felix Roos 3e99f345ef better headings 2023-02-05 16:40:29 +01:00
Felix Roos 0a77770ef3 remove double directive 2023-02-05 16:37:00 +01:00
Felix Roos 379b8392dd add snapshot 2023-02-05 16:32:51 +01:00
Felix Roos 0fdae70461 improve samples doc 2023-02-05 16:28:43 +01:00
Felix Roos 087423c754 Merge pull request #409 from tidalcycles/docs
improve effects doc
2023-02-05 16:27:59 +01:00
Felix Roos ce64c98fb7 add adsr image link 2023-02-05 15:08:17 +01:00
Felix Roos e11a8a9dbb Merge pull request #410 from bwagner/patch-3
Update effects.mdx
2023-02-05 14:56:03 +01:00
Bernhard Wagner ef4a68a24d Update effects.mdx
Wether -> Whether
2023-02-05 14:54:27 +01:00
Felix Roos 0ec65c85d3 fix examples + snapshots 2023-02-05 14:52:01 +01:00
Felix Roos 74326f226d format 2023-02-05 14:42:08 +01:00
Felix Roos 554e0d7a39 remove envelope section from synths page 2023-02-05 14:38:47 +01:00
Felix Roos 1b99ac92dd categorize effects + add envelope doc
+ use short, more logical filter params as default
2023-02-05 14:34:59 +01:00
Felix Roos ff0b77908a Merge branch 'main' into docs 2023-02-05 14:34:16 +01:00
Felix Roos 83d19638ba Merge pull request #407 from tidalcycles/shabda-doc
add shabda doc
2023-02-03 19:54:47 +01:00
Felix Roos a7d484b1a8 add shabda doc 2023-02-03 19:53:01 +01:00
Felix Roos d87beb37b8 Merge pull request #405 from tidalcycles/fix-share-on-subpath
fix: share url on subpath
2023-02-02 21:45:56 +01:00
Felix Roos dcdb383960 fix: share url on subpath
fixes https://github.com/tidalcycles/strudel/issues/389
2023-02-02 21:43:24 +01:00
Felix Roos 478a7e95ea Merge pull request #404 from tidalcycles/csound-update
update csound + fix sound output
2023-02-02 21:35:45 +01:00
Felix Roos f7b3b582cc update csound + fix sound output
see https://github.com/csound/csound/issues/1690#issuecomment-1414301145
2023-02-02 21:31:44 +01:00
Felix Roos d1719dc277 Merge pull request #403 from tidalcycles/downgrade-csound
pin @csound/browser to 6.18.3 + bump
2023-02-02 19:53:36 +01:00
Felix Roos 9d8c812aa3 pin @csound/browser to 6.18.3 + bump 2023-02-02 19:51:16 +01:00
Felix Roos 4121fc926e write more about packages / monorepo setup 2023-01-28 17:59:59 +01:00
Felix Roos fd9ab987e6 Merge branch 'main' into docs 2023-01-28 14:23:01 +01:00
146 changed files with 7072 additions and 3266 deletions
+3
View File
@@ -15,3 +15,6 @@ vite.config.js
!**/*.mjs
**/*.tsx
**/*.ts
**/*.json
**/dev-dist
**/dist
+3 -1
View File
@@ -37,4 +37,6 @@ talk/public/EmuSP12
talk/public/samples
server/samples/old
repl/stats.html
coverage
coverage
public/icons/apple-splash-*
dev-dist
+2 -1
View File
@@ -8,4 +8,5 @@ packages/mini/krill-parser.js
packages/xen/tunejs.js
paper
pnpm-lock.yaml
pnpm-workspace.yaml
pnpm-workspace.yaml
**/dev-dist
+1 -1
View File
@@ -7,7 +7,7 @@
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"bracketSameLine": false,
"arrowParens": "always",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
+22
View File
@@ -82,6 +82,10 @@ Please report any problems you've had with the setup instructions!
## Code Style
To make sure the code changes only where it should, we are using prettier to unify the code style.
- You can format all files at once by running `pnpm prettier` from the project root
- Run `pnpm format-check` from the project root to check if all files are well formatted
If you use VSCode, you can
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
@@ -89,6 +93,24 @@ If you use VSCode, you can
3. Choose "Configure Default Formatter..."
4. Select prettier
## ESLint
To prevent unwanted runtime errors, this project uses [eslint](https://eslint.org/).
- You can check for lint errors by running `pnpm lint`
There are also eslint extensions / plugins for most editors.
## Running Tests
- Run all tests with `pnpm test`
- Run all tests with UI using `pnpm test-ui`
## Running all CI Checks
When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests.
You can run the same check with `pnpm check`
## Package Workflow
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
+3 -2
View File
@@ -7,14 +7,15 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
- Try it here: <https://strudel.tidalcycles.org/>
- Docs: <https://strudel.tidalcycles.org/learn/>
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
## Running Locally
After cloning the project, you can run the REPL locally:
```bash
npm run setup
npm run repl
pnpm run setup
pnpm run repl
```
## Using Strudel In Your Project
+2 -5
View File
@@ -20,11 +20,8 @@ Example: <https://felixroos.github.io/strudel/swatch/>
### 6. edit `website/astro.config.mjs` to use site: `https://<your-username>.github.io` and base `/strudel`, like this
```js
export default defineConfig({
/* ... rest of config ... */
site: 'https://<your-username>.github.io',
base: '/strudel',
});
const site = 'https://<your-username>.github.io';
const base = '/strudel';
```
### 7. commit & push the changes
+4 -3
View File
@@ -46,14 +46,15 @@
},
"homepage": "https://strudel.tidalcycles.org",
"dependencies": {
"dependency-tree": "^9.0.0",
"vitest": "^0.25.7",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/xen": "workspace:*"
"@strudel.cycles/xen": "workspace:*",
"acorn": "^8.8.1",
"dependency-tree": "^9.0.0",
"vitest": "^0.25.7"
},
"devDependencies": {
"@vitest/ui": "^0.25.7",
+308 -341
View File
File diff suppressed because it is too large Load Diff
+22 -17
View File
@@ -10,44 +10,49 @@ import { logger } from './logger.mjs';
export class Cyclist {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
this.started = false;
this.cps = 1; // TODO
this.phase = 0;
this.getTime = getTime;
this.cps = 1;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.lastBegin = 0; // query begin of last tick
this.lastEnd = 0; // query end of last tick
this.getTime = getTime; // get absolute time
this.onToggle = onToggle;
this.latency = latency;
this.latency = latency; // fixed trigger time offset
const round = (x) => Math.round(x * 1000) / 1000;
this.clock = createClock(
getTime,
// called slightly before each cycle
(phase, duration, tick) => {
if (tick === 0) {
this.origin = phase;
}
const begin = round(phase - this.origin);
this.phase = begin - latency;
const end = round(begin + duration);
const time = getTime();
try {
const haps = this.pattern.queryArc(begin, end); // get Haps
const time = getTime();
const begin = this.lastEnd;
this.lastBegin = begin;
const end = round(begin + duration * this.cps);
this.lastEnd = end;
const haps = this.pattern.queryArc(begin, end);
const tickdeadline = phase - time; // time left till phase begins
this.lastTick = time + tickdeadline;
haps.forEach((hap) => {
if (hap.part.begin.equals(hap.whole.begin)) {
const deadline = hap.whole.begin + this.origin - time + latency;
const duration = hap.duration * 1;
onTrigger?.(hap, deadline, duration);
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
const duration = hap.duration / this.cps;
onTrigger?.(hap, deadline, duration, this.cps);
}
});
} catch (e) {
logger(`[cyclist] error: ${e.message}`);
onError?.(e);
}
}, // called slightly before each cycle
},
interval, // duration of each cycle
);
}
getPhase() {
return this.getTime() - this.origin - this.latency;
}
now() {
return this.getTime() - this.origin + this.clock.minLatency;
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
}
setStarted(v) {
this.started = v;
+1 -7
View File
@@ -11,15 +11,9 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { Pattern, timeCat, register, silence } from './pattern.mjs';
import { rotate, flatten } from './util.mjs';
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
import Fraction from './fraction.mjs';
const splitAt = function (index, value) {
return [value.slice(0, index), value.slice(index)];
};
const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
const left = function (n, x) {
const [ons, offs] = n;
const [xs, ys] = x;
-8
View File
@@ -6,12 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { isPattern } from './index.mjs';
let scoped = false;
export const evalScope = async (...args) => {
if (scoped) {
console.warn('evalScope was called more than once.');
}
scoped = true;
const results = await Promise.allSettled(args);
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
results.forEach((result, i) => {
@@ -42,9 +37,6 @@ function safeEval(str, options = {}) {
}
export const evaluate = async (code, transpiler) => {
if (!scoped) {
await evalScope(); // at least scope Pattern.prototype.boostrap
}
if (transpiler) {
code = transpiler(code); // transform syntactically correct js code to semantically usable code
}
+13
View File
@@ -126,6 +126,19 @@ export class Hap {
setContext(context) {
return new Hap(this.whole, this.part, this.value, context);
}
ensureObjectValue() {
/* if (isNote(hap.value)) {
// supports primitive hap values that look like notes
hap.value = { note: hap.value };
} */
if (typeof this.value !== 'object') {
throw new Error(
`expected hap.value to be an object, but got "${this.value}". Hint: append .note() or .s() to the end`,
'error',
);
}
}
}
export default Hap;
+161 -24
View File
@@ -27,6 +27,7 @@ export class Pattern {
* Create a pattern. As an end user, you will most likely not create a Pattern directly.
*
* @param {function} query - The function that maps a {@link State} to an array of {@link Hap}.
* @noAutocomplete
*/
constructor(query) {
this.query = query;
@@ -51,6 +52,7 @@ export class Pattern {
/**
* see {@link Pattern#withValue}
* @noAutocomplete
*/
fmap(func) {
return this.withValue(func);
@@ -62,6 +64,7 @@ export class Pattern {
* pattern of functions.
* @param {Function} whole_func
* @param {Function} func
* @noAutocomplete
* @returns Pattern
*/
appWhole(whole_func, pat_val) {
@@ -97,6 +100,7 @@ export class Pattern {
* are not the same but do intersect, the resulting hap has a timespan of the
* intersection. This applies to both the part and the whole timespan.
* @param {Pattern} pat_val
* @noAutocomplete
* @returns Pattern
*/
appBoth(pat_val) {
@@ -117,6 +121,7 @@ export class Pattern {
* are preserved from the pattern of functions (often referred to as the left
* hand or inner pattern).
* @param {Pattern} pat_val
* @noAutocomplete
* @returns Pattern
*/
appLeft(pat_val) {
@@ -147,6 +152,7 @@ export class Pattern {
* pattern of values, i.e. structure is preserved from the right hand/outer
* pattern.
* @param {Pattern} pat_val
* @noAutocomplete
* @returns Pattern
*/
appRight(pat_val) {
@@ -332,6 +338,7 @@ export class Pattern {
* const haps = pattern.queryArc(0, 1)
* console.log(haps)
* silence
* @noAutocomplete
*/
queryArc(begin, end) {
try {
@@ -347,6 +354,7 @@ export class Pattern {
* some calculations easier to express, as all haps are then constrained to
* happen within a cycle.
* @returns Pattern
* @noAutocomplete
*/
splitQueries() {
const pat = this;
@@ -361,6 +369,7 @@ export class Pattern {
* timespan before passing it to the original pattern.
* @param {Function} func the function to apply
* @returns Pattern
* @noAutocomplete
*/
withQuerySpan(func) {
return new Pattern((state) => this.query(state.withSpan(func)));
@@ -382,6 +391,7 @@ export class Pattern {
* begin and end time of the query timespan.
* @param {Function} func the function to apply
* @returns Pattern
* @noAutocomplete
*/
withQueryTime(func) {
return new Pattern((state) => this.query(state.withSpan((span) => span.withTime(func))));
@@ -393,6 +403,7 @@ export class Pattern {
* present, `whole` timespans).
* @param {Function} func
* @returns Pattern
* @noAutocomplete
*/
withHapSpan(func) {
return new Pattern((state) => this.query(state).map((hap) => hap.withSpan(func)));
@@ -403,6 +414,7 @@ export class Pattern {
* begin and end time of the hap timespans.
* @param {Function} func the function to apply
* @returns Pattern
* @noAutocomplete
*/
withHapTime(func) {
return this.withHapSpan((span) => span.withTime(func));
@@ -412,6 +424,7 @@ export class Pattern {
* Returns a new pattern with the given function applied to the list of haps returned by every query.
* @param {Function} func
* @returns Pattern
* @noAutocomplete
*/
withHaps(func) {
return new Pattern((state) => func(this.query(state)));
@@ -421,6 +434,7 @@ export class Pattern {
* As with {@link Pattern#withHaps}, but applies the function to every hap, rather than every list of haps.
* @param {Function} func
* @returns Pattern
* @noAutocomplete
*/
withHap(func) {
return this.withHaps((haps) => haps.map(func));
@@ -430,6 +444,7 @@ export class Pattern {
* Returns a new pattern with the context field set to every hap set to the given value.
* @param {*} context
* @returns Pattern
* @noAutocomplete
*/
setContext(context) {
return this.withHap((hap) => hap.setContext(context));
@@ -439,6 +454,7 @@ export class Pattern {
* Returns a new pattern with the given function applied to the context field of every hap.
* @param {Function} func
* @returns Pattern
* @noAutocomplete
*/
withContext(func) {
return this.withHap((hap) => hap.setContext(func(hap.context)));
@@ -447,6 +463,7 @@ export class Pattern {
/**
* Returns a new pattern with the context field of every hap set to an empty object.
* @returns Pattern
* @noAutocomplete
*/
stripContext() {
return this.withHap((hap) => hap.setContext({}));
@@ -458,6 +475,7 @@ export class Pattern {
* @param {Number} start
* @param {Number} end
* @returns Pattern
* @noAutocomplete
*/
withLocation(start, end) {
const location = {
@@ -500,6 +518,7 @@ export class Pattern {
* Returns a new Pattern, which only returns haps that meet the given test.
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
* @returns Pattern
* @noAutocomplete
*/
filterHaps(hap_test) {
return new Pattern((state) => this.query(state).filter(hap_test));
@@ -510,6 +529,7 @@ export class Pattern {
* inside haps.
* @param {Function} value_test
* @returns Pattern
* @noAutocomplete
*/
filterValues(value_test) {
return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value)));
@@ -519,6 +539,7 @@ export class Pattern {
* Returns a new pattern, with haps containing undefined values removed from
* query results.
* @returns Pattern
* @noAutocomplete
*/
removeUndefineds() {
return this.filterValues((val) => val != undefined);
@@ -529,6 +550,7 @@ export class Pattern {
* with an onset is one with a `whole` timespan that begins at the same time
* as its `part` timespan.
* @returns Pattern
* @noAutocomplete
*/
onsetsOnly() {
// Returns a new pattern that will only return haps where the start
@@ -541,6 +563,7 @@ export class Pattern {
* Returns a new pattern, with 'continuous' haps (those without 'whole'
* timespans) removed from query results.
* @returns Pattern
* @noAutocomplete
*/
discreteOnly() {
// removes continuous haps that don't have a 'whole' timespan
@@ -550,6 +573,7 @@ export class Pattern {
/**
* Combines adjacent haps with the same value and whole. Only
* intended for use in tests.
* @noAutocomplete
*/
defragmentHaps() {
// remove continuous haps
@@ -604,6 +628,7 @@ export class Pattern {
* @param {Boolean} with_context - set to true, otherwise the context field
* will be stripped from the resulting haps.
* @returns [Hap]
* @noAutocomplete
*/
firstCycle(with_context = false) {
var self = this;
@@ -615,6 +640,7 @@ export class Pattern {
/**
* Accessor for a list of values returned by querying the first cycle.
* @noAutocomplete
*/
get firstCycleValues() {
return this.firstCycle().map((hap) => hap.value);
@@ -622,6 +648,7 @@ export class Pattern {
/**
* More human-readable version of the {@link Pattern#firstCycleValues} accessor.
* @noAutocomplete
*/
get showFirstCycle() {
return this.firstCycle().map(
@@ -633,6 +660,7 @@ export class Pattern {
* Returns a new pattern, which returns haps sorted in temporal order. Mainly
* of use when comparing two patterns for equality, in tests.
* @returns Pattern
* @noAutocomplete
*/
sortHapsByPart() {
return this.withHaps((haps) =>
@@ -812,7 +840,7 @@ export class Pattern {
}
//////////////////////////////////////////////////////////////////////
// functions relating to chords/patterns of lists
// functions relating to chords/patterns of lists/lists of patterns
// returns Array<Hap[]> where each list of haps satisfies eq
function groupHapsBy(eq, haps) {
@@ -861,6 +889,35 @@ Pattern.prototype.arp = function (pat) {
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
};
/*
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
* @name weave
* @memberof Pattern
* @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd"))
* @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd"))
addToPrototype('weave', function (t, ...pats) {
return this.weaveWith(t, ...pats.map((x) => set.out(x)));
});
*/
/*
* Like 'weave', but accepts functions rather than patterns, which are applied to the pattern.
* @name weaveWith
* @memberof Pattern
addToPrototype('weaveWith', function (t, ...funcs) {
const pat = this;
const l = funcs.length;
t = Fraction(t);
if (l == 0) {
return silence;
}
return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t);
});
*/
//////////////////////////////////////////////////////////////////////
// compose matrix functions
@@ -1103,6 +1160,7 @@ export const silence = new Pattern(() => []);
* @returns {Pattern}
* @example
* pure('e4') // "e4"
* @noAutocomplete
*/
export function pure(value) {
function query(state) {
@@ -1226,6 +1284,20 @@ export function timeCat(...timepats) {
return stack(...pats);
}
/**
* Allows to arrange multiple patterns together over multiple cycles.
* Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use.
*
* @return {Pattern}
* @example
* arrange([4, "<c a f e>(3,8)"],[2, "<g a>(5,8)"]).note()
*/
export function arrange(...sections) {
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
return timeCat(...sections).slow(total);
}
export function fastcat(...pats) {
return slowcat(...pats)._fast(pats.length);
}
@@ -1343,41 +1415,48 @@ export const func = curry((a, b) => reify(b).func(a));
*
* @param {string} name name of the function
* @param {function} func function with 1 or more params, where last is the current pattern
* @noAutocomplete
*
*/
export function register(name, func) {
export function register(name, func, patternify = true) {
if (Array.isArray(name)) {
const result = {};
for (const name_item of name) {
result[name_item] = register(name_item, func);
result[name_item] = register(name_item, func, patternify);
}
return result;
}
const arity = func.length;
var pfunc; // the patternified function
pfunc = function (...args) {
args = args.map(reify);
const pat = args[args.length - 1];
if (arity === 1) {
return func(pat);
}
const [left, ...right] = args.slice(0, -1);
let mapFn = (...args) => {
// make sure to call func with the correct argument count
// args.length is expected to be <= arity-1
// so we set undefined args explicitly undefined
Array(arity - 1)
.fill()
.map((_, i) => args[i] ?? undefined);
return func(...args, pat);
if (patternify) {
pfunc = function (...args) {
args = args.map(reify);
const pat = args[args.length - 1];
if (arity === 1) {
return func(pat);
}
const [left, ...right] = args.slice(0, -1);
let mapFn = (...args) => {
// make sure to call func with the correct argument count
// args.length is expected to be <= arity-1
// so we set undefined args explicitly undefined
Array(arity - 1)
.fill()
.map((_, i) => args[i] ?? undefined);
return func(...args, pat);
};
mapFn = curry(mapFn, null, arity - 1);
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
};
mapFn = curry(mapFn, null, arity - 1);
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
};
} else {
pfunc = function (...args) {
args = args.map(reify);
return func(...args);
};
}
Pattern.prototype[name] = function (...args) {
args = args.map(reify);
// For methods that take a single argument (plus 'this'), allow
// multiple arguments but sequence them
if (arity === 2 && args.length !== 1) {
@@ -1385,6 +1464,7 @@ export function register(name, func) {
} else if (arity !== args.length + 1) {
throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`);
}
args = args.map(reify);
return pfunc(...args, this);
};
@@ -1448,15 +1528,17 @@ export const ceil = register('ceil', function (pat) {
* Assumes a numerical pattern, containing unipolar values in the range 0 ..
* 1. Returns a new pattern with values scaled to the bipolar range -1 .. 1
* @returns Pattern
* @noAutocomplete
*/
export const toBipolar = register('toBipolar', function (pat) {
return pat.fmap((x) => x * 2 - 1);
});
/**
* Assumes a numerical pattern, containing bipolar values in the range -1 ..
* 1. Returns a new pattern with values scaled to the unipolar range 0 .. 1
* Assumes a numerical pattern, containing bipolar values in the range -1 .. 1
* Returns a new pattern with values scaled to the unipolar range 0 .. 1
* @returns Pattern
* @noAutocomplete
*/
export const fromBipolar = register('fromBipolar', function (pat) {
return pat.fmap((x) => (x + 1) / 2);
@@ -2162,6 +2244,61 @@ const _loopAt = function (factor, pat, cps = 1) {
.slow(factor);
};
/*
* Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers.
* @name slice
* @memberof Pattern
* @returns Pattern
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
*/
const slice = register(
'slice',
function (npat, ipat, opat) {
return npat.innerBind((n) =>
ipat.outerBind((i) =>
opat.outerBind((o) => {
// If it's not an object, assume it's a string and make it a 's' control parameter
o = o instanceof Object ? o : { s: o };
// Remember we must stay pure and avoid editing the object directly
const toAdd = { begin: i / n, end: (i + 1) / n, _slices: n };
return pure({ ...toAdd, ...o });
}),
),
);
},
false, // turns off auto-patternification
);
/*
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
* @name splice
* @memberof Pattern
* @returns Pattern
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
*/
const splice = register(
'splice',
function (npat, ipat, opat) {
const sliced = slice(npat, ipat, opat);
return sliced.withHap(function (hap) {
return hap.withValue((v) => ({
...{
speed: (1 / v._slices / hap.whole.duration) * (v.speed || 1),
unit: 'c',
},
...v,
}));
});
},
false, // turns off auto-patternification
);
const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
return _loopAt(factor, pat, 1);
});
+20 -4
View File
@@ -2,6 +2,7 @@ import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
export function repl({
interval,
@@ -17,13 +18,12 @@ export function repl({
}) {
const scheduler = new Cyclist({
interval,
onTrigger: async (hap, deadline, duration) => {
onTrigger: async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration);
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
const cps = 1;
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
@@ -42,6 +42,17 @@ export function repl({
}
try {
beforeEval?.({ code });
scheduler.setCps(1); // reset cps in case the code does not contain a setCps call
// problem: when the code does contain a setCps after an awaited promise,
// the cps will be 1 until the promise resolves
// example:
/*
await new Promise(resolve => setTimeout(resolve,1000))
setCps(.5)
note("c a f e")
*/
// to make sure the setCps inside the code is called immediately,
// it has to be placed first
let { pattern } = await _evaluate(code, transpiler);
logger(`[eval] code updated`);
@@ -58,5 +69,10 @@ export function repl({
const stop = () => scheduler.stop();
const start = () => scheduler.start();
const pause = () => scheduler.pause();
return { scheduler, evaluate, start, stop, pause };
const setCps = (cps) => scheduler.setCps(cps);
evalScope({
setCps,
setcps: setCps,
});
return { scheduler, evaluate, start, stop, pause, setCps };
}
+28
View File
@@ -0,0 +1,28 @@
/*
controls.test.mjs - <short description TODO>
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/controls.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 controls from '../controls.mjs';
import { mini } from '../../mini/mini.mjs';
import { describe, it, expect } from 'vitest';
describe('controls', () => {
it('should support controls', () => {
expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
});
it('should support compound controls', () => {
expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
{ s: 'bd', n: 3 },
{ s: 'sd', n: 4, gain: 1.4 },
]);
});
it('should support ignore extra elements in compound controls', () => {
expect(controls.s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
{ s: 'bd', n: 3, gain: 0.4 },
{ s: 'sd', n: 4, gain: 0.5 },
]);
});
});
+44
View File
@@ -959,4 +959,48 @@ describe('Pattern', () => {
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
});
});
/*describe('composable functions', () => {
it('Can compose functions', () => {
sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
});
it('Can compose by method chaining operators with controls', () => {
sameFirst(s('bd').apply(set.n(3).fast(2)), s('bd').set.n(3).fast(2));
});
it('Can compose by method chaining operators and alignments with controls', () => {
sameFirst(s('bd').apply(set.in.n(3).fast(2)), s('bd').set.n(3).fast(2));
// sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2));
});
});
describe('weave', () => {
it('Can distribute patterns along a pattern', () => {
sameFirst(n(0, 1).weave(2, s('bd', silence), s(silence, 'sd')), sequence(s('bd').n(0), s('sd').n(1)));
});
});
*/
describe('slice', () => {
it('Can slice a sample', () => {
sameFirst(
s('break').slice(4, sequence(0, 1, 2, 3)),
sequence(
{ begin: 0, end: 0.25, s: 'break', _slices: 4 },
{ begin: 0.25, end: 0.5, s: 'break', _slices: 4 },
{ begin: 0.5, end: 0.75, s: 'break', _slices: 4 },
{ begin: 0.75, end: 1, s: 'break', _slices: 4 },
),
);
});
});
describe('splice', () => {
it('Can splice a sample', () => {
sameFirst(
s('break').splice(4, sequence(0, 1, 2, 3)),
sequence(
{ begin: 0, end: 0.25, s: 'break', _slices: 4, unit: 'c', speed: 1 },
{ begin: 0.25, end: 0.5, s: 'break', _slices: 4, unit: 'c', speed: 1 },
{ begin: 0.5, end: 0.75, s: 'break', _slices: 4, unit: 'c', speed: 1 },
{ begin: 0.75, end: 1, s: 'break', _slices: 4, unit: 'c', speed: 1 },
),
);
});
});
});
+8
View File
@@ -58,6 +58,7 @@ export const valueToMidi = (value, fallbackValue) => {
/**
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
* @noAutocomplete
*/
export const getFreq = (noteOrMidi) => {
if (typeof noteOrMidi === 'number') {
@@ -68,6 +69,7 @@ export const getFreq = (noteOrMidi) => {
/**
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
* @noAutocomplete
*/
export const midi2note = (n) => {
const oct = Math.floor(n / 12) - 1;
@@ -204,3 +206,9 @@ export function parseFractional(numOrString) {
}
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
export const splitAt = function (index, value) {
return [value.slice(0, index), value.slice(index)];
};
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
+1 -1
View File
@@ -44,6 +44,6 @@ function createClock(
};
const getPhase = () => phase;
// setCallback
return { setDuration, start, stop, pause, duration, getPhase, minLatency };
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
}
export default createClock;
+2 -3
View File
@@ -28,9 +28,7 @@ export const csound = register('csound', (instrument, pat) => {
logger('[csound] not loaded yet', 'warning');
return;
}
if (typeof hap.value !== 'object') {
throw new Error('csound only support objects as hap values');
}
hap.ensureObjectValue();
let { gain = 0.8 } = hap.value;
gain *= 0.2;
@@ -92,6 +90,7 @@ async function load() {
['message'].forEach((k) => _csound.on(k, (...args) => eventLogger(k, args)));
await _csound.setOption('-m0d'); // see -m flag https://csound.com/docs/manual/CommandFlags.html
await _csound.setOption('--sample-accurate');
await _csound.setOption('-odac');
await _csound.compileCsdText(csd);
// await _csound.compileOrc(livecodeOrc);
await _csound.compileOrc(presetsOrc);
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/csound",
"version": "0.6.0",
"version": "0.6.2",
"description": "csound bindings for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -32,7 +32,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@csound/browser": "^6.18.3",
"@csound/browser": "6.18.5",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
},
+30 -15
View File
@@ -5,8 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import { Pattern, isPattern, isNote, getPlayableNoteValue, logger } from '@strudel.cycles/core';
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { toMidi } from '@strudel.cycles/core';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -63,7 +64,7 @@ function getDevice(output, outputs) {
}
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.midi = function (output, channel = 1) {
Pattern.prototype.midi = function (output) {
if (!supportsMidi()) {
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
}
@@ -90,11 +91,6 @@ Pattern.prototype.midi = function (output, channel = 1) {
);
}
return this.onTrigger((time, hap) => {
let note = getPlayableNoteValue(hap);
const velocity = hap.context?.velocity ?? 0.9;
if (!isNote(note)) {
throw new Error('not a note: ' + note);
}
if (!midiReady) {
return;
}
@@ -106,15 +102,34 @@ Pattern.prototype.midi = function (output, channel = 1) {
.join(' | ')}`,
);
}
// console.log('midi', value, output);
hap.ensureObjectValue();
// calculate time
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
time = time * 1000 + timingOffset;
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
// await enableWebMidi()
device.playNote(note, channel, {
time,
duration: hap.duration.valueOf() * 1000 - 5,
attack: velocity,
});
// destructure value
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
const duration = hap.duration.valueOf() * 1000 - 5;
if (note) {
const midiNumber = toMidi(note);
device.playNote(midiNumber, midichan, {
time,
duration,
attack: velocity,
});
}
if (ccv && ccn) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1');
}
if (!['string', 'number'].includes(typeof ccn)) {
throw new Error('expected ccn to be a number or a string');
}
const scaled = Math.round(ccv * 127);
device.sendControlChange(ccn, scaled, midichan, { time });
}
});
};
+143 -120
View File
@@ -182,21 +182,21 @@ function peg$parse(input, options) {
var peg$c8 = "#";
var peg$c9 = "^";
var peg$c10 = "_";
var peg$c11 = ":";
var peg$c12 = "[";
var peg$c13 = "]";
var peg$c14 = "{";
var peg$c15 = "}";
var peg$c16 = "%";
var peg$c17 = "<";
var peg$c18 = ">";
var peg$c19 = "@";
var peg$c20 = "!";
var peg$c21 = "(";
var peg$c22 = ")";
var peg$c23 = "/";
var peg$c24 = "*";
var peg$c25 = "?";
var peg$c11 = "[";
var peg$c12 = "]";
var peg$c13 = "{";
var peg$c14 = "}";
var peg$c15 = "%";
var peg$c16 = "<";
var peg$c17 = ">";
var peg$c18 = "@";
var peg$c19 = "!";
var peg$c20 = "(";
var peg$c21 = ")";
var peg$c22 = "/";
var peg$c23 = "*";
var peg$c24 = "?";
var peg$c25 = ":";
var peg$c26 = "struct";
var peg$c27 = "target";
var peg$c28 = "euclid";
@@ -237,21 +237,21 @@ function peg$parse(input, options) {
var peg$e15 = peg$literalExpectation("#", false);
var peg$e16 = peg$literalExpectation("^", false);
var peg$e17 = peg$literalExpectation("_", false);
var peg$e18 = peg$literalExpectation(":", false);
var peg$e19 = peg$literalExpectation("[", false);
var peg$e20 = peg$literalExpectation("]", false);
var peg$e21 = peg$literalExpectation("{", false);
var peg$e22 = peg$literalExpectation("}", false);
var peg$e23 = peg$literalExpectation("%", false);
var peg$e24 = peg$literalExpectation("<", false);
var peg$e25 = peg$literalExpectation(">", false);
var peg$e26 = peg$literalExpectation("@", false);
var peg$e27 = peg$literalExpectation("!", false);
var peg$e28 = peg$literalExpectation("(", false);
var peg$e29 = peg$literalExpectation(")", false);
var peg$e30 = peg$literalExpectation("/", false);
var peg$e31 = peg$literalExpectation("*", false);
var peg$e32 = peg$literalExpectation("?", false);
var peg$e18 = peg$literalExpectation("[", false);
var peg$e19 = peg$literalExpectation("]", false);
var peg$e20 = peg$literalExpectation("{", false);
var peg$e21 = peg$literalExpectation("}", false);
var peg$e22 = peg$literalExpectation("%", false);
var peg$e23 = peg$literalExpectation("<", false);
var peg$e24 = peg$literalExpectation(">", false);
var peg$e25 = peg$literalExpectation("@", false);
var peg$e26 = peg$literalExpectation("!", false);
var peg$e27 = peg$literalExpectation("(", false);
var peg$e28 = peg$literalExpectation(")", false);
var peg$e29 = peg$literalExpectation("/", false);
var peg$e30 = peg$literalExpectation("*", false);
var peg$e31 = peg$literalExpectation("?", false);
var peg$e32 = peg$literalExpectation(":", false);
var peg$e33 = peg$literalExpectation("struct", false);
var peg$e34 = peg$literalExpectation("target", false);
var peg$e35 = peg$literalExpectation("euclid", false);
@@ -280,35 +280,36 @@ function peg$parse(input, options) {
var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) };
var peg$f12 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
var peg$f12 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
var peg$f13 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
for (const op of ops) {
op(result);
}
return result;
};
var peg$f13 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f14 = function(tail) { return { alignment: 'stack', list: tail }; };
var peg$f15 = function(tail) { return { alignment: 'rand', list: tail }; };
var peg$f16 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } };
var peg$f17 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
var peg$f18 = function(sc) { return sc; };
var peg$f19 = function(s) { return { name: "struct", args: { mini:s }}};
var peg$f20 = function(s) { return { name: "target", args : { name:s}}};
var peg$f21 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
var peg$f22 = function(a) { return { name: "stretch", args :{ amount: a}}};
var peg$f23 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
var peg$f24 = function(a) { return { name: "shift", args :{ amount: a}}};
var peg$f25 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
var peg$f26 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
var peg$f27 = function(s, v) { return v};
var peg$f28 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
var peg$f29 = function(sg) {return sg};
var peg$f30 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
var peg$f31 = function(sc) { return sc };
var peg$f32 = function(c) { return c };
var peg$f33 = function(v) { return new CommandStub("setcps", { value: v})};
var peg$f34 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
var peg$f35 = function() { return new CommandStub("hush")};
var peg$f14 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f15 = function(tail) { return { alignment: 'stack', list: tail }; };
var peg$f16 = function(tail) { return { alignment: 'rand', list: tail }; };
var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } };
var peg$f18 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
var peg$f19 = function(sc) { return sc; };
var peg$f20 = function(s) { return { name: "struct", args: { mini:s }}};
var peg$f21 = function(s) { return { name: "target", args : { name:s}}};
var peg$f22 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
var peg$f23 = function(a) { return { name: "stretch", args :{ amount: a}}};
var peg$f24 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
var peg$f25 = function(a) { return { name: "shift", args :{ amount: a}}};
var peg$f26 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
var peg$f27 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
var peg$f28 = function(s, v) { return v};
var peg$f29 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
var peg$f30 = function(sg) {return sg};
var peg$f31 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
var peg$f32 = function(sc) { return sc };
var peg$f33 = function(c) { return c };
var peg$f34 = function(v) { return new CommandStub("setcps", { value: v})};
var peg$f35 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
var peg$f36 = function() { return new CommandStub("hush")};
var peg$currPos = 0;
var peg$savedPos = 0;
var peg$posDetailsCache = [{ line: 1, column: 1 }];
@@ -848,15 +849,6 @@ function peg$parse(input, options) {
s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e17); }
}
if (s0 === peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 58) {
s0 = peg$c11;
peg$currPos++;
} else {
s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e18); }
}
}
}
}
}
@@ -899,11 +891,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 91) {
s2 = peg$c12;
s2 = peg$c11;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e19); }
if (peg$silentFails === 0) { peg$fail(peg$e18); }
}
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
@@ -911,11 +903,11 @@ function peg$parse(input, options) {
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 93) {
s6 = peg$c13;
s6 = peg$c12;
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e20); }
if (peg$silentFails === 0) { peg$fail(peg$e19); }
}
if (s6 !== peg$FAILED) {
s7 = peg$parsews();
@@ -943,11 +935,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 123) {
s2 = peg$c14;
s2 = peg$c13;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e21); }
if (peg$silentFails === 0) { peg$fail(peg$e20); }
}
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
@@ -955,11 +947,11 @@ function peg$parse(input, options) {
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 125) {
s6 = peg$c15;
s6 = peg$c14;
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e22); }
if (peg$silentFails === 0) { peg$fail(peg$e21); }
}
if (s6 !== peg$FAILED) {
s7 = peg$parsepolymeter_steps();
@@ -990,11 +982,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 37) {
s1 = peg$c16;
s1 = peg$c15;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e23); }
if (peg$silentFails === 0) { peg$fail(peg$e22); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseslice();
@@ -1019,11 +1011,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 60) {
s2 = peg$c17;
s2 = peg$c16;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e24); }
if (peg$silentFails === 0) { peg$fail(peg$e23); }
}
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
@@ -1031,11 +1023,11 @@ function peg$parse(input, options) {
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 62) {
s6 = peg$c18;
s6 = peg$c17;
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e25); }
if (peg$silentFails === 0) { peg$fail(peg$e24); }
}
if (s6 !== peg$FAILED) {
s7 = peg$parsews();
@@ -1088,6 +1080,9 @@ function peg$parse(input, options) {
s0 = peg$parseop_replicate();
if (s0 === peg$FAILED) {
s0 = peg$parseop_degrade();
if (s0 === peg$FAILED) {
s0 = peg$parseop_tail();
}
}
}
}
@@ -1102,11 +1097,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 64) {
s1 = peg$c19;
s1 = peg$c18;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e26); }
if (peg$silentFails === 0) { peg$fail(peg$e25); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
@@ -1130,11 +1125,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 33) {
s1 = peg$c20;
s1 = peg$c19;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e27); }
if (peg$silentFails === 0) { peg$fail(peg$e26); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
@@ -1158,11 +1153,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 40) {
s1 = peg$c21;
s1 = peg$c20;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e28); }
if (peg$silentFails === 0) { peg$fail(peg$e27); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1186,11 +1181,11 @@ function peg$parse(input, options) {
}
s12 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 41) {
s13 = peg$c22;
s13 = peg$c21;
peg$currPos++;
} else {
s13 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e29); }
if (peg$silentFails === 0) { peg$fail(peg$e28); }
}
if (s13 !== peg$FAILED) {
peg$savedPos = s0;
@@ -1224,11 +1219,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 47) {
s1 = peg$c23;
s1 = peg$c22;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e30); }
if (peg$silentFails === 0) { peg$fail(peg$e29); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseslice();
@@ -1252,11 +1247,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 42) {
s1 = peg$c24;
s1 = peg$c23;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e31); }
if (peg$silentFails === 0) { peg$fail(peg$e30); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseslice();
@@ -1280,11 +1275,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 63) {
s1 = peg$c25;
s1 = peg$c24;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e32); }
if (peg$silentFails === 0) { peg$fail(peg$e31); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
@@ -1301,6 +1296,34 @@ function peg$parse(input, options) {
return s0;
}
function peg$parseop_tail() {
var s0, s1, s2;
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 58) {
s1 = peg$c25;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e32); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f12(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parseslice_with_ops() {
var s0, s1, s2, s3;
@@ -1314,7 +1337,7 @@ function peg$parse(input, options) {
s3 = peg$parseslice_op();
}
peg$savedPos = s0;
s0 = peg$f12(s1, s2);
s0 = peg$f13(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1339,7 +1362,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f13(s1);
s1 = peg$f14(s1);
}
s0 = s1;
@@ -1388,7 +1411,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f14(s1);
s1 = peg$f15(s1);
}
s0 = s1;
@@ -1437,7 +1460,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f15(s1);
s1 = peg$f16(s1);
}
s0 = s1;
@@ -1458,7 +1481,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f16(s1, s2);
s0 = peg$f17(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1478,7 +1501,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f17(s1, s2);
s0 = peg$f18(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1499,7 +1522,7 @@ function peg$parse(input, options) {
s4 = peg$parsequote();
if (s4 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f18(s3);
s0 = peg$f19(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1561,7 +1584,7 @@ function peg$parse(input, options) {
s3 = peg$parsemini_or_operator();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f19(s3);
s0 = peg$f20(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1594,7 +1617,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f20(s4);
s0 = peg$f21(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1639,7 +1662,7 @@ function peg$parse(input, options) {
s7 = null;
}
peg$savedPos = s0;
s0 = peg$f21(s3, s5, s7);
s0 = peg$f22(s3, s5, s7);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1672,7 +1695,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f22(s3);
s0 = peg$f23(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1701,7 +1724,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f23(s3);
s0 = peg$f24(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1730,7 +1753,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f24(s3);
s0 = peg$f25(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1759,7 +1782,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f25(s3);
s0 = peg$f26(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1801,7 +1824,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f26(s4);
s0 = peg$f27(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1876,11 +1899,11 @@ function peg$parse(input, options) {
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 91) {
s3 = peg$c12;
s3 = peg$c11;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e19); }
if (peg$silentFails === 0) { peg$fail(peg$e18); }
}
if (s3 !== peg$FAILED) {
s4 = peg$parsews();
@@ -1893,7 +1916,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) {
peg$savedPos = s7;
s7 = peg$f27(s5, s9);
s7 = peg$f28(s5, s9);
} else {
peg$currPos = s7;
s7 = peg$FAILED;
@@ -1910,7 +1933,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) {
peg$savedPos = s7;
s7 = peg$f27(s5, s9);
s7 = peg$f28(s5, s9);
} else {
peg$currPos = s7;
s7 = peg$FAILED;
@@ -1922,15 +1945,15 @@ function peg$parse(input, options) {
}
s7 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 93) {
s8 = peg$c13;
s8 = peg$c12;
peg$currPos++;
} else {
s8 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e20); }
if (peg$silentFails === 0) { peg$fail(peg$e19); }
}
if (s8 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f28(s5, s6);
s0 = peg$f29(s5, s6);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1976,7 +1999,7 @@ function peg$parse(input, options) {
s4 = peg$parsecomment();
}
peg$savedPos = s0;
s0 = peg$f29(s1);
s0 = peg$f30(s1);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1998,7 +2021,7 @@ function peg$parse(input, options) {
s5 = peg$parsemini_or_operator();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f30(s1, s5);
s0 = peg$f31(s1, s5);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2023,7 +2046,7 @@ function peg$parse(input, options) {
s1 = peg$parsemini_or_operator();
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f31(s1);
s1 = peg$f32(s1);
}
s0 = s1;
if (s0 === peg$FAILED) {
@@ -2056,7 +2079,7 @@ function peg$parse(input, options) {
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f32(s2);
s0 = peg$f33(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2081,7 +2104,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f33(s3);
s0 = peg$f34(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2110,7 +2133,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f34(s3);
s0 = peg$f35(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2136,7 +2159,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f35();
s1 = peg$f36();
}
s0 = s1;
+5 -2
View File
@@ -96,7 +96,7 @@ quote = '"' / "'"
// ------------------ steps and cycles ---------------------------
// single step definition (e.g bd)
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_"
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
// define a sub cycle e.g. [1 2, 3 [4]]
@@ -119,7 +119,7 @@ slice = step / sub_cycle / polymeter / slow_sequence
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
// at this point, we assume we can represent them as regular sequence operators
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail
op_weight = "@" a:number
{ return x => x.options_['weight'] = a }
@@ -139,6 +139,9 @@ op_fast = "*"a:slice
op_degrade = "?"a:number?
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
op_tail = ":" s:slice
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
slice_with_ops = s:slice ops:slice_op*
{ const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
+6
View File
@@ -18,6 +18,7 @@ const applyOptions = (parent, code) => (pat, i) => {
const ast = parent.source_[i];
const options = ast.options_;
const ops = options?.ops;
if (ops) {
for (const op of ops) {
switch (op.type_) {
@@ -66,6 +67,11 @@ const applyOptions = (parent, code) => (pat, i) => {
pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
break;
}
case 'tail': {
const friend = patternifyAST(op.arguments_.element, code);
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
break;
}
default: {
console.warn(`operator "${op.type_}" not implemented`);
}
+3
View File
@@ -140,6 +140,9 @@ describe('mini', () => {
expect(haps.length < 230).toBe(true);
// 'Had too many cycles remaining after degradeBy 0.8');
});
it('supports lists', () => {
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
});
/*it('supports the random choice operator ("|") with nesting', () => {
const numCycles = 900;
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
+5 -3
View File
@@ -39,14 +39,16 @@ let startedAt = -1;
/**
*
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
* For more info, read [MIDI & OSC in the docs](https://strudel.tidalcycles.org/learn/input-output)
*
* @name osc
* @memberof Pattern
* @returns Pattern
*/
Pattern.prototype.osc = async function () {
const osc = await connect();
return this.onTrigger((time, hap, currentTime, cps = 1) => {
Pattern.prototype.osc = function () {
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
hap.ensureObjectValue();
const osc = await connect();
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
// time should be audio time of onset
+33 -14
View File
@@ -13,24 +13,43 @@ npm i @strudel.cycles/react
Here is a minimal example of how to set up a MiniRepl:
```jsx
import { evalScope, controls } from '@strudel.cycles/core';
import * as React from 'react';
import '@strudel.cycles/react/dist/style.css';
import { MiniRepl } from '@strudel.cycles/react';
import { prebake } from '../repl/src/prebake.mjs';
import { evalScope, controls } from '@strudel.cycles/core';
import { samples, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
/* probably import other strudel packages */
);
async function prebake() {
await samples(
'https://strudel.tidalcycles.org/tidal-drum-machines.json',
'github:ritchse/tidal-drum-machines/main/machines/'
);
await samples(
'https://strudel.tidalcycles.org/EmuSP12.json',
'https://strudel.tidalcycles.org/EmuSP12/'
);
}
prebake();
async function init() {
await evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/tonal')
);
await prebake();
initAudioOnFirstClick();
}
export function Repl({ tune }) {
return <MiniRepl tune={tune} hideOutsideView={true} />;
if (typeof window !== 'undefined') {
init();
}
export default function App() {
return <MiniRepl tune={`s("bd sd,hh*4")`} />;
}
```
For a more sophisticated example, check out the [nano-repl](./examples/nano-repl/)!
- Open [example on stackblitz](https://stackblitz.com/edit/react-ts-saaair?file=tune.tsx,App.tsx)
- Also check out the [nano-repl](./examples/nano-repl/) for a more sophisticated example
@@ -0,0 +1,15 @@
# nano-repl
this is an example of how to create a repl with strudel and react.
## Usage
after cloning the strudel repo:
```sh
pnpm i
cd packages/react/examples/nano-repl
pnpm dev
```
you should now have a repl running at `http://localhost:5173/`
@@ -66,7 +66,7 @@ function App() {
const [code, setCode] = useState(defaultTune);
const [view, setView] = useState();
// const [code, setCode] = useState(`"c3".note().slow(2)`);
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern } = useStrudel({
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern, started } = useStrudel({
code,
defaultOutput: webaudioOutput,
getTime,
@@ -75,8 +75,8 @@ function App() {
useHighlighting({
view,
pattern,
active: !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.getPhase(),
active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.now(),
});
const error = evalError || schedulerError;
@@ -2,6 +2,18 @@
@tailwind components;
@tailwind utilities;
:root {
--background: #222;
--lineBackground: #22222250;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
--selectionMatch: #036dd626;
--lineHighlight: #00000050;
--gutterBackground: transparent;
--gutterForeground: #8a919966;
}
body {
background: #123;
}
@@ -6,9 +6,23 @@ This program is free software: you can redistribute it and/or modify it under th
module.exports = {
// TODO: find out if leaving out tutorial path works now
content: ['./src/**/*.{js,jsx,ts,tsx}'],
content: ['./src/**/*.{js,jsx,ts,tsx}', '../../src/**/*.{html,js,jsx,md,mdx,ts,tsx}'],
theme: {
extend: {},
extend: {
colors: {
// codemirror-theme settings
background: 'var(--background)',
lineBackground: 'var(--lineBackground)',
foreground: 'var(--foreground)',
caret: 'var(--caret)',
selection: 'var(--selection)',
selectionMatch: 'var(--selectionMatch)',
gutterBackground: 'var(--gutterBackground)',
gutterForeground: 'var(--gutterForeground)',
gutterBorder: 'var(--gutterBorder)',
lineHighlight: 'var(--lineHighlight)',
},
},
},
plugins: [],
};
+10 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/react",
"version": "0.6.0",
"version": "0.6.4",
"description": "React components for strudel",
"main": "src/index.js",
"publishConfig": {
@@ -32,10 +32,13 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.4.0",
"@codemirror/lang-javascript": "^6.1.1",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.7.3",
"@lezer/highlight": "^1.1.3",
"@replit/codemirror-emacs": "^6.0.0",
"@replit/codemirror-vim": "^6.0.6",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
@@ -44,17 +47,17 @@
"react-hook-inview": "^4.5.0"
},
"peerDependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^2.2.0",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.18",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.0.24",
"vite": "^3.2.2"
}
-2
View File
@@ -8,8 +8,6 @@ evalScope(
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
);
@@ -0,0 +1,77 @@
import { createRoot } from 'react-dom/client';
import jsdoc from '../../../../doc.json';
const getDocLabel = (doc) => doc.name || doc.longname;
const getInnerText = (html) => {
var div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
};
export function Autocomplete({ doc }) {
return (
<div className="prose dark:prose-invert max-h-[400px] overflow-auto">
<h3 className="pt-0 mt-0">{getDocLabel(doc)}</h3>
<div dangerouslySetInnerHTML={{ __html: doc.description }} />
<ul>
{doc.params?.map(({ name, type, description }, i) => (
<li key={i}>
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
</li>
))}
</ul>
<div>
{doc.examples?.map((example, i) => (
<div key={i}>
<pre
className="cursor-pointer"
onMouseDown={(e) => {
console.log('ola!');
navigator.clipboard.writeText(example);
e.stopPropagation();
}}
>
{example}
</pre>
</div>
))}
</div>
</div>
);
}
const jsdocCompletions = jsdoc.docs
.filter(
(doc) =>
getDocLabel(doc) &&
!getDocLabel(doc).startsWith('_') &&
!['package'].includes(doc.kind) &&
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
)
// https://codemirror.net/docs/ref/#autocomplete.Completion
.map((doc) /*: Completion */ => ({
label: getDocLabel(doc),
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
info: () => {
const node = document.createElement('div');
// if Autocomplete is non-interactive, it could also be rendered at build time..
// .. using renderToStaticMarkup
createRoot(node).render(<Autocomplete doc={doc} />);
return node;
},
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
}));
export const strudelAutocomplete = (context /* : CompletionContext */) => {
let word = context.matchBefore(/\w*/);
if (word.from == word.to && !context.explicit) return null;
return {
from: word.from,
options: jsdocCompletions,
/* options: [
{ label: 'match', type: 'keyword' },
{ label: 'hello', type: 'variable', info: '(World)' },
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
], */
};
};
+43 -9
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import _CodeMirror from '@uiw/react-codemirror';
import { EditorView, Decoration } from '@codemirror/view';
import { StateField, StateEffect } from '@codemirror/state';
@@ -6,6 +6,10 @@ import { javascript } from '@codemirror/lang-javascript';
import strudelTheme from '../themes/strudel-theme';
import './style.css';
import { useCallback } from 'react';
import { autocompletion } from '@codemirror/autocomplete';
//import { strudelAutocomplete } from './Autocomplete';
import { vim } from '@replit/codemirror-vim';
import { emacs } from '@replit/codemirror-emacs';
export const setFlash = StateEffect.define();
const flashField = StateField.define({
@@ -49,19 +53,20 @@ const highlightField = StateField.define({
try {
for (let e of tr.effects) {
if (e.is(setHighlights)) {
const { haps } = e.value;
const marks =
e.value
haps
.map((hap) =>
(hap.context.locations || []).map(({ start, end }) => {
const color = hap.context.color || '#FFCA28';
// const color = hap.context.color || e.value.color || '#FFCA28';
let from = tr.newDoc.line(start.line).from + start.column;
let to = tr.newDoc.line(end.line).from + end.column;
const l = tr.newDoc.length;
if (from > l || to > l) {
return; // dont mark outside of range, as it will throw an error
}
// const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
//const mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
const mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
return mark.range(from, to);
}),
)
@@ -79,9 +84,27 @@ const highlightField = StateField.define({
provide: (f) => EditorView.decorations.from(f),
});
const extensions = [javascript(), strudelTheme, highlightField, flashField];
const staticExtensions = [
javascript(),
highlightField,
flashField,
// javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
// autocompletion({ override: [strudelAutocomplete] }),
autocompletion({ override: [] }), // wait for https://github.com/uiwjs/react-codemirror/pull/458
];
export default function CodeMirror({ value, onChange, onViewChanged, onSelectionChange, options, editorDidMount }) {
export default function CodeMirror({
value,
onChange,
onViewChanged,
onSelectionChange,
theme,
keybindings,
fontSize = 18,
fontFamily = 'monospace',
options,
editorDidMount,
}) {
const handleOnChange = useCallback(
(value) => {
onChange?.(value);
@@ -102,16 +125,27 @@ export default function CodeMirror({ value, onChange, onViewChanged, onSelection
},
[onSelectionChange],
);
const extensions = useMemo(() => {
let bindings = {
vim,
emacs,
};
if (bindings[keybindings]) {
return [...staticExtensions, bindings[keybindings]()];
}
return staticExtensions;
}, [keybindings]);
return (
<>
<div style={{ fontSize, fontFamily }} className="w-full">
<_CodeMirror
value={value}
theme={theme || strudelTheme}
onChange={handleOnChange}
onCreateEditor={handleOnCreateEditor}
onUpdate={handleOnUpdate}
extensions={extensions}
/>
</>
</div>
);
}
+52 -20
View File
@@ -7,13 +7,22 @@ import useHighlighting from '../hooks/useHighlighting.mjs';
import useStrudel from '../hooks/useStrudel.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
import { Icon } from './Icon';
import styles from './MiniRepl.module.css';
import './style.css';
import { logger } from '@strudel.cycles/core';
import useEvent from '../hooks/useEvent.mjs';
import useKeydown from '../hooks/useKeydown.mjs';
const getTime = () => getAudioContext().currentTime;
export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTime, punchcard, canvasHeight = 200 }) {
export function MiniRepl({
tune,
hideOutsideView = false,
enableKeyboard,
drawTime,
punchcard,
canvasHeight = 200,
theme,
}) {
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
const evalOnMount = !!drawTime;
const drawContext = useCallback(
@@ -63,6 +72,27 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTi
getTime: () => scheduler.now(),
});
// keyboard shortcuts
useKeydown(
useCallback(
async (e) => {
if (view?.hasFocus) {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === 'Period') {
stop();
e.preventDefault();
}
}
}
},
[activateCode, stop, view],
),
);
// set active pattern on ctrl+enter
useLayoutEffect(() => {
if (enableKeyboard) {
@@ -98,20 +128,32 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTi
);
return (
<div className={styles.container} ref={ref}>
<div className={styles.header}>
<div className={styles.buttons}>
<button className={cx(styles.button, started ? 'animate-pulse' : '')} onClick={() => togglePlay()}>
<div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight" ref={ref}>
<div className="flex justify-between bg-lineHighlight">
<div className="flex">
<button
className={cx(
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
started ? 'animate-pulse' : '',
)}
onClick={() => togglePlay()}
>
<Icon type={started ? 'stop' : 'play'} />
</button>
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
<button
className={cx(
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
)}
onClick={() => activateCode()}
>
<Icon type="refresh" />
</button>
</div>
{error && <div className={styles.error}>{error.message}</div>}
{error && <div className="text-right p-1 text-sm text-red-200">{error.message}</div>}
</div>
<div className={styles.body}>
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
<div className="overflow-auto relative">
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} theme={theme} />}
</div>
{drawTime && (
<canvas
@@ -140,13 +182,3 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTi
function useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
}
// TODO: dedupe
function useEvent(name, onTrigger, useCapture = false) {
useEffect(() => {
document.addEventListener(name, onTrigger, useCapture);
return () => {
document.removeEventListener(name, onTrigger, useCapture);
};
}, [onTrigger]);
}
@@ -1,27 +0,0 @@
.container {
@apply rounded-md overflow-hidden bg-[#222222];
}
.header {
@apply flex justify-between bg-slate-700 border-t border-slate-500;
}
.buttons {
@apply flex;
}
.button {
@apply cursor-pointer w-16 flex items-center justify-center p-1 bg-slate-700 border-r border-slate-500 text-white hover:bg-slate-600;
}
.buttonDisabled {
@apply w-16 flex items-center justify-center p-1 bg-slate-600 text-slate-400 cursor-not-allowed;
}
.error {
@apply text-right p-1 text-sm text-red-200;
}
.body {
@apply overflow-auto relative;
}
+17 -5
View File
@@ -1,14 +1,26 @@
:root {
--background: #222;
--lineBackground: #22222250;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
--selectionMatch: #036dd626;
--lineHighlight: #00000050;
--gutterBackground: transparent;
--gutterForeground: #8a919966;
}
.cm-editor {
background-color: transparent !important;
height: 100%;
z-index: 11;
font-size: 18px;
}
.cm-theme {
width: 100%;
height: 100%;
}
.cm-theme-light {
width: 100%;
}
.cm-line > * {
background: #00000095;
}
+12
View File
@@ -0,0 +1,12 @@
import { useEffect } from 'react';
function useEvent(name, onTrigger, useCapture = false) {
useEffect(() => {
document.addEventListener(name, onTrigger, useCapture);
return () => {
document.removeEventListener(name, onTrigger, useCapture);
};
}, [onTrigger]);
}
export default useEvent;
+5 -4
View File
@@ -1,5 +1,6 @@
import { useEffect, useRef } from 'react';
import { setHighlights } from '../components/CodeMirror6';
const round = (x) => Math.round(x * 1000) / 1000;
function useHighlighting({ view, pattern, active, getTime }) {
const highlights = useRef([]);
@@ -14,14 +15,14 @@ function useHighlighting({ view, pattern, active, getTime }) {
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
// see https://github.com/tidalcycles/strudel/issues/108
const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy
const span = [begin, audioTime + 1 / 60];
const span = [round(begin), round(audioTime + 1 / 60)];
lastEnd.current = span[1];
highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
highlights.current = highlights.current.concat(haps); // add potential new onsets
view.dispatch({ effects: setHighlights.of(highlights.current) }); // highlight all still active + new active haps
view.dispatch({ effects: setHighlights.of({ haps: highlights.current }) }); // highlight all still active + new active haps
} catch (err) {
view.dispatch({ effects: setHighlights.of([]) });
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
}
frame = requestAnimationFrame(updateHighlights);
});
@@ -30,7 +31,7 @@ function useHighlighting({ view, pattern, active, getTime }) {
};
} else {
highlights.current = [];
view.dispatch({ effects: setHighlights.of([]) });
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
}
}
}, [pattern, active, view]);
+5 -7
View File
@@ -10,7 +10,6 @@ function useStrudel({
getTime,
evalOnMount = false,
initialCode = '',
autolink = false,
beforeEval,
afterEval,
editPattern,
@@ -33,7 +32,7 @@ function useStrudel({
const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
// TODO: make sure this hook reruns when scheduler.started changes
const { scheduler, evaluate, start, stop, pause } = useMemo(
const { scheduler, evaluate, start, stop, pause, setCps } = useMemo(
() =>
repl({
interval,
@@ -51,15 +50,13 @@ function useStrudel({
setCode(code);
beforeEval?.();
},
afterEval: ({ pattern: _pattern, code }) => {
afterEval: (res) => {
const { pattern: _pattern, code } = res;
setActiveCode(code);
setPattern(_pattern);
setEvalError();
setSchedulerError();
if (autolink) {
window.location.hash = '#' + encodeURIComponent(btoa(code));
}
afterEval?.();
afterEval?.(res);
},
onToggle: (v) => {
setStarted(v);
@@ -156,6 +153,7 @@ function useStrudel({
stop,
pause,
togglePlay,
setCps,
};
}
+6 -4
View File
@@ -1,9 +1,11 @@
// import 'tailwindcss/tailwind.css';
export { default as CodeMirror, flash } from './components/CodeMirror6';
export * from './components/MiniRepl';
export { default as useHighlighting } from './hooks/useHighlighting';
export { default as CodeMirror, flash } from './components/CodeMirror6'; // !SSR
export * from './components/MiniRepl'; // !SSR
export { default as useHighlighting } from './hooks/useHighlighting'; // !SSR
export { default as useStrudel } from './hooks/useStrudel'; // !SSR
export { default as usePostMessage } from './hooks/usePostMessage';
export { default as useStrudel } from './hooks/useStrudel';
export { default as useKeydown } from './hooks/useKeydown';
export { default as useEvent } from './hooks/useEvent';
export { default as strudelTheme } from './themes/strudel-theme';
export { default as cx } from './cx';
+2 -3
View File
@@ -1,10 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { createRoot } from 'react-dom/client';
ReactDOM.render(
createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root'),
);
+39
View File
@@ -0,0 +1,39 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: '#9bbc0f',
foreground: '#0f380f', // whats that?
caret: '#0f380f',
selection: '#306230',
selectionMatch: '#ffffff26',
lineHighlight: '#8bac0f',
lineBackground: '#9bbc0f50',
//lineBackground: 'transparent',
gutterBackground: 'transparent',
gutterForeground: '#0f380f',
light: true,
};
export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.keyword, color: '#0f380f' },
{ tag: t.operator, color: '#0f380f' },
{ tag: t.special(t.variableName), color: '#0f380f' },
{ tag: t.typeName, color: '#0f380f' },
{ tag: t.atom, color: '#0f380f' },
{ tag: t.number, color: '#0f380f' },
{ tag: t.definition(t.variableName), color: '#0f380f' },
{ tag: t.string, color: '#0f380f' },
{ tag: t.special(t.string), color: '#0f380f' },
{ tag: t.comment, color: '#0f380f' },
{ tag: t.variableName, color: '#0f380f' },
{ tag: t.tagName, color: '#0f380f' },
{ tag: t.bracket, color: '#0f380f' },
{ tag: t.meta, color: '#0f380f' },
{ tag: t.attributeName, color: '#0f380f' },
{ tag: t.propertyName, color: '#0f380f' },
{ tag: t.className, color: '#0f380f' },
{ tag: t.invalid, color: '#0f380f' },
],
});
+37
View File
@@ -0,0 +1,37 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'black',
foreground: 'white', // whats that?
caret: 'white',
selection: '#ffffff20',
selectionMatch: '#036dd626',
lineHighlight: '#ffffff10',
lineBackground: '#00000050',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
{ tag: t.typeName, color: 'white' },
{ tag: t.atom, color: 'white' },
{ tag: t.number, color: 'white' },
{ tag: t.definition(t.variableName), color: 'white' },
{ tag: t.string, color: 'white' },
{ tag: t.special(t.string), color: 'white' },
{ tag: t.comment, color: 'white' },
{ tag: t.variableName, color: 'white' },
{ tag: t.tagName, color: 'white' },
{ tag: t.bracket, color: 'white' },
{ tag: t.meta, color: 'white' },
{ tag: t.attributeName, color: 'white' },
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
],
});
+40
View File
@@ -0,0 +1,40 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: '#051DB5',
lineBackground: '#051DB550',
foreground: 'white', // whats that?
caret: 'white',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
{ tag: t.typeName, color: 'white' },
{ tag: t.atom, color: 'white' },
{ tag: t.number, color: 'white' },
{ tag: t.definition(t.variableName), color: 'white' },
{ tag: t.string, color: 'white' },
{ tag: t.special(t.string), color: 'white' },
{ tag: t.comment, color: 'white' },
{ tag: t.variableName, color: 'white' },
{ tag: t.tagName, color: 'white' },
{ tag: t.bracket, color: 'white' },
{ tag: t.meta, color: 'white' },
{ tag: t.attributeName, color: 'white' },
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
],
});
+36
View File
@@ -0,0 +1,36 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'black',
foreground: '#41FF00', // whats that?
caret: '#41FF00',
selection: '#ffffff20',
selectionMatch: '#036dd626',
lineHighlight: '#ffffff10',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: '#41FF00' },
{ tag: t.operator, color: '#41FF00' },
{ tag: t.special(t.variableName), color: '#41FF00' },
{ tag: t.typeName, color: '#41FF00' },
{ tag: t.atom, color: '#41FF00' },
{ tag: t.number, color: '#41FF00' },
{ tag: t.definition(t.variableName), color: '#41FF00' },
{ tag: t.string, color: '#41FF00' },
{ tag: t.special(t.string), color: '#41FF00' },
{ tag: t.comment, color: '#41FF00' },
{ tag: t.variableName, color: '#41FF00' },
{ tag: t.tagName, color: '#41FF00' },
{ tag: t.bracket, color: '#41FF00' },
{ tag: t.meta, color: '#41FF00' },
{ tag: t.attributeName, color: '#41FF00' },
{ tag: t.propertyName, color: '#41FF00' },
{ tag: t.className, color: '#41FF00' },
{ tag: t.invalid, color: '#41FF00' },
],
});
+38
View File
@@ -0,0 +1,38 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'white',
foreground: 'black', // whats that?
caret: 'black',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#ffffff26',
lineHighlight: '#cccccc50',
lineBackground: '#ffffff50',
gutterBackground: 'transparent',
gutterForeground: 'black',
light: true,
};
export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.keyword, color: 'black' },
{ tag: t.operator, color: 'black' },
{ tag: t.special(t.variableName), color: 'black' },
{ tag: t.typeName, color: 'black' },
{ tag: t.atom, color: 'black' },
{ tag: t.number, color: 'black' },
{ tag: t.definition(t.variableName), color: 'black' },
{ tag: t.string, color: 'black' },
{ tag: t.special(t.string), color: 'black' },
{ tag: t.comment, color: 'black' },
{ tag: t.variableName, color: 'black' },
{ tag: t.tagName, color: 'black' },
{ tag: t.bracket, color: 'black' },
{ tag: t.meta, color: 'black' },
{ tag: t.attributeName, color: 'black' },
{ tag: t.propertyName, color: 'black' },
{ tag: t.className, color: 'black' },
{ tag: t.invalid, color: 'black' },
],
});
+15 -1
View File
@@ -1,7 +1,21 @@
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
extend: {
colors: {
// codemirror-theme settings
background: 'var(--background)',
lineBackground: 'var(--lineBackground)',
foreground: 'var(--foreground)',
caret: 'var(--caret)',
selection: 'var(--selection)',
selectionMatch: 'var(--selectionMatch)',
gutterBackground: 'var(--gutterBackground)',
gutterForeground: 'var(--gutterForeground)',
gutterBorder: 'var(--gutterBorder)',
lineHighlight: 'var(--lineHighlight)',
},
},
},
plugins: [],
corePlugins: {
+15
View File
@@ -9,6 +9,7 @@ This program is free software: you can redistribute it and/or modify it under th
import '../tonal.mjs'; // need to import this to add prototypes
import { pure, controls, seq } from '@strudel.cycles/core';
import { describe, it, expect } from 'vitest';
import { mini } from '../../mini/mini.mjs';
const { n } = controls;
describe('tonal', () => {
@@ -30,4 +31,18 @@ describe('tonal', () => {
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with colon', () => {
expect(
n(0, 1, 2)
.scale('C:major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with mininotation colon', () => {
expect(
n(0, 1, 2)
.scale(mini('C:major'))
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
});
+15 -6
View File
@@ -123,28 +123,37 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
/**
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
*
* The scale name has the form "TO? N" wher
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
*
* - T = Tonic
* - O = Octave (optional, defaults to 3)
* - N = Name of scale, available names can be found [here](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
* The root note defaults to octave 3, if no octave number is given.
*
* @memberof Pattern
* @name scale
* @param {string} scale Name of scale
* @returns Pattern
* @example
* "0 2 4 6 4 2".scale("C2:major").note()
* @example
* "0 2 4 6 4 2"
* .scale(seq('C2 major', 'C2 minor').slow(2))
* .scale("C2:<major minor>")
* .note()
* @example
* "0 1 2 3 4 5 6 7".rev().scale("C2:<major minor>").note()
* .s("folkharp")
*/
export const scale = register('scale', function (scale /* : string */, pat) {
export const scale = register('scale', function (scale, pat) {
// Supports ':' list syntax in mininotation
if (Array.isArray(scale)) {
scale = scale.join(' ');
}
return pat.withHap((hap) => {
const isObject = typeof hap.value === 'object';
let note = isObject ? hap.value.n : hap.value;
const asNumber = Number(note);
if (!isNaN(asNumber)) {
// TODO: worth keeping for supporting ':' in (non-mininotation) strings?
scale = scale.replaceAll(':', ' ');
let [tonic, scaleName] = Scale.tokenize(scale);
const { pc, oct = 3 } = Note.get(tonic);
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
+3 -1
View File
@@ -111,7 +111,9 @@ let sampleCache = { current: undefined };
/**
* Loads a collection of samples to use with `s`
*
* @example
* samples('github:tidalcycles/Dirt-Samples/master');
* s("[bd ~]*2, [~ hh]*2, ~ sd")
* @example
* samples({
* bd: '808bd/BD0000.WAV',
+14 -50
View File
@@ -97,17 +97,6 @@ const getSoundfontKey = (s) => {
return;
};
const splitSN = (s, n) => {
if (!s.includes(':')) {
return [s, n];
}
let [s2, n2] = s.split(':');
if (isNaN(Number(n2))) {
return [s, n];
}
return [s2, n2];
};
let workletsLoading;
function loadWorklets() {
if (workletsLoading) {
@@ -191,23 +180,11 @@ function effectSend(input, effect, wet) {
}
// export const webaudioOutput = async (t, hap, ct, cps) => {
export const webaudioOutput = async (hap, deadline, hapDuration) => {
export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
const ac = getAudioContext();
/* if (isNote(hap.value)) {
// supports primitive hap values that look like notes
hap.value = { note: hap.value };
} */
if (typeof hap.value !== 'object') {
logger(
`hap.value "${hap.value}" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
'error',
);
/* throw new Error(
`hap.value "${hap.value}"" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
); */
return;
}
// calculate correct time (tone.js workaround)
hap.ensureObjectValue();
// calculate absolute time
let t = ac.currentTime + deadline;
// destructure value
let {
@@ -220,20 +197,14 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
note,
gain = 0.8,
// low pass
lpf,
cutoff = lpf,
lpq = 1,
resonance = lpq,
cutoff,
resonance = 1,
// high pass
hpf,
hcutoff = hpf,
hpq = 1,
hresonance = hpq,
hcutoff,
hresonance = 1,
// band pass
bpf,
bandf = bpf,
bpq = 1,
bandq = bpq,
bandf,
bandq = 1,
//
coarse,
crush,
@@ -253,7 +224,6 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
orbit = 1,
room,
size = 2,
roomsize = size,
} = hap.value;
const { velocity = 1 } = hap.context;
gain *= velocity; // legacy fix for velocity
@@ -262,12 +232,6 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
if (bank && s) {
s = `${bank}_${s}`;
}
if (typeof s === 'string') {
[s, n] = splitSN(s, n);
}
if (typeof note === 'string') {
[note, n] = splitSN(note, n);
}
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = hap.value;
@@ -324,7 +288,7 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
if (unit === 'c') {
// are there other units?
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration;
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * cps;
}
let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value;
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
@@ -385,8 +349,8 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
}
// reverb
let reverbSend;
if (room > 0 && roomsize > 0) {
const reverbNode = getReverb(orbit, roomsize);
if (room > 0 && size > 0) {
const reverbNode = getReverb(orbit, size);
reverbSend = effectSend(post, reverbNode, room);
}
@@ -397,7 +361,7 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
chain[0].onended = () => chain.concat([delaySend, reverbSend]).forEach((n) => n?.disconnect());
};
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps);
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps, cps);
Pattern.prototype.webaudio = function () {
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
+1
View File
@@ -59,6 +59,7 @@ export function loadWebDirt(config) {
* @returns Pattern
* @example
* s("bd*2 hh sd hh").n("<0 1>").webdirt()
* @noAutocomplete
*/
Pattern.prototype.webdirt = function () {
// create a WebDirt object and initialize Web Audio context
File diff suppressed because one or more lines are too long
+597
View File
@@ -0,0 +1,597 @@
[
{
"id": "toussaintEuclideanAlgorithmGenerates2005",
"type": "paper-conference",
"abstract": "The Euclidean algorithm (which comes down to us from Euclid's Elements) computes the greatest common divisor of two given integers. It is shown here that the structure of the Euclidean algorithm may be used to automatically generate, very efficiently, a large family of rhythms used as timelines (ostinatos), in traditional world music. These rhythms, here dubbed Euclidean rhythms, have the property that their onset patterns are distributed as evenly as possible in a mathematically precise sense, and optimal manner.",
"container-title": "In Proceedings of BRIDGES: Mathematical Connections in Art, Music and Science",
"page": "4756",
"title": "The Euclidean Algorithm Generates Traditional Musical Rhythms",
"URL": "http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231",
"author": [
{
"family": "Toussaint",
"given": "Godfried"
}
],
"issued": {
"date-parts": [
[
"2005"
]
]
}
},
{
"id": "roberts2015",
"type": "paper-conference",
"abstract": "We describe research extending the interactive affordances of textual code fragments in creative coding environments. In particular we examine the potential of source code both to display the state of running processes and also to alter state using means other than traditional text editing. In contrast to previous research that has focused on the inclusion of additional interactive widgets inside or alongside text editors, our research adds a parsing stage to the runtime evaluation of code fragments and imparts additional interactive capabilities on the source code itself. After implementing various techniques in the creative coding environment Gibber, we evaluate our research through a survey on the various methods of visual feedback provided by our research. In addition to results quantifying preferences for certain techniques over others, we found near unanimous support among survey respondents for including similar techniques in other live coding environments.",
"collection-title": "NIME 2015",
"container-title": "Proceedings of the international conference on New Interfaces for Musical Expression",
"event-place": "Baton Rouge, Louisiana, USA",
"ISBN": "978-0-692-49547-6",
"page": "126131",
"publisher": "The School of Music and the Center for Computation and Technology (CCT), Louisiana State University",
"publisher-place": "Baton Rouge, Louisiana, USA",
"source": "ACM Digital Library",
"title": "Beyond Editing: Extended Interaction with Textual Code Fragments",
"title-short": "Beyond Editing",
"author": [
{
"family": "Roberts",
"given": "Charles"
},
{
"family": "Wright",
"given": "Matthew"
},
{
"family": "Kuchera-Morin",
"given": "JoAnn"
}
],
"accessed": {
"date-parts": [
[
"2021",
1,
27
]
]
},
"issued": {
"date-parts": [
[
"2015",
5,
30
]
]
}
},
{
"id": "rohrhuberSuperDirt2022",
"type": "software",
"abstract": "Tidal Audio Engine",
"genre": "SuperCollider",
"license": "GPL-2.0",
"note": "original-date: 2015-11-03T20:18:44Z",
"publisher": "musikinformatik",
"source": "GitHub",
"title": "SuperDirt",
"URL": "https://github.com/musikinformatik/SuperDirt",
"author": [
{
"family": "Rohrhuber",
"given": "Julian"
}
],
"accessed": {
"date-parts": [
[
"2022",
6,
24
]
]
},
"issued": {
"date-parts": [
[
"2022",
6,
24
]
]
}
},
{
"id": "mcleanAlgorithmicPattern2020",
"type": "paper-conference",
"abstract": "This paper brings together two main perspectives on algorithmic pattern. First, the writing of musical patterns in live coding performance, and second, the weaving of patterns in textiles. In both cases, algorithmic pattern is an interface between the human and the outcome, where small changes have far-reaching impact on the results. By bringing contemporary live coding and ancient textile approaches together, we reach a common view of pattern as algorithmic movement (e.g. looping, shifting, reflecting, interfering) in the making of things. This works beyond the usual definition of pattern used in musical interfaces, of mere repeating sequences. We conclude by considering the place of algorithmic pattern in a wider activity of making.",
"container-title": "Proceedings of the International Conference on New Interfaces for Musical Expression",
"event-place": "Birmingham, UK",
"event-title": "NIME2020",
"page": "265--270",
"publisher-place": "Birmingham, UK",
"title": "Algorithmic Pattern",
"URL": "https://zenodo.org/record/4813352",
"author": [
{
"family": "Mclean",
"given": "Alex"
}
],
"accessed": {
"date-parts": [
[
"2022",
1,
9
]
]
},
"issued": {
"date-parts": [
[
"2020",
6,
1
]
]
}
},
{
"id": "mcleanFeedforward2020",
"type": "paper-conference",
"abstract": "This is an improvised, from-scratch live coding performance. The NIME interface which this performance showcases is the new Feedfoward editor for the TidalCycles live coding environment. Feedforward is written in Haskell using the ncurses library for terminal-based user interfaces. It runs on low-powered hardware including the Raspberry Pi Zero, with formative testing of prototypes conducted with several groups of children between the ages of 8 and 14. Feedforward has a number of features designed to support improvised, multi-pattern live coding. Individual Tidal patterns are addressable with hotkeys for fast mute and unmuting. Each pattern has a stereo VU meter, to aid the quick matching of sound to pattern within a mix. In addition, TidalCycles has been extended to store context with each event, so that source code positions in its polyrhythmic sequence mini-notation are tracked. This allows steps to be highlighted in the source code when- ever they are active. This works even when Tidal combinators have been applied to manipulate the timeline. Formal evaluation has yet to take place, but this feature appears to support learning of how pattern manipulations work in Tidal. Feedforward and TidalCycles is free/open source software under a GPL licence version 3.0.",
"container-title": "Proceedings of New Interfaces for Musical Expression",
"event-place": "Birmingham",
"event-title": "NIME2020",
"publisher-place": "Birmingham",
"title": "Feedforward",
"URL": "https://zenodo.org/record/6353969",
"author": [
{
"family": "McLean",
"given": "Alex"
}
],
"accessed": {
"date-parts": [
[
"2022",
6,
24
]
]
},
"issued": {
"date-parts": [
[
"2020",
7,
21
]
]
}
},
{
"id": "robertsGibberLiveCoding2012",
"type": "paper-conference",
"abstract": "We present Gibber: a live coding environment for web browsers. Gibber performances are written in pure Java-Script with no syntactical additions or modifications; this enables Gibber code to be executed in any web page viewed inside a browser implementing a realtime audio API. Gib-ber offers an array of synthesis options (FM, granular, subtractive, physical modeling), audio effects and sequenc-ing objects to control them. The Gibber environment en-ables simple networked performances where multiple users simultaneously control a remote instance of Gibber. We strove to make the syntax of Gibber clear and concise; when coupled with the ability to run examples in any web page this gives Gibber interesting possibilities as an edu-cational tool. 1.",
"container-title": "In Proceedings of the 2012 International Computer Music Conference",
"source": "CiteSeer",
"title": "Gibber: Live coding audio in the browser",
"title-short": "Gibber",
"author": [
{
"family": "Roberts",
"given": "Charles"
},
{
"family": "Kuchera-morin",
"given": "Joann"
}
],
"issued": {
"date-parts": [
[
"2012"
]
]
}
},
{
"id": "ogbornEstuaryBrowserbasedCollaborative2017",
"type": "paper-conference",
"abstract": "This paper describes the initial design and development of Estuary, a browser-based collaborative projectional editing environment built on top of the popular TidalCycles language for the live coding of musical pattern. Key features of Estuary include a strict form of structure editing (making syntactical errors impossible), a click-only border-free approach to interface design, explicit notations to modulate the liveness of different parts of the code, and a server-based network collaboration system that can be used for many simultaneous collaborative live coding performances, as well as to present different views of the same live coding activity. Estuary has been developed using Reflex-DOM, a Haskell-based framework for web development whose strictness promises robustness and security advantages.",
"container-title": "Proceedings of the International Conference on Live Coding",
"event-place": "Morelia",
"event-title": "ICLC2017",
"language": "en",
"page": "11",
"publisher-place": "Morelia",
"source": "Zotero",
"title": "Estuary: Browser-based Collaborative Projectional Live Coding of Musical Patterns",
"author": [
{
"family": "Ogborn",
"given": "David"
},
{
"family": "Beverley",
"given": "Jamie"
},
{
"family": "Navarro del Angel",
"given": "Luis"
},
{
"family": "Tsabary",
"given": "Eldad"
},
{
"family": "McLean",
"given": "Alex"
}
],
"issued": {
"date-parts": [
[
"2017"
]
]
}
},
{
"id": "mcleanTidalVortexZero2022",
"type": "paper-conference",
"abstract": "In this paper we introduce version zero of TidalVortex, an alternative implementation of the TidalCycles live coding system, using the Python programming language.  This is open-ended work, exploring what happens when we try to extract the 'essence' of a system like TidalCycles and translate it into another programming language, while taking advantage of the affordance of its new host. First, we review the substantial prior art in porting TidalCycles, and in representing musical patterns in Python. We then compare equivalent patterns written in Haskell (TidalCycles) and Python (TidalVortex), and relate implementation details of how functional reactive paradigms have translated from the pure functional, strongly typed Haskell to the more multi-paradigm, dynamically typed Python. Finally, we conclude with reflections and generalisable outcomes.",
"container-title": "Proceedings of the 7th International Conference on Live Coding",
"DOI": "10.5281/zenodo.6456380",
"event-place": "Limerick, Ireland",
"event-title": "ICMC2022",
"note": "event-title: International Conference on Computer Music (ICMC)",
"publisher": "Zenodo",
"publisher-place": "Limerick, Ireland",
"title": "TidalVortex Zero",
"URL": "https://zenodo.org/record/6456380/export/hx",
"author": [
{
"family": "McLean",
"given": "Alex"
},
{
"family": "Forment",
"given": "Raphaël"
},
{
"family": "Le Beux",
"given": "Sylvain"
},
{
"family": "Silvani",
"given": "Damián"
}
],
"accessed": {
"date-parts": [
[
"2022",
6,
24
]
]
},
"issued": {
"date-parts": [
[
"2022",
4,
12
]
]
}
},
{
"id": "solomonPurescriptocarina2022",
"type": "software",
"abstract": "Web audio graphs as a stream",
"genre": "PureScript",
"license": "Apache-2.0",
"note": "original-date: 2021-03-25T12:19:55Z",
"source": "GitHub",
"title": "purescript-ocarina",
"URL": "https://github.com/mikesol/purescript-ocarina",
"author": [
{
"family": "Solomon",
"given": "Mike"
}
],
"accessed": {
"date-parts": [
[
"2022",
6,
24
]
]
},
"issued": {
"date-parts": [
[
"2022",
6,
20
]
]
}
},
{
"id": "orlareyFaustProgrammingLanguage2023",
"type": "software",
"abstract": "Functional programming language for signal processing and sound synthesis",
"genre": "C++",
"note": "original-date: 2016-11-05T13:09:11Z",
"publisher": "GRAME",
"source": "GitHub",
"title": "Faust - Programming Language for Audio Applications and Plugins",
"URL": "https://github.com/grame-cncm/faust",
"author": [
{
"family": "Orlarey",
"given": "Yann"
},
{
"family": "Fober",
"given": "Dominique"
},
{
"family": "Letz",
"given": "Stéphane"
}
],
"accessed": {
"date-parts": [
[
"2023",
3,
14
]
]
},
"issued": {
"date-parts": [
[
"2023",
3,
13
]
]
}
},
{
"id": "jackHydra2023",
"type": "software",
"abstract": "Livecoding networked visuals in the browser",
"genre": "JavaScript",
"license": "AGPL-3.0",
"note": "original-date: 2022-06-13T16:57:34Z",
"source": "GitHub",
"title": "Hydra",
"URL": "https://github.com/ojack/hydra",
"author": [
{
"family": "Jack",
"given": "Olivia"
}
],
"accessed": {
"date-parts": [
[
"2023",
3,
14
]
]
},
"issued": {
"date-parts": [
[
"2023",
3,
8
]
]
}
},
{
"id": "lanGlicol2023",
"type": "software",
"abstract": "Graph-oriented live coding language and music DSP library written in Rust",
"genre": "Rust",
"license": "MIT",
"note": "original-date: 2020-07-13T22:13:33Z",
"source": "GitHub",
"title": "Glicol",
"URL": "https://github.com/chaosprint/glicol",
"author": [
{
"family": "Lan",
"given": "Qichao"
}
],
"accessed": {
"date-parts": [
[
"2023",
3,
14
]
]
},
"issued": {
"date-parts": [
[
"2023",
3,
14
]
]
}
},
{
"id": "ogbornWebDirt2022",
"type": "software",
"abstract": "Sampling engine implemented in Web Audio API (rough re-creation of Dirt)",
"genre": "JavaScript",
"license": "GPL-3.0",
"note": "original-date: 2016-05-04T21:19:04Z",
"source": "GitHub",
"title": "WebDirt",
"URL": "https://github.com/dktr0/WebDirt",
"author": [
{
"family": "Ogborn",
"given": "David"
}
],
"accessed": {
"date-parts": [
[
"2023",
3,
14
]
]
},
"issued": {
"date-parts": [
[
"2022",
12,
14
]
]
}
},
{
"id": "roosStrudelAlgorithmicPatterns2022",
"type": "paper-conference",
"abstract": "This paper introduces Strudel (or sometimes StrudelCycles), an alternative implementation of the Tidal (or Tidal-Cycles) live coding system, using the JavaScript programming language. Strudel is an attempt to make live coding more accessible, by creating a system that runs entirely in the browser, while opening Tidals approach to algorithmic patterns (Mclean 2020) up to modern audio/visual web technologies. The Strudel REPL is a live code editor dedicated to manipulating Strudel patterns while they play, with builtin visual feedback. While Strudel is written in JavaScript, the API is optimized for simplicity and readability by applying code transformations on the syntax tree level, allowing language operations that would otherwise be impossible. The application supports multiple ways to output sound, including Tone.js, Web Audio nodes, OSC (Open Sound Control) messages, Web Serial andWeb MIDI. The project is split into multiple packages, allowing granular reuse in other applications. Apart from TidalCycles, Strudel draws inspiration from many prior existing projects like TidalVortex (McLean et al. 2022), Gibber (Roberts and Kuchera-morin 2012), Estuary (Ogborn et al. 2017), Hydra (Jack [2022] 2022), Ocarina (Solomon [2021] 2022) and Feedforward (McLean 2020).",
"container-title": "Proceedings of Web Audio Conference",
"event-place": "Cannes, France",
"event-title": "WAC2022",
"note": "DOI: 10.5281/zenodo.6821023\npublisher: Zenodo",
"publisher-place": "Cannes, France",
"title": "Strudel: Algorithmic Patterns for the Web",
"URL": "https://zenodo.org/record/6821023",
"author": [
{
"family": "Roos",
"given": "Felix"
},
{
"family": "McLean",
"given": "Alex"
}
],
"accessed": {
"date-parts": [
[
"2022",
11,
29
]
]
},
"issued": {
"date-parts": [
[
"2022",
6,
28
]
]
}
},
{
"id": "yiWebAssemblyAudioWorkletCsound2018",
"type": "paper-conference",
"abstract": "This paper describes WebAssembly AudioWorklet (WAAW)\nCsound, one of the implementations of Web Audio Csound.\nWe begin by introducing the background to this current implementation, stemming from the two first ports of Csound\nto the web platform using Native Clients and asm.js. The\ntechnology of WebAssembly is then introduced and discussed in its more relevant aspects. The AudioWorklet interface of Web Audio API is explored, together with its use in\nWAAW Csound. We complement this discussion by considering the overarching question of support for multiple platforms, which implement different versions of Web Audio.\nSome initial examples of the system are presented to illustrate various potential applications. Finally, we complement\nthe paper by discussing current issues that are fundamental\nfor this project and others that rely on the development of\na robust support for WASM-based audio computing.",
"event-place": "Berlin, Germany",
"event-title": "Web Audio Conference WAC-2018",
"language": "en",
"publisher-place": "Berlin, Germany",
"source": "mural.maynoothuniversity.ie",
"title": "WebAssembly AudioWorklet Csound",
"URL": "https://mural.maynoothuniversity.ie/16018/",
"author": [
{
"family": "Yi",
"given": "Steven"
},
{
"family": "Lazzarini",
"given": "Victor"
},
{
"family": "Costello",
"given": "Edward"
}
],
"accessed": {
"date-parts": [
[
"2023",
3,
14
]
]
},
"issued": {
"date-parts": [
[
"2018"
]
]
}
},
{
"id": "majdaPeggy2023",
"type": "software",
"abstract": "Peggy: Parser generator for JavaScript",
"genre": "JavaScript",
"license": "MIT",
"note": "original-date: 2020-11-21T11:52:39Z",
"publisher": "peggyjs",
"source": "GitHub",
"title": "Peggy",
"URL": "https://github.com/peggyjs/peggy",
"author": [
{
"family": "Majda",
"given": "David"
}
],
"accessed": {
"date-parts": [
[
"2023",
3,
14
]
]
},
"issued": {
"date-parts": [
[
"2023",
3,
14
]
]
}
}
]
+97
View File
@@ -0,0 +1,97 @@
**Submission ID:** 8
**Title:** Strudel: live coding patterns on the Web
**Status:** Accept
**Reviewer 1:**
Author Comments: * Summary of the contribution:\
This paper describes Strudel, a Web-based implementation of Tidal Cycles. It is generally a good overview of the system which could help prospective users understand how it works beyond the manual, and it provides many code examples. The most interesting contribution is the comparison between the Haskell and JavaScript features for this case.
* On theme (1-5):\
1
* Citation quality (1-5):\
5
* Missing references:\
not answered
* Suggested improvements:\
This paper is a good introduction to Strudel. The style is mostly illustrative, but it is hard to understand where the Tidal syntax is being introduced vs where the specific Strudel features are used. Thus, the best way to improve it would be to focus on Strudel. Also, it would be good to give a better hint on what output method would be preferable. Also the title of Section 9 should compare Strudel to Tidal or Haskell to JavaScript.
The paper is driven by examples, but many are a bit difficult to follow, here are some issues:\
Section 4 - the mini-notation example does not look terser despite the text.\
Section 5 - echo funciton is not used.\
Sections 7.11 and 7.12 - the same (mini-notation) example is used so it is not clear what relation there is between transpilation and using mini-notation. Also please reference peggy\
Sectin 7.3.2 The sawtooth waveform has been used in all examples, so it would be clearer if it was used here (maybe cutoff as well).
Generally, beyond the language differences, what does Strudel contribute with respect to Haskell-based Tidal? I would say that there is a lot to be gained in terms of installation, availability and potential for collaboration.
* Clarity (1-5):\
4
* Corrections required:\
No contentious / incorrect claims
* Other Changes if Published:\
not answered
* Correct Template used?\
Yes
* All materials provided?\
Yes
* Wordcount respected?\
Yes
* Have any deviations, errors or omissions impacted your ability to give a fair review?\
No
**Reviewer 2:**
Author Comments: * Summary of the contribution:\
This proposal presents Strudel, an online, JavaScript-native approach to algorithmic pattern composition that in many ways mirrors TidalCycles. TidalCycles, developed by one of the authors, is a key software piece for live coding. Perhaps the most difficult aspects of Tidal are related to Haskell and Haskell's dependency system that makes it very difficult to install. Haskell in itself is a difficult language to grasp; if Tidal users would like to expand and 'personalise' the possibilities of the software, they would have to get familiar with the particularities of Haskell. JavaScript is a multi-paradigm language that is used for web development can make pattern-based, algorithmic composition more popular. The process of bringing Tidal's paradigm to a dynamic type system seems to be an important part of this research. I enjoy and find very interesting the dialectical process that occurs between Strudel and Tidal, where the implementation of Tidal concepts in Strudel simultaneously transforms Tidal, either by introspection (finding bugs, etc.) or by bringing back to Tidal new insights that are clear in JavaScript. I think moving to TypeScript makes sense; perhaps a small section where the decision to use JavaScript is discussed a little further could be helpful for readers (beyond accessibility). Overall, this paper is clear and presents software that is a key development for live coding and will be a great addition to the next ICLC.
* On theme (1-5):\
4
* Citation quality (1-5):\
5
* Missing references:\
not answered
* Suggested improvements:\
Tidal-Strudel parity seems to be on the immediate horizon, but I wonder if this parity is transient and if the ultimate goal of Strudel is to ultimately diverge from Tidal. Perhaps the project is in its early stages, and this is not clear, but it would be interesting to get some insights on the authors' intentions. Personally, I think that a key discussion that needs to happen in live coding communities and development nodes is that of what is understood as multilingual live coding. Tidal's paradigm is implemented in a number of programming languages (as well as platforms such as Estuary), each with its own set of features and challenges: some are functional reactive programming, type strict oriented, while others are multi-paradigm, dynamic type oriented, etc. This seems to suggest that it is possible for many programming languages to adopt one single understanding of how to make music. Perhaps further considerations should be made as to whether we need one single grammar to name and express all music structures or whether we need environments and software that help difference to proliferate in terms of music/sound art/art idiosyncrasies. Perhaps questions in this direction are beyond the scope of this paper and I hope this comment is taken as productive feedback for the authors.
* Clarity (1-5):\
4
* Corrections required:\
Nothing contentious or incorrect to note.
* Other Changes if Published:\
The example shown on page 3 under "Pattern Example" seems to be different from its explanation. The explanations do not mention scale but mention echo, which is not present in the example. I would double check all the code examples inc ase I missed something like this.
* Correct Template used?\
Yes
* All materials provided?\
Yes
* Wordcount respected?\
Yes
* Have any deviations, errors or omissions impacted your ability to give a fair review?\
no
* General remarks:\
This proposal presents Strudel, an online, JavaScript-native approach to algorithmic pattern composition that in many ways mirrors TidalCycles. TidalCycles, developed by one of the authors, is a key software piece for live coding. Perhaps the most difficult aspects of Tidal are related to Haskell and Haskell's dependency system that makes it very difficult to install. Haskell in itself is a difficult language to grasp; if Tidal users would like to expand and 'personalise' the possibilities of the software, they would have to get familiar with the particularities of Haskell. JavaScript is a multi-paradigm language that can make pattern-based, algorithmic composition more popular. The process of bringing Tidal's paradigm to a dynamic type system seems to be an important part of this research. I enjoy and find very interesting the dialectical process that occurs between Strudel and Tidal, where the implementation of Tidal concepts in Strudel simultaneously transforms Tidal, either by introspection (finding bugs, etc.) or by bringing back to Tidal new insights that are clear in JavaScript. I think moving to TypeScript makes sense; perhaps a small section where the decision to use JavaScript is discussed a little further could be helpful for readers (beyond accessibility). I am curious, did the authors consider PureScript? I can see how, in terms of popularity, PureScript is a lateral move coming from Haskell, but an explicit explanation about the (maybe) obvious candidates would be interesting.\
Tidal-Strudel parity seems to be on the immediate horizon, but I wonder if this parity is transient and if the ultimate goal of Strudel is to ultimately diverge from Tidal. Perhaps the project is in its early stages, and this is not clear, but it would be interesting to get some insights on the authors' intentions. Personally, I think that a key discussion that needs to happen in live coding communities and development nodes is that of what is understood as multilingual live coding. Tidal's paradigm is implemented in a number of programming languages (as well as platforms such as Estuary), each with its own set of features and challenges: some are functional reactive programming, type strict oriented, while others are multi-paradigm, dynamic type oriented, etc. This seems to suggest that it is possible for many programming languages to adopt one single understanding of how to make music. Perhaps further considerations should be made as to whether we need one single grammar to name and express all music structures or whether we need environments and software that help difference to proliferate in terms of music/sound art/art idiosyncrasies. Perhaps questions in this direction are beyond the scope of this paper and I hope this last comment is taken as productive feedback for the authors.
Overall, this paper is clear and presents software that is a key development for live coding and will be a great addition to the next ICLC.
PS. The example shown on page 3 under "Pattern Example" seems to be different from its explanation. The explanations do not mention scale but mention echo, which is not present in the example.
+52 -57
View File
@@ -8,7 +8,7 @@ author:
affiliation: Then Try This
email: alex@slab.org
abstract: |
This paper introduces Strudel, which brings the TidalCycles approach to live coding algorithmic patterns to native JavaScript and the web. We begin by giving a little background of the first year of development, before sharing some detail about its implementation and examples of use. We go on to outline the wide range of synthesis and other outputs available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), WebSerial and CSound, and introduce Strudel's REPL live editor, including its built-in visualisations. We then compare Strudel with Tidal, the trade-offs involved between JavaScript and Haskell, and the unique capabilities offered by Strudel for aligning patterns, before concluding with some thoughts about the future.
This paper introduces Strudel, which faithfully ports the TidalCycles approach to live coding algorithmic patterns to native JavaScript and the web. We begin by giving a little background of the first year of development, before sharing some detail about its implementation and examples of use. We go on to outline the wide range of synthesis and other outputs available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), WebSerial and CSound, and introduce Strudel's REPL live editor, including its built-in visualisations. We then compare Strudel with Tidal, the trade-offs involved between JavaScript and Haskell, and the unique capabilities offered by Strudel for aligning patterns, before concluding with some thoughts about the future.
bibliography: citations.json
fontsize: 11pt
geometry: margin=2cm
@@ -23,37 +23,36 @@ date: '2022-12-14'
In the following paper, we introduce *Strudel*, an alternative
implementation of the TidalCycles (or 'Tidal' for short) live coding
system, using the JavaScript programming language. Strudel is an
system. Whereas Tidal is implemented in the pure functional Haskell programming language, Strudel brings it to the multi-paradigm, web-centric JavaScript language. Strudel is an
attempt to make live coding more accessible, by creating a system that
runs entirely in the browser, while opening Tidal's approach to
algorithmic patterns [@mcleanAlgorithmicPattern2020a] up to modern
can run entirely in the browser, while opening Tidal's approach to
algorithmic patterns [@mcleanAlgorithmicPattern2020] up to modern
audio/visual web technologies. The Strudel REPL is a live code editor
dedicated to manipulating patterns while they play, with builtin
visual feedback. While Strudel is written in JavaScript, the API is
optimized for simplicity and readability by applying code
visual feedback. While Strudel is written in standard JavaScript, it does support some optimisations for simplicity, readability and editability by applying code
transformations on the syntax tree level, allowing language operations
that would otherwise be impossible. The application supports multiple
ways to output sound, including Tone.js, Web Audio Nodes, OSC (Open
Sound Control) messages, Web Serial, Web MIDI and Csound. The project
is split into multiple packages, allowing granular reuse in other
applications. Apart from TidalCycles, Strudel draws inspiration from
many prior existing projects like TidalVortex
several other existing projects like TidalVortex
[@mcleanTidalVortexZero2022], Gibber [@robertsGibberLiveCoding2012],
Estuary [@ogbornEstuaryBrowserbasedCollaborative2017], Hydra
[@jackHydra2022], Ocarina [@solomonPurescriptocarina2022] and
Feedforward [@mcleanFeedforward2020]. This paper expands the Strudel
Demo paper for the Web Audio Conference 2022 [@StrudelWAC2022].
[@jackHydra2023], Ocarina [@solomonPurescriptocarina2022] and
Feedforward [@mcleanFeedforward2020]. This paper builds upon a
demo paper presented at Web Audio Conference 2022 [@roosStrudelAlgorithmicPatterns2022].
The first tentative commit to the Strudel project was on 22nd January
2022 by Alex McLean, with the core representation implemented over the
following few days. Although this was his first attempt at a
JavaScript-based application, by 27th January, Alex had managed to
upload the initial version to the 'npm' javascript package database,
upload an initial version to the 'npm' JavaScript package database,
sharing with the wider community for comment. By 4th February, Felix
Roos had discovered Strudel and contributed a 'REPL' user interface to
it, and then contributed a scheduler the next day, so that Strudel
could already make sound. At this point, Alex and Felix shared
ownership to the repository, and the project has since proved to be a
it, and a scheduler the following day, so that Strudel
could already begin to make sound. At this point, Alex and Felix shared
ownership of the repository, and the project has since proved to be a
productive confluence of Felix's own work into music representation
and visualisation, with Alex's experience with making Tidal. Felix has
since become the primary contributor to Strudel, with Alex continuing
@@ -61,24 +60,24 @@ to jump between developing both Strudel and Tidal. Aspects of
Strudel's development have therefore fed back into TidalCycles, and
both systems have maintained a shared conceptual underpinning. We plan
to continue working towards feature parity between these systems,
although within the syntactical trade-offs and library ecosystems of
although within the different syntactical trade-offs and library ecosystems of
JavaScript and Haskell, some divergence is inevitable and healthy.
Over the first year of its life, Strudel is now a fully-fledged live
coding environment, porting Tidal's core represention of patterns,
Now celebrating the first year of its life, Strudel is now a fully-fledged live
coding environment, porting Tidal's core representation of patterns,
pattern transformations, and mini-notation for polymetric sequences,
combined with a wealth of features for synthesising and visualising
those patterns.
# From Tidal to Strudel and back
As mentioned above, the original Tidal is implemented as a domain specific language (DSL) embedded in the Haskell pure functional programming language, and takes advantage of Haskell's terse syntax and advanced, 'strong' type system. JavaScript on the other hand, is a multi-paradigm programming language, with a dynamic type system. Because Tidal leans heavily on many of Haskell's more unique features, it was not always clear that it could meaningfully be ported to a multi-paradigm scripting language. However, this possibility was already demonstrated with an earlier port to Python [TidalVortex; @mcleanTidalVortexZero2022], and we have now successfully implemented Tidal's pure functional representation of patterns in Strudel, including partial application, currying, and the functor, applicative and monadic structures that underlie Tidal's expressive pattern transformations. The result is a terse and highly composable system, where everything is either a pattern, or a function for combining and manipulating patterns, offering a rich creative ground for exploration.
As mentioned earlier, the original Tidal is implemented as a domain specific language (DSL) embedded in the Haskell pure functional programming language, and takes advantage of Haskell's terse syntax and advanced, 'strong' type system. JavaScript on the other hand, is a multi-paradigm programming language, with a dynamic type system. Because Tidal leans heavily on many of Haskell's more unique features, it was not always clear that it could meaningfully be ported to a multi-paradigm scripting language. However, this possibility was already demonstrated with an earlier port to Python [TidalVortex; @mcleanTidalVortexZero2022], and we have now successfully implemented Tidal's pure functional representation of patterns in Strudel, including partial application, currying, and the functor, applicative and monadic structures that underlie Tidal's expressive pattern transformations. The result is a terse and highly composable system, where everything is either a pattern, or a function for combining and manipulating patterns, offering a rich creative ground for exploration.
This development process has been far from a one-way port, however. The process of porting Tidal's concepts has also opened up new possibilities, some just from revisiting every design decision, and some from the particular affordances and constraints offered by JavaScript. This has lead to new features (and indeed bugfixes) that have found their way back to Tidal where appropriate, and ongoing work that we will return to in the conclusion of this paper.
# Representing Patterns
Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern can output a set of events that happen within that time span. It depends on the structure of the Pattern how the events are located in time.
Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern will output a set of events that happen within that time span. It depends on the structure of the Pattern how the events are located in time.
From now on, this process of generating events from a time span will be called **querying**.
Example:
@@ -101,7 +100,7 @@ The resulting events are:
Each event has a value, a begin time and an end time, where time is represented as a fraction. In the above case, the events are placed in sequential order, where c3 takes the first half, and e3 and g3 together take the second half. This temporal placement is the result of the `sequence` function, which divides its arguments equally over one cycle. If an argument is an array, the same rule applies to that part of the cycle. In the example, e3 and g3 are divided equally over the second half of the whole cycle.
Note that the query function is not just a way to access a pattern, but true to the principles of functional programming, is the pattern itself. This means that in theory there is no way to change a pattern, it is opaque as a pure function. In practice though, Strudel and Tidal are all about transforming patterns, so how is this done? The answer is, by replacing the pattern with a new one, that calls the old one. This new one is only able to manipulate the query before passing it to the old pattern, and manipulate the results from it before returning them to caller. But, this is enough to support all the temporal and structural manipulations provided by Strudel (and Tidal's) extensive library of functions.
Note that the query function is not just a way to access a pattern, but true to the principles of functional programming, is the pattern itself. This means that in theory there is no way to change a pattern, it is opaque as a pure function. In practice though, Strudel and Tidal are all about transforming patterns, so how is this done? The answer is, by replacing the pattern with a new one, that calls the old one. This new function is only able to manipulate the query before passing it to the old pattern, and manipulate the results from it before returning them to caller. But, this is enough to support all the temporal and structural manipulations provided by Strudel (and Tidal's) extensive library of functions.
The above examples do not represent how Strudel is used in practice. In the live coding editor, the user only has to type in the pattern itself, the querying will be handled by the scheduler. The scheduler will repeatedly query the pattern for events, which are then scheduled as sound synthesis or other event triggers.
Also, the above event data structure has been simplified for readability.
@@ -110,7 +109,7 @@ Also, the above event data structure has been simplified for readability.
# Making Patterns
In practice, the end-user live coder will not deal with constructing patterns directly, but will rather build patterns using Strudel's extensive combinator library to create, combine and transform patterns.
In practice, the end-user live coder will not deal with constructing patterns directly, but will rather build patterns using Strudel's extensive combinator library, to create, combine and transform patterns.
The live coder will rarely use the `sequence` function as seen above, as sequencing is implicit in many functions. For example in the following, the `note` function constructs a pattern of notes, sequencing its arguments in the same manner as the previous example.
@@ -118,29 +117,29 @@ The live coder will rarely use the `sequence` function as seen above, as sequenc
note(c3, [e3, g3])
```
Perhaps more often, they will use the mini-notation for even terser notation of rhythmic sequences: ^[This last example is also valid Tidal code, albeit the parenthesis is not required in its Haskell syntax in this case. Tidal does not support passing sequences as lists directly to the `note` function, however.].
Perhaps more often, they will use the mini-notation for even terser notation of rhythmic sequences: ^[This example is also valid Tidal code, albeit the parenthesis is not required in its Haskell syntax in this case. Tidal does not support passing sequences as lists directly to the `note` function, however.].
```js
note("c3 [e3 g3]")
```
Such sequences are often treated only as a starting point for manipulation, where they then undergo pattern transformations such as repetition, symmetry, interference/combination or randomisation, potentially at multiple timescales. Because Strudel patterns are represented as pure functions of time rather than as data structures, very long and complex generative results can be represented and manipulated without having to store the resulting sequences in memory.
Such sequences are often treated only as a starting point for manipulation, where functions are then applied which represent pattern transformations such as repetition, symmetry, interference/combination or randomisation, potentially at multiple timescales. Because Strudel patterns are represented as pure functions of time rather than as data structures, very long and complex generative results can be represented and manipulated without having to store the resulting sequences in memory.
# Pattern Example
The following example showcases how patterns can be utilized to create musical complexity from simple parts, using repetition and interference:
The following example showcases how patterns can be utilised to create musical complexity from simple parts, using repetition and interference:
```js
"<0 2 [4 6](3,4,1) 3>"
.off(1/4, add(2))
.off(1/2, add(6))
.scale('D minor')
.legato(.25)
.scale('D3 minor')
.note().s("sawtooth square")
.legato(.25)
.delay(.8).delaytime(.125)
```
The pattern starts with a rhythm of numbers in mini-notation, which are later interpreted inside the scale of D minor.
The pattern starts with a rhythmic pattern of numbers expressed in mini-notation, which are later interpreted inside the scale of D minor.
The first line could also be expressed without mini-notation:
```js
@@ -153,16 +152,16 @@ These numbers then undergo various pattern transformations. Here is a short desc
- `brackets`: elements inside brackets are divided equally over the time of their parent
- `.euclid(p, s, o)`: place p pulses evenly over s steps, with offset o [@toussaintEuclideanAlgorithmGenerates2005]
- `.off(n, f)`: layers a pattern on top of itself, with the new layer offset by n cycles, and with function f applied
- `.legato(n)`: multiply the duration of all events in a pattern by a factor of n
- `.echo(t, n, v)`: copy each event t times, with n cycles in between each copy, decreasing velocity by v
- `.note()`: interpretes values as notes
- `.scale(name)`: interpretes numbers as indices inside the given scale
- `.note()`: interprets values as notes
- `.s(name)`: play back each event with the given sound
- `.delay(wet)`: add delay
- `.legato(n)`: multiply the duration of all events in a pattern by a factor of n
- `.delay(amount)`: delay effect send amount
- `.delaytime(t)`: set delay time
Much of the above will be familiar to Tidal users.
<!-- This example shows some of Strudel's unique support for chords and transposition familiar to students of Western music theory. This differs a little from Tidal's approach and thanks to the integration of the javascript library XXX (*TODO* ? or is this all your work Felix?), Strudel's support for tonal transformations such as voice leading is perhaps respects more advanced than Tidal. -->
<!-- This example shows some of Strudel's unique support for chords and transposition familiar to students of Western music theory. This differs a little from Tidal's approach and thanks to the integration of the JavaScript library XXX (*TODO* ? or is this all your work Felix?), Strudel's support for tonal transformations such as voice leading is perhaps respects more advanced than Tidal. -->
# Ways to make Sound (and other events)
@@ -181,11 +180,11 @@ While the Web Audio API takes a *fire-and-forget* approach, creating a lot of To
Strudel's new default output uses the Web Audio API to create a new audio graph for each event. It currently supports basic oscillators, sample playback, various effects and an experimental support for soundfonts.
WebDirt [@ogbornDktr0WebDirt2022] was created as part of the Estuary Live Coding System [@ogbornEstuaryBrowserbasedCollaborative2017], and proved to be a solid choice for handling samples in Strudel as well. We are however focused on working more directly with the Web Audio API to be able to integrate new features more tightly.
WebDirt [@ogbornWebDirt2022] was created as part of the Estuary Live Coding System [@ogbornEstuaryBrowserbasedCollaborative2017], and proved to be a solid choice for handling samples in Strudel as well. We are however focused on working more directly with the Web Audio API to be able to integrate new features more tightly.
Using the OSC protocol via Strudel's provided Node.js-based OSC proxy server, it is possible to send network messages to trigger events. This is mainly used to render sound using SuperDirt [@SuperDirt2022], which is the well-developed Supercollider-based synthesis framework that Tidal live coders generally use as standard.
Using the OSC protocol via Strudel's provided Node.js-based OSC proxy server, it is possible to send network messages to trigger events. This is mainly used to render sound using SuperDirt [@rohrhuberSuperDirt2022], which is the well-developed Supercollider-based synthesis framework used by Tidal live coders as standard.
Recently, the experimental integration of Csound proved to bring a new dimension of sound design capabilities to Strudel. Thanks to the WebAssembly distribution of this classic system [@CsoundWebAssembly], Csound 'orchestra' synthesisers can be embedded in and then patterned with Strudel code.
Recently, the experimental integration of Csound proved to bring a new dimension of sound design capabilities to Strudel. Thanks to the WebAssembly distribution of this classic system [@yiWebAssemblyAudioWorkletCsound2018], Csound 'orchestra' synthesisers can be embedded in and then patterned with Strudel code.
MIDI output can also be used to send MIDI messages to either external instruments or to other programs on the same device. Unlike OSC, Strudel is able to send MIDI directly without requiring additional proxy software, but only from web browsers that support it (at the time of writing, this means Chromium-based browsers).
@@ -231,17 +230,13 @@ mini("c3 [e3 g3]*2").withMiniLocation([1,0,0],[1,14,14])
Here, the string is wrapped in `mini`, which will create a pattern from a mini-notation string. Additionally, the `withMiniLocation` method passes the original source code location of the string to the pattern, which enables highlighting active events.
Other convenient features like pseudo variables, operator overloading and top level await are possible with transpilation.
After the transpilation, the code is ready to be evaluated into a `Pattern`.
Behind the scenes, the user code string is parsed with `acorn`, turning it into an Abstract Syntax Tree (AST). The AST allows changing the structure of the code before generating the transpiled version using `escodegen`.
Other convenient features like pseudo variables, operator overloading and top level await are possible with transpilation. After the transpilation, the code is ready to be evaluated into a `Pattern`. Behind the scenes, the user code string is parsed with `acorn`, turning it into an Abstract Syntax Tree (AST). The AST allows changing the structure of the code before generating the transpiled version using `escodegen`.
### Mini-notation
While the transpilation allows JavaScript to express Patterns in a less verbose way, it is still preferable to use the mini-notation as a more compact way to express rhythm. Strudel aims to provide the same mini-notation features and syntax as used in Tidal.
While the transpilation allows us to express patterns in JavaScript in a less verbose way, it is still preferable to use the mini-notation as a more compact way to express rhythm. Strudel aims to provide the same mini-notation features and syntax as used in Tidal.
The mini-notation parser is implemented using `peggy`, which allows generating performant parsers for Domain Specific Languages (DSLs) using a concise grammar notation. The generated parser turns the mini-notation string into an AST which is used to call the respective Strudel functions with the given structure. For example, `"c3 [e3 g3]*2"` will result in the following calls:
The mini-notation parser is implemented using `peggy` [@majdaPeggy2023], which allows generating performant parsers for Domain Specific Languages (DSLs) using a concise grammar notation. The generated parser turns the mini-notation string into an AST which is used to call the respective Strudel functions with the required structure. For example, `"c3 [e3 g3]*2"` will result in the following calls:
```js
seq(
@@ -255,12 +250,12 @@ seq(
### Highlighting Locations
As seen in the examples above, both the JS and the mini-notation parser add source code locations using `withMiniLocation` and `withLocation` methods. While the JS parser adds locations relative to the user code as a whole, the mini-notation adds locations relative to the position of the mini-notation string. The absolute location of elements within mini-notation can be calculated by simply adding both locations together. This absolute location can be used to highlight active events in real time.
As seen in the examples above, both the JS and the mini-notation parser add source code locations using `withMiniLocation` and `withLocation` methods. While the JS parser adds locations relative to the user code as a whole, the mini-notation adds locations relative to the position of the mini-notation string. The absolute location of elements within mini-notation can be calculated by simply adding both locations together. This absolute location is then used to highlight active events in real time.
## Scheduling Events
After an instance of `Pattern` is obtained from the user code,
it is used by the scheduler to get queried for events. Once started, the scheduler runs at a fixed interval to query the active pattern for events within the current interval's time span. A simplified implementation looks like this:
the scheduler queries it for events. Once started, the scheduler runs at a fixed interval to query the active pattern for events within the current interval's time span. A simplified implementation looks like this:
```js
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
@@ -277,7 +272,7 @@ setInterval(() => {
}, interval * 1000); // query each "interval" seconds
```
Note that the above code is simplified for illustrative purposes. The actual implementation has to work around imprecise callbacks of `setInterval`. More about the implementation details can be read in [this blog post](https://loophole-letters.vercel.app/web-audio-scheduling).
Note that the above code is simplified for illustrative purposes, for example the actual implementation has to work around imprecise callbacks of `setInterval`. ^[Further implementation details of the Strudel scheduler can be read in this blog post: [loophole-letters.vercel.app/web-audio-scheduling](https://loophole-letters.vercel.app/web-audio-scheduling).]
The fact that `Pattern.queryArc` is a pure function that maps a time span to a set of events allows us to choose any interval we like without changing the resulting output. It also means that when the pattern is changed from outside, the next scheduling callback will work with the new pattern, keeping its clock running.
@@ -310,7 +305,7 @@ const { x, y } = createParams('x', 'y')
x(sine.range(0, 200)).y(cosine.range(0,200))
```
This example creates the custom control parameters `x` and `y` which are then used to form a pattern that descibes the coordinates of a circle.
This example creates the custom control parameters `x` and `y` which are then used to form a pattern that describes the coordinates of a circle.
### Outputs
@@ -328,13 +323,13 @@ function onTrigger(hap, deadline, duration) {
}
```
The above example will create an `OscillatorNode` for each event, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
The above example will create an `OscillatorNode` for each event, where the frequency is controlled by the `note` parameter. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
# Pattern alignment and combination
One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective of their structure. Its declarative approach means a live coder does not have to think about the details of *how* this is done, only *what* is to be done.
As a simple example, consider two number patterns `"0 [1 2] 3"`, and `"10 20"`. The first has three contiguous steps of equal lengths, with the second step broken down into two substeps, giving four events in total. There are a very large number of ways in which the structure of these two patterns could be combined, but the default method in both Strudel and Tidal is to line up the cycles of the two patterns, and then take events from the first pattern and match them with those in the second pattern. Therefore, the following two lines are equivalent:
As a simple example, consider two number patterns `"0 [1 2] 3"`, and `"10 20"`. The first has three contiguous steps of equal lengths, the second of which is broken down into two substeps, giving four events in total. The second pattern simply has two steps, taking up half a cycle each. There are a very large number of ways in which the structure of these two patterns could be combined, but the default method in both Strudel and Tidal is to line up the cycles of the two patterns, and then take events from the first pattern and match them with those in the second pattern. Therefore, the following two lines are equivalent:
```js
"0 [1 2] 3".add("10 20")
@@ -389,20 +384,20 @@ versus Strudel's simplicity.
To demonstrate this, consider the following Tidal pattern:
```haskell
iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4
iter 4 $ every 3 (||+ n "10 20") $ n "20 21 23" # s "triangle" # crush 4
```
This can be directly translated to the Strudel equivalent:
```js
iter(4, every(3, add.squeeze("10 20"), n("0 1 3").s("triangle").crush(4)))
iter(4, every(3, x => x.add.squeeze(n("10 20")), n("20 21 23").s("triangle").crush(4)))
```
Although for a more canonical Strudel expression, we would reorder it
as:
```js
n("0 1 3").every(3, add.squeeze("10 20")).iter(4).s("triangle").crush(4)
n("20 21 23").every(3, x => x.add.squeeze(n("10 20"))).iter(4).s("triangle").crush(4)
```
The Strudel example uses the `.` method call operator for all
@@ -426,17 +421,17 @@ features.
With Strudel, we have little choice but to embrace the affordances and
constraints offered by JavaScript, and while designing a
domain-specific language based on method calls is a
domain-specific language based on chaining method calls is a
challenge, through creative adoption of functional programming
techniques like partial application, we are so far very happy with the
results. Tidal's functional reactive approach to pattern-making has in
general translated well to JavaScript, and opportunities and
constraints have overall traded off to create a very approachable and
useable live coding environment.
constraints have traded off to create a very approachable and
usable live coding environment.
## The trade-off of flexible typing
We have identified one problem with porting Tidal to JavaScript where we have missed Haskell's strict typing and type inference. In both Tidal and Strudel, time is rational, where any point in time is represented as the ratio of two integers. This allows representation of musical ratios such that are impossible to represent accurately using the more common floating point numbers. However while libraries are available that support rational numbers in JavaScript, the lack of strict typing means that it is easy to implement pattern methods where computationally expensive conversion from floating point to rational numbers are performed late, and therefore often enough to overload the CPUs, due to the large number of iterative calculations required to estimate a ratio for a given floating point number. To mitigate this problem, we might consider moving to TypeScript in the future.
We have identified one problem with porting Tidal to JavaScript where we have missed Haskell's strict typing and type inference. In both Tidal and Strudel, time is rational, where any point in time is represented as the ratio of two integers. This allows representation of musical ratios such that are impossible to represent accurately using the more common floating point numbers. However while libraries are available that support rational numbers in JavaScript, the lack of strict typing means that computationally expensive conversion from floating point to rational numbers may be performed late, and therefore often enough to overload the CPUs, due to the large number of iterative calculations required to estimate a ratio for a given floating point number. To mitigate this problem, we might consider moving to TypeScript in the future.
# Future Outlook
@@ -446,14 +441,14 @@ The project is still young, with many features on the horizon. As general guidin
2. consistent with Tidal's approach to pattern
3. modular and extensible
While Haskell's type system makes it a great language for the ongoing development of Tidal's inner representation of pattern, JavaScript's vibrant ecosystem, flexibility and accessibility makes it a great host for more ad-hoc experiments, including interface design. For the future, it is planned to integrate additional alternative sound engines such as Glicol [@lanChaosprintGlicol2022] and Faust [@FaustProgrammingLanguage2022]. Strudel is already approaching feature parity with Tidal, but there are more Tidal functions to be ported, and work to be done to improve compatibility with Tidal's mini-notation. Tidal version 2.0 is under development, which brings a new representation for sequences to its patterns, which will then be brought to Strudel. Besides sound, other ways to render events are being explored, such as graphical, and choreographic output. We are also looking into alternative ways of editing patterns, including multi-user editing for network music, parsing a novel syntax to escape the constraints of JavaScript, and developing hardware/e-textile interfaces. In summary, there is a lot of fun ahead.
While Haskell's type system makes it a great language for the ongoing development of Tidal's inner representation of pattern, JavaScript's vibrant ecosystem, flexibility and accessibility makes it a great host for more ad-hoc experiments, including interface design. For the future, it is planned to integrate additional alternative sound engines such as Glicol [@lanGlicol2023] and Faust [@orlareyFaustProgrammingLanguage2023]. Strudel is already approaching feature parity with Tidal, but there are more Tidal functions to be ported, and work to be done to improve compatibility with Tidal's mini-notation. Tidal version 2.0 is under development, which brings a new representation for sequences to its patterns, which will then be brought to Strudel. Besides sound, other ways to render events are being explored, such as graphical, and choreographic output. We are also looking into alternative ways of editing patterns, including multi-user editing for network music, parsing a novel syntax to escape the constraints of JavaScript, and developing hardware/e-textile interfaces. In summary, there is a lot of fun ahead.
# Links
The Strudel REPL is available at <https://strudel.tidalcycles.org>, including an interactive tutorial.
The repository is at <https://github.com/tidalcycles/strudel>, all the code is open source under the AGPL-3.0 License.
# Acknowledgments
# Acknowledgements
Thanks to the Strudel and wider Tidal, live coding, WebAudio and free/open source software communities for inspiration and support. Alex McLean's work on this project is supported by a UKRI Future Leaders Fellowship [grant number MR/V025260/1].
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

-818
View File
@@ -1,818 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta name="generator" content="pandoc" />
<meta name="date" content="2022-12-14" />
<title>Strudel: live coding patterns on the Web</title>
<style type="text/css">code{white-space: pre;}</style>
<style type="text/css">
pre > code.sourceCode { white-space: pre; position: relative; }
pre > code.sourceCode > span { display: inline-block; line-height: 1.25; }
pre > code.sourceCode > span:empty { height: 1.2em; }
.sourceCode { overflow: visible; }
code.sourceCode > span { color: inherit; text-decoration: inherit; }
div.sourceCode { margin: 1em 0; }
pre.sourceCode { margin: 0; }
@media screen {
div.sourceCode { overflow: auto; }
}
@media print {
pre > code.sourceCode { white-space: pre-wrap; }
pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
}
pre.numberSource code
{ counter-reset: source-line 0; }
pre.numberSource code > span
{ position: relative; left: -4em; counter-increment: source-line; }
pre.numberSource code > span > a:first-child::before
{ content: counter(source-line);
position: relative; left: -1em; text-align: right; vertical-align: baseline;
border: none; display: inline-block;
-webkit-touch-callout: none; -webkit-user-select: none;
-khtml-user-select: none; -moz-user-select: none;
-ms-user-select: none; user-select: none;
padding: 0 4px; width: 4em;
color: #aaaaaa;
}
pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; }
div.sourceCode
{ }
@media screen {
pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
}
code span.al { color: #ff0000; font-weight: bold; } /* Alert */
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
code span.at { color: #7d9029; } /* Attribute */
code span.bn { color: #40a070; } /* BaseN */
code span.bu { color: #008000; } /* BuiltIn */
code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
code span.ch { color: #4070a0; } /* Char */
code span.cn { color: #880000; } /* Constant */
code span.co { color: #60a0b0; font-style: italic; } /* Comment */
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
code span.do { color: #ba2121; font-style: italic; } /* Documentation */
code span.dt { color: #902000; } /* DataType */
code span.dv { color: #40a070; } /* DecVal */
code span.er { color: #ff0000; font-weight: bold; } /* Error */
code span.ex { } /* Extension */
code span.fl { color: #40a070; } /* Float */
code span.fu { color: #06287e; } /* Function */
code span.im { color: #008000; font-weight: bold; } /* Import */
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
code span.kw { color: #007020; font-weight: bold; } /* Keyword */
code span.op { color: #666666; } /* Operator */
code span.ot { color: #007020; } /* Other */
code span.pp { color: #bc7a00; } /* Preprocessor */
code span.sc { color: #4070a0; } /* SpecialChar */
code span.ss { color: #bb6688; } /* SpecialString */
code span.st { color: #4070a0; } /* String */
code span.va { color: #19177c; } /* Variable */
code span.vs { color: #4070a0; } /* VerbatimString */
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
</style>
<link rel="stylesheet" href="css/iclc.css" />
</head>
<body>
<div id="header">
<h1 class="title">Strudel: live coding patterns on the Web</h1>
<ul id="authorlist">
<li>true</li>
<li>true</li>
</ul>
<h3 class="date">2022-12-14</h3>
</div>
<h2 class="abstract">Abstract</h2>
<div id="abstract">
<p>This paper introduces Strudel, which brings the TidalCycles approach
to live coding algorithmic patterns to native JavaScript and the web. We
begin by giving a little background of the first year of development,
before sharing some detail about its implementation and examples of use.
We go on to outline the wide range of synthesis and other outputs
available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt),
WebSerial and CSound, and introduce Strudels REPL live editor,
including its built-in visualisations. We then compare Strudel with
Tidal, the trade-offs involved between JavaScript and Haskell, and the
unique capabilities offered by Strudel for aligning patterns.</p>
</div>
<h1 data-number="1" id="introduction"><span
class="header-section-number">1</span> Introduction</h1>
<p>In the following paper, we introduce <em>Strudel</em>, an alternative
implementation of the TidalCycles (or Tidal for short) live coding
system, using the JavaScript programming language. Strudel is an attempt
to make live coding more accessible, by creating a system that runs
entirely in the browser, while opening Tidals approach to algorithmic
patterns <span class="citation"
data-cites="mcleanAlgorithmicPattern2020a">(Mclean 2020)</span> up to
modern audio/visual web technologies. The Strudel REPL is a live code
editor dedicated to manipulating patterns while they play, with builtin
visual feedback. While Strudel is written in JavaScript, the API is
optimized for simplicity and readability by applying code
transformations on the syntax tree level, allowing language operations
that would otherwise be impossible. The application supports multiple
ways to output sound, including Tone.js, Web Audio Nodes, OSC (Open
Sound Control) messages, Web Serial, Web MIDI and Csound. The project is
split into multiple packages, allowing granular reuse in other
applications. Apart from TidalCycles, Strudel draws inspiration from
many prior existing projects like TidalVortex <span class="citation"
data-cites="mcleanTidalVortexZero2022">(McLean et al. 2022)</span>,
Gibber <span class="citation"
data-cites="robertsGibberLiveCoding2012">(Roberts and Kuchera-morin
2012)</span>, Estuary <span class="citation"
data-cites="ogbornEstuaryBrowserbasedCollaborative2017">(Ogborn et al.
2017)</span>, Hydra <span class="citation"
data-cites="jackHydra2022">(Jack [2022] 2022)</span>, Ocarina <span
class="citation" data-cites="solomonPurescriptocarina2022">(Solomon
[2021] 2022)</span> and Feedforward <span class="citation"
data-cites="mcleanFeedforward2020">(McLean 2020)</span>. This paper
expands the Strudel Demo paper for the Web Audio Conference 2022 <span
class="citation" data-cites="StrudelWAC2022">(Roos and McLean
2022)</span>.</p>
<p>The first tentative commit to the Strudel project was on 22nd January
2022 by Alex McLean, with the core representation implemented over the
following few days. Although this was his first attempt at a
JavaScript-based application, by 27th January, Alex had managed to
upload the initial version to the npm javascript package database,
sharing with the wider community for comment. By 4th February, Felix
Roos had discovered Strudel and contributed a REPL user interface to
it, and then contributed a scheduler the next day, so that Strudel could
already make sound. At this point, Alex and Felix shared ownership to
the repository, and the project has since proved to be a productive
confluence of Felixs own work into music representation and
visualisation, with Alexs experience with making Tidal. Felix has since
become the primary contributor to Strudel, with Alex continuing to jump
between developing both Strudel and Tidal. Aspects of Strudels
development have therefore fed back into TidalCycles, and both systems
have maintained a shared conceptual underpinning. We plan to continue
working towards feature parity between these systems, although within
the syntactical trade-offs and library ecosystems of JavaScript and
Haskell, some divergence is inevitable and healthy.</p>
<p>Over the first year of its life, Strudel is now a fully-fledged live
coding environment, porting Tidals core represention of patterns,
pattern transformations, and mininotation for polymetric sequences,
combined with a wealth of features for synthesising and visualising
those patterns.</p>
<h1 data-number="2" id="from-tidal-to-strudel-and-back"><span
class="header-section-number">2</span> From Tidal to Strudel and
back</h1>
<p>As mentioned above, the original Tidal is implemented as a domain
specific language (DSL) embedded in the Haskell pure functional
programming language, and takes advantage of Haskells terse syntax and
advanced, strong type system. JavaScript on the other hand, is a
multi-paradigm programming language, with a dynamic type system. Because
Tidal leans heavily on many of Haskells more unique features, it was
not always clear that it could meaningfully be ported to a
multi-paradigm scripting language. However, this possibility was already
demonstrated with an earlier port to Python [TidalVortex; <span
class="citation" data-cites="mcleanTidalVortexZero2022">McLean et al.
(2022)</span>], and we have now successfully implemented Tidals pure
functional representation of patterns in Strudel, including partial
application, currying, and the functor, applicative and monadic
structures that underlie Tidals expressive pattern transformations. The
result is a terse and highly composable system, where everything is
either a pattern, or a function for combining and manipulating patterns,
offering a rich creative ground for exploration.</p>
<p>This development process has been far from a one-way port, however.
The process of porting Tidals concepts has also opened up new
possibilities, some just from revisiting every design decision, and some
from the particular affordances and constraints offered by JavaScript.
This has lead to new features (and indeed bugfixes) that have found
their way back to Tidal where appropriate, and ongoing work that we will
return to in the conclusion of this paper.</p>
<h1 data-number="3" id="representing-patterns"><span
class="header-section-number">3</span> Representing Patterns</h1>
<p>Patterns are the essence of Tidal. Its patterns are abstract entities
that represent flows of time as functions, adapting a technique called
pure functional reactive programming. Taking a time span as its input, a
Pattern can output a set of events that happen within that time span. It
depends on the structure of the Pattern how the events are located in
time. From now on, this process of generating events from a time span
will be called <strong>querying</strong>. Example:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> pattern <span class="op">=</span> <span class="fu">sequence</span>(c3<span class="op">,</span> [e3<span class="op">,</span> g3])</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> events <span class="op">=</span> pattern<span class="op">.</span><span class="fu">queryArc</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">1</span>)</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="bu">console</span><span class="op">.</span><span class="fu">log</span>(events<span class="op">.</span><span class="fu">map</span>(e <span class="kw">=&gt;</span> e<span class="op">.</span><span class="fu">show</span>()))</span></code></pre></div>
<p>In this example, we create a pattern using the <code>sequence</code>
function and <strong>query</strong> it for the time span from
<code>0</code> to <code>1</code>. Those numbers represent units of time
called <strong>cycles</strong>. The length of one cycle depends on the
tempo, which defaults to one cycle per second. The resulting events
are:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>[{ <span class="dt">value</span><span class="op">:</span> <span class="st">&#39;c3&#39;</span><span class="op">,</span> <span class="dt">begin</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">end</span><span class="op">:</span> <span class="dv">1</span><span class="op">/</span><span class="dv">2</span> }<span class="op">,</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>{ <span class="dt">value</span><span class="op">:</span> <span class="st">&#39;e3&#39;</span><span class="op">,</span> <span class="dt">begin</span><span class="op">:</span> <span class="dv">1</span><span class="op">/</span><span class="dv">2</span><span class="op">,</span> <span class="dt">end</span><span class="op">:</span> <span class="dv">3</span><span class="op">/</span><span class="dv">4</span> }<span class="op">,</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>{ <span class="dt">value</span><span class="op">:</span> <span class="st">&#39;g3&#39;</span><span class="op">,</span> <span class="dt">begin</span><span class="op">:</span> <span class="dv">3</span><span class="op">/</span><span class="dv">4</span><span class="op">,</span> <span class="dt">end</span><span class="op">:</span> <span class="dv">1</span> }]</span></code></pre></div>
<p>Each event has a value, a begin time and an end time, where time is
represented as a fraction. In the above case, the events are placed in
sequential order, where c3 takes the first half, and e3 and g3 together
take the second half. This temporal placement is the result of the
<code>sequence</code> function, which divides its arguments equally over
one cycle. If an argument is an array, the same rule applies to that
part of the cycle. In the example, e3 and g3 are divided equally over
the second half of the whole cycle.</p>
<p>The above examples do not represent how Strudel is used in practice.
In the live coding editor, the user only has to type in the pattern
itself, the querying will be handled by the scheduler. The scheduler
will repeatedly query the pattern for events, which are then scheduled
as sound synthesis or other event triggers. Also, the above event data
structure has been simplified for readability.</p>
<figure>
<img src="images/strudel-screenshot2.png" style="width:60.0%"
alt="Screenshot of the Strudel REPL live coding editor, including piano-roll visualisation." />
<figcaption aria-hidden="true">Screenshot of the Strudel REPL live
coding editor, including piano-roll visualisation.</figcaption>
</figure>
<h1 data-number="4" id="making-patterns"><span
class="header-section-number">4</span> Making Patterns</h1>
<p>In practice, the end-user live coder will not deal with constructing
patterns directly, but will rather build patterns using Strudels
extensive combinator library to create, combine and transform
patterns.</p>
<p>The live coder will rarely use the <code>sequence</code> function as
seen above, as sequencing is implicit in many functions. For example in
the following, the <code>note</code> function constructs a pattern of
notes, sequencing its arguments in the same manner as the previous
example.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="fu">note</span>(c3<span class="op">,</span> [e3<span class="op">,</span> g3])</span></code></pre></div>
<p>Perhaps more often, they will use the mini-notation for even terser
notation of rhythmic sequences: [^This last example is also valid Tidal
code, albeit the parenthesis is not required in its Haskell syntax in
this case. Tidal does not support passing sequences as lists directly to
the <code>note</code> function, however.].</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="fu">note</span>(<span class="st">&quot;c3 [e3 g3]&quot;</span>)</span></code></pre></div>
<p>Such sequences are often treated only a starting point for
manipulation, where they then undergo pattern transformations such as
repetition, symmetry, interference/combination or randomisation,
potentially at multiple timescales. Because Strudel patterns are
represented as pure functions of time rather than as data structures,
very long and complex generative results can be represented and
manipulated without having to store the resulting sequences in
memory.</p>
<h1 data-number="5" id="pattern-example"><span
class="header-section-number">5</span> Pattern Example</h1>
<p>The following example showcases how patterns can be utilized to
create musical complexity from simple parts, using repetition and
interference:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;&lt;0 2 [4 6](3,4,1) 3&gt;&quot;</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">off</span>(<span class="dv">1</span><span class="op">/</span><span class="dv">4</span><span class="op">,</span> <span class="fu">add</span>(<span class="dv">2</span>))</span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">off</span>(<span class="dv">1</span><span class="op">/</span><span class="dv">2</span><span class="op">,</span> <span class="fu">add</span>(<span class="dv">6</span>))</span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">scale</span>(<span class="st">&#39;D minor&#39;</span>)</span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">legato</span>(<span class="op">.</span><span class="dv">25</span>)</span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">note</span>()<span class="op">.</span><span class="fu">s</span>(<span class="st">&quot;sawtooth square&quot;</span>)</span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">delay</span>(<span class="op">.</span><span class="dv">8</span>)<span class="op">.</span><span class="fu">delaytime</span>(<span class="op">.</span><span class="dv">125</span>)</span></code></pre></div>
<p>The pattern starts with a rhythm of numbers in mini notation, which
are later interpreted inside the scale of D minor. The first line could
also be expressed without mini notation:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="fu">cat</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">2</span><span class="op">,</span> [<span class="dv">4</span><span class="op">,</span> <span class="dv">6</span>]<span class="op">.</span><span class="fu">euclid</span>(<span class="dv">3</span><span class="op">,</span> <span class="dv">4</span><span class="op">,</span> <span class="dv">1</span>)<span class="op">,</span> <span class="dv">3</span>)</span></code></pre></div>
<p>These numbers then undergo various pattern transformations. Here is a
short description of all the functions used:</p>
<ul>
<li><code>cat</code>: play elements sequentially, where each lasts one
cycle</li>
<li><code>brackets</code>: elements inside brackets are divided equally
over the time of their parent</li>
<li><code>.euclid(p, s, o)</code>: place p pulses evenly over s steps,
with offset o <span class="citation"
data-cites="toussaintEuclideanAlgorithmGenerates2005">(Toussaint
2005)</span></li>
<li><code>.off(n, f)</code>: layers a pattern on top of itself, with the
new layer offset by n cycles, and with function f applied</li>
<li><code>.legato(n)</code>: multiply the duration of all events in a
pattern by a factor of n</li>
<li><code>.echo(t, n, v)</code>: copy each event t times, with n cycles
in between each copy, decreasing velocity by v</li>
<li><code>.note()</code>: interpretes values as notes</li>
<li><code>.s(name)</code>: play back each event with the given
sound</li>
<li><code>.delay(wet)</code>: add delay</li>
<li><code>.delaytime(t)</code>: set delay time</li>
</ul>
<p>Much of the above will be familiar to Tidal users.</p>
<!-- This example shows some of Strudel's unique support for chords and transposition familiar to students of Western music theory. This differs a little from Tidal's approach and thanks to the integration of the javascript library XXX (*TODO* ? or is this all your work Felix?), Strudel's support for tonal transformations such as voice leading is perhaps respects more advanced than Tidal. -->
<h1 data-number="6" id="ways-to-make-sound-and-other-events"><span
class="header-section-number">6</span> Ways to make Sound (and other
events)</h1>
<p>To generate sound, Strudel supports bindings for different
outputs:</p>
<ul>
<li>Tone.js (deprecated)</li>
<li>Web Audio API</li>
<li>WebDirt, a js recreation of Tidals <em>Dirt</em> sample engine
(deprecated)</li>
<li>OSC via osc-js, compatible with superdirt</li>
<li>Csound via the Csound WebAssembly build</li>
<li>MIDI via WebMIDI</li>
<li>Serial via WebSerial</li>
</ul>
<p>At first, we used Tone.js as sound output, but it proved to be
limited for the use case of Strudel, where each individual event could
potentially have a completely different audio graph. While the Web Audio
API takes a <em>fire-and-forget</em> approach, creating a lot of Tone.js
instruments and effects causes performance issues quickly. For that
reason, we chose to search for alternatives.</p>
<p>Strudels new default output uses the Web Audio API to create a new
audio graph for each event. It currently supports basic oscillators,
sample playback, various effects and an experimental support for
soundfonts.</p>
<p>WebDirt <span class="citation"
data-cites="ogbornDktr0WebDirt2022">(Ogborn [2016] 2022)</span> was
created as part of the Estuary Live Coding System <span class="citation"
data-cites="ogbornEstuaryBrowserbasedCollaborative2017">(Ogborn et al.
2017)</span>, and proved to be a solid choice for handling samples in
Strudel as well. We are however focused on working more directly with
the Web Audio API to be able to integrate new features more tightly.</p>
<p>Using the OSC protocol via Strudels provided Node.js-based OSC proxy
server, it is possible to send network messages to trigger events. This
is mainly used to render sound using SuperDirt <span class="citation"
data-cites="SuperDirt2022">(<em>SuperDirt</em> [2015] 2022)</span>,
which is the well-developed Supercollider-based synthesis framework that
Tidal live coders generally use as standard.</p>
<p>Recently, the experimental integration of Csound proved to bring a
new dimension of sound design capabilities to Strudel. Thanks to the
WebAssembly distribution of this classic system <span class="citation"
data-cites="CsoundWebAssembly">(Yi, Lazzarini, and Costello
2018)</span>, Csound orchestra synthesisers can be embedded in and
then patterned with Strudel code.</p>
<p>MIDI output can also be used to send MIDI messages to either external
instruments or to other programs on the same device. Unlike OSC, Strudel
is able to send MIDI directly without requiring additional proxy
software, but only from web browsers that support it (at the time of
writing, this means Chromium-based browsers).</p>
<p>Finally, Strudel supports Serial output, for example to trigger
events via microcontrollers. This has already been explored for robot
choreography by Kate Sicchio and Alex McLean, via a performance
presented at the International Conference on Live Interfaces 2022.</p>
<h1 data-number="7" id="the-strudel-repl"><span
class="header-section-number">7</span> The Strudel REPL</h1>
<p>While Strudel can be used as a library in any JavaScript codebase,
its main, reference user interface is the Strudel REPL[^REPL stands for
read, evaluate, print/play, loop. It is friendly jargon for an
interactive programming interface from computing heritage, usually for a
commandline interface but also applied to live coding editors.], which
is a browser-based live coding environment. This live code editor is
dedicated to manipulating Strudel patterns while they play. The REPL
features built-in visual feedback, which highlights which elements in
the patterned (mini-notation) sequences are influencing the event that
is currently being played. This feedback is designed to support both
learning and live use of Strudel.</p>
<p>Besides a UI for playback control and meta information, the main part
of the REPL interface is the code editor powered by CodeMirror. In it,
the user can edit and evaluate pattern code live, using one of the
available synthesis outputs to create music and/or sound art. The
control flow of the REPL follows 3 basic steps:</p>
<ol type="1">
<li>The user writes and updates code. Each update transpiles and
evaluates it to create a <code>Pattern</code> instance</li>
<li>While the REPL is running, the <code>Scheduler</code> queries the
active <code>Pattern</code> by a regular interval, generating
<code>Events</code> (also known as <code>Haps</code> in Strudel) for the
next time span.</li>
<li>For each scheduling tick, all generated <code>Events</code> are
triggered by calling their <code>onTrigger</code> method, which is set
by the output.</li>
</ol>
<figure>
<img
src="https://github.com/tidalcycles/strudel/raw/talk/talk/public/strudelflow.png?raw=true"
style="width:43.0%" alt="REPL control flow" />
<figcaption aria-hidden="true">REPL control flow</figcaption>
</figure>
<h2 data-number="7.1" id="user-code"><span
class="header-section-number">7.1</span> User Code</h2>
<p>To create a <code>Pattern</code> from the user code, two steps are
needed:</p>
<ol type="1">
<li>Transpile the JS input code to make it functional</li>
<li>Evaluate the transpiled code</li>
</ol>
<h3 data-number="7.1.1" id="transpilation-evaluation"><span
class="header-section-number">7.1.1</span> Transpilation &amp;
Evaluation</h3>
<p>In the JavaScript world, using transpilation is a common practise to
be able to use language features that are not supported by the base
language. Tools like <code>babel</code> will transpile code that
contains unsupported language features into a version of the code
without those features.</p>
<p>In the same tradition, Strudel can add a transpilation step to
simplify the user code in the context of live coding. For example, the
Strudel REPL lets the user create mini notation patterns using just
double quoted strings, while single quoted strings remain what they
are:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;c3 [e3 g3]*2&quot;</span></span></code></pre></div>
<p>is transpiled to:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="fu">mini</span>(<span class="st">&quot;c3 [e3 g3]*2&quot;</span>)<span class="op">.</span><span class="fu">withMiniLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span>]<span class="op">,</span>[<span class="dv">1</span><span class="op">,</span><span class="dv">14</span><span class="op">,</span><span class="dv">14</span>])</span></code></pre></div>
<p>Here, the string is wrapped in <code>mini</code>, which will create a
pattern from a mini notation string. Additionally, the
<code>withMiniLocation</code> method passes the original source code
location of the string to the pattern, which enables highlighting active
events.</p>
<p>Other convenient features like pseudo variables, operator overloading
and top level await are possible with transpilation.</p>
<p>After the transpilation, the code is ready to be evaluated into a
<code>Pattern</code>.</p>
<p>Behind the scenes, the user code string is parsed with
<code>acorn</code>, turning it into an Abstract Syntax Tree (AST). The
AST allows changing the structure of the code before generating the
transpiled version using <code>escodegen</code>.</p>
<h3 data-number="7.1.2" id="mini-notation"><span
class="header-section-number">7.1.2</span> Mini Notation</h3>
<p>While the transpilation allows JavaScript to express Patterns in a
less verbose way, it is still preferable to use the Mini Notation as a
more compact way to express rhythm. Strudel aims to provide the same
Mini Notation features and syntax as used in Tidal.</p>
<p>The Mini Notation parser is implemented using <code>peggy</code>,
which allows generating performant parsers for Domain Specific Languages
(DSLs) using a concise grammar notation. The generated parser turns the
Mini Notation string into an AST which is used to call the respective
Strudel functions with the given structure. For example,
<code>"c3 [e3 g3]*2"</code> will result in the following calls:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="fu">seq</span>(</span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a> <span class="fu">reify</span>(<span class="st">&#39;c3&#39;</span>)<span class="op">.</span><span class="fu">withLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">1</span><span class="op">,</span><span class="dv">1</span>]<span class="op">,</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">4</span><span class="op">,</span><span class="dv">4</span>])<span class="op">,</span></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a> <span class="fu">seq</span>(</span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">reify</span>(<span class="st">&#39;e3&#39;</span>)<span class="op">.</span><span class="fu">withLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">5</span><span class="op">,</span><span class="dv">5</span>]<span class="op">,</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">8</span><span class="op">,</span><span class="dv">8</span>])<span class="op">,</span></span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">reify</span>(<span class="st">&#39;g3&#39;</span>)<span class="op">.</span><span class="fu">withLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">8</span><span class="op">,</span><span class="dv">8</span>]<span class="op">,</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">10</span><span class="op">,</span><span class="dv">10</span>])<span class="op">,</span></span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a> )<span class="op">.</span><span class="fu">fast</span>(<span class="dv">2</span>)</span>
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a>)</span></code></pre></div>
<h3 data-number="7.1.3" id="highlighting-locations"><span
class="header-section-number">7.1.3</span> Highlighting Locations</h3>
<p>As seen in the examples above, both the JS and the Mini Notation
parser add source code locations using <code>withMiniLocation</code> and
<code>withLocation</code> methods. While the JS parser adds locations
relative to the user code as a whole, the Mini Notation adds locations
relative to the position of the mini notation string. The absolute
location of elements within Mini Notation can be calculated by simply
adding both locations together. This absolute location can be used to
highlight active events in real time.</p>
<h2 data-number="7.2" id="scheduling-events"><span
class="header-section-number">7.2</span> Scheduling Events</h2>
<p>After an instance of <code>Pattern</code> is obtained from the user
code, it is used by the scheduler to get queried for events. Once
started, the scheduler runs at a fixed interval to query active pattern
for events withing the current intervals time span. A simplified
implementation looks like this:</p>
<div class="sourceCode" id="cb10"><pre
class="sourceCode js"><code class="sourceCode javascript"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> pattern <span class="op">=</span> <span class="fu">seq</span>(<span class="st">&#39;c3&#39;</span><span class="op">,</span> [<span class="st">&#39;e3&#39;</span><span class="op">,</span> <span class="st">&#39;g3&#39;</span>])<span class="op">;</span> <span class="co">// pattern from user</span></span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> interval <span class="op">=</span> <span class="fl">0.5</span><span class="op">;</span> <span class="co">// query interval in seconds</span></span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> time <span class="op">=</span> <span class="dv">0</span><span class="op">;</span> <span class="co">// beginning of current time span</span></span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> minLatency <span class="op">=</span> <span class="op">.</span><span class="dv">1</span><span class="op">;</span> <span class="co">// min time before a hap should trigger</span></span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a><span class="pp">setInterval</span>(() <span class="kw">=&gt;</span> {</span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> haps <span class="op">=</span> pattern<span class="op">.</span><span class="fu">queryArc</span>(time<span class="op">,</span> time <span class="op">+</span> interval)<span class="op">;</span></span>
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a> time <span class="op">+=</span> interval<span class="op">;</span> <span class="co">// increment time</span></span>
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a> haps<span class="op">.</span><span class="fu">forEach</span>((hap) <span class="kw">=&gt;</span> {</span>
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> deadline <span class="op">=</span> hap<span class="op">.</span><span class="at">whole</span><span class="op">.</span><span class="at">begin</span> <span class="op">-</span> time <span class="op">+</span> minLatency<span class="op">;</span></span>
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a> <span class="fu">onTrigger</span>(hap<span class="op">,</span> deadline<span class="op">,</span> duration)<span class="op">;</span></span>
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a> })<span class="op">;</span></span>
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a>}<span class="op">,</span> interval <span class="op">*</span> <span class="dv">1000</span>)<span class="op">;</span> <span class="co">// query each &quot;interval&quot; seconds</span></span></code></pre></div>
<p>Note that the above code is simplified for illustrative purposes. The
actual implementation has to work around imprecise callbacks of
<code>setInterval</code>. More about the implementation details can be
read in <a
href="https://loophole-letters.vercel.app/web-audio-scheduling">this
blog post</a>.</p>
<p>The fact that <code>Pattern.queryArc</code> is a pure function that
maps a time span to a set of events allows us to choose any interval we
like without changing the resulting output. It also means that when the
pattern is changed from outside, the next scheduling callback will work
with the new pattern, keeping its clock running.</p>
<p>The latency between the time the pattern is evaluated and the change
is heard is between <code>minLatency</code> and
<code>interval + minLatency</code>, in our example between 100ms and
600ms. In Strudel, the current query interval is 50ms with a minLatency
of 100ms, meaning the latency is between 50ms and 150ms.</p>
<h2 data-number="7.3" id="output"><span
class="header-section-number">7.3</span> Output</h2>
<p>The last step is to trigger each event in the chosen output. This is
where the given time and value of each event is used to generate audio
or any other form of time based output. The default output of the
Strudel REPL is the WebAudio output. To understand what an output does,
we first have to understand what control parameters are.</p>
<h3 data-number="7.3.1" id="control-parameters"><span
class="header-section-number">7.3.1</span> Control Parameters</h3>
<p>To be able to manipulate multiple aspects of sound in parallel, so
called control parameters are used to shape the value of each event.
Example:</p>
<div class="sourceCode" id="cb11"><pre
class="sourceCode js"><code class="sourceCode javascript"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="fu">note</span>(<span class="st">&quot;c3 e3&quot;</span>)<span class="op">.</span><span class="fu">cutoff</span>(<span class="dv">1000</span>)<span class="op">.</span><span class="fu">s</span>(<span class="st">&#39;sawtooth&#39;</span>)</span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="fu">queryArc</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">1</span>)<span class="op">.</span><span class="fu">map</span>(hap <span class="kw">=&gt;</span> hap<span class="op">.</span><span class="at">value</span>)</span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a><span class="co">/* [</span></span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a><span class="co"> { note: &#39;c3&#39;, cutoff: 1000, s: &#39;sawtooth&#39; }</span></span>
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a><span class="co"> { note: &#39;e3&#39;, cutoff: 1000, s: &#39;sawtooth&#39; }</span></span>
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a><span class="co">] */</span></span></code></pre></div>
<p>Here, the control parameter functions <code>note</code>,
<code>cutoff</code> and <code>s</code> are used, where each controls a
different property in the value object. Each control parameter function
accepts a primitive value, a list of values to be sequenced into a
<code>Pattern</code>, or a <code>Pattern</code>. In the example,
<code>note</code> gets a <code>Pattern</code> from a Mini Notation
expression (double quoted), while <code>cutoff</code> and <code>s</code>
are given a <code>Number</code> and a (single quoted)
<code>String</code> respectively.</p>
<p>Strudel comes with a large default set of control parameter functions
that are based on the ones used by Tidal and SuperDirt, focusing on
music and audio terminology. It is however possible to create custom
control paramters for any purpose:</p>
<div class="sourceCode" id="cb12"><pre
class="sourceCode js"><code class="sourceCode javascript"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> { x<span class="op">,</span> y } <span class="op">=</span> <span class="fu">createParams</span>(<span class="st">&#39;x&#39;</span><span class="op">,</span> <span class="st">&#39;y&#39;</span>)</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a><span class="fu">x</span>(sine<span class="op">.</span><span class="fu">range</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">200</span>))<span class="op">.</span><span class="fu">y</span>(cosine<span class="op">.</span><span class="fu">range</span>(<span class="dv">0</span><span class="op">,</span><span class="dv">200</span>))</span></code></pre></div>
<p>This example creates the custom control parameters <code>x</code> and
<code>y</code> which are then used to form a pattern that descibes the
coordinates of a circle.</p>
<h3 data-number="7.3.2" id="outputs"><span
class="header-section-number">7.3.2</span> Outputs</h3>
<p>Now that we know how the value of an event is manipulated using
control parameters, we can look at how outputs can use that value to
generate anything. The scheduler above was calling the
<code>onTrigger</code> function which is used to implement the output. A
very simple version of the web audio output could look like this:</p>
<div class="sourceCode" id="cb13"><pre
class="sourceCode js"><code class="sourceCode javascript"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">onTrigger</span>(hap<span class="op">,</span> deadline<span class="op">,</span> duration) {</span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> { note } <span class="op">=</span> hap<span class="op">.</span><span class="at">value</span><span class="op">;</span></span>
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> time <span class="op">=</span> <span class="fu">getAudioContext</span>()<span class="op">.</span><span class="at">currentTime</span> <span class="op">+</span> deadline<span class="op">;</span></span>
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> o <span class="op">=</span> <span class="fu">getAudioContext</span>()<span class="op">.</span><span class="fu">createOscillator</span>()<span class="op">;</span></span>
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="at">frequency</span><span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="fu">getFreq</span>(note)<span class="op">;</span></span>
<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="fu">start</span>(time)<span class="op">;</span></span>
<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="fu">stop</span>(time <span class="op">+</span> <span class="bu">event</span><span class="op">.</span><span class="at">duration</span>)<span class="op">;</span></span>
<span id="cb13-8"><a href="#cb13-8" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="fu">connect</span>(<span class="fu">getAudioContext</span>()<span class="op">.</span><span class="at">destination</span>)<span class="op">;</span></span>
<span id="cb13-9"><a href="#cb13-9" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
<p>The above example will create an <code>OscillatorNode</code> for each
event, where the frequency is controlled by the <code>note</code> param.
In essence, this is how the WebAudio API output of Strudel works, only
with many more parameters to control synths, samples and effects.</p>
<h1 data-number="8" id="pattern-alignment-and-combination"><span
class="header-section-number">8</span> Pattern alignment and
combination</h1>
<p>One core aspect of Strudel, inherited from Tidal, is the flexible way
that patterns can be combined, irrespective of their structure. Its
declarative approach means a live coder does not have to think about the
details of <em>how</em> this is done, only <em>what</em> is to be
done.</p>
<p>As a simple example, consider two number patterns
<code>"0 [1 2] 3"</code>, and <code>"10 20"</code>. The first has three
contiguous steps of equal lengths, with the second step broken down into
two substeps, giving four events in total. There are a very large number
of ways in which the structure of these two patterns could be combined,
but the default method in both Strudel and Tidal is to line up the
cycles of the two patterns, and then take events from the first pattern
and match them with those in the second pattern. Therefore, the
following two lines are equivalent:</p>
<div class="sourceCode" id="cb14"><pre
class="sourceCode js"><code class="sourceCode javascript"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;0 [1 2] 3&quot;</span><span class="op">.</span><span class="fu">add</span>(<span class="st">&quot;10 20&quot;</span>)</span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;10 [11 22] 23&quot;</span></span></code></pre></div>
<p>Where the events only partially overlap, they are treated as
fragments of the event in the first pattern. This is a little difficult
to conceptualise, but lets start by comparing the two patterns in the
following example:</p>
<div class="sourceCode" id="cb15"><pre
class="sourceCode js"><code class="sourceCode javascript"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;0 1 2&quot;</span><span class="op">.</span><span class="fu">add</span>(<span class="st">&quot;10 20&quot;</span>)</span>
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;10 [11 21] 20&quot;</span></span></code></pre></div>
<p>They are similar to the previous example in that the number
<code>1</code> is split in two, with its two halves added to
<code>10</code> and <code>20</code> respectively. However, the
<code>11</code> remembers that it is a fragment of that original
<code>1</code> event, and so is treated as having a duration of a third
of a cycle, despite only being active for a sixth of a cycle. Likewise,
the <code>21</code> is also a fragment of that original <code>1</code>
event, but a fragment of its second half. Because the start of its event
is missing, it wouldnt actually trigger a sound (unless it underwent
further pattern transformations/combinations).</p>
<p>In practice, the effect of this default, implicit method for
combining two patterns is that the second pattern is added <em>in</em>
to the first one, and indeed this can be made explicit:</p>
<div class="sourceCode" id="cb16"><pre
class="sourceCode js"><code class="sourceCode javascript"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;0 1 2&quot;</span><span class="op">.</span><span class="at">add</span><span class="op">.</span><span class="fu">in</span>(<span class="st">&quot;10 20&quot;</span>)</span></code></pre></div>
<p>This makes way for other ways to align the pattern, and several are
already defined, in particular:</p>
<ul>
<li><code>in</code> - as explained above, aligns cycles, and applies
values from the pattern on the right <em>in</em> to the pattern on the
left.</li>
<li><code>out</code> - as with <code>in</code>, but values are applied
<em>out</em> of the pattern on the left (i.e. <em>in</em> to the one on
the right).</li>
<li><code>mix</code> - structures from both patterns are combined, so
that the new events are not fragments but are created at intersections
of events from both sides.</li>
<li><code>squeeze</code> - cycles from the pattern on the right are
squeezed into events on the left. So that
e.g. <code>"0 1 2".add.squeeze("10 20")</code> is equivalent to
<code>"[10 20] [11 21] [12 22]"</code>.</li>
<li><code>squeezeout</code> - as with <code>squeeze</code>, but cycles
from the left are squeezed into events on the right. So,
<code>"0 1 2".add.squeezeout("10 20")</code> is equivalent to
<code>[10 11 12] [20 21 22]</code>.</li>
<li><code>trig</code> is similar to <code>squeezeout</code> in that
cycles from the right are aligned with events on the left. However those
cycles are not squeezed, rather they are truncated to fit the event.
So <code>"0 1 2 3 4 5 6 7".add.trig("10 [20 30]")</code> would be
equivalent to <code>10 11 12 13 20 21 30 31</code>. In effect, events on
the right trigger cycles on the left.</li>
<li><code>trigzero</code> is similar to <code>trig</code>, but the
pattern is triggered from its very first cycle, rather than from the
current cycle. <code>trig</code> and <code>trigzero</code> therefore
only give different results where the leftmost pattern differs from one
cycle to the next.</li>
</ul>
<p>We will save going deeper into the background, design and
practicalities of these alignment functions for future publications.
However in the next section, we take them as a case study for looking at
the different design affordances offered by Haskell to Tidal, and
JavaScript to Strudel.</p>
<h1 data-number="9" id="comparing-strudel-and-haskell-in-use"><span
class="header-section-number">9</span> Comparing Strudel and Haskell in
use</h1>
<p>Unlike Haskell, JavaScript lacks the ability to define custom infix
operators, or change the meaning of existing ones. So the above Strudel
example of <code>"0 1 2".add.out("10 20")</code> is equivalent to the
Tidal expression <code>"0 1 2" +| "10 20"</code>, where the vertical bar
in the operator <code>+|</code> stands for <code>out</code> (where
<code>a |+ b</code> would be equivalent of
<code>a.add.in(b)</code>).</p>
<p>From this we can already see that Tidal tends towards brevity through
mixing infix operators with functions, and Strudel tends towards
spelling out operations which are joined together with the
<code>.</code> operator. This then is the design trade-off of Tidals
tersity, versus Strudels simplicity.</p>
<p>To demonstrate this, consider the following Tidal pattern:</p>
<pre class="tidal"><code>iter 4 $ every 3 (||+ n &quot;10 20&quot;) $ (n &quot;0 1 3&quot;) # s &quot;triangle&quot; # crush 4</code></pre>
<p>This can be directly translated to the Strudel equivalent:</p>
<div class="sourceCode" id="cb18"><pre
class="sourceCode js"><code class="sourceCode javascript"><span id="cb18-1"><a href="#cb18-1" aria-hidden="true" tabindex="-1"></a><span class="fu">iter</span>(<span class="dv">4</span><span class="op">,</span> <span class="fu">every</span>(<span class="dv">3</span><span class="op">,</span> add<span class="op">.</span><span class="fu">squeeze</span>(<span class="st">&quot;10 20&quot;</span>)<span class="op">,</span> <span class="fu">n</span>(<span class="st">&quot;0 1 3&quot;</span>)<span class="op">.</span><span class="fu">s</span>(<span class="st">&quot;triangle&quot;</span>)<span class="op">.</span><span class="fu">crush</span>(<span class="dv">4</span>)))</span></code></pre></div>
<p>Although for a more canonical Strudel expression, we would reorder it
as:</p>
<div class="sourceCode" id="cb19"><pre
class="sourceCode js"><code class="sourceCode javascript"><span id="cb19-1"><a href="#cb19-1" aria-hidden="true" tabindex="-1"></a><span class="fu">n</span>(<span class="st">&quot;0 1 3&quot;</span>)<span class="op">.</span><span class="fu">every</span>(<span class="dv">3</span><span class="op">,</span> add<span class="op">.</span><span class="fu">squeeze</span>(<span class="st">&quot;10 20&quot;</span>))<span class="op">.</span><span class="fu">iter</span>(<span class="dv">4</span>)<span class="op">.</span><span class="fu">s</span>(<span class="st">&quot;triangle&quot;</span>)<span class="op">.</span><span class="fu">crush</span>(<span class="dv">4</span>)</span></code></pre></div>
<p>The Strudel example uses the <code>.</code> method call operator for
all operations and combinations, whereas the Tidal example has
<code>#</code> for the default method for combining patterns and uses
infix operators for other methods. The lack of parenthesis in the Tidal
example is partly due to the way that arguments are applied to Haskells
functions, and partly due to the use of the <code>$</code> operator as
an alternative way to establish precedence and control the order of
evaluation.</p>
<p>Considering the above, we argue that the Haskell syntax is a little
cleaner, but that the Strudel syntax is easier to learn. Our informal
observation is that while Haskells dollar <code>$</code> operator is
very useful in making code easier to work with, it is one of the most
difficult aspects of Tidal use for beginners to learn. On the other
hand, the deeper levels of parenthesis in Strudel code can be difficult
to keep track of, especially while coding under pressure of live musical
performance. However this difficulty can be largely be mitigated by
reordering expressions, and further mitigated by supporting editor
features.</p>
<p>With Strudel, we have little choice but to embrace the affordances
and constraints offered by JavaScript, and while designing a
domain-specific language entirely based on method calls is a challenge,
through creative adoption of functional programming techniques like
partial application, we are so far very happy with the results. Tidals
functional reactive approach to pattern-making has in general translated
well to JavaScript, and opportunities and constraints have overall
traded off to create a very approachable and useable live coding
environment.</p>
<h2 data-number="9.1" id="the-trade-off-of-flexible-typing"><span
class="header-section-number">9.1</span> The trade-off of flexible
typing</h2>
<p>We have identified one problem with porting Tidal to JavaScript where
we have missed Haskells strict typing and type inference. In both Tidal
and Strudel, time is rational, where any point in time is represented as
the ratio of two integers. This allows representation of musical ratios
such that are impossible to represent accurately using the more common
floating point numbers. However while libraries are available that
support rational numbers in JavaScript, the lack of strict typing means
that it is easy to implement pattern methods where computationally
expensive conversion from floating point to rational numbers are
performed late, and therefore often enough to overload the CPUs, due to
the large number of iterative calculations required to estimate a ratio
for a given floating point number. To mitigate this problem, we might
consider moving to TypeScript in the future.</p>
<h1 data-number="10" id="future-outlook"><span
class="header-section-number">10</span> Future Outlook</h1>
<p>The project is still young, with many features on the horizon. As
general guiding principles, Strudel aims to be</p>
<ol type="1">
<li>accessible</li>
<li>consistent with Tidals approach to pattern</li>
<li>modular and extensible</li>
</ol>
<p>While Haskells type system makes it a great language for the ongoing
development of Tidals inner representation of pattern, JavaScripts
vibrant ecosystem, flexibility and accessibility makes it a great host
for more ad-hoc experiments, including interface design. For the future,
it is planned to integrate additional alternative sound engines such as
Glicol <span class="citation" data-cites="lanChaosprintGlicol2022">(Lan
[2020] 2022)</span> and Faust <span class="citation"
data-cites="FaustProgrammingLanguage2022">(<em>Faust - Programming
Language for Audio Applications and Plugins</em> [2016] 2022)</span>.
Strudel is already approaching feature parity with Tidal, but there are
more Tidal functions to be ported, and work to be done to improve
compatibility with Tidals mininotation. Tidal version 2.0 is under
development, which brings a new representation for sequences to its
patterns, which will then be brought to Strudel. Besides sound, other
ways to render events are being explored, such as graphical, and
choreographic output. We are also looking into alternative ways of
editing patterns, including multi-user editing for network music,
parsing a novel syntax to escape the constraints of javascript, and
developing hardware/e-textile interfaces. In summary, there is a lot of
fun ahead.</p>
<h1 data-number="11" id="links"><span
class="header-section-number">11</span> Links</h1>
<p>The Strudel REPL is available at <a
href="https://strudel.tidalcycles.org"
class="uri">https://strudel.tidalcycles.org</a>, including an
interactive tutorial. The repository is at <a
href="https://github.com/tidalcycles/strudel"
class="uri">https://github.com/tidalcycles/strudel</a>, all the code is
open source under the AGPL-3.0 License.</p>
<h1 data-number="12" id="acknowledgments"><span
class="header-section-number">12</span> Acknowledgments</h1>
<p>Thanks to the Strudel and wider Tidal, live coding, WebAudio and
free/open source software communities for inspiration and support. Alex
McLeans work on this project is supported by a UKRI Future Leaders
Fellowship [grant number MR/V025260/1].</p>
<h1 class="unnumbered" id="references">References</h1>
<div id="refs" class="references csl-bib-body hanging-indent"
role="doc-bibliography">
<div id="ref-FaustProgrammingLanguage2022" class="csl-entry"
role="doc-biblioentry">
<em>Faust - Programming Language for Audio Applications and
Plugins</em>. (2016) 2022. C++. GRAME. <a
href="https://github.com/grame-cncm/faust">https://github.com/grame-cncm/faust</a>.
</div>
<div id="ref-jackHydra2022" class="csl-entry" role="doc-biblioentry">
Jack, Olivia. (2022) 2022. <em>Hydra</em>. <a
href="https://github.com/ojack/hydra">https://github.com/ojack/hydra</a>.
</div>
<div id="ref-lanChaosprintGlicol2022" class="csl-entry"
role="doc-biblioentry">
Lan, Qichao. (2020) 2022. <em>Chaosprint/Glicol</em>. Rust. <a
href="https://github.com/chaosprint/glicol">https://github.com/chaosprint/glicol</a>.
</div>
<div id="ref-mcleanAlgorithmicPattern2020a" class="csl-entry"
role="doc-biblioentry">
Mclean, Alex. 2020. <span>“Algorithmic Pattern.”</span> In
<em>Proceedings of the International Conference on New Interfaces for
Musical Expression</em>, 265--270. Birmingham, UK. <a
href="https://zenodo.org/record/4813352">https://zenodo.org/record/4813352</a>.
</div>
<div id="ref-mcleanFeedforward2020" class="csl-entry"
role="doc-biblioentry">
McLean, Alex. 2020. <span>“Feedforward.”</span> In <em>Proceedings of
New Interfaces for Musical Expression</em>. Birmingham. <a
href="https://zenodo.org/record/6353969">https://zenodo.org/record/6353969</a>.
</div>
<div id="ref-mcleanTidalVortexZero2022" class="csl-entry"
role="doc-biblioentry">
McLean, Alex, Raphaël Forment, Sylvain Le Beux, and Damián Silvani.
2022. <span>“TidalVortex Zero.”</span> In <em>Proceedings of the 7th
International Conference on Live Coding</em>. Limerick, Ireland: Zenodo.
<a
href="https://doi.org/10.5281/zenodo.6456380">https://doi.org/10.5281/zenodo.6456380</a>.
</div>
<div id="ref-ogbornDktr0WebDirt2022" class="csl-entry"
role="doc-biblioentry">
Ogborn, David. (2016) 2022. <em>Dktr0/WebDirt</em>. JavaScript. <a
href="https://github.com/dktr0/WebDirt">https://github.com/dktr0/WebDirt</a>.
</div>
<div id="ref-ogbornEstuaryBrowserbasedCollaborative2017"
class="csl-entry" role="doc-biblioentry">
Ogborn, David, Jamie Beverley, Luis Navarro del Angel, Eldad Tsabary,
and Alex McLean. 2017. <span>“Estuary: Browser-Based Collaborative
Projectional Live Coding of Musical Patterns.”</span> In <em>Proceedings
of the International Conference on Live Coding</em>, 11. Morelia.
</div>
<div id="ref-robertsGibberLiveCoding2012" class="csl-entry"
role="doc-biblioentry">
Roberts, Charles, and Joann Kuchera-morin. 2012. <span>“Gibber: Live
Coding Audio in the Browser.”</span> In <em>In Proceedings of the 2012
International Computer Music Conference</em>.
</div>
<div id="ref-StrudelWAC2022" class="csl-entry" role="doc-biblioentry">
Roos, Felix, and Alex McLean. 2022. <span>“Strudel: Algorithmic Patterns
for the Web.”</span> In. Zenodo. <a
href="https://doi.org/10.5281/zenodo.6768844">https://doi.org/10.5281/zenodo.6768844</a>.
</div>
<div id="ref-solomonPurescriptocarina2022" class="csl-entry"
role="doc-biblioentry">
Solomon, Mike. (2021) 2022. <em>Purescript-Ocarina</em>. PureScript. <a
href="https://github.com/mikesol/purescript-ocarina">https://github.com/mikesol/purescript-ocarina</a>.
</div>
<div id="ref-SuperDirt2022" class="csl-entry" role="doc-biblioentry">
<em>SuperDirt</em>. (2015) 2022. SuperCollider. musikinformatik. <a
href="https://github.com/musikinformatik/SuperDirt">https://github.com/musikinformatik/SuperDirt</a>.
</div>
<div id="ref-toussaintEuclideanAlgorithmGenerates2005" class="csl-entry"
role="doc-biblioentry">
Toussaint, Godfried. 2005. <span>“The Euclidean Algorithm Generates
Traditional Musical Rhythms.”</span> In <em>In Proceedings of BRIDGES:
Mathematical Connections in Art, Music and Science</em>, 4756. <a
href="http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231">http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231</a>.
</div>
<div id="ref-CsoundWebAssembly" class="csl-entry"
role="doc-biblioentry">
Yi, Steven, Victor Lazzarini, and Edward Costello. 2018.
<span>“WebAssembly AudioWorklet Csound.”</span> In. Berlin, Germany. <a
href="https://mural.maynoothuniversity.ie/16018/">https://mural.maynoothuniversity.ie/16018/</a>.
</div>
</div>
</body>
</html>
Binary file not shown.
+1926 -67
View File
File diff suppressed because it is too large Load Diff
+421 -130
View File
@@ -848,6 +848,23 @@ exports[`runs examples > example "arpWith" example index 0 1`] = `
]
`;
exports[`runs examples > example "arrange" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:c ]",
"[ 3/8 → 1/2 | note:c ]",
"[ 3/4 → 7/8 | note:c ]",
"[ 1/1 → 9/8 | note:a ]",
"[ 11/8 → 3/2 | note:a ]",
"[ 7/4 → 15/8 | note:a ]",
"[ 2/1 → 17/8 | note:f ]",
"[ 19/8 → 5/2 | note:f ]",
"[ 11/4 → 23/8 | note:f ]",
"[ 3/1 → 25/8 | note:e ]",
"[ 27/8 → 7/2 | note:e ]",
"[ 15/4 → 31/8 | note:e ]",
]
`;
exports[`runs examples > example "attack" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:c3 attack:0 ]",
@@ -861,7 +878,29 @@ exports[`runs examples > example "attack" example index 0 1`] = `
]
`;
exports[`runs examples > example "bandf" example index 0 1`] = `
exports[`runs examples > example "bank" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd bank:RolandTR909 ]",
"[ 1/2 → 1/1 | s:sd bank:RolandTR909 ]",
"[ 1/1 → 3/2 | s:bd bank:RolandTR909 ]",
"[ 3/2 → 2/1 | s:sd bank:RolandTR909 ]",
"[ 2/1 → 5/2 | s:bd bank:RolandTR909 ]",
"[ 5/2 → 3/1 | s:sd bank:RolandTR909 ]",
"[ 3/1 → 7/2 | s:bd bank:RolandTR909 ]",
"[ 7/2 → 4/1 | s:sd bank:RolandTR909 ]",
]
`;
exports[`runs examples > example "begin" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:rave begin:0 ]",
"[ 1/1 → 2/1 | s:rave begin:0.25 ]",
"[ 2/1 → 3/1 | s:rave begin:0.5 ]",
"[ 3/1 → 4/1 | s:rave begin:0.75 ]",
]
`;
exports[`runs examples > example "bpf" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd bandf:1000 ]",
"[ 1/2 → 1/1 | s:sd bandf:1000 ]",
@@ -886,7 +925,7 @@ exports[`runs examples > example "bandf" example index 0 1`] = `
]
`;
exports[`runs examples > example "bandq" example index 0 1`] = `
exports[`runs examples > example "bpq" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd bandf:500 bandq:0 ]",
"[ 1/2 → 1/1 | s:sd bandf:500 bandq:0 ]",
@@ -899,28 +938,6 @@ exports[`runs examples > example "bandq" example index 0 1`] = `
]
`;
exports[`runs examples > example "bank" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd bank:RolandTR909 ]",
"[ 1/2 → 1/1 | s:sd bank:RolandTR909 ]",
"[ 1/1 → 3/2 | s:bd bank:RolandTR909 ]",
"[ 3/2 → 2/1 | s:sd bank:RolandTR909 ]",
"[ 2/1 → 5/2 | s:bd bank:RolandTR909 ]",
"[ 5/2 → 3/1 | s:sd bank:RolandTR909 ]",
"[ 3/1 → 7/2 | s:bd bank:RolandTR909 ]",
"[ 7/2 → 4/1 | s:sd bank:RolandTR909 ]",
]
`;
exports[`runs examples > example "begin" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:rave begin:0 ]",
"[ 1/1 → 2/1 | s:rave begin:0.25 ]",
"[ 2/1 → 3/1 | s:rave begin:0.5 ]",
"[ 3/1 → 4/1 | s:rave begin:0.75 ]",
]
`;
exports[`runs examples > example "cat" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:hh ]",
@@ -1060,6 +1077,27 @@ exports[`runs examples > example "chunkBack" example index 0 1`] = `
]
`;
exports[`runs examples > example "clip" example index 0 1`] = `
[
"[ 0/1 → 1/5 | note:c s:piano clip:1 ]",
"[ 1/5 → 2/5 | note:a s:piano clip:1 ]",
"[ 2/5 → 3/5 | note:f s:piano clip:1 ]",
"[ 3/5 → 4/5 | note:e s:piano clip:1 ]",
"[ 1/1 → 6/5 | note:c s:piano clip:1 ]",
"[ 6/5 → 7/5 | note:a s:piano clip:1 ]",
"[ 7/5 → 8/5 | note:f s:piano clip:1 ]",
"[ 8/5 → 9/5 | note:e s:piano clip:1 ]",
"[ 2/1 → 11/5 | note:c s:piano clip:1 ]",
"[ 11/5 → 12/5 | note:a s:piano clip:1 ]",
"[ 12/5 → 13/5 | note:f s:piano clip:1 ]",
"[ 13/5 → 14/5 | note:e s:piano clip:1 ]",
"[ 3/1 → 16/5 | note:c s:piano clip:1 ]",
"[ 16/5 → 17/5 | note:a s:piano clip:1 ]",
"[ 17/5 → 18/5 | note:f s:piano clip:1 ]",
"[ 18/5 → 19/5 | note:e s:piano clip:1 ]",
]
`;
exports[`runs examples > example "coarse" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd coarse:1 ]",
@@ -1254,31 +1292,6 @@ exports[`runs examples > example "cut" example index 0 1`] = `
]
`;
exports[`runs examples > example "cutoff" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd cutoff:4000 ]",
"[ 1/2 → 1/1 | s:sd cutoff:4000 ]",
"[ 0/1 → 1/3 | s:hh cutoff:4000 ]",
"[ 1/3 → 2/3 | s:hh cutoff:4000 ]",
"[ 2/3 → 1/1 | s:hh cutoff:4000 ]",
"[ 1/1 → 3/2 | s:bd cutoff:2000 ]",
"[ 3/2 → 2/1 | s:sd cutoff:2000 ]",
"[ 1/1 → 4/3 | s:hh cutoff:2000 ]",
"[ 4/3 → 5/3 | s:hh cutoff:2000 ]",
"[ 5/3 → 2/1 | s:hh cutoff:2000 ]",
"[ 2/1 → 5/2 | s:bd cutoff:1000 ]",
"[ 5/2 → 3/1 | s:sd cutoff:1000 ]",
"[ 2/1 → 7/3 | s:hh cutoff:1000 ]",
"[ 7/3 → 8/3 | s:hh cutoff:1000 ]",
"[ 8/3 → 3/1 | s:hh cutoff:1000 ]",
"[ 3/1 → 7/2 | s:bd cutoff:500 ]",
"[ 7/2 → 4/1 | s:sd cutoff:500 ]",
"[ 3/1 → 10/3 | s:hh cutoff:500 ]",
"[ 10/3 → 11/3 | s:hh cutoff:500 ]",
"[ 11/3 → 4/1 | s:hh cutoff:500 ]",
]
`;
exports[`runs examples > example "decay" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:c3 decay:0.1 sustain:0 ]",
@@ -1403,6 +1416,19 @@ exports[`runs examples > example "delay" example index 0 1`] = `
]
`;
exports[`runs examples > example "delay" example index 1 1`] = `
[
"[ 0/1 → 1/2 | s:bd delay:0.65 delaytime:0.25 delayfeedback:0.9 ]",
"[ 1/2 → 1/1 | s:bd delay:0.65 delaytime:0.125 delayfeedback:0.7 ]",
"[ 1/1 → 3/2 | s:bd delay:0.65 delaytime:0.25 delayfeedback:0.9 ]",
"[ 3/2 → 2/1 | s:bd delay:0.65 delaytime:0.125 delayfeedback:0.7 ]",
"[ 2/1 → 5/2 | s:bd delay:0.65 delaytime:0.25 delayfeedback:0.9 ]",
"[ 5/2 → 3/1 | s:bd delay:0.65 delaytime:0.125 delayfeedback:0.7 ]",
"[ 3/1 → 7/2 | s:bd delay:0.65 delaytime:0.25 delayfeedback:0.9 ]",
"[ 7/2 → 4/1 | s:bd delay:0.65 delaytime:0.125 delayfeedback:0.7 ]",
]
`;
exports[`runs examples > example "delayfeedback" example index 0 1`] = `
[
"[ (0/1 → 1/1) ⇝ 2/1 | s:bd delay:0.25 delayfeedback:0.25 ]",
@@ -1898,7 +1924,7 @@ exports[`runs examples > example "gain" example index 0 1`] = `
]
`;
exports[`runs examples > example "hcutoff" example index 0 1`] = `
exports[`runs examples > example "hpf" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd hcutoff:4000 ]",
"[ 1/2 → 1/1 | s:sd hcutoff:4000 ]",
@@ -1927,7 +1953,36 @@ exports[`runs examples > example "hcutoff" example index 0 1`] = `
]
`;
exports[`runs examples > example "hresonance" example index 0 1`] = `
exports[`runs examples > example "hpf" example index 1 1`] = `
[
"[ 0/1 → 1/2 | s:bd hcutoff:2000 ]",
"[ 1/2 → 1/1 | s:sd hcutoff:2000 ]",
"[ 0/1 → 1/4 | s:hh hcutoff:2000 ]",
"[ 1/4 → 1/2 | s:hh hcutoff:2000 ]",
"[ 1/2 → 3/4 | s:hh hcutoff:2000 ]",
"[ 3/4 → 1/1 | s:hh hcutoff:2000 ]",
"[ 1/1 → 3/2 | s:bd hcutoff:2000 hresonance:25 ]",
"[ 3/2 → 2/1 | s:sd hcutoff:2000 hresonance:25 ]",
"[ 1/1 → 5/4 | s:hh hcutoff:2000 hresonance:25 ]",
"[ 5/4 → 3/2 | s:hh hcutoff:2000 hresonance:25 ]",
"[ 3/2 → 7/4 | s:hh hcutoff:2000 hresonance:25 ]",
"[ 7/4 → 2/1 | s:hh hcutoff:2000 hresonance:25 ]",
"[ 2/1 → 5/2 | s:bd hcutoff:2000 ]",
"[ 5/2 → 3/1 | s:sd hcutoff:2000 ]",
"[ 2/1 → 9/4 | s:hh hcutoff:2000 ]",
"[ 9/4 → 5/2 | s:hh hcutoff:2000 ]",
"[ 5/2 → 11/4 | s:hh hcutoff:2000 ]",
"[ 11/4 → 3/1 | s:hh hcutoff:2000 ]",
"[ 3/1 → 7/2 | s:bd hcutoff:2000 hresonance:25 ]",
"[ 7/2 → 4/1 | s:sd hcutoff:2000 hresonance:25 ]",
"[ 3/1 → 13/4 | s:hh hcutoff:2000 hresonance:25 ]",
"[ 13/4 → 7/2 | s:hh hcutoff:2000 hresonance:25 ]",
"[ 7/2 → 15/4 | s:hh hcutoff:2000 hresonance:25 ]",
"[ 15/4 → 4/1 | s:hh hcutoff:2000 hresonance:25 ]",
]
`;
exports[`runs examples > example "hpq" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd hcutoff:2000 hresonance:0 ]",
"[ 1/2 → 1/1 | s:sd hcutoff:2000 hresonance:0 ]",
@@ -1959,19 +2014,19 @@ exports[`runs examples > example "hresonance" example index 0 1`] = `
exports[`runs examples > example "hurry" example index 0 1`] = `
[
"[ 0/1 → 3/4 | s:bd speed:1 ]",
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd:2 speed:1 ]",
"[ 3/4 ⇜ (1/1 → 3/2) | s:sd:2 speed:1 ]",
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd n:2 speed:1 ]",
"[ 3/4 ⇜ (1/1 → 3/2) | s:sd n:2 speed:1 ]",
"[ 3/2 → 15/8 | s:bd speed:2 ]",
"[ (15/8 → 2/1) ⇝ 9/4 | s:sd:2 speed:2 ]",
"[ 15/8 ⇜ (2/1 → 9/4) | s:sd:2 speed:2 ]",
"[ (15/8 → 2/1) ⇝ 9/4 | s:sd n:2 speed:2 ]",
"[ 15/8 ⇜ (2/1 → 9/4) | s:sd n:2 speed:2 ]",
"[ 9/4 → 21/8 | s:bd speed:2 ]",
"[ 21/8 → 3/1 | s:sd:2 speed:2 ]",
"[ 21/8 → 3/1 | s:sd n:2 speed:2 ]",
"[ 3/1 → 51/16 | s:bd speed:4 ]",
"[ 51/16 → 27/8 | s:sd:2 speed:4 ]",
"[ 51/16 → 27/8 | s:sd n:2 speed:4 ]",
"[ 27/8 → 57/16 | s:bd speed:4 ]",
"[ 57/16 → 15/4 | s:sd:2 speed:4 ]",
"[ 57/16 → 15/4 | s:sd n:2 speed:4 ]",
"[ 15/4 → 63/16 | s:bd speed:4 ]",
"[ (63/16 → 4/1) ⇝ 33/8 | s:sd:2 speed:4 ]",
"[ (63/16 → 4/1) ⇝ 33/8 | s:sd n:2 speed:4 ]",
]
`;
@@ -2385,6 +2440,97 @@ exports[`runs examples > example "loopAtCps" example index 0 1`] = `
]
`;
exports[`runs examples > example "lpf" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd cutoff:4000 ]",
"[ 1/2 → 1/1 | s:sd cutoff:4000 ]",
"[ 0/1 → 1/3 | s:hh cutoff:4000 ]",
"[ 1/3 → 2/3 | s:hh cutoff:4000 ]",
"[ 2/3 → 1/1 | s:hh cutoff:4000 ]",
"[ 1/1 → 3/2 | s:bd cutoff:2000 ]",
"[ 3/2 → 2/1 | s:sd cutoff:2000 ]",
"[ 1/1 → 4/3 | s:hh cutoff:2000 ]",
"[ 4/3 → 5/3 | s:hh cutoff:2000 ]",
"[ 5/3 → 2/1 | s:hh cutoff:2000 ]",
"[ 2/1 → 5/2 | s:bd cutoff:1000 ]",
"[ 5/2 → 3/1 | s:sd cutoff:1000 ]",
"[ 2/1 → 7/3 | s:hh cutoff:1000 ]",
"[ 7/3 → 8/3 | s:hh cutoff:1000 ]",
"[ 8/3 → 3/1 | s:hh cutoff:1000 ]",
"[ 3/1 → 7/2 | s:bd cutoff:500 ]",
"[ 7/2 → 4/1 | s:sd cutoff:500 ]",
"[ 3/1 → 10/3 | s:hh cutoff:500 ]",
"[ 10/3 → 11/3 | s:hh cutoff:500 ]",
"[ 11/3 → 4/1 | s:hh cutoff:500 ]",
]
`;
exports[`runs examples > example "lpf" example index 1 1`] = `
[
"[ 0/1 → 1/8 | s:bd cutoff:1000 resonance:0 ]",
"[ 1/8 → 1/4 | s:bd cutoff:1000 resonance:0 ]",
"[ 1/4 → 3/8 | s:bd cutoff:1000 resonance:10 ]",
"[ 3/8 → 1/2 | s:bd cutoff:1000 resonance:10 ]",
"[ 1/2 → 5/8 | s:bd cutoff:1000 resonance:20 ]",
"[ 5/8 → 3/4 | s:bd cutoff:1000 resonance:20 ]",
"[ 3/4 → 7/8 | s:bd cutoff:1000 resonance:30 ]",
"[ 7/8 → 1/1 | s:bd cutoff:1000 resonance:30 ]",
"[ 1/1 → 9/8 | s:bd cutoff:1000 resonance:0 ]",
"[ 9/8 → 5/4 | s:bd cutoff:1000 resonance:0 ]",
"[ 5/4 → 11/8 | s:bd cutoff:1000 resonance:10 ]",
"[ 11/8 → 3/2 | s:bd cutoff:1000 resonance:10 ]",
"[ 3/2 → 13/8 | s:bd cutoff:1000 resonance:20 ]",
"[ 13/8 → 7/4 | s:bd cutoff:1000 resonance:20 ]",
"[ 7/4 → 15/8 | s:bd cutoff:1000 resonance:30 ]",
"[ 15/8 → 2/1 | s:bd cutoff:1000 resonance:30 ]",
"[ 2/1 → 17/8 | s:bd cutoff:1000 resonance:0 ]",
"[ 17/8 → 9/4 | s:bd cutoff:1000 resonance:0 ]",
"[ 9/4 → 19/8 | s:bd cutoff:1000 resonance:10 ]",
"[ 19/8 → 5/2 | s:bd cutoff:1000 resonance:10 ]",
"[ 5/2 → 21/8 | s:bd cutoff:1000 resonance:20 ]",
"[ 21/8 → 11/4 | s:bd cutoff:1000 resonance:20 ]",
"[ 11/4 → 23/8 | s:bd cutoff:1000 resonance:30 ]",
"[ 23/8 → 3/1 | s:bd cutoff:1000 resonance:30 ]",
"[ 3/1 → 25/8 | s:bd cutoff:1000 resonance:0 ]",
"[ 25/8 → 13/4 | s:bd cutoff:1000 resonance:0 ]",
"[ 13/4 → 27/8 | s:bd cutoff:1000 resonance:10 ]",
"[ 27/8 → 7/2 | s:bd cutoff:1000 resonance:10 ]",
"[ 7/2 → 29/8 | s:bd cutoff:1000 resonance:20 ]",
"[ 29/8 → 15/4 | s:bd cutoff:1000 resonance:20 ]",
"[ 15/4 → 31/8 | s:bd cutoff:1000 resonance:30 ]",
"[ 31/8 → 4/1 | s:bd cutoff:1000 resonance:30 ]",
]
`;
exports[`runs examples > example "lpq" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd cutoff:2000 resonance:0 ]",
"[ 1/2 → 1/1 | s:sd cutoff:2000 resonance:0 ]",
"[ 0/1 → 1/4 | s:hh cutoff:2000 resonance:0 ]",
"[ 1/4 → 1/2 | s:hh cutoff:2000 resonance:0 ]",
"[ 1/2 → 3/4 | s:hh cutoff:2000 resonance:0 ]",
"[ 3/4 → 1/1 | s:hh cutoff:2000 resonance:0 ]",
"[ 1/1 → 3/2 | s:bd cutoff:2000 resonance:10 ]",
"[ 3/2 → 2/1 | s:sd cutoff:2000 resonance:10 ]",
"[ 1/1 → 5/4 | s:hh cutoff:2000 resonance:10 ]",
"[ 5/4 → 3/2 | s:hh cutoff:2000 resonance:10 ]",
"[ 3/2 → 7/4 | s:hh cutoff:2000 resonance:10 ]",
"[ 7/4 → 2/1 | s:hh cutoff:2000 resonance:10 ]",
"[ 2/1 → 5/2 | s:bd cutoff:2000 resonance:20 ]",
"[ 5/2 → 3/1 | s:sd cutoff:2000 resonance:20 ]",
"[ 2/1 → 9/4 | s:hh cutoff:2000 resonance:20 ]",
"[ 9/4 → 5/2 | s:hh cutoff:2000 resonance:20 ]",
"[ 5/2 → 11/4 | s:hh cutoff:2000 resonance:20 ]",
"[ 11/4 → 3/1 | s:hh cutoff:2000 resonance:20 ]",
"[ 3/1 → 7/2 | s:bd cutoff:2000 resonance:30 ]",
"[ 7/2 → 4/1 | s:sd cutoff:2000 resonance:30 ]",
"[ 3/1 → 13/4 | s:hh cutoff:2000 resonance:30 ]",
"[ 13/4 → 7/2 | s:hh cutoff:2000 resonance:30 ]",
"[ 7/2 → 15/4 | s:hh cutoff:2000 resonance:30 ]",
"[ 15/4 → 4/1 | s:hh cutoff:2000 resonance:30 ]",
]
`;
exports[`runs examples > example "lrate" example index 0 1`] = `
[
"[ 0/1 → 1/1 | n:0 s:supersquare leslie:1 lrate:1 ]",
@@ -2456,40 +2602,26 @@ exports[`runs examples > example "mul" example index 0 1`] = `
exports[`runs examples > example "n" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:superpiano n:0 ]",
"[ 1/12/1 | s:superpiano n:1 ]",
"[ 2/1 → 3/1 | s:superpiano n:2 ]",
"[ 3/1 → 4/1 | s:superpiano n:3 ]",
]
`;
exports[`runs examples > example "n" example index 1 1`] = `
[
"[ 0/11/1 | s:superpiano n:c4 ]",
"[ 1/1 → 2/1 | s:superpiano n:d4 ]",
"[ 2/1 → 3/1 | s:superpiano n:e4 ]",
"[ 3/1 → 4/1 | s:superpiano n:g4 ]",
]
`;
exports[`runs examples > example "n" example index 2 1`] = `
[
"[ 0/1 → 1/4 | n:0 s:east ]",
"[ 1/4 → 1/2 | n:1 s:east ]",
"[ 1/2 → 3/4 | n:2 s:east ]",
"[ 3/4 → 1/1 | n:3 s:east ]",
"[ 1/1 → 5/4 | n:0 s:east ]",
"[ 5/4 → 3/2 | n:1 s:east ]",
"[ 3/2 → 7/4 | n:2 s:east ]",
"[ 7/4 → 2/1 | n:3 s:east ]",
"[ 2/1 → 9/4 | n:0 s:east ]",
"[ 9/4 → 5/2 | n:1 s:east ]",
"[ 5/2 → 11/4 | n:2 s:east ]",
"[ 11/4 → 3/1 | n:3 s:east ]",
"[ 3/1 → 13/4 | n:0 s:east ]",
"[ 13/4 → 7/2 | n:1 s:east ]",
"[ 7/2 → 15/4 | n:2 s:east ]",
"[ 15/4 → 4/1 | n:3 s:east ]",
"[ 0/1 → 1/2 | s:bd n:0 ]",
"[ 1/21/1 | s:sd n:0 ]",
"[ 0/1 → 1/3 | s:hh n:0 ]",
"[ 1/3 → 2/3 | s:hh n:0 ]",
"[ 2/3 → 1/1 | s:hh n:0 ]",
"[ 1/1 → 3/2 | s:bd n:1 ]",
"[ 3/2 → 2/1 | s:sd n:1 ]",
"[ 1/1 → 4/3 | s:hh n:1 ]",
"[ 4/3 → 5/3 | s:hh n:1 ]",
"[ 5/32/1 | s:hh n:1 ]",
"[ 2/1 → 5/2 | s:bd n:0 ]",
"[ 5/2 → 3/1 | s:sd n:0 ]",
"[ 2/1 → 7/3 | s:hh n:0 ]",
"[ 7/3 → 8/3 | s:hh n:0 ]",
"[ 8/3 → 3/1 | s:hh n:0 ]",
"[ 3/1 → 7/2 | s:bd n:1 ]",
"[ 7/2 → 4/1 | s:sd n:1 ]",
"[ 3/1 → 10/3 | s:hh n:1 ]",
"[ 10/3 → 11/3 | s:hh n:1 ]",
"[ 11/3 → 4/1 | s:hh n:1 ]",
]
`;
@@ -2530,6 +2662,69 @@ exports[`runs examples > example "never" example index 0 1`] = `
]
`;
exports[`runs examples > example "note" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c ]",
"[ 1/4 → 1/2 | note:a ]",
"[ 1/2 → 3/4 | note:f ]",
"[ 3/4 → 1/1 | note:e ]",
"[ 1/1 → 5/4 | note:c ]",
"[ 5/4 → 3/2 | note:a ]",
"[ 3/2 → 7/4 | note:f ]",
"[ 7/4 → 2/1 | note:e ]",
"[ 2/1 → 9/4 | note:c ]",
"[ 9/4 → 5/2 | note:a ]",
"[ 5/2 → 11/4 | note:f ]",
"[ 11/4 → 3/1 | note:e ]",
"[ 3/1 → 13/4 | note:c ]",
"[ 13/4 → 7/2 | note:a ]",
"[ 7/2 → 15/4 | note:f ]",
"[ 15/4 → 4/1 | note:e ]",
]
`;
exports[`runs examples > example "note" example index 1 1`] = `
[
"[ 0/1 → 1/4 | note:c4 ]",
"[ 1/4 → 1/2 | note:a4 ]",
"[ 1/2 → 3/4 | note:f4 ]",
"[ 3/4 → 1/1 | note:e4 ]",
"[ 1/1 → 5/4 | note:c4 ]",
"[ 5/4 → 3/2 | note:a4 ]",
"[ 3/2 → 7/4 | note:f4 ]",
"[ 7/4 → 2/1 | note:e4 ]",
"[ 2/1 → 9/4 | note:c4 ]",
"[ 9/4 → 5/2 | note:a4 ]",
"[ 5/2 → 11/4 | note:f4 ]",
"[ 11/4 → 3/1 | note:e4 ]",
"[ 3/1 → 13/4 | note:c4 ]",
"[ 13/4 → 7/2 | note:a4 ]",
"[ 7/2 → 15/4 | note:f4 ]",
"[ 15/4 → 4/1 | note:e4 ]",
]
`;
exports[`runs examples > example "note" example index 2 1`] = `
[
"[ 0/1 → 1/4 | note:60 ]",
"[ 1/4 → 1/2 | note:69 ]",
"[ 1/2 → 3/4 | note:65 ]",
"[ 3/4 → 1/1 | note:64 ]",
"[ 1/1 → 5/4 | note:60 ]",
"[ 5/4 → 3/2 | note:69 ]",
"[ 3/2 → 7/4 | note:65 ]",
"[ 7/4 → 2/1 | note:64 ]",
"[ 2/1 → 9/4 | note:60 ]",
"[ 9/4 → 5/2 | note:69 ]",
"[ 5/2 → 11/4 | note:65 ]",
"[ 11/4 → 3/1 | note:64 ]",
"[ 3/1 → 13/4 | note:60 ]",
"[ 13/4 → 7/2 | note:69 ]",
"[ 7/2 → 15/4 | note:65 ]",
"[ 15/4 → 4/1 | note:64 ]",
]
`;
exports[`runs examples > example "octave" example index 0 1`] = `
[
"[ 0/1 → 1/1 | n:0 s:supersquare octave:3 ]",
@@ -3048,35 +3243,6 @@ exports[`runs examples > example "reset" example index 0 1`] = `
]
`;
exports[`runs examples > example "resonance" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd cutoff:2000 resonance:0 ]",
"[ 1/2 → 1/1 | s:sd cutoff:2000 resonance:0 ]",
"[ 0/1 → 1/4 | s:hh cutoff:2000 resonance:0 ]",
"[ 1/4 → 1/2 | s:hh cutoff:2000 resonance:0 ]",
"[ 1/2 → 3/4 | s:hh cutoff:2000 resonance:0 ]",
"[ 3/4 → 1/1 | s:hh cutoff:2000 resonance:0 ]",
"[ 1/1 → 3/2 | s:bd cutoff:2000 resonance:10 ]",
"[ 3/2 → 2/1 | s:sd cutoff:2000 resonance:10 ]",
"[ 1/1 → 5/4 | s:hh cutoff:2000 resonance:10 ]",
"[ 5/4 → 3/2 | s:hh cutoff:2000 resonance:10 ]",
"[ 3/2 → 7/4 | s:hh cutoff:2000 resonance:10 ]",
"[ 7/4 → 2/1 | s:hh cutoff:2000 resonance:10 ]",
"[ 2/1 → 5/2 | s:bd cutoff:2000 resonance:20 ]",
"[ 5/2 → 3/1 | s:sd cutoff:2000 resonance:20 ]",
"[ 2/1 → 9/4 | s:hh cutoff:2000 resonance:20 ]",
"[ 9/4 → 5/2 | s:hh cutoff:2000 resonance:20 ]",
"[ 5/2 → 11/4 | s:hh cutoff:2000 resonance:20 ]",
"[ 11/4 → 3/1 | s:hh cutoff:2000 resonance:20 ]",
"[ 3/1 → 7/2 | s:bd cutoff:2000 resonance:30 ]",
"[ 7/2 → 4/1 | s:sd cutoff:2000 resonance:30 ]",
"[ 3/1 → 13/4 | s:hh cutoff:2000 resonance:30 ]",
"[ 13/4 → 7/2 | s:hh cutoff:2000 resonance:30 ]",
"[ 7/2 → 15/4 | s:hh cutoff:2000 resonance:30 ]",
"[ 15/4 → 4/1 | s:hh cutoff:2000 resonance:30 ]",
]
`;
exports[`runs examples > example "restart" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd ]",
@@ -3161,16 +3327,29 @@ exports[`runs examples > example "room" example index 0 1`] = `
]
`;
exports[`runs examples > example "room" example index 1 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.9 size:1 ]",
"[ 1/2 → 1/1 | s:sd room:0.9 size:1 ]",
"[ 1/1 → 3/2 | s:bd room:0.9 size:4 ]",
"[ 3/2 → 2/1 | s:sd room:0.9 size:4 ]",
"[ 2/1 → 5/2 | s:bd room:0.9 size:1 ]",
"[ 5/2 → 3/1 | s:sd room:0.9 size:1 ]",
"[ 3/1 → 7/2 | s:bd room:0.9 size:4 ]",
"[ 7/2 → 4/1 | s:sd room:0.9 size:4 ]",
]
`;
exports[`runs examples > example "roomsize" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.8 roomsize:0 ]",
"[ 1/2 → 1/1 | s:sd room:0.8 roomsize:0 ]",
"[ 1/1 → 3/2 | s:bd room:0.8 roomsize:1 ]",
"[ 3/2 → 2/1 | s:sd room:0.8 roomsize:1 ]",
"[ 2/1 → 5/2 | s:bd room:0.8 roomsize:2 ]",
"[ 5/2 → 3/1 | s:sd room:0.8 roomsize:2 ]",
"[ 3/1 → 7/2 | s:bd room:0.8 roomsize:4 ]",
"[ 7/2 → 4/1 | s:sd room:0.8 roomsize:4 ]",
"[ 0/1 → 1/2 | s:bd room:0.8 size:0 ]",
"[ 1/2 → 1/1 | s:sd room:0.8 size:0 ]",
"[ 1/1 → 3/2 | s:bd room:0.8 size:1 ]",
"[ 3/2 → 2/1 | s:sd room:0.8 size:1 ]",
"[ 2/1 → 5/2 | s:bd room:0.8 size:2 ]",
"[ 5/2 → 3/1 | s:sd room:0.8 size:2 ]",
"[ 3/1 → 7/2 | s:bd room:0.8 size:4 ]",
"[ 7/2 → 4/1 | s:sd room:0.8 size:4 ]",
]
`;
@@ -3234,6 +3413,27 @@ exports[`runs examples > example "s" example index 0 1`] = `
]
`;
exports[`runs examples > example "s" example index 1 1`] = `
[
"[ 0/1 → 1/4 | s:bd n:0 ]",
"[ 1/4 → 1/2 | s:bd n:1 ]",
"[ 1/2 → 3/4 | s:bd n:0 gain:0.3 ]",
"[ 3/4 → 1/1 | s:bd n:1 gain:1.4 ]",
"[ 1/1 → 5/4 | s:bd n:0 ]",
"[ 5/4 → 3/2 | s:bd n:1 ]",
"[ 3/2 → 7/4 | s:bd n:0 gain:0.3 ]",
"[ 7/4 → 2/1 | s:bd n:1 gain:1.4 ]",
"[ 2/1 → 9/4 | s:bd n:0 ]",
"[ 9/4 → 5/2 | s:bd n:1 ]",
"[ 5/2 → 11/4 | s:bd n:0 gain:0.3 ]",
"[ 11/4 → 3/1 | s:bd n:1 gain:1.4 ]",
"[ 3/1 → 13/4 | s:bd n:0 ]",
"[ 13/4 → 7/2 | s:bd n:1 ]",
"[ 7/2 → 15/4 | s:bd n:0 gain:0.3 ]",
"[ 15/4 → 4/1 | s:bd n:1 gain:1.4 ]",
]
`;
exports[`runs examples > example "samples" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
@@ -3259,6 +3459,31 @@ exports[`runs examples > example "samples" example index 0 1`] = `
]
`;
exports[`runs examples > example "samples" example index 1 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
"[ 1/2 → 3/4 | s:bd ]",
"[ 1/4 → 1/2 | s:hh ]",
"[ 3/4 → 1/1 | s:hh ]",
"[ 1/2 → 1/1 | s:sd ]",
"[ 1/1 → 5/4 | s:bd ]",
"[ 3/2 → 7/4 | s:bd ]",
"[ 5/4 → 3/2 | s:hh ]",
"[ 7/4 → 2/1 | s:hh ]",
"[ 3/2 → 2/1 | s:sd ]",
"[ 2/1 → 9/4 | s:bd ]",
"[ 5/2 → 11/4 | s:bd ]",
"[ 9/4 → 5/2 | s:hh ]",
"[ 11/4 → 3/1 | s:hh ]",
"[ 5/2 → 3/1 | s:sd ]",
"[ 3/1 → 13/4 | s:bd ]",
"[ 7/2 → 15/4 | s:bd ]",
"[ 13/4 → 7/2 | s:hh ]",
"[ 15/4 → 4/1 | s:hh ]",
"[ 7/2 → 4/1 | s:sd ]",
]
`;
exports[`runs examples > example "saw" example index 0 1`] = `
[
"[ 0/1 → 1/32 | note:c3 ]",
@@ -3302,6 +3527,35 @@ exports[`runs examples > example "saw" example index 1 1`] = `
`;
exports[`runs examples > example "scale" example index 0 1`] = `
[
"[ 0/1 → 1/6 | note:C2 ]",
"[ 1/6 → 1/3 | note:E2 ]",
"[ 1/3 → 1/2 | note:G2 ]",
"[ 1/2 → 2/3 | note:B2 ]",
"[ 2/3 → 5/6 | note:G2 ]",
"[ 5/6 → 1/1 | note:E2 ]",
"[ 1/1 → 7/6 | note:C2 ]",
"[ 7/6 → 4/3 | note:E2 ]",
"[ 4/3 → 3/2 | note:G2 ]",
"[ 3/2 → 5/3 | note:B2 ]",
"[ 5/3 → 11/6 | note:G2 ]",
"[ 11/6 → 2/1 | note:E2 ]",
"[ 2/1 → 13/6 | note:C2 ]",
"[ 13/6 → 7/3 | note:E2 ]",
"[ 7/3 → 5/2 | note:G2 ]",
"[ 5/2 → 8/3 | note:B2 ]",
"[ 8/3 → 17/6 | note:G2 ]",
"[ 17/6 → 3/1 | note:E2 ]",
"[ 3/1 → 19/6 | note:C2 ]",
"[ 19/6 → 10/3 | note:E2 ]",
"[ 10/3 → 7/2 | note:G2 ]",
"[ 7/2 → 11/3 | note:B2 ]",
"[ 11/3 → 23/6 | note:G2 ]",
"[ 23/6 → 4/1 | note:E2 ]",
]
`;
exports[`runs examples > example "scale" example index 1 1`] = `
[
"[ 0/1 → 1/6 | note:C2 ]",
"[ 1/6 → 1/3 | note:E2 ]",
@@ -3330,6 +3584,43 @@ exports[`runs examples > example "scale" example index 0 1`] = `
]
`;
exports[`runs examples > example "scale" example index 2 1`] = `
[
"[ 7/8 → 1/1 | note:C2 s:folkharp ]",
"[ 3/4 → 7/8 | note:D2 s:folkharp ]",
"[ 5/8 → 3/4 | note:E2 s:folkharp ]",
"[ 1/2 → 5/8 | note:F2 s:folkharp ]",
"[ 3/8 → 1/2 | note:G2 s:folkharp ]",
"[ 1/4 → 3/8 | note:A2 s:folkharp ]",
"[ 1/8 → 1/4 | note:B2 s:folkharp ]",
"[ 0/1 → 1/8 | note:C3 s:folkharp ]",
"[ 15/8 → 2/1 | note:C2 s:folkharp ]",
"[ 7/4 → 15/8 | note:D2 s:folkharp ]",
"[ 13/8 → 7/4 | note:Eb2 s:folkharp ]",
"[ 3/2 → 13/8 | note:F2 s:folkharp ]",
"[ 11/8 → 3/2 | note:G2 s:folkharp ]",
"[ 5/4 → 11/8 | note:Ab2 s:folkharp ]",
"[ 9/8 → 5/4 | note:Bb2 s:folkharp ]",
"[ 1/1 → 9/8 | note:C3 s:folkharp ]",
"[ 23/8 → 3/1 | note:C2 s:folkharp ]",
"[ 11/4 → 23/8 | note:D2 s:folkharp ]",
"[ 21/8 → 11/4 | note:E2 s:folkharp ]",
"[ 5/2 → 21/8 | note:F2 s:folkharp ]",
"[ 19/8 → 5/2 | note:G2 s:folkharp ]",
"[ 9/4 → 19/8 | note:A2 s:folkharp ]",
"[ 17/8 → 9/4 | note:B2 s:folkharp ]",
"[ 2/1 → 17/8 | note:C3 s:folkharp ]",
"[ 31/8 → 4/1 | note:C2 s:folkharp ]",
"[ 15/4 → 31/8 | note:D2 s:folkharp ]",
"[ 29/8 → 15/4 | note:Eb2 s:folkharp ]",
"[ 7/2 → 29/8 | note:F2 s:folkharp ]",
"[ 27/8 → 7/2 | note:G2 s:folkharp ]",
"[ 13/4 → 27/8 | note:Ab2 s:folkharp ]",
"[ 25/8 → 13/4 | note:Bb2 s:folkharp ]",
"[ 3/1 → 25/8 | note:C3 s:folkharp ]",
]
`;
exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:C3 ]",
+26 -26
View File
@@ -298,11 +298,11 @@ exports[`renders tunes > tune: bassFuge 1`] = `
"[ -3/4 ⇜ (0/1 → 3/4) ⇝ 5/4 | note:C5 s:flbass n:0 gain:0.3 cutoff:2924.3791043233605 resonance:10 clip:1 ]",
"[ -3/4 ⇜ (3/4 → 1/1) ⇝ 5/4 | note:A4 s:flbass n:0 gain:0.3 cutoff:2924.3791043233605 resonance:10 clip:1 ]",
"[ -3/4 ⇜ (3/4 → 1/1) ⇝ 5/4 | note:C5 s:flbass n:0 gain:0.3 cutoff:2924.3791043233605 resonance:10 clip:1 ]",
"[ 0/1 → 1/2 | s:bd:1 ]",
"[ 1/2 → 1/1 | s:bd:1 ]",
"[ 1/2 → 1/1 | s:sd:0 ]",
"[ 1/4 → 1/2 | s:hh:0 ]",
"[ 3/4 → 1/1 | s:hh:0 ]",
"[ 0/1 → 1/2 | s:bd n:1 ]",
"[ 1/2 → 1/1 | s:bd n:1 ]",
"[ 1/2 → 1/1 | s:sd n:0 ]",
"[ 1/4 → 1/2 | s:hh n:0 ]",
"[ 3/4 → 1/1 | s:hh n:0 ]",
]
`;
@@ -313,7 +313,7 @@ exports[`renders tunes > tune: belldub 1`] = `
"[ (5/8 → 1/1) ⇝ 5/4 | s:hh room:0 end:0.04483079938329212 ]",
"[ 0/1 → 5/16 | s:mt gain:0.5 room:0.5 ]",
"[ (15/16 → 1/1) ⇝ 5/4 | s:lt gain:0.5 room:0.5 ]",
"[ (0/1 → 1/1) ⇝ 5/1 | s:misc:2 speed:1 delay:0.5 delaytime:0.3333333333333333 gain:0.4 ]",
"[ (0/1 → 1/1) ⇝ 5/1 | s:misc n:2 speed:1 delay:0.5 delaytime:0.3333333333333333 gain:0.4 ]",
"[ (5/8 → 1/1) ⇝ 5/4 | note:F3 s:sawtooth gain:0.5 cutoff:400.16785462816676 decay:0.05380063255866716 sustain:0 delay:0.9 room:1 ]",
"[ (5/8 → 1/1) ⇝ 5/4 | note:A3 s:sawtooth gain:0.5 cutoff:400.16785462816676 decay:0.05380063255866716 sustain:0 delay:0.9 room:1 ]",
"[ (5/8 → 1/1) ⇝ 5/4 | note:Bb3 s:sawtooth gain:0.5 cutoff:400.16785462816676 decay:0.05380063255866716 sustain:0 delay:0.9 room:1 ]",
@@ -6974,16 +6974,16 @@ exports[`renders tunes > tune: flatrave 1`] = `
"[ 1/2 → 1/1 | s:bd bank:RolandTR909 ]",
"[ 1/2 → 1/1 | s:cp bank:RolandTR909 ]",
"[ 1/2 → 1/1 | s:sd bank:RolandTR909 ]",
"[ 0/1 → 1/4 | s:hh:1 end:0.02000058072071123 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 0/1 ⇜ (1/8 → 1/4) | s:hh:1 end:0.02000058072071123 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/4 → 3/8 | s:hh:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/4 → 3/8 | s:hh:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 3/8 → 1/2 | s:hh:1 end:0.020023446730265706 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 5/8 → 3/4 | s:hh:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 5/8 → 3/4 | s:hh:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 3/4 → 7/8 | s:hh:1 end:0.02013941880355398 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | s:hh:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | s:hh:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 0/1 → 1/4 | s:hh n:1 end:0.02000058072071123 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 0/1 ⇜ (1/8 → 1/4) | s:hh n:1 end:0.02000058072071123 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/4 → 3/8 | s:hh n:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/4 → 3/8 | s:hh n:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 3/8 → 1/2 | s:hh n:1 end:0.020023446730265706 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 5/8 → 3/4 | s:hh n:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 5/8 → 3/4 | s:hh n:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 3/4 → 7/8 | s:hh n:1 end:0.02013941880355398 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | s:hh n:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | s:hh n:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | note:G1 s:sawtooth decay:0.1 sustain:0 ]",
"[ 1/4 → 3/8 | note:G1 s:sawtooth decay:0.1 sustain:0 ]",
"[ 1/2 → 5/8 | note:G1 s:sawtooth decay:0.1 sustain:0 ]",
@@ -8127,8 +8127,8 @@ exports[`renders tunes > tune: loungeSponge 1`] = `
exports[`renders tunes > tune: meltingsubmarine 1`] = `
[
"[ (0/1 → 1/1) ⇝ 3/2 | s:bd:5 speed:0.7519542165100574 ]",
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd:1 speed:0.7931522866332671 ]",
"[ (0/1 → 1/1) ⇝ 3/2 | s:bd n:5 speed:0.7519542165100574 ]",
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd n:1 speed:0.7931522866332671 ]",
"[ 3/8 → 3/4 | s:hh27 speed:0.7285963821098448 ]",
"[ (3/4 → 1/1) ⇝ 9/8 | s:hh27 speed:0.77531205091027 ]",
"[ (0/1 → 1/1) ⇝ 3/2 | note:33.129885541275144 decay:0.15 sustain:0 s:sawtooth gain:0.4 cutoff:3669.6267869262615 ]",
@@ -8364,15 +8364,15 @@ exports[`renders tunes > tune: randomBells 1`] = `
exports[`renders tunes > tune: sampleDemo 1`] = `
[
"[ 0/1 → 1/4 | s:woodblock:1 ]",
"[ 1/4 → 3/8 | s:woodblock:2 ]",
"[ 0/1 → 1/8 | s:brakedrum:1 ]",
"[ 3/4 → 7/8 | s:brakedrum:1 ]",
"[ 3/8 → 1/2 | s:woodblock:2 speed:2 ]",
"[ 1/2 → 1/1 | s:snare_rim:0 speed:2 ]",
"[ 0/1 → 1/4 | s:woodblock n:1 ]",
"[ 1/4 → 3/8 | s:woodblock n:2 ]",
"[ 0/1 → 1/8 | s:brakedrum n:1 ]",
"[ 3/4 → 7/8 | s:brakedrum n:1 ]",
"[ 3/8 → 1/2 | s:woodblock n:2 speed:2 ]",
"[ 1/2 → 1/1 | s:snare_rim n:0 speed:2 ]",
"[ (0/1 → 1/1) ⇝ 8/1 | s:gong speed:2 ]",
"[ 3/8 → 1/2 | s:brakedrum:1 speed:2 ]",
"[ 3/4 → 1/1 | s:cowbell:3 speed:2 ]",
"[ 3/8 → 1/2 | s:brakedrum n:1 speed:2 ]",
"[ 3/4 → 1/1 | s:cowbell n:3 speed:2 ]",
"[ -3/4 ⇜ (0/1 → 1/4) | note:Bb3 s:clavisynth gain:0.2 delay:0.25 pan:0 ]",
"[ (3/4 → 1/1) ⇝ 7/4 | note:Bb3 s:clavisynth gain:0.2 delay:0.25 pan:1 ]",
"[ -1/4 ⇜ (0/1 → 3/4) | note:F3 s:clavisynth gain:0.2 delay:0.25 pan:1 ]",
+7
View File
@@ -19,6 +19,13 @@ npm run build # <- builds repl + tutorial to ../docs
npm run preview # <- test static build
```
## Generate PWA icons
```sh
cd website/public
npx pwa-asset-generator icon.png icons
```
# Standard Readme of Astro Starter Kit: Docs Site
```bash
+94 -14
View File
@@ -1,23 +1,47 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
import react from '@astrojs/react';
import mdx from '@astrojs/mdx';
import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeUrls from 'rehype-urls';
import tailwind from '@astrojs/tailwind';
import AstroPWA from '@vite-pwa/astro';
// import { visualizer } from 'rollup-plugin-visualizer';
const site = `https://strudel.tidalcycles.org`; // root url without a path
const base = '/'; // base path of the strudel site
// this rehype plugin converts relative anchor links to absolute ones
// it wokrs by prepending the absolute page path to anchor links
// example: #gain -> /learn/effects/#gain
// this is necessary when using a base href like <base href={base} />
// in this setup, relative anchor links will always link to base, instead of the current page
function absoluteAnchors() {
return (tree, file) => {
const chunks = file.history[0].split('/src/pages/'); // file.history[0] is the file path
const path = chunks[chunks.length - 1].slice(0, -4); // only path inside src/pages, without .mdx
return rehypeUrls((url) => {
if (!url.href.startsWith('#')) {
return;
}
const baseWithSlash = base.endsWith('/') ? base : base + '/';
const absoluteUrl = baseWithSlash + path + url.href;
// console.log(url.href + ' -> ', absoluteUrl);
return absoluteUrl;
})(tree);
};
}
const options = {
// See https://mdxjs.com/advanced/plugins
remarkPlugins: [
remarkToc,
// E.g. `remark-frontmatter`
],
rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings],
rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'append' }], absoluteAnchors],
};
// https://astro.build/config
@@ -29,9 +53,75 @@ export default defineConfig({
react(),
mdx(options),
tailwind(),
AstroPWA({
registerType: 'autoUpdate',
injectRegister: 'auto',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,json,wav,mp3,ogg}'],
runtimeCaching: [
{
urlPattern: ({ url }) =>
[
/^https:\/\/raw\.githubusercontent\.com\/.*/i,
/^https:\/\/freesound\.org\/.*/i,
/^https:\/\/cdn\.freesound\.org\/.*/i,
/^https:\/\/shabda\.ndre\.gr\/.*/i,
].some((regex) => regex.test(url)),
handler: 'CacheFirst',
options: {
cacheName: 'external-samples',
expiration: {
maxEntries: 5000,
maxAgeSeconds: 60 * 60 * 24 * 30, // <== 14 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
],
},
devOptions: {
enabled: false,
},
manifest: {
includeAssets: ['favicon.ico', 'icons/apple-icon-180.png', 'favicon.svg'],
name: 'Strudel REPL',
short_name: 'Strudel',
description:
'Strudel is a music live coding environment for the browser, porting the TidalCycles pattern language to JavaScript.',
theme_color: '#222222',
icons: [
{
src: 'icons/manifest-icon-192.maskable.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any',
},
{
src: 'icons/manifest-icon-192.maskable.png',
sizes: '192x192',
type: 'image/png',
purpose: 'maskable',
},
{
src: 'icons/manifest-icon-512.maskable.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any',
},
{
src: 'icons/manifest-icon-512.maskable.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
],
},
}),
],
site: `https://strudel.tidalcycles.org`,
base: '/',
site,
base,
vite: {
ssr: {
// Example: Force a broken package to skip SSR processing, if needed
@@ -39,13 +129,3 @@ export default defineConfig({
},
},
});
/*
build: {
outDir: '../out',
sourcemap: true,
rollupOptions: {
plugins: [visualizer({ template: 'treemap' })],
},
},
*/
+12 -3
View File
@@ -21,6 +21,8 @@
"@docsearch/react": "^3.1.0",
"@headlessui/react": "^1.7.7",
"@heroicons/react": "^2.0.13",
"@nanostores/persistent": "^0.7.0",
"@nanostores/react": "^0.4.1",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/csound": "workspace:*",
"@strudel.cycles/midi": "workspace:*",
@@ -34,23 +36,30 @@
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/xen": "workspace:*",
"@supabase/supabase-js": "^1.35.3",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/node": "^18.0.0",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@uiw/codemirror-themes-all": "^4.19.8",
"astro": "^1.7.2",
"canvas": "^2.11.0",
"fraction.js": "^4.2.0",
"nanoid": "^4.0.0",
"nanostores": "^0.7.4",
"preact": "^10.7.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1",
"rehype-urls": "^1.1.1",
"remark-toc": "^8.0.1",
"tailwindcss": "^3.2.4",
"canvas": "^2.11.0"
"tailwindcss": "^3.2.4"
},
"devDependencies": {
"html-escaper": "^3.0.3"
"@vite-pwa/astro": "^0.0.1",
"html-escaper": "^3.0.3",
"vite-plugin-pwa": "^0.14.1",
"workbox-window": "^6.5.4"
}
}
+326
View File
@@ -0,0 +1,326 @@
<svg height="100%" viewBox="0.00 0.00 538.13 507.20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet">
<defs><pattern id="warning" width="12" height="12" patternUnits="userSpaceOnUse" patternTransform="rotate(45 50 50)">
<line class="line0" stroke-width="6px" x1="3" x2="3" y2="12"></line>
<line class="line1" stroke-width="6px" x1="9" x2="9" y2="12"></line>
</pattern></defs><g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 503.2)">
<!-- @strudel.cycles/core@0.6.8 -->
<g id="node1" class="node module-_strudel_cycles_core maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/core@0.6.8">
<title>@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M174.8,-37.3C174.8,-37.3 56.9,-37.3 56.9,-37.3 53.66,-37.3 50.43,-34.07 50.43,-30.83 50.43,-30.83 50.43,-24.37 50.43,-24.37 50.43,-21.13 53.66,-17.9 56.9,-17.9 56.9,-17.9 174.8,-17.9 174.8,-17.9 178.03,-17.9 181.26,-21.13 181.26,-24.37 181.26,-24.37 181.26,-30.83 181.26,-30.83 181.26,-34.07 178.03,-37.3 174.8,-37.3"></path>
<text text-anchor="middle" x="115.85" y="-24.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/core@0.6.8</text>
</g>
<!-- fraction.js@4.2.0 -->
<g id="node21" class="node module-fraction_js maintainer-infusion license-mit" data-module="fraction.js@4.2.0">
<title>fraction.js@4.2.0</title>
<path fill="none" stroke="black" d="M343.81,-19.3C343.81,-19.3 273.9,-19.3 273.9,-19.3 270.67,-19.3 267.44,-16.07 267.44,-12.83 267.44,-12.83 267.44,-6.37 267.44,-6.37 267.44,-3.13 270.67,0.1 273.9,0.1 273.9,0.1 343.81,0.1 343.81,0.1 347.05,0.1 350.28,-3.13 350.28,-6.37 350.28,-6.37 350.28,-12.83 350.28,-12.83 350.28,-16.07 347.05,-19.3 343.81,-19.3"></path>
<text text-anchor="middle" x="308.86" y="-6.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">fraction.js@4.2.0</text>
</g>
<!-- @strudel.cycles/core@0.6.8&#45;&gt;fraction.js@4.2.0 -->
<!-- @strudel.cycles/csound@0.6.2 -->
<g id="node2" class="node module-_strudel_cycles_csound maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/csound@0.6.2">
<title>@strudel.cycles/csound@0.6.2</title>
<path fill="none" stroke="black" d="M180.53,-296.3C180.53,-296.3 51.16,-296.3 51.16,-296.3 47.93,-296.3 44.7,-293.07 44.7,-289.83 44.7,-289.83 44.7,-283.37 44.7,-283.37 44.7,-280.13 47.93,-276.9 51.16,-276.9 51.16,-276.9 180.53,-276.9 180.53,-276.9 183.77,-276.9 187,-280.13 187,-283.37 187,-283.37 187,-289.83 187,-289.83 187,-293.07 183.77,-296.3 180.53,-296.3"></path>
<text text-anchor="middle" x="115.85" y="-283.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/csound@0.6.2</text>
</g>
<!-- @strudel.cycles/csound@0.6.2&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @strudel.cycles/webaudio@0.6.1 -->
<g id="node12" class="node module-_strudel_cycles_webaudio maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/webaudio@0.6.1">
<title>@strudel.cycles/webaudio@0.6.1</title>
<path fill="none" stroke="black" d="M186.63,-259.3C186.63,-259.3 45.06,-259.3 45.06,-259.3 41.83,-259.3 38.59,-256.07 38.59,-252.83 38.59,-252.83 38.59,-246.37 38.59,-246.37 38.59,-243.13 41.83,-239.9 45.06,-239.9 45.06,-239.9 186.63,-239.9 186.63,-239.9 189.87,-239.9 193.1,-243.13 193.1,-246.37 193.1,-246.37 193.1,-252.83 193.1,-252.83 193.1,-256.07 189.87,-259.3 186.63,-259.3"></path>
<text text-anchor="middle" x="115.85" y="-246.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/webaudio@0.6.1</text>
</g>
<!-- @strudel.cycles/csound@0.6.2&#45;&gt;@strudel.cycles/webaudio@0.6.1 -->
<!-- @csound/browser@6.18.5 -->
<g id="node14" class="node module-_csound_browser collapsed maintainer-skyne maintainer-kunstmusik maintainer-eddyc maintainer-hlolli license-lgpl_2_1" data-module="@csound/browser@6.18.5">
<title>@csound/browser@6.18.5</title>
<path fill="none" stroke="black" d="M364.3,-257.3C364.3,-257.3 253.41,-257.3 253.41,-257.3 250.18,-257.3 246.95,-254.07 246.95,-250.83 246.95,-250.83 246.95,-244.37 246.95,-244.37 246.95,-241.13 250.18,-237.9 253.41,-237.9 253.41,-237.9 364.3,-237.9 364.3,-237.9 367.54,-237.9 370.77,-241.13 370.77,-244.37 370.77,-244.37 370.77,-250.83 370.77,-250.83 370.77,-254.07 367.54,-257.3 364.3,-257.3"></path>
<text text-anchor="middle" x="308.86" y="-244.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@csound/browser@6.18.5</text>
</g>
<!-- @strudel.cycles/csound@0.6.2&#45;&gt;@csound/browser@6.18.5 -->
<!-- @strudel.cycles/embed@0.2.0 -->
<g id="node3" class="node module-_strudel_cycles_embed maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/embed@0.2.0">
<title>@strudel.cycles/embed@0.2.0</title>
<path fill="none" stroke="black" d="M179.69,-462.3C179.69,-462.3 52.01,-462.3 52.01,-462.3 48.77,-462.3 45.54,-459.07 45.54,-455.83 45.54,-455.83 45.54,-449.37 45.54,-449.37 45.54,-446.13 48.77,-442.9 52.01,-442.9 52.01,-442.9 179.69,-442.9 179.69,-442.9 182.92,-442.9 186.16,-446.13 186.16,-449.37 186.16,-449.37 186.16,-455.83 186.16,-455.83 186.16,-459.07 182.92,-462.3 179.69,-462.3"></path>
<text text-anchor="middle" x="115.85" y="-449.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/embed@0.2.0</text>
</g>
<!-- @strudel.cycles/midi@0.6.0 -->
<g id="node4" class="node module-_strudel_cycles_midi maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/midi@0.6.0">
<title>@strudel.cycles/midi@0.6.0</title>
<path fill="none" stroke="black" d="M175.04,-370.3C175.04,-370.3 56.66,-370.3 56.66,-370.3 53.42,-370.3 50.19,-367.07 50.19,-363.83 50.19,-363.83 50.19,-357.37 50.19,-357.37 50.19,-354.13 53.42,-350.9 56.66,-350.9 56.66,-350.9 175.04,-350.9 175.04,-350.9 178.27,-350.9 181.5,-354.13 181.5,-357.37 181.5,-357.37 181.5,-363.83 181.5,-363.83 181.5,-367.07 178.27,-370.3 175.04,-370.3"></path>
<text text-anchor="middle" x="115.85" y="-357.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/midi@0.6.0</text>
</g>
<!-- @strudel.cycles/midi@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @strudel.cycles/webaudio@0.6.0 -->
<g id="node15" class="node module-_strudel_cycles_webaudio maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/webaudio@0.6.0">
<title>@strudel.cycles/webaudio@0.6.0</title>
<path fill="none" stroke="black" d="M379.64,-370.3C379.64,-370.3 238.07,-370.3 238.07,-370.3 234.84,-370.3 231.6,-367.07 231.6,-363.83 231.6,-363.83 231.6,-357.37 231.6,-357.37 231.6,-354.13 234.84,-350.9 238.07,-350.9 238.07,-350.9 379.64,-350.9 379.64,-350.9 382.88,-350.9 386.11,-354.13 386.11,-357.37 386.11,-357.37 386.11,-363.83 386.11,-363.83 386.11,-367.07 382.88,-370.3 379.64,-370.3"></path>
<text text-anchor="middle" x="308.86" y="-357.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/webaudio@0.6.0</text>
</g>
<!-- @strudel.cycles/midi@0.6.0&#45;&gt;@strudel.cycles/webaudio@0.6.0 -->
<!-- webmidi@3.1.4 -->
<g id="node25" class="node module-webmidi collapsed maintainer-djipco license-apache_2_0" data-module="webmidi@3.1.4">
<title>webmidi@3.1.4</title>
<path fill="none" stroke="black" d="M340.52,-407.3C340.52,-407.3 277.2,-407.3 277.2,-407.3 273.97,-407.3 270.73,-404.07 270.73,-400.83 270.73,-400.83 270.73,-394.37 270.73,-394.37 270.73,-391.13 273.97,-387.9 277.2,-387.9 277.2,-387.9 340.52,-387.9 340.52,-387.9 343.75,-387.9 346.98,-391.13 346.98,-394.37 346.98,-394.37 346.98,-400.83 346.98,-400.83 346.98,-404.07 343.75,-407.3 340.52,-407.3"></path>
<text text-anchor="middle" x="308.86" y="-394.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">webmidi@3.1.4</text>
</g>
<!-- @strudel.cycles/midi@0.6.0&#45;&gt;webmidi@3.1.4 -->
<!-- @strudel.cycles/mini@0.6.0 -->
<g id="node5" class="node module-_strudel_cycles_mini maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/mini@0.6.0">
<title>@strudel.cycles/mini@0.6.0</title>
<path fill="none" stroke="black" d="M175.04,-111.3C175.04,-111.3 56.66,-111.3 56.66,-111.3 53.42,-111.3 50.19,-108.07 50.19,-104.83 50.19,-104.83 50.19,-98.37 50.19,-98.37 50.19,-95.13 53.42,-91.9 56.66,-91.9 56.66,-91.9 175.04,-91.9 175.04,-91.9 178.27,-91.9 181.5,-95.13 181.5,-98.37 181.5,-98.37 181.5,-104.83 181.5,-104.83 181.5,-108.07 178.27,-111.3 175.04,-111.3"></path>
<text text-anchor="middle" x="115.85" y="-98.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/mini@0.6.0</text>
</g>
<!-- @strudel.cycles/mini@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @strudel.cycles/osc@0.6.0 -->
<g id="node6" class="node module-_strudel_cycles_osc maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/osc@0.6.0">
<title>@strudel.cycles/osc@0.6.0</title>
<path fill="none" stroke="black" d="M172.53,-222.3C172.53,-222.3 59.16,-222.3 59.16,-222.3 55.93,-222.3 52.7,-219.07 52.7,-215.83 52.7,-215.83 52.7,-209.37 52.7,-209.37 52.7,-206.13 55.93,-202.9 59.16,-202.9 59.16,-202.9 172.53,-202.9 172.53,-202.9 175.76,-202.9 179,-206.13 179,-209.37 179,-209.37 179,-215.83 179,-215.83 179,-219.07 175.76,-222.3 172.53,-222.3"></path>
<text text-anchor="middle" x="115.85" y="-209.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/osc@0.6.0</text>
</g>
<!-- @strudel.cycles/osc@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- osc&#45;js@2.4.0 -->
<g id="node22" class="node module-osc_js collapsed maintainer-andreasdz license-mit" data-module="osc-js@2.4.0">
<title>osc-js@2.4.0</title>
<path fill="none" stroke="black" d="M334.18,-220.3C334.18,-220.3 283.53,-220.3 283.53,-220.3 280.3,-220.3 277.06,-217.07 277.06,-213.83 277.06,-213.83 277.06,-207.37 277.06,-207.37 277.06,-204.13 280.3,-200.9 283.53,-200.9 283.53,-200.9 334.18,-200.9 334.18,-200.9 337.42,-200.9 340.65,-204.13 340.65,-207.37 340.65,-207.37 340.65,-213.83 340.65,-213.83 340.65,-217.07 337.42,-220.3 334.18,-220.3"></path>
<text text-anchor="middle" x="308.86" y="-207.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">osc-js@2.4.0</text>
</g>
<!-- @strudel.cycles/osc@0.6.0&#45;&gt;osc&#45;js@2.4.0 -->
<!-- @strudel.cycles/react@0.6.0 -->
<g id="node7" class="node module-_strudel_cycles_react collapsed maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/react@0.6.0">
<title>@strudel.cycles/react@0.6.0</title>
<path fill="none" stroke="black" d="M175.73,-499.3C175.73,-499.3 55.96,-499.3 55.96,-499.3 52.73,-499.3 49.49,-496.07 49.49,-492.83 49.49,-492.83 49.49,-486.37 49.49,-486.37 49.49,-483.13 52.73,-479.9 55.96,-479.9 55.96,-479.9 175.73,-479.9 175.73,-479.9 178.97,-479.9 182.2,-483.13 182.2,-486.37 182.2,-486.37 182.2,-492.83 182.2,-492.83 182.2,-496.07 178.97,-499.3 175.73,-499.3"></path>
<text text-anchor="middle" x="115.85" y="-486.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/react@0.6.0</text>
</g>
<!-- @strudel.cycles/serial@0.6.0 -->
<g id="node8" class="node module-_strudel_cycles_serial maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/serial@0.6.0">
<title>@strudel.cycles/serial@0.6.0</title>
<path fill="none" stroke="black" d="M177.19,-148.3C177.19,-148.3 54.5,-148.3 54.5,-148.3 51.27,-148.3 48.04,-145.07 48.04,-141.83 48.04,-141.83 48.04,-135.37 48.04,-135.37 48.04,-132.13 51.27,-128.9 54.5,-128.9 54.5,-128.9 177.19,-128.9 177.19,-128.9 180.42,-128.9 183.66,-132.13 183.66,-135.37 183.66,-135.37 183.66,-141.83 183.66,-141.83 183.66,-145.07 180.42,-148.3 177.19,-148.3"></path>
<text text-anchor="middle" x="115.85" y="-135.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/serial@0.6.0</text>
</g>
<!-- @strudel.cycles/serial@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @strudel.cycles/soundfonts@0.6.0 -->
<g id="node9" class="node module-_strudel_cycles_soundfonts maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/soundfonts@0.6.0">
<title>@strudel.cycles/soundfonts@0.6.0</title>
<path fill="none" stroke="black" d="M189.15,-333.3C189.15,-333.3 42.55,-333.3 42.55,-333.3 39.31,-333.3 36.08,-330.07 36.08,-326.83 36.08,-326.83 36.08,-320.37 36.08,-320.37 36.08,-317.13 39.31,-313.9 42.55,-313.9 42.55,-313.9 189.15,-313.9 189.15,-313.9 192.38,-313.9 195.62,-317.13 195.62,-320.37 195.62,-320.37 195.62,-326.83 195.62,-326.83 195.62,-330.07 192.38,-333.3 189.15,-333.3"></path>
<text text-anchor="middle" x="115.85" y="-320.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/soundfonts@0.6.0</text>
</g>
<!-- @strudel.cycles/soundfonts@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @strudel.cycles/soundfonts@0.6.0&#45;&gt;@strudel.cycles/webaudio@0.6.0 -->
<!-- sfumato@0.1.2 -->
<g id="node23" class="node module-sfumato maintainer-felixroos license-isc" data-module="sfumato@0.1.2">
<title>sfumato@0.1.2</title>
<path fill="none" stroke="black" d="M338.96,-333.3C338.96,-333.3 278.76,-333.3 278.76,-333.3 275.52,-333.3 272.29,-330.07 272.29,-326.83 272.29,-326.83 272.29,-320.37 272.29,-320.37 272.29,-317.13 275.52,-313.9 278.76,-313.9 278.76,-313.9 338.96,-313.9 338.96,-313.9 342.19,-313.9 345.43,-317.13 345.43,-320.37 345.43,-320.37 345.43,-326.83 345.43,-326.83 345.43,-330.07 342.19,-333.3 338.96,-333.3"></path>
<text text-anchor="middle" x="308.86" y="-320.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">sfumato@0.1.2</text>
</g>
<!-- @strudel.cycles/soundfonts@0.6.0&#45;&gt;sfumato@0.1.2 -->
<!-- soundfont2@0.4.0 -->
<g id="node24" class="node module-soundfont2 maintainer-mrten license-mit" data-module="soundfont2@0.4.0">
<title>soundfont2@0.4.0</title>
<path fill="none" stroke="black" d="M513.22,-314.3C513.22,-314.3 438.89,-314.3 438.89,-314.3 435.66,-314.3 432.42,-311.07 432.42,-307.83 432.42,-307.83 432.42,-301.37 432.42,-301.37 432.42,-298.13 435.66,-294.9 438.89,-294.9 438.89,-294.9 513.22,-294.9 513.22,-294.9 516.45,-294.9 519.69,-298.13 519.69,-301.37 519.69,-301.37 519.69,-307.83 519.69,-307.83 519.69,-311.07 516.45,-314.3 513.22,-314.3"></path>
<text text-anchor="middle" x="476.06" y="-301.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">soundfont2@0.4.0</text>
</g>
<!-- @strudel.cycles/soundfonts@0.6.0&#45;&gt;soundfont2@0.4.0 -->
<!-- @strudel.cycles/tonal@0.6.0 -->
<g id="node10" class="node module-_strudel_cycles_tonal maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/tonal@0.6.0">
<title>@strudel.cycles/tonal@0.6.0</title>
<path fill="none" stroke="black" d="M176.36,-425.3C176.36,-425.3 55.33,-425.3 55.33,-425.3 52.1,-425.3 48.86,-422.07 48.86,-418.83 48.86,-418.83 48.86,-412.37 48.86,-412.37 48.86,-409.13 52.1,-405.9 55.33,-405.9 55.33,-405.9 176.36,-405.9 176.36,-405.9 179.6,-405.9 182.83,-409.13 182.83,-412.37 182.83,-412.37 182.83,-418.83 182.83,-418.83 182.83,-422.07 179.6,-425.3 176.36,-425.3"></path>
<text text-anchor="middle" x="115.85" y="-412.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/tonal@0.6.0</text>
</g>
<!-- @strudel.cycles/tonal@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @tonaljs/tonal@4.10.0 -->
<g id="node16" class="node module-_tonaljs_tonal collapsed maintainer-danigb license-mit" data-module="@tonaljs/tonal@4.10.0">
<title>@tonaljs/tonal@4.10.0</title>
<path fill="none" stroke="black" d="M523.73,-463.3C523.73,-463.3 428.38,-463.3 428.38,-463.3 425.15,-463.3 421.91,-460.07 421.91,-456.83 421.91,-456.83 421.91,-450.37 421.91,-450.37 421.91,-447.13 425.15,-443.9 428.38,-443.9 428.38,-443.9 523.73,-443.9 523.73,-443.9 526.96,-443.9 530.2,-447.13 530.2,-450.37 530.2,-450.37 530.2,-456.83 530.2,-456.83 530.2,-460.07 526.96,-463.3 523.73,-463.3"></path>
<text text-anchor="middle" x="476.06" y="-450.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@tonaljs/tonal@4.10.0</text>
</g>
<!-- @strudel.cycles/tonal@0.6.0&#45;&gt;@tonaljs/tonal@4.10.0 -->
<!-- chord&#45;voicings@0.0.1 -->
<g id="node18" class="node module-chord_voicings maintainer-felixroos license-isc" data-module="chord-voicings@0.0.1">
<title>chord-voicings@0.0.1</title>
<path fill="none" stroke="black" d="M354.5,-444.3C354.5,-444.3 263.21,-444.3 263.21,-444.3 259.98,-444.3 256.74,-441.07 256.74,-437.83 256.74,-437.83 256.74,-431.37 256.74,-431.37 256.74,-428.13 259.98,-424.9 263.21,-424.9 263.21,-424.9 354.5,-424.9 354.5,-424.9 357.74,-424.9 360.97,-428.13 360.97,-431.37 360.97,-431.37 360.97,-437.83 360.97,-437.83 360.97,-441.07 357.74,-444.3 354.5,-444.3"></path>
<text text-anchor="middle" x="308.86" y="-431.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">chord-voicings@0.0.1</text>
</g>
<!-- @strudel.cycles/tonal@0.6.0&#45;&gt;chord&#45;voicings@0.0.1 -->
<!-- @strudel.cycles/tonal@0.6.0&#45;&gt;webmidi@3.1.4 -->
<!-- @strudel.cycles/transpiler@0.6.0 -->
<g id="node11" class="node module-_strudel_cycles_transpiler maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/transpiler@0.6.0">
<title>@strudel.cycles/transpiler@0.6.0</title>
<path fill="none" stroke="black" d="M185.91,-74.3C185.91,-74.3 45.79,-74.3 45.79,-74.3 42.55,-74.3 39.32,-71.07 39.32,-67.83 39.32,-67.83 39.32,-61.37 39.32,-61.37 39.32,-58.13 42.55,-54.9 45.79,-54.9 45.79,-54.9 185.91,-54.9 185.91,-54.9 189.14,-54.9 192.38,-58.13 192.38,-61.37 192.38,-61.37 192.38,-67.83 192.38,-67.83 192.38,-71.07 189.14,-74.3 185.91,-74.3"></path>
<text text-anchor="middle" x="115.85" y="-61.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/transpiler@0.6.0</text>
</g>
<!-- @strudel.cycles/transpiler@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- acorn@8.8.2 -->
<g id="node17" class="node module-acorn collapsed maintainer-marijn maintainer-adrianheine maintainer-rreverser license-mit" data-module="acorn@8.8.2">
<title>acorn@8.8.2</title>
<path fill="none" stroke="black" d="M333.45,-130.3C333.45,-130.3 284.27,-130.3 284.27,-130.3 281.03,-130.3 277.8,-127.07 277.8,-123.83 277.8,-123.83 277.8,-117.37 277.8,-117.37 277.8,-114.13 281.03,-110.9 284.27,-110.9 284.27,-110.9 333.45,-110.9 333.45,-110.9 336.68,-110.9 339.92,-114.13 339.92,-117.37 339.92,-117.37 339.92,-123.83 339.92,-123.83 339.92,-127.07 336.68,-130.3 333.45,-130.3"></path>
<text text-anchor="middle" x="308.86" y="-117.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">acorn@8.8.2</text>
</g>
<!-- @strudel.cycles/transpiler@0.6.0&#45;&gt;acorn@8.8.2 -->
<!-- escodegen@2.0.0 -->
<g id="node19" class="node module-escodegen collapsed maintainer-constellation maintainer-michaelficarra license-bsd_2_clause" data-module="escodegen@2.0.0">
<title>escodegen@2.0.0</title>
<path fill="none" stroke="black" d="M344.33,-93.3C344.33,-93.3 273.39,-93.3 273.39,-93.3 270.15,-93.3 266.92,-90.07 266.92,-86.83 266.92,-86.83 266.92,-80.37 266.92,-80.37 266.92,-77.13 270.15,-73.9 273.39,-73.9 273.39,-73.9 344.33,-73.9 344.33,-73.9 347.56,-73.9 350.8,-77.13 350.8,-80.37 350.8,-80.37 350.8,-86.83 350.8,-86.83 350.8,-90.07 347.56,-93.3 344.33,-93.3"></path>
<text text-anchor="middle" x="308.86" y="-80.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">escodegen@2.0.0</text>
</g>
<!-- @strudel.cycles/transpiler@0.6.0&#45;&gt;escodegen@2.0.0 -->
<!-- estree&#45;walker@3.0.3 -->
<g id="node20" class="node module-estree_walker collapsed maintainer-rich_harris license-mit" data-module="estree-walker@3.0.3">
<title>estree-walker@3.0.3</title>
<path fill="none" stroke="black" d="M351.26,-56.3C351.26,-56.3 266.46,-56.3 266.46,-56.3 263.22,-56.3 259.99,-53.07 259.99,-49.83 259.99,-49.83 259.99,-43.37 259.99,-43.37 259.99,-40.13 263.22,-36.9 266.46,-36.9 266.46,-36.9 351.26,-36.9 351.26,-36.9 354.49,-36.9 357.73,-40.13 357.73,-43.37 357.73,-43.37 357.73,-49.83 357.73,-49.83 357.73,-53.07 354.49,-56.3 351.26,-56.3"></path>
<text text-anchor="middle" x="308.86" y="-43.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">estree-walker@3.0.3</text>
</g>
<!-- @strudel.cycles/transpiler@0.6.0&#45;&gt;estree&#45;walker@3.0.3 -->
<!-- @strudel.cycles/webaudio@0.6.1&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @strudel.cycles/xen@0.6.0 -->
<g id="node13" class="node module-_strudel_cycles_xen maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/xen@0.6.0">
<title>@strudel.cycles/xen@0.6.0</title>
<path fill="none" stroke="black" d="M173.25,-185.3C173.25,-185.3 58.44,-185.3 58.44,-185.3 55.21,-185.3 51.98,-182.07 51.98,-178.83 51.98,-178.83 51.98,-172.37 51.98,-172.37 51.98,-169.13 55.21,-165.9 58.44,-165.9 58.44,-165.9 173.25,-165.9 173.25,-165.9 176.48,-165.9 179.72,-169.13 179.72,-172.37 179.72,-172.37 179.72,-178.83 179.72,-178.83 179.72,-182.07 176.48,-185.3 173.25,-185.3"></path>
<text text-anchor="middle" x="115.85" y="-172.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/xen@0.6.0</text>
</g>
<!-- @strudel.cycles/xen@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @strudel.cycles/webaudio@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- chord&#45;voicings@0.0.1&#45;&gt;@tonaljs/tonal@4.10.0 -->
<g id="edge27" class="edge">
<title>chord-voicings@0.0.1-&gt;@tonaljs/tonal@4.10.0</title>
<path fill="none" stroke="black" d="M361.22,-440.51C377.14,-442.34 394.88,-444.38 411.53,-446.29"></path>
<polygon fill="black" stroke="black" points="411.39,-449.8 421.73,-447.47 412.19,-442.85 411.39,-449.8"></polygon>
</g>
<!-- sfumato@0.1.2&#45;&gt;soundfont2@0.4.0 -->
<g id="edge28" class="edge">
<title>sfumato@0.1.2-&gt;soundfont2@0.4.0</title>
<path fill="none" stroke="black" d="M345.7,-319.48C368.06,-316.91 397.22,-313.55 422.19,-310.68"></path>
<polygon fill="black" stroke="black" points="422.72,-314.14 432.25,-309.52 421.92,-307.19 422.72,-314.14"></polygon>
</g>
<g id="edge1" class="edge">
<title>@strudel.cycles/core@0.6.8-&gt;fraction.js@4.2.0</title>
<path fill="none" stroke="black" d="M181.25,-21.53C206.16,-19.18 234.23,-16.54 257.6,-14.34"></path>
<polygon fill="black" stroke="black" points="258.02,-17.81 267.64,-13.39 257.36,-10.84 258.02,-17.81"></polygon>
</g><g id="edge3" class="edge">
<title>@strudel.cycles/csound@0.6.2-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M44.23,-280.41C33.55,-276.09 24.08,-269.5 17.96,-259.6 -5.99,-220.85 -5.99,-93.35 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge4" class="edge">
<title>@strudel.cycles/csound@0.6.2-&gt;@strudel.cycles/webaudio@0.6.1</title>
<path fill="none" stroke="black" d="M115.85,-276.84C115.85,-274.39 115.85,-271.94 115.85,-269.49"></path>
<polygon fill="black" stroke="black" points="119.35,-269.43 115.85,-259.43 112.35,-269.43 119.35,-269.43"></polygon>
</g><g id="edge6" class="edge">
<title>@strudel.cycles/midi@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M50.17,-356.56C37.1,-352.37 25.19,-345.3 17.96,-333.6 1.66,-307.23 1.66,-80.97 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge7" class="edge">
<title>@strudel.cycles/midi@0.6.0-&gt;@strudel.cycles/webaudio@0.6.0</title>
<path fill="none" stroke="black" d="M181.51,-360.6C194.37,-360.6 208.05,-360.6 221.49,-360.6"></path>
<polygon fill="black" stroke="black" points="221.71,-364.1 231.7,-360.6 221.7,-357.1 221.71,-364.1"></polygon>
</g><g id="edge5" class="edge">
<title>@strudel.cycles/midi@0.6.0-&gt;webmidi@3.1.4</title>
<path fill="none" stroke="black" d="M166.53,-370.22C195.48,-375.83 231.78,-382.86 260.34,-388.4"></path>
<polygon fill="black" stroke="black" points="260.01,-391.9 270.49,-390.36 261.34,-385.02 260.01,-391.9"></polygon>
</g><g id="edge8" class="edge">
<title>@strudel.cycles/mini@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M50.17,-97.56C37.1,-93.37 25.19,-86.3 17.96,-74.6 7.78,-58.12 21.54,-47.3 41.67,-40.25"></path>
<polygon fill="black" stroke="black" points="42.78,-43.57 51.3,-37.27 40.71,-36.88 42.78,-43.57"></polygon>
</g><g id="edge10" class="edge">
<title>@strudel.cycles/osc@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M52.7,-209.33C38.65,-205.26 25.65,-198.05 17.96,-185.6 2.66,-160.84 2.66,-79.36 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge9" class="edge">
<title>@strudel.cycles/osc@0.6.0-&gt;osc-js@2.4.0</title>
<path fill="none" stroke="black" d="M179.13,-211.95C207.93,-211.65 241.33,-211.3 266.9,-211.03"></path>
<polygon fill="black" stroke="black" points="267.02,-214.53 276.99,-210.92 266.95,-207.53 267.02,-214.53"></polygon>
</g><g id="edge11" class="edge">
<title>@strudel.cycles/serial@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M48.17,-133.89C35.89,-129.63 24.82,-122.7 17.96,-111.6 4.64,-90.05 4.64,-76.15 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge14" class="edge">
<title>@strudel.cycles/soundfonts@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M36.71,-313.85C29.15,-309.7 22.6,-304.1 17.96,-296.6 3.83,-273.73 3.83,-77.47 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge15" class="edge">
<title>@strudel.cycles/soundfonts@0.6.0-&gt;@strudel.cycles/webaudio@0.6.0</title>
<path fill="none" stroke="black" d="M166.53,-333.22C191.53,-338.07 222.01,-343.97 248.26,-349.05"></path>
<polygon fill="black" stroke="black" points="247.6,-352.49 258.09,-350.96 248.94,-345.62 247.6,-352.49"></polygon>
</g><g id="edge12" class="edge">
<title>@strudel.cycles/soundfonts@0.6.0-&gt;sfumato@0.1.2</title>
<path fill="none" stroke="black" d="M195.64,-323.6C218.22,-323.6 242.08,-323.6 261.97,-323.6"></path>
<polygon fill="black" stroke="black" points="261.98,-327.1 271.98,-323.6 261.98,-320.1 261.98,-327.1"></polygon>
</g><g id="edge13" class="edge">
<title>@strudel.cycles/soundfonts@0.6.0-&gt;soundfont2@0.4.0</title>
<path fill="none" stroke="black" d="M164.14,-313.99C184.8,-310.25 209.37,-306.44 231.73,-304.6 297.18,-299.22 372.86,-300.38 422.21,-302.11"></path>
<polygon fill="black" stroke="black" points="422.21,-305.61 432.33,-302.49 422.47,-298.62 422.21,-305.61"></polygon>
</g><g id="edge19" class="edge">
<title>@strudel.cycles/tonal@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M68.74,-405.99C49.45,-399.45 29.07,-388.58 17.96,-370.6 -0.5,-340.73 -0.5,-84.47 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge16" class="edge">
<title>@strudel.cycles/tonal@0.6.0-&gt;@tonaljs/tonal@4.10.0</title>
<path fill="none" stroke="black" d="M168.82,-425.25C177.92,-427.57 187.2,-430.33 195.73,-433.6 212.82,-440.15 214.01,-449.01 231.73,-453.6 291.41,-469.07 362.13,-466.9 411.9,-462.17"></path>
<polygon fill="black" stroke="black" points="412.28,-465.65 421.88,-461.17 411.58,-458.69 412.28,-465.65"></polygon>
</g><g id="edge17" class="edge">
<title>@strudel.cycles/tonal@0.6.0-&gt;chord-voicings@0.0.1</title>
<path fill="none" stroke="black" d="M182.85,-422.17C203.51,-424.22 226.22,-426.48 246.55,-428.5"></path>
<polygon fill="black" stroke="black" points="246.33,-432 256.62,-429.5 247.02,-425.03 246.33,-432"></polygon>
</g><g id="edge18" class="edge">
<title>@strudel.cycles/tonal@0.6.0-&gt;webmidi@3.1.4</title>
<path fill="none" stroke="black" d="M182.85,-409.38C208.4,-406.97 237.07,-404.27 260.51,-402.06"></path>
<polygon fill="black" stroke="black" points="260.91,-405.54 270.54,-401.12 260.26,-398.57 260.91,-405.54"></polygon>
</g><g id="edge23" class="edge">
<title>@strudel.cycles/transpiler@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M115.85,-54.84C115.85,-52.39 115.85,-49.94 115.85,-47.49"></path>
<polygon fill="black" stroke="black" points="119.35,-47.43 115.85,-37.43 112.35,-47.43 119.35,-47.43"></polygon>
</g><g id="edge20" class="edge">
<title>@strudel.cycles/transpiler@0.6.0-&gt;acorn@8.8.2</title>
<path fill="none" stroke="black" d="M168.82,-74.25C177.92,-76.57 187.2,-79.33 195.73,-82.6 212.82,-89.15 214.66,-95.99 231.73,-102.6 243.09,-107 255.84,-110.53 267.61,-113.25"></path>
<polygon fill="black" stroke="black" points="267.12,-116.73 277.64,-115.44 268.61,-109.89 267.12,-116.73"></polygon>
</g><g id="edge21" class="edge">
<title>@strudel.cycles/transpiler@0.6.0-&gt;escodegen@2.0.0</title>
<path fill="none" stroke="black" d="M192.61,-72.14C214.17,-74.28 237.12,-76.56 256.83,-78.52"></path>
<polygon fill="black" stroke="black" points="256.51,-82.01 266.81,-79.52 257.21,-75.04 256.51,-82.01"></polygon>
</g><g id="edge22" class="edge">
<title>@strudel.cycles/transpiler@0.6.0-&gt;estree-walker@3.0.3</title>
<path fill="none" stroke="black" d="M192.61,-57.46C211.68,-55.66 231.85,-53.76 249.89,-52.06"></path>
<polygon fill="black" stroke="black" points="250.33,-55.54 259.96,-51.11 249.68,-48.57 250.33,-55.54"></polygon>
</g><g id="edge24" class="edge">
<title>@strudel.cycles/webaudio@0.6.1-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M38.54,-240.82C30.22,-236.58 22.97,-230.7 17.96,-222.6 -1.66,-190.84 -1.66,-86.36 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge25" class="edge">
<title>@strudel.cycles/xen@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M51.94,-172.1C38.18,-168 25.52,-160.82 17.96,-148.6 -4,-113.06 -4,-90.14 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge26" class="edge">
<title>@strudel.cycles/webaudio@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M241.66,-350.81C237.99,-348.55 234.63,-345.84 231.73,-342.6 143.1,-243.48 284.75,-144.37 195.73,-45.6 193.96,-43.64 192.03,-41.87 189.96,-40.27"></path>
<polygon fill="black" stroke="black" points="191.74,-37.25 181.4,-34.93 188.03,-43.19 191.74,-37.25"></polygon>
</g><g id="edge2" class="edge">
<title>@strudel.cycles/csound@0.6.2-&gt;@csound/browser@6.18.5</title>
<path fill="none" stroke="black" d="M164.06,-276.96C190.24,-271.62 222.97,-264.93 250.6,-259.29"></path>
<polygon fill="black" stroke="black" points="251.43,-262.69 260.52,-257.26 250.02,-255.84 251.43,-262.69"></polygon>
</g></g>
</svg>

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.
+51
View File
@@ -0,0 +1,51 @@
Copyright 2022 The 3270font Authors (https://github.com/rbanffy/3270font)
Copyright (c) 2011-2022, Ricardo Banffy.
Copyright (c) 1993-2011, Paul Mattes.
Copyright (c) 2004-2005, Don Russell.
Copyright (c) 2004, Dick Altenbern.
Copyright (c) 1990, Jeff Sparkes.
Copyright (c) 1989, Georgia Tech Research Corporation (GTRC), Atlanta, GA 30332.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Ricardo Banffy, Paul Mattes, Don Russell,
Dick Altenbern, Jeff Sparkes, GTRC nor the names of their contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL RICARDO BANFFY, PAUL MATTES, DON RUSSELL, DICK ALTENBERN, JEFF
SPARKES OR GTRC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The Debian Logo glyph is based on the Debian Open Use Logo and is
Copyright (c) 1999 Software in the Public Interest, Inc., and it is
incorporated here under the terms of the Creative Commons
Attribution-ShareAlike 3.0 Unported License. The logo is released
under the terms of the GNU Lesser General Public License, version 3 or
any later version, or, at your option, of the Creative Commons
Attribution-ShareAlike 3.0 Unported License.
Ubuntu, the Ubuntu logo and the Circle of Friends symbol are
registered trademarks of Canonical Ltd.
The Fontforge SFD font description file is optionally licensed under
the SIL Open Font License v1.1 with no Reserved Font Name. This
license is available with a FAQ at http://scripts.sil.org/OFL.
@@ -0,0 +1,428 @@
Attribution-ShareAlike 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More_considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-ShareAlike 4.0 International Public
License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-ShareAlike 4.0 International Public License ("Public
License"). To the extent this Public License may be interpreted as a
contract, You are granted the Licensed Rights in consideration of Your
acceptance of these terms and conditions, and the Licensor grants You
such rights in consideration of benefits the Licensor receives from
making the Licensed Material available under these terms and
conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. BY-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
e. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name
of a Creative Commons Public License. The License Elements of this
Public License are Attribution and ShareAlike.
h. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
i. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
k. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
l. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
m. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part; and
b. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. Additional offer from the Licensor -- Adapted Material.
Every recipient of Adapted Material from You
automatically receives an offer from the Licensor to
exercise the Licensed Rights in the Adapted Material
under the conditions of the Adapter's License You apply.
c. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share
Adapted Material You produce, the following conditions also apply.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
later, or a BY-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition
in any reasonable manner based on the medium, means, and
context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms
or conditions on, or apply any Effective Technological
Measures to, Adapted Material that restrict exercise of the
rights granted under the Adapter's License You apply.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material,
including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the “Licensor.” The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.
+92
View File
@@ -0,0 +1,92 @@
Copyright (c) 2012, Cody "CodeMan38" Boisclair (cody@zone38.net), with Reserved Font Name "Press Start 2P"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

+121 -123
View File
@@ -1,66 +1,64 @@
---
// fetch all commits for just this page's path
type Props = {
path: string;
path: string;
};
const { path } = Astro.props as Props;
const resolvedPath = `examples/docs/${path}`;
const url = `https://api.github.com/repos/withastro/astro/commits?path=${resolvedPath}`;
const commitsURL = `https://github.com/withastro/astro/commits/main/${resolvedPath}`;
const resolvedPath = `website/src/pages${path}.mdx`;
const url = `https://api.github.com/repos/tidalcycles/strudel/commits?path=${resolvedPath}`;
const commitsURL = `https://github.com/tidalcycles/strudel/commits/main/${resolvedPath}`;
type Commit = {
author: {
id: string;
login: string;
};
author: {
id: string;
login: string;
};
};
async function getCommits(url: string) {
try {
const token = import.meta.env.SNOWPACK_PUBLIC_GITHUB_TOKEN ?? 'hello';
if (!token) {
throw new Error(
'Cannot find "SNOWPACK_PUBLIC_GITHUB_TOKEN" used for escaping rate-limiting.'
);
}
try {
const token = import.meta.env.SNOWPACK_PUBLIC_GITHUB_TOKEN ?? 'hello';
if (!token) {
throw new Error('Cannot find "SNOWPACK_PUBLIC_GITHUB_TOKEN" used for escaping rate-limiting.');
}
const auth = `Basic ${Buffer.from(token, 'binary').toString('base64')}`;
const auth = `Basic ${Buffer.from(token, 'binary').toString('base64')}`;
const res = await fetch(url, {
method: 'GET',
headers: {
Authorization: auth,
'User-Agent': 'astro-docs/1.0',
},
});
const res = await fetch(url, {
method: 'GET',
headers: {
Authorization: auth,
'User-Agent': 'astro-docs/1.0',
},
});
const data = await res.json();
const data = await res.json();
if (!res.ok) {
throw new Error(
`Request to fetch commits failed. Reason: ${res.statusText}
Message: ${data.message}`
);
}
if (!res.ok) {
throw new Error(
`Request to fetch commits failed. Reason: ${res.statusText}
Message: ${data.message}`,
);
}
return data as Commit[];
} catch (e) {
console.warn(`[error] /src/components/AvatarList.astro
return data as Commit[];
} catch (e) {
console.warn(`[error] /src/components/AvatarList.astro
${(e as any)?.message ?? e}`);
return [] as Commit[];
}
return [] as Commit[];
}
}
function removeDups(arr: Commit[]) {
const map = new Map<string, Commit['author']>();
const map = new Map<string, Commit['author']>();
for (let item of arr) {
const author = item.author;
// Deduplicate based on author.id
//map.set(author.id, { login: author.login, id: author.id });
author && map.set(author.id, { login: author.login, id: author.id });
}
for (let item of arr) {
const author = item.author;
// Deduplicate based on author.id
map.set(author.id, { login: author.login, id: author.id });
}
return [...map.values()];
return [...map.values()];
}
const data = await getCommits(url);
@@ -70,102 +68,102 @@ const additionalContributors = unique.length - recentContributors.length; // lis
---
<!-- Thanks to @5t3ph for https://smolcss.dev/#smol-avatar-list! -->
<div class="contributors">
<ul class="avatar-list" style={`--avatar-count: ${recentContributors.length}`}>
{
recentContributors.map((item) => (
<li>
<a href={`https://github.com/${item.login}`}>
<img
alt={`Contributor ${item.login}`}
title={`Contributor ${item.login}`}
width="64"
height="64"
src={`https://avatars.githubusercontent.com/u/${item.id}`}
/>
</a>
</li>
))
}
</ul>
{
additionalContributors > 0 && (
<span>
<a href={commitsURL}>{`and ${additionalContributors} additional contributor${
additionalContributors > 1 ? 's' : ''
}.`}</a>
</span>
)
}
{unique.length === 0 && <a href={commitsURL}>Contributors</a>}
<div class="contributors px-4 mb-8">
<ul class="avatar-list" style={`--avatar-count: ${recentContributors.length}`}>
{
recentContributors.map((item) => (
<li>
<a href={`https://github.com/${item.login}`}>
<img
alt={`Contributor ${item.login}`}
title={`Contributor ${item.login}`}
width="64"
height="64"
src={`https://avatars.githubusercontent.com/u/${item.id}`}
/>
</a>
</li>
))
}
</ul>
{
additionalContributors > 0 && (
<span>
<a href={commitsURL}>{`and ${additionalContributors} additional contributor${
additionalContributors > 1 ? 's' : ''
}.`}</a>
</span>
)
}
{unique.length === 0 && <a href={commitsURL}>Contributors</a>}
</div>
<style>
.avatar-list {
--avatar-size: 2.5rem;
--avatar-count: 3;
.avatar-list {
--avatar-size: 2.5rem;
--avatar-count: 3;
display: grid;
list-style: none;
/* Default to displaying most of the avatar to
display: grid;
list-style: none;
/* Default to displaying most of the avatar to
enable easier access on touch devices, ensuring
the WCAG touch target size is met or exceeded */
grid-template-columns: repeat(var(--avatar-count), max(44px, calc(var(--avatar-size) / 1.15)));
/* `padding` matches added visual dimensions of
grid-template-columns: repeat(var(--avatar-count), max(44px, calc(var(--avatar-size) / 1.15)));
/* `padding` matches added visual dimensions of
the `box-shadow` to help create a more accurate
computed component size */
padding: 0.08em;
font-size: var(--avatar-size);
}
padding: 0.08em;
font-size: var(--avatar-size);
}
@media (any-hover: hover) and (any-pointer: fine) {
.avatar-list {
/* We create 1 extra cell to enable the computed
@media (any-hover: hover) and (any-pointer: fine) {
.avatar-list {
/* We create 1 extra cell to enable the computed
width to match the final visual width */
grid-template-columns: repeat(calc(var(--avatar-count) + 1), calc(var(--avatar-size) / 1.75));
}
}
grid-template-columns: repeat(calc(var(--avatar-count) + 1), calc(var(--avatar-size) / 1.75));
}
}
.avatar-list li {
width: var(--avatar-size);
height: var(--avatar-size);
}
.avatar-list li {
width: var(--avatar-size);
height: var(--avatar-size);
}
.avatar-list li:hover ~ li a,
.avatar-list li:focus-within ~ li a {
transform: translateX(33%);
}
.avatar-list li:hover ~ li a,
.avatar-list li:focus-within ~ li a {
transform: translateX(33%);
}
.avatar-list img,
.avatar-list a {
display: block;
border-radius: 50%;
}
.avatar-list img,
.avatar-list a {
display: block;
border-radius: 50%;
}
.avatar-list a {
transition: transform 180ms ease-in-out;
}
.avatar-list a {
transition: transform 180ms ease-in-out;
}
.avatar-list img {
width: 100%;
height: 100%;
object-fit: cover;
background-color: #fff;
box-shadow: 0 0 0 0.05em #fff, 0 0 0 0.08em rgba(0, 0, 0, 0.15);
}
.avatar-list img {
width: 100%;
height: 100%;
object-fit: cover;
background-color: #fff;
box-shadow: 0 0 0 0.05em #fff, 0 0 0 0.08em rgba(0, 0, 0, 0.15);
}
.avatar-list a:focus {
outline: 2px solid transparent;
/* Double-layer trick to work for dark and light backgrounds */
box-shadow: 0 0 0 0.08em var(--theme-accent), 0 0 0 0.12em white;
}
.avatar-list a:focus {
outline: 2px solid transparent;
/* Double-layer trick to work for dark and light backgrounds */
box-shadow: 0 0 0 0.08em var(--theme-accent), 0 0 0 0.12em white;
}
.contributors {
display: flex;
align-items: center;
}
.contributors {
display: flex;
align-items: center;
}
.contributors > * + * {
margin-left: 0.75rem;
}
.contributors > * + * {
margin-left: 0.75rem;
}
</style>
@@ -1,19 +0,0 @@
---
import AvatarList from './AvatarList.astro';
type Props = {
path: string;
};
const { path } = Astro.props as Props;
---
<footer>
<AvatarList path={path} />
</footer>
<style>
footer {
margin-top: auto;
padding: 2rem;
border-top: 3px solid var(--theme-divider);
}
</style>
+65 -23
View File
@@ -1,4 +1,5 @@
---
import { pwaInfo } from 'virtual:pwa-info';
import '../styles/index.css';
const { BASE_URL } = import.meta.env;
@@ -12,34 +13,75 @@ const base = BASE_URL;
<link rel="icon" type="image/svg+xml" href="favicon.ico" />
<link rel="sitemap" href="./sitemap.xml" />
<meta
name="description"
content="Strudel is a music live coding environment for the browser, porting the TidalCycles pattern language to JavaScript."
/>
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/icons/apple-icon-180.png" sizes="180x180" />
<meta name="theme-color" content="#222222" />
<base href={base} />
<!-- Preload Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital@0;1&display=swap" rel="stylesheet" />
<!-- Scrollable a11y code helper -->
<script src="./make-scrollable-code-focusable.js" is:inline></script>
<!-- This is intentionally inlined to avoid FOUC -->
<script is:inline>
/* const root = document.documentElement;
const theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
root.classList.add('theme-dark');
} else {
root.classList.remove('theme-dark');
} */
</script>
<script src="/src/pwa.ts"></script>
<!-- this does not work for some reason: -->
<!-- <style is:global define:vars={strudelTheme}></style> -->
<!-- the following variables are just a fallback to make sure everything is readable without JS -->
<style is:global>
:root {
--background: #222;
--lineBackground: #22222250;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
--selectionMatch: #036dd626;
--lineHighlight: #00000050;
--gutterBackground: transparent;
--gutterForeground: #8a919966;
}
</style>
{pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} />}
<!-- Global site tag (gtag.js) - Google Analytics -->
<!-- <script async src="https://www.googletagmanager.com/gtag/js?id=G-TEL60V1WM9" is:inline></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-TEL60V1WM9');
</script> -->
import { settings } from '../repl/themes.mjs';
import { settingsMap } from '../settings.mjs';
import { listenKeys } from 'nanostores';
const themeStyle = document.createElement('style');
themeStyle.id = 'strudel-theme';
document.head.append(themeStyle);
function activateTheme(name) {
if (!settings[name]) {
console.warn('theme', name, 'has no settings.. defaulting to strudelTheme settings');
}
const themeSettings = settings[name] || settings.strudelTheme;
// set css variables
themeStyle.innerHTML = `:root {
${Object.entries(themeSettings)
// important to override fallback
.map(([key, value]) => `--${key}: ${value} !important;`)
.join('\n')}
}`;
// tailwind dark mode
if (themeSettings.light) {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
}
activateTheme(settingsMap.get().theme);
listenKeys(settingsMap, ['theme'], ({ theme }) => activateTheme(theme));
// https://medium.com/quick-code/100vh-problem-with-ios-safari-92ab23c852a8
const appHeight = () => {
const doc = document.documentElement;
doc.style.setProperty('--app-height', `${window.innerHeight - 1}px`);
};
if (typeof window !== 'undefined') {
window.addEventListener('resize', appHeight);
appHeight();
}
</script>
+3 -14
View File
@@ -2,8 +2,8 @@
import { SITE, OPEN_GRAPH, Frontmatter } from '../config';
export interface Props {
frontmatter: Frontmatter;
canonicalUrl: URL;
frontmatter: Frontmatter;
canonicalUrl: URL;
}
const { frontmatter, canonicalUrl } = Astro.props as Props;
@@ -23,11 +23,7 @@ const imageAlt = frontmatter.image?.alt ?? OPEN_GRAPH.image.alt;
<meta property="og:locale" content={frontmatter.ogLocale ?? SITE.defaultLanguage} />
<meta property="og:image" content={canonicalImageSrc} />
<meta property="og:image:alt" content={imageAlt} />
<meta
name="description"
property="og:description"
content={frontmatter.description ?? SITE.description}
/>
<meta name="description" property="og:description" content={frontmatter.description ?? SITE.description} />
<meta property="og:site_name" content={SITE.title} />
<!-- Twitter Tags -->
@@ -37,10 +33,3 @@ const imageAlt = frontmatter.image?.alt ?? OPEN_GRAPH.image.alt;
<meta name="twitter:description" content={frontmatter.description ?? SITE.description} />
<meta name="twitter:image" content={canonicalImageSrc} />
<meta name="twitter:image:alt" content={imageAlt} />
<!--
TODO: Add json+ld data, maybe https://schema.org/APIReference makes sense?
Docs: https://developers.google.com/search/docs/advanced/structured-data/intro-structured-data
https://www.npmjs.com/package/schema-dts seems like a great resource for implementing this.
Even better, there's a React component that integrates with `schema-dts`: https://github.com/google/react-schemaorg
-->
+6 -3
View File
@@ -19,13 +19,16 @@ const langCode = 'en'; // getLanguageFromURL(currentPage);
const sidebar = SIDEBAR[langCode];
---
<nav class="flex justify-between py-2 px-4 items-center h-14 max-h-14 bg-[#161616]" title="Top Navigation">
<nav
class="flex justify-between py-2 px-4 items-center h-14 max-h-14 bg-lineHighlight text-foreground"
title="Top Navigation"
>
<!-- <div class="menu-toggle">
<SidebarToggle client:idle />
</div> -->
<div class="flex overflow-visible items-center grow" style="overflow:visible">
<a href="/" class="flex items-center text-2xl space-x-2">
<h1 class="text-white font-bold flex space-x-2 items-baseline text-xl">
<h1 class="font-bold flex space-x-2 items-baseline text-xl">
<span>🌀</span>
<div class="flex space-x-1 items-baseline">
<span class="">strudel</span>
@@ -36,7 +39,7 @@ const sidebar = SIDEBAR[langCode];
</div>
{/* KNOWN_LANGUAGE_CODES.length > 1 && <LanguageSelect lang={lang} client:idle /> */}
<div class="search-item h-10">
<!-- <Search client:idle /> -->
<!-- <Search client:idle /> -->
</div>
<a href="./" class="hidden md:flex cursor-pointer items-center space-x-1"
><CommandLineIcon className="w-5 h-5" /><span>go to REPL</span>
@@ -14,7 +14,7 @@ const langCode = 'en'; // getLanguageFromURL(currentPage);
const sidebar = SIDEBAR[langCode];
---
<nav aria-labelledby="grid-left" class="max-h-full overflow-auto pb-20">
<nav aria-labelledby="grid-left" class="max-h-full overflow-auto pb-20 text-foreground">
<ul>
{
Object.entries(sidebar).map(([header, children]) => (
@@ -23,12 +23,12 @@ const sidebar = SIDEBAR[langCode];
<h2>{header}</h2>
<ul>
{children.map((child) => {
const url = '.' + Astro.site?.pathname + child.link;
const url = Astro.site?.pathname + child.link;
return (
<li class="">
<a
class={`pl-4 py-0.5 w-full hover:bg-header block${
currentPageMatch === child.link ? ' bg-header' : ''
class={`pl-4 py-0.5 w-full hover:bg-lineHighlight block${
currentPageMatch === child.link ? ' bg-lineHighlight' : ''
}`}
href={url}
aria-current={currentPageMatch === child.link ? 'page' : false}
@@ -22,7 +22,7 @@ const currentPage = Astro.url.pathname;
<span>On this Page:</span>
<TableOfContents client:media="(max-width: 50em)" headings={headings} currentPage={currentPage} />
</nav> -->
<div class="prose prose-invert max-w-full pb-8">
<div class="prose dark:prose-invert max-w-full pb-8">
<slot />
</div>
</section>

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