Compare commits

..

266 Commits

Author SHA1 Message Date
Felix Roos 90a58858ca Publish
- @strudel/codemirror@0.8.4
 - @strudel.cycles/core@0.8.2
 - @strudel.cycles/csound@0.8.0
 - @strudel.cycles/eval@0.8.0
 - @strudel.cycles/midi@0.8.0
 - @strudel.cycles/mini@0.8.2
 - @strudel.cycles/osc@0.8.0
 - @strudel.cycles/react@0.8.0
 - @strudel.cycles/serial@0.8.0
 - @strudel.cycles/soundfonts@0.8.2
 - @strudel.cycles/tonal@0.8.2
 - @strudel.cycles/tone@0.8.0
 - @strudel.cycles/transpiler@0.8.2
 - @strudel/web@0.8.3
 - @strudel.cycles/webaudio@0.8.2
 - @strudel.cycles/webdirt@0.8.0
 - @strudel.cycles/xen@0.8.0
2023-06-30 08:17:19 +02:00
Felix Roos 8149b85a55 Merge pull request #627 from tidalcycles/fix-clock-drift
fix: midi clock drift
2023-06-30 08:14:40 +02:00
Felix Roos e79b6c514a Merge pull request #621 from tidalcycles/tauri-fs
desktop: play samples from disk
2023-06-29 22:02:27 +02:00
Felix Roos aaa894d74b Merge pull request #620 from tidalcycles/loopAt
cps dependent functions
2023-06-29 21:59:42 +02:00
Felix Roos b55721f16f fix: midi clock drift 2023-06-29 21:58:15 +02:00
Felix Roos b39d948d71 fix: build 2023-06-29 12:32:59 +02:00
Felix Roos 0fee279e3d rename fay to fit 2023-06-29 12:05:31 +02:00
Felix Roos c7d8a9cb82 fix: playFile import 2023-06-29 12:05:09 +02:00
Felix Roos b50075f102 feat: can now load disk samples via samples
+ encapsulate file logic from FilesTab
2023-06-29 10:47:20 +02:00
Felix Roos fb3ca9f960 feat: register fn for sample prefix handling 2023-06-29 10:46:32 +02:00
Felix Roos 058b971870 can now play samples from file system
- ui still wip
2023-06-29 02:02:00 +02:00
Felix Roos 610ea414c1 expose tauri fs api 2023-06-29 02:01:22 +02:00
Felix Roos cc6997baec - encapsulate sample map handling
- add resolveUrl param
2023-06-29 02:00:59 +02:00
Felix Roos d126c28b85 fix: loopAt with cps 1 for fallback 2023-06-28 22:25:49 +02:00
Felix Roos c1649b9dc0 - fix: loopAt for non default cps value
- add fay to repitch samples to fit hap duration
- add setcpm function
2023-06-28 22:13:05 +02:00
Felix Roos b0a8d12112 Merge pull request #520 from paikwiki/patch-1
Fix typo on packages.mdx
2023-06-26 22:45:04 +02:00
Felix Roos b5160d38cb another dummy commit 2023-06-26 22:43:14 +02:00
Felix Roos 360e9b5936 dummy commit 2023-06-26 22:41:47 +02:00
Felix Roos 85a6cd828e Merge pull request #606 from tidalcycles/setting-patterning
patterning ui settings
2023-06-26 22:35:02 +02:00
Felix Roos e698f19a64 Merge pull request #614 from tidalcycles/spirals
add spiral viz
2023-06-26 22:32:46 +02:00
Felix Roos 83c1b580dd Merge pull request #613 from vasilymilovidov/tauri
tauri test
2023-06-26 22:17:46 +02:00
vvm 9273e1f8ed fix tauri icons 2023-06-26 13:13:22 +04:00
vvm 5287825279 tauri workflow fixes 2023-06-26 11:27:21 +04:00
vvm 5567959afd add tauri github workflow 2023-06-26 01:58:16 +04:00
Felix Roos eaab36b051 add spiral viz 2023-06-25 14:23:18 +02:00
vvm 9a4f3af580 add README 2023-06-24 02:34:02 +04:00
vvm b83664dfce add build optimizations 2023-06-24 02:32:57 +04:00
vvm 349f7ae965 init 2023-06-23 21:14:55 +04:00
vvm 6f41716ab5 init 2023-06-23 21:06:10 +04:00
Felix Roos abeb62dea8 Merge pull request #612 from tidalcycles/fix-links
fix: doc links
2023-06-23 09:59:43 +02:00
Felix Roos 7cfed80443 Merge pull request #598 from tidalcycles/clip-support-floats
clip now works like legato in tidal
2023-06-23 09:55:18 +02:00
Felix Roos 36aa7d9b25 Merge pull request #607 from tidalcycles/doc-fixes
tonal fixes
2023-06-18 10:36:17 +02:00
Felix Roos 2a449ad3c0 rename tonal api -> tonal functions 2023-06-18 10:32:51 +02:00
Felix Roos a71c5957cc fix: better fix 2023-06-16 14:23:39 +02:00
Felix Roos eef6c7d55e quick fix: typo 2023-06-16 14:20:49 +02:00
Felix Roos 3872322da1 allow patterning theme + font settings 2023-06-16 13:45:13 +02:00
Felix Roos f1e850a62a fix: doc links 2023-06-16 09:20:29 +02:00
Felix Roos 8d9278419d fix: flatten scale lists 2023-06-15 20:19:31 +02:00
Felix Roos 0862827a51 Merge pull request #581 from roipoussiere/line_wrap
editor: enable line wrapping
2023-06-15 12:51:40 +02:00
Roipoussiere f37a220a05 add checkbox in settings to enable line wrapping 2023-06-15 11:52:29 +02:00
Felix Roos aa51c2b6dd Merge remote-tracking branch 'upstream/main' into line_wrap 2023-06-15 10:59:52 +02:00
Felix Roos 92da70a7ae Merge remote-tracking branch 'origin/main' into clip-support-floats 2023-06-15 10:46:10 +02:00
Felix Roos a55862fc25 Merge pull request #602 from tidalcycles/ratio-function
add ratio function
2023-06-15 10:22:46 +02:00
Felix Roos 09b14a5702 add ratio function 2023-06-14 23:19:04 +02:00
Felix Roos 260b831815 refactor: legato -> clip 2023-06-14 00:22:55 +02:00
Felix Roos 665cf6ac65 Merge pull request #588 from roipoussiere/enable_autocomplete
enable auto-completion
2023-06-12 23:24:28 +02:00
Felix Roos 5e90021698 update snapshot 2023-06-12 23:12:40 +02:00
Felix Roos 16aeb7e12c docs: update clip entry 2023-06-12 23:11:07 +02:00
Felix Roos b4cbec173b only accept undefined for clip 2023-06-12 22:58:25 +02:00
Felix Roos 8c9e06c329 - clip now works like legato in tidal
- supports floats
- hap.duration now respects clip value
- hap.endClipped is now end*clip
- visualizations show clipped length
- clip(0) will now be silence
2023-06-12 22:39:32 +02:00
Felix Roos e4a35fdd72 Merge pull request #597 from tidalcycles/cursor-fix
improve cursor
2023-06-11 22:04:17 +02:00
Felix Roos 370d82e66c + disable cursor blink
+ make cursor thicker
2023-06-11 20:21:11 +02:00
Felix Roos 876e851517 Merge pull request #570 from dariacotocu/branch-daria
Solmization added
2023-06-11 20:00:45 +02:00
Felix Roos 190fe37a9f move solmization logic from midi2note to sol2note 2023-06-11 19:55:24 +02:00
Felix Roos 623661f8d2 Merge remote-tracking branch 'upstream/main' into branch-daria 2023-06-11 19:54:55 +02:00
Felix Roos f31bc3f8c5 Merge pull request #591 from tidalcycles/fix-fast0
fix: division by zero
2023-06-11 19:45:00 +02:00
Felix Roos 12fc48544c Merge pull request #596 from tidalcycles/fix-option-dot
Fix option dot
2023-06-11 19:44:40 +02:00
Felix Roos 3b4e44a9fd fix: main repl option-dot on mac 2023-06-11 13:55:11 +02:00
Felix Roos 7850fb727e fix: allow f for flat notes like tidal 2023-06-11 13:23:45 +02:00
Felix Roos 9b3122837e fix: division by zero 2023-06-11 13:15:52 +02:00
Felix Roos d4c59cc73b fix:typo + remove notes 2023-06-11 11:21:19 +02:00
Felix Roos fe36bf54b2 Merge commit '4d8308a763a632cc4dd93ed2a2ab129eb957af72' 2023-06-09 21:39:14 +02:00
Felix Roos 4d8308a763 fix: link goes to wrong language 2023-06-09 21:33:32 +02:00
Felix Roos 11f26c1e2c Merge pull request #587 from tidalcycles/workshop-new
New Workshop
2023-06-09 21:02:11 +02:00
Felix Roos e10104d401 bring de improvements to en workshop 2023-06-09 20:59:40 +02:00
Roipoussiere 5e09c3c318 add autocompletion extension from useMemo 2023-06-09 18:28:16 +02:00
Felix Roos 576b88221d Merge remote-tracking branch 'origin/main' into workshop-new 2023-06-09 18:00:16 +02:00
Felix Roos 7dc2a0091b more detail 2023-06-09 17:59:08 +02:00
Felix Roos 89a4a85589 more friendly heading 2023-06-09 17:58:05 +02:00
Felix Roos a3baf07a5c Merge pull request #580 from roipoussiere/music_metadata
Music metadata
2023-06-09 17:31:58 +02:00
Roipoussiere 8e6909478c autocompletion: lint 2023-06-09 16:42:31 +02:00
Roipoussiere 2d2d0cabb3 Add option in settings to enable autocompletion 2023-06-09 16:38:48 +02:00
Roipoussiere 6f04a1b8fa autocomplete: add vanilla js completion 2023-06-09 14:52:07 +02:00
Roipoussiere a9ad3a56ad autocomplete: avoid tabs overlapping 2023-06-09 14:30:20 +02:00
Roipoussiere 3069b14e9c enable autocompletion 2023-06-09 13:41:40 +02:00
Felix Roos ae868c6372 hide gutter in mini repl 2023-06-09 00:46:04 +02:00
Felix Roos c287ff554c optimize sounds chapter after teach test 2023-06-09 00:26:40 +02:00
Felix Roos 3ccbd35fad add label support to pianoroll 2023-06-09 00:25:54 +02:00
Felix Roos 51cc69208f fix: repl link to docs was old 2023-06-09 00:25:14 +02:00
Felix Roos 1fec2f7a46 fix: build 2023-06-08 21:31:32 +02:00
Felix Roos ffffdd2d7d fix: format 2023-06-08 20:55:26 +02:00
Felix Roos f8c3e3d5aa add missing sounds by hand 2023-06-08 20:43:45 +02:00
Felix Roos eccce6fa84 remove dirt samples
+ load a small selection for workshop instead
2023-06-08 17:48:54 +02:00
Felix Roos 44be6cee2c Merge pull request #585 from srenatus/typo
learn/tonal: fix typo in "scaleTran[s]pose"
2023-06-08 09:33:34 +02:00
Stephan Renatus 799afd511a learn/tonal: fix typo in "scaleTran[s]pose" 2023-06-08 08:17:38 +02:00
Felix Roos 37c4541468 translate link 2023-06-07 21:50:26 +02:00
Felix Roos 119a66acd0 - translate getting-started
- fix de links
- link english docs / german workshop
2023-06-07 21:49:57 +02:00
Felix Roos 105193a452 replace tutorial with workshop
+ move parts of tutorial to more
+ move more before Pattern Functions
2023-06-07 21:22:01 +02:00
Felix Roos 02d793e9e0 Merge remote-tracking branch 'origin/main' into workshop-new 2023-06-07 20:19:34 +02:00
Felix Roos 90647a8ba8 Merge pull request #582 from roipoussiere/line_numbers
repl: add option to display line numbers
2023-06-07 20:19:11 +02:00
Felix Roos 06950a6bd8 translate rest of workshop to german 2023-06-07 16:50:54 +02:00
Roipoussiere 0c2147d9c5 repl: add option to display line numbers 2023-06-07 15:34:57 +02:00
Roipoussiere 51e8174689 metadata: add quotes syntax for titles 2023-06-07 12:26:23 +02:00
Roipoussiere 185318a70c metadata: fix typos 2023-06-07 11:02:38 +02:00
Felix Roos a88759ba94 translate notes chapter 2023-06-06 22:43:51 +02:00
Roipoussiere 96ac054392 metadata: improve doc 2023-06-06 21:34:53 +02:00
Roipoussiere bd7f4dd73f metadata: add tests 2023-06-06 21:34:53 +02:00
Roipoussiere 2e22152089 metadata: allow several values and one-liners 2023-06-06 21:34:53 +02:00
Roipoussiere 98a89aea11 lint: metadata parsing 2023-06-06 21:34:53 +02:00
Roipoussiere 372e33c066 doc: add section about metadata 2023-06-06 21:34:53 +02:00
Roipoussiere 52641db590 load titles from metadata in examples page 2023-06-06 21:34:53 +02:00
Roipoussiere f9ad5f0d01 add tunes metadata 2023-06-06 21:34:53 +02:00
Roipoussiere 3db1c7a521 swatch: look for song titles in code metadata 2023-06-06 21:34:53 +02:00
Roipoussiere b6d1f96930 editor: enable line wrapping 2023-06-06 13:29:55 +02:00
Felix Roos 61a9b01062 started german translation of workshop 2023-06-05 23:48:04 +02:00
Felix Roos f0faf5b42e Merge remote-tracking branch 'origin/main' into workshop-new 2023-06-05 22:48:50 +02:00
Felix Roos 11733d5e05 Merge pull request #563 from tidalcycles/vanilla-highlighting
Vanilla JS Refactoring
2023-06-05 21:35:49 +02:00
Felix Roos c4a38d9008 + pattern effects chapter
+ recap page
+ only load mini repls when visible
2023-05-30 06:16:02 +02:00
Felix Roos 0edd7e14b7 add compound adsr + ds controls 2023-05-29 12:39:36 +02:00
Felix Roos 8b7bb7b6ae add adsr section to effects chapter 2023-05-29 12:39:17 +02:00
Felix Roos 39d5955e58 add function recap to sounds chapter 2023-05-29 12:39:05 +02:00
Felix Roos 536327f403 effects chapter mostly finished 2023-05-29 02:18:36 +02:00
Felix Roos 8679dc63be pianoroll: also reflect gain in transparency 2023-05-29 02:18:27 +02:00
Felix Roos 1e9979ae18 start fleshing out effects chapter 2023-05-28 12:42:24 +02:00
Felix Roos d2dffe3186 MiniRepl: consume font settings 2023-05-28 12:42:15 +02:00
Felix Roos 9971867e2f clamp function 2023-05-28 12:41:53 +02:00
Felix Roos 8c93e578a0 clamp delayfeedback 2023-05-28 12:41:46 +02:00
Felix Roos 1ba5d2e1ca finish notes chapter 2023-05-27 21:52:44 +02:00
Felix Roos ed792fc0d4 continue notes chapter 2023-05-27 16:29:05 +02:00
Felix Roos 4e575c44b3 begin first notes page 2023-05-27 13:31:18 +02:00
Felix Roos fc06181217 - add claviature flag to minirepl
- bring back option+dot on macos
- consume more editor settings in minirepl
2023-05-27 13:30:57 +02:00
Felix Roos 0d6fcf78d8 hide mini repl headers + improve workshop 2023-05-26 16:05:53 +02:00
Felix Roos 82225f0b81 started workshop pages 2023-05-26 14:12:53 +02:00
Felix Roos 2177e13b49 Merge branch 'vanilla-highlighting' into workshop 2023-05-25 21:38:34 +02:00
Daria Cotocu b63f4eb503 Update solmization.test.js 2023-05-24 18:58:11 +01:00
Daria Cotocu cd7bc09f9c Update solmization.test.js 2023-05-24 18:43:24 +01:00
Daria Cotocu ccf775e976 Format code 2023-05-24 17:57:05 +01:00
Daria Cotocu 811346e83d Solmization added 2023-05-24 17:24:34 +01:00
Felix Roos d3240a75d0 fix: dupe style.css to Repl.css
for some reason they were missing after update
+ darken line backgrounds
+ change preview port to keep workbox out of dev server
2023-05-12 11:56:50 +02:00
Felix Roos d93e396ce5 fix: nano-repl 2023-05-12 10:21:05 +02:00
Felix Roos d74cc234c4 fix: thinner outline for highlights with .color 2023-05-12 10:06:55 +02:00
Felix Roos 0fda83d8de bump soundfonts 2023-05-12 10:00:55 +02:00
Felix Roos a1ec8545fb format 2023-05-10 13:03:06 +02:00
Felix Roos 087f574355 update web readme + add more html examples 2023-05-10 12:49:45 +02:00
Felix Roos 1de784b817 add headless-serverless-buildless example 2023-05-10 12:20:27 +02:00
Felix Roos db79c05d9a bump web pkg dependencies 2023-05-10 12:11:51 +02:00
Felix Roos 1fdb1f0da6 bump webaudio package 2023-05-10 12:09:20 +02:00
Felix Roos e2bfaa6233 remove soundfonts from web package for now 2023-05-10 12:06:16 +02:00
Felix Roos 6089849d35 rename init to initStrudel + add to window 2023-05-10 11:53:26 +02:00
Felix Roos 6e02bf59e9 Revert "refactor: remove old draw logic"
This reverts commit 95719654f3.
2023-05-10 11:44:55 +02:00
Felix Roos 12228c56d9 improve api for web package 2023-05-07 22:36:26 +02:00
Felix Roos 3b631cb6af add @strudel/web umbrella package + example 2023-05-07 11:56:35 +02:00
Felix Roos b27a58df7e bump more packages to 0.8.0 2023-05-05 23:58:47 +02:00
Felix Roos 308f139193 bump codemirror again to get new core version 2023-05-05 23:52:55 +02:00
Felix Roos 692fc4e11d revert rename core package 2023-05-05 23:52:04 +02:00
Felix Roos b52f65c4d7 rename core package + bump to 0.8.0 2023-05-05 23:50:06 +02:00
Felix Roos 299275e368 lockfile 2023-05-05 23:47:15 +02:00
Felix Roos bb71c519db bump codemirror package 2023-05-05 23:46:42 +02:00
Felix Roos 9ec7109dc8 + simplify vanilla setup drastically
+ move repl + drawer boilerplate inside StrudelMirror
2023-05-05 23:43:07 +02:00
Felix Roos 5b67fccb1b vanilla-repl-cm6: draw first frame 2023-05-05 16:17:22 +02:00
Felix Roos 9807c4b7a1 move Framer + Drawer to core
... from vite-vanilla-repl-cm6
2023-05-05 16:06:11 +02:00
Felix Roos 9039600f6d bump codemirror to 0.8.1 2023-05-05 15:31:19 +02:00
Felix Roos b5b0156e08 fix: codemirror reeval 2023-05-05 15:31:04 +02:00
Felix Roos 352e647d37 add build script for codemirror package 2023-05-05 15:15:45 +02:00
Felix Roos e6b66f2525 lockfile 2023-05-05 15:14:46 +02:00
Felix Roos f5075906e2 add codemirror package
+ use it in vite-vanilla-repl-cm6
2023-05-05 15:13:17 +02:00
Felix Roos a6f57bced8 prettier 2023-05-05 12:00:13 +02:00
Felix Roos d35bf9591c vanilla-repl-cm6: better theme 2023-05-05 11:54:09 +02:00
Felix Roos d96f3f9dd4 vanilla-repl-cm6: style + codesplitting 2023-05-05 11:09:44 +02:00
Felix Roos 1f4c2f8c5a draw scheduler in vanilla js 2023-05-05 10:52:21 +02:00
Felix Roos 95719654f3 refactor: remove old draw logic
+ pianoroll now uses .onPaint
2023-05-05 09:52:07 +02:00
Felix Roos 4b921c47f5 vanilla-repl-cm6: add flash effect 2023-05-05 09:11:40 +02:00
Felix Roos f069f53fae vanilla-repl-cm6: add keybindings 2023-05-05 09:06:27 +02:00
Felix Roos 1494cc38fc add vite-vanilla-repl-cm6 2023-05-05 08:55:43 +02:00
Felix Roos a035412956 Merge pull request #559 from tidalcycles/patchday
Patchday
2023-05-05 08:34:36 +02:00
Felix Roos 08662ea831 Merge pull request #557 from tidalcycles/some-voicings
add basic triads and guidetone voicings
2023-05-02 21:48:32 +02:00
Felix Roos d97103268b add basic triads and guidetone voicings 2023-05-02 21:40:20 +02:00
Felix Roos a959e2e34d update remaining packages 2023-04-30 08:30:29 +02:00
Felix Roos 602bfbdd79 update a bunch of minor versions 2023-04-30 08:20:51 +02:00
Felix Roos 0785bdd37a update lerna 2023-04-30 08:04:56 +02:00
Felix Roos 8b62b6e1b1 update codemirror + tailwindcss 2023-04-30 08:02:06 +02:00
Felix Roos d1c8ebd15b remove c8 2023-04-30 07:55:23 +02:00
Felix Roos 13088e4001 - remove gh-pages
- update peggy
- rebuild parser
2023-04-30 07:48:08 +02:00
Felix Roos 5c6fa1c82b update vite to v4 2023-04-29 08:10:12 +02:00
Felix Roos fd81867e7b fix: controls import broke on ssr 2023-04-29 07:45:21 +02:00
Felix Roos 78293e64dd lockfile 2023-04-29 07:45:09 +02:00
Felix Roos 930fc10aec bump some minor versions 2023-04-29 07:45:04 +02:00
Felix Roos 0b2a4fda44 update supabase-js 2023-04-29 07:24:03 +02:00
Felix Roos efdce6d4cd update astro 2023-04-29 06:55:34 +02:00
Felix Roos c0b0ea4493 remove preact 2023-04-29 06:25:19 +02:00
Felix Roos 0fcec0a0ca upgrade jsdoc 2023-04-28 13:05:50 +02:00
Felix Roos c0701d8a35 Merge remote-tracking branch 'origin/main' into patchday 2023-04-28 12:50:20 +02:00
Felix Roos ae51092044 Merge pull request #556 from tidalcycles/fix-prebake
fix: make soundfonts import dynamic
2023-04-28 12:46:56 +02:00
Felix Roos b7fcccb8db fix: make soundfonts import dynamic
- it broke the whole docs page
2023-04-28 12:42:07 +02:00
Felix Roos b21a52f5f3 update @vite-pwa/astro 2023-04-28 12:23:10 +02:00
Felix Roos 2bdcfe13e6 update @vitest/ui 2023-04-28 12:20:35 +02:00
Felix Roos 12a97ec429 update nanostores 2023-04-28 12:18:45 +02:00
Felix Roos 99298ce9f2 update vite-plugin-pwa 2023-04-28 12:14:07 +02:00
Felix Roos a0ed25d373 use vitests@^0.28.0 in packages 2023-04-28 12:12:05 +02:00
Felix Roos dc6c29c668 update vitest to ^0.28.0 2023-04-28 12:08:48 +02:00
Felix Roos 3a85f28b5c install with pnpm 8.3.1 2023-04-28 10:33:47 +02:00
Felix Roos 95c44b1ce2 Merge pull request #553 from tidalcycles/highlighting-colors
fix: colorable highlighting
2023-04-22 15:43:22 +02:00
Felix Roos b88e0ecb78 fix: colorable highlighting 2023-04-22 15:25:00 +02:00
Felix Roos c26f6b72d9 Merge pull request #550 from tidalcycles/fix-soundfont-loading
fix: load soundfonts in prebake
2023-04-06 00:11:02 +02:00
Felix Roos 6cd3cc96a3 fix: do not prebake in tests
+ pull piano function to separate file for tests
2023-04-06 00:08:23 +02:00
Felix Roos c138cf0b76 fix: load soundfonts in prebake 2023-04-06 00:00:49 +02:00
Felix Roos 8edb086178 Merge pull request #548 from tidalcycles/stop-pause
fix: reset time on stop
2023-03-29 22:23:07 +02:00
Felix Roos 0f74b5a4b1 Merge pull request #547 from tidalcycles/fix-mini-whitespace
fix: allow whitespace at the end of a mini pattern
2023-03-29 22:16:37 +02:00
Felix Roos 6de2feff05 fix: reset time on stop 2023-03-29 22:05:13 +02:00
Felix Roos a9811ee955 fix: allow whitespace at the end of a mini pattern
- fixes #546
2023-03-29 21:57:55 +02:00
Felix Roos 98b6c11a82 Merge pull request #544 from tidalcycles/fonts
add firacode font
2023-03-24 22:10:55 +01:00
Felix Roos 3df2929d12 dummy commit (github cert error :-/) 2023-03-24 22:06:40 +01:00
Felix Roos 30d1f423cb fix: remove loading state on eval error 2023-03-24 21:50:50 +01:00
Felix Roos e0c5566f35 add 2 FiraCode fonts 2023-03-24 21:23:37 +01:00
Felix Roos c5a2ae47d7 Merge pull request #542 from tidalcycles/loading-animation
feat: add loader bar to animate loading state
2023-03-24 12:54:20 +01:00
Felix Roos 17654b885b feat: add loader bar to animate loading state 2023-03-24 12:41:00 +01:00
Felix Roos acaf0ca0af Merge pull request #539 from tidalcycles/setcps-reset-only-on-shuffle
do not reset cps before eval #517
2023-03-23 22:39:26 +01:00
Felix Roos 6a201be1fd Merge remote-tracking branch 'origin/main' into setcps-reset-only-on-shuffle 2023-03-23 22:35:57 +01:00
Felix Roos 8617b9164e do not reset cps before eval #517 2023-03-23 22:34:25 +01:00
Felix Roos 991e5f1b3c Merge pull request #538 from tidalcycles/improve-loading
improve initial loading + wait before eval
2023-03-23 22:27:31 +01:00
Felix Roos 55c533c947 improve initial loading + wait before eval 2023-03-23 21:56:20 +01:00
Felix Roos d17543d5d9 Merge pull request #537 from tidalcycles/fix-keypress-period
fix period key for dvorak + remove duplicated code
2023-03-23 21:40:19 +01:00
Felix Roos 73169563c4 fix period key for dvorak + remove duplicated code 2023-03-23 21:37:38 +01:00
Felix Roos d351cb9f7f Merge pull request #535 from tidalcycles/update-lerna
Update lerna
2023-03-23 11:44:56 +01:00
Felix Roos 251be60630 update package publishing guide 2023-03-23 11:39:33 +01:00
Felix Roos 18d7222804 Publish
- @strudel.cycles/core@0.7.2
 - @strudel.cycles/csound@0.7.1
 - @strudel.cycles/eval@0.7.1
 - @strudel.cycles/midi@0.7.1
 - @strudel.cycles/mini@0.7.2
 - @strudel.cycles/osc@0.7.1
 - @strudel.cycles/react@0.7.1
 - @strudel.cycles/serial@0.7.1
 - @strudel.cycles/soundfonts@0.7.1
 - @strudel.cycles/tonal@0.7.1
 - @strudel.cycles/tone@0.7.1
 - @strudel.cycles/transpiler@0.7.1
 - @strudel.cycles/webaudio@0.7.1
 - @strudel.cycles/webdirt@0.7.1
 - @strudel.cycles/xen@0.7.1
2023-03-23 11:34:07 +01:00
Felix Roos ff1c37d361 bump core + mini to 0.7.1 2023-03-23 11:26:23 +01:00
Felix Roos 3abe6db3de Publish
- @strudel.cycles/core@0.7.0
 - @strudel.cycles/csound@0.7.0
 - @strudel.cycles/eval@0.7.0
 - @strudel.cycles/midi@0.7.0
 - @strudel.cycles/mini@0.7.0
 - @strudel.cycles/osc@0.7.0
 - @strudel.cycles/react@0.7.0
 - @strudel.cycles/serial@0.7.0
 - @strudel.cycles/soundfonts@0.7.0
 - @strudel.cycles/tonal@0.7.0
 - @strudel.cycles/tone@0.7.0
 - @strudel.cycles/transpiler@0.7.0
 - @strudel.cycles/webaudio@0.7.0
 - @strudel.cycles/webdirt@0.7.0
 - @strudel.cycles/xen@0.7.0
2023-03-23 11:21:02 +01:00
Felix Roos b9ca9d3b06 update lerna 2023-03-23 11:17:21 +01:00
Felix Roos 9bbc04843d Merge pull request #534 from tidalcycles/soundfont-freq-support
feat: add freq support to gm soundfonts
2023-03-23 10:21:55 +01:00
Felix Roos ddf61a6438 Merge pull request #531 from ijc8/main
Maintain random seed state in parser, not globally
2023-03-23 10:18:58 +01:00
Felix Roos ba35a81e9b - feat: add freq support to gm soundfonts
- refactor: toMidi -> noteToMidi
- refactor: fromMidi -> midiToFreq
2023-03-23 10:18:24 +01:00
Felix Roos 13133583ca Merge pull request #527 from bwagner/main
FIXES: alias pm for polymeter
2023-03-21 22:35:17 +01:00
Felix Roos e91b981740 Merge pull request #529 from revolunet/patch-1
fix(footer): fix link to tidalcycles
2023-03-21 22:29:06 +01:00
Bernhard Wagner 1458ee64ec FIXES: alias pm for polymeter 2023-03-21 21:20:57 +01:00
Ian Clester 4e2a5e1663 Add pairwise independence test, tweak randOffset 2023-03-20 22:27:35 -04:00
Ian Clester c98b0e9687 Format code with prettier 2023-03-20 19:19:42 -04:00
Ian Clester 4e25990de9 Uncomment test for random choice operator (|) 2023-03-20 17:46:57 -04:00
Ian Clester 4b51f1a3fc Sync up ? with degrade() 2023-03-20 15:26:27 -04:00
Ian Clester 5490b01004 Maintain random seed state in parser, not globally 2023-03-20 14:59:43 -04:00
Julien Bouquillon 43176e1f1d fix(footer): fix link to tidalcycles 2023-03-20 01:38:21 +01:00
Bernhard Wagner fffd3c07ca FIXES: alias pm for polymeter
(return statement was missing)
2023-03-18 22:12:12 +01:00
Felix Roos 952bd38e70 Merge pull request #525 from bwagner/patch-4
Update intro.mdx
2023-03-18 20:25:21 +01:00
Felix Roos 8589f5cc89 Merge pull request #524 from bwagner/patch-3
Update samples.mdx
2023-03-18 20:24:38 +01:00
Bernhard Wagner ec594d52f8 Update intro.mdx
shortly -> concisely
2023-03-18 19:31:57 +01:00
Bernhard Wagner 848cbf9c60 Update samples.mdx
fixes few typos and the former `samples` tab is now `sounds`
2023-03-18 15:37:59 +01:00
Felix Roos e02be7dd9a Merge pull request #521 from tidalcycles/fix-envelope
fix: envelopes in chrome
2023-03-17 09:03:10 +01:00
Felix Roos 33d35e47a6 fix: envelope in chrome 2023-03-17 08:56:05 +01:00
Felix Roos 18d62b32c2 Merge pull request #516 from tidalcycles/source-nodes
registerSound API + improved sounds tab + regroup soundfonts
2023-03-16 16:13:30 +01:00
Felix Roos 58ed51b49c snapshot 2023-03-16 15:49:56 +01:00
Felix Roos 9f9fe529e4 fix only tune that uses soundfonts 2023-03-16 15:49:08 +01:00
Felix Roos aad721890a pass duration to source callback 2023-03-16 15:48:59 +01:00
Felix Roos e63a5d10c8 regroup sounds
- extra tab for drum-machines
- combine synths and soundfonts
2023-03-16 15:21:16 +01:00
Felix Roos 15681262de use only _ for easier copying via double click 2023-03-16 15:19:38 +01:00
Felix Roos b5d3aa81f9 format 2023-03-16 12:25:08 +01:00
Felix Roos a688061616 group soundfonts into readable names 2023-03-16 12:23:58 +01:00
Felix Roos f788d347ec add envelope to soundfonts 2023-03-15 23:02:05 +01:00
Felix Roos a33ce67dd4 fix: endless preview 2023-03-15 23:01:56 +01:00
Felix Roos 8a22c5627a fix: cut glitches due to envelope release trouble 2023-03-15 22:42:03 +01:00
Felix Roos 7b529a30f4 style sounds tab 2023-03-14 22:54:12 +01:00
Felix Roos 4910b34f26 Merge pull request #518 from tidalcycles/fonts
add 2 illegible fonts
2023-03-14 21:54:52 +01:00
paikwiki 74ad521fc9 Fix typo on packages.mdx
I found a broken link.

Because it omits protocol, page move to wrong page(https://strudel.tidalcycles.org/github.com/tidalcycles/strudel)

I add protocol(https://) on that link.
2023-03-14 13:01:40 +09:00
Felix Roos 6a73b43e3a add 2 illegible fonts 2023-03-12 21:16:46 +01:00
Felix Roos b59b26460d soundfont list curation part 2 2023-03-12 20:48:29 +01:00
Felix Roos aa324ae25d start curating the soundfont list 2023-03-10 11:33:55 +01:00
Felix Roos ff412f6396 level down soundfonts
+ do not load in prebake (breaks static rendering)
+ instead load on import
2023-03-09 23:19:19 +01:00
Felix Roos ac148b2f32 register soundfonts as sounds too 2023-03-09 22:48:40 +01:00
Felix Roos 6f5d096e6d rename setSound to registerSound 2023-03-09 21:32:40 +01:00
Felix Roos ff1e6714c6 document setSound api 2023-03-09 21:27:24 +01:00
Felix Roos 41d82eaadf add cleanup to sound tab 2023-03-09 10:53:35 +01:00
Felix Roos be6b011204 proper audio node cleanup
+ cleanup onTriggerSample
2023-03-09 10:46:45 +01:00
Felix Roos 65e48c05f0 sounds onset and offset can be triggered independently
+ sounds tab now supports mousedown / mouseup to listen
2023-03-09 08:35:20 +01:00
Felix Roos cee08ea67d sounds list can now filter out defaults 2023-03-08 00:33:21 +01:00
Felix Roos 35ef26c013 can now pass meta data to setSound
+ added types for synth and sample
+ list sample count for samples in sounds tab (as before)
2023-03-07 21:43:01 +01:00
Felix Roos b08a0b8102 use nanostore for soundmap
+ rename tab samples to sounds
+ listed sounds are now reactive
2023-03-06 23:11:09 +01:00
Felix Roos 6059c69995 first draft of soundMap to register keys for s
+ refactor sampler to use it
+ refactor synth to use it
+ add 'source' control
+ wip: samples tab
+ wip: webadirt ?
+ wip: soundfonts
2023-03-05 23:27:37 +01:00
207 changed files with 23383 additions and 10521 deletions
+62
View File
@@ -0,0 +1,62 @@
name: Tauri Builder
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
release:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
rust_target: x86_64-unknown-linux-gnu
- os: macos-latest
rust_target: x86_64-apple-darwin
- os: macos-latest
rust_target: aarch64-apple-darwin
- os: windows-latest
rust_target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8.6.2
- name: Node.js setup
uses: actions/setup-node@v3
with:
node-version: latest
# node-version-file: '.nvmrc'
- name: Install Rust (Stable)
run:
curl https://sh.rustup.rs -sSf | sh -s -- -y
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
- name: Install app dependencies from lockfile and build web
run: pnpm install
- name: Build the app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# tauri-action replaces \_\_VERSION\_\_ with the app version
tagName: ${{ github.ref_name }}
releaseName: "Strudel v__VERSION__"
releaseBody: |
See the assets to download this version and install.
releaseDraft: true
prerelease: false
+5 -1
View File
@@ -39,4 +39,8 @@ server/samples/old
repl/stats.html
coverage
public/icons/apple-splash-*
dev-dist
dev-dist
Dirt-Samples
tidal-drum-machines
webaudiofontdata
src-tauri/target
+9 -2
View File
@@ -31,7 +31,7 @@ Use one of the Communication Channels listed above.
## Improve the Docs
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/learn/getting-started),
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/workshop/getting-started/),
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
## Propose a Feature
@@ -123,7 +123,14 @@ To publish all packages that have been changed since the last release, run:
```sh
npm login
npx lerna publish
# this will increment all the versions in package.json files of non private packages to selected versions
npx lerna version --no-private
# publish all packages inside /packages using pnpm! don't use lerna to publish!!
pnpm --filter "./packages/**" publish --dry-run
# the last command was only a dry-run, make sure everything looks ok, if yes, run the same command without flag
```
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
+11 -13
View File
@@ -18,7 +18,6 @@
"build": "npm run prebuild && cd website && npm run build",
"preview": "cd website && npm run preview",
"osc": "cd packages/osc && npm run server",
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
"lint": "eslint . --ext mjs,js --quiet",
@@ -53,22 +52,21 @@
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/xen": "workspace:*",
"acorn": "^8.8.1",
"dependency-tree": "^9.0.0",
"vitest": "^0.25.7"
"dependency-tree": "^9.0.0"
},
"devDependencies": {
"@vitest/ui": "^0.25.7",
"c8": "^7.12.0",
"canvas": "^2.11.0",
"eslint": "^8.28.0",
"@tauri-apps/cli": "^1.4.0",
"@vitest/ui": "^0.28.0",
"canvas": "^2.11.2",
"eslint": "^8.39.0",
"eslint-plugin-import": "^2.27.5",
"events": "^3.3.0",
"gh-pages": "^4.0.0",
"jsdoc": "^3.6.10",
"jsdoc": "^4.0.2",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^7.1.1",
"lerna": "^4.0.0",
"prettier": "^2.8.1",
"rollup-plugin-visualizer": "^5.8.1"
"jsdoc-to-markdown": "^8.0.0",
"lerna": "^6.6.1",
"prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.8.1",
"vitest": "^0.28.0"
}
}
+3
View File
@@ -0,0 +1,3 @@
# @strudel/codemirror
This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it.
+199
View File
@@ -0,0 +1,199 @@
import { EditorState } from '@codemirror/state';
import { EditorView, keymap, Decoration, lineNumbers, highlightActiveLineGutter } from '@codemirror/view';
import { defaultKeymap } from '@codemirror/commands';
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
import { javascript } from '@codemirror/lang-javascript';
import { StateField, StateEffect } from '@codemirror/state';
import { oneDark } from './themes/one-dark';
import { repl, Drawer } from '@strudel.cycles/core';
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
let state = EditorState.create({
doc: initialCode,
extensions: [
theme,
javascript(),
lineNumbers(),
highlightField,
highlightActiveLineGutter(),
syntaxHighlighting(defaultHighlightStyle),
keymap.of(defaultKeymap),
flashField,
EditorView.updateListener.of((v) => onChange(v)),
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate(),
},
{
key: 'Ctrl-.',
run: () => onStop(),
},
]),
],
});
return new EditorView({
state,
parent: root,
});
}
//
// highlighting
//
export const setHighlights = StateEffect.define();
export const highlightField = StateField.define({
create() {
return Decoration.none;
},
update(highlights, tr) {
try {
for (let e of tr.effects) {
if (e.is(setHighlights)) {
const { haps } = e.value;
const marks =
haps
.map((hap) =>
(hap.context.locations || []).map(({ start, end }) => {
// 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: 2px solid #FFCA28;` },
});
return mark.range(from, to);
}),
)
.flat()
.filter(Boolean) || [];
highlights = Decoration.set(marks, true);
}
}
return highlights;
} catch (err) {
// console.warn('highlighting error', err);
return Decoration.set([]);
}
},
provide: (f) => EditorView.decorations.from(f),
});
// helper to simply trigger highlighting for given haps
export function highlightHaps(view, haps) {
view.dispatch({ effects: setHighlights.of({ haps }) });
}
//
// flash
//
export const setFlash = StateEffect.define();
const flashField = StateField.define({
create() {
return Decoration.none;
},
update(flash, tr) {
try {
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash = Decoration.set([]);
}
}
}
return flash;
} catch (err) {
console.warn('flash error', err);
return flash;
}
},
provide: (f) => EditorView.decorations.from(f),
});
export const flash = (view, ms = 200) => {
view.dispatch({ effects: setFlash.of(true) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, ms);
};
export class StrudelMirror {
constructor(options) {
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
this.code = initialCode;
this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
this.highlight(currentFrame);
onDraw?.(haps, time, currentFrame);
}, drawTime);
const prebaked = prebake();
prebaked.then(async () => {
if (!onDraw) {
return;
}
const { scheduler, evaluate } = await this.repl;
// draw first frame instantly
prebaked.then(async () => {
await evaluate(this.code, false);
this.drawer.invalidate(scheduler);
onDraw?.(this.drawer.visibleHaps, 0, []);
});
});
this.repl = repl({
...replOptions,
onToggle: async (started) => {
replOptions?.onToggle?.(started);
const { scheduler } = await this.repl;
if (started) {
this.drawer.start(scheduler);
} else {
this.drawer.stop();
}
},
beforeEval: async () => {
await prebaked;
},
afterEval: (options) => {
replOptions?.afterEval?.(options);
this.drawer.invalidate();
},
});
this.editor = initEditor({
root,
initialCode,
onChange: (v) => {
this.code = v.state.doc.toString();
},
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
});
}
async evaluate() {
const { evaluate } = await this.repl;
this.flash();
await evaluate(this.code);
}
async stop() {
const { scheduler } = await this.repl;
scheduler.stop();
}
flash(ms) {
flash(this.editor, ms);
}
highlight(haps) {
highlightHaps(this.editor, haps);
}
}
+47
View File
@@ -0,0 +1,47 @@
{
"name": "@strudel/codemirror",
"version": "0.8.4",
"description": "Codemirror Extensions for Strudel",
"main": "codemirror.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": [
"Alex McLean <alex@slab.org>"
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/language": "^6.6.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^4.3.3"
}
}
+139
View File
@@ -0,0 +1,139 @@
import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags as t } from '@lezer/highlight';
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
const chalky = '#e5c07b',
coral = '#e06c75',
cyan = '#56b6c2',
invalid = '#ffffff',
ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
malibu = '#61afef',
sage = '#98c379',
whiskey = '#d19a66',
violet = '#c678dd',
darkBackground = '#21252b',
highlightBackground = '#2c313a',
background = '#282c34',
tooltipBackground = '#353a42',
selection = '#3E4451',
cursor = '#528bff';
/// The colors used in the theme, as CSS color strings.
export const color = {
chalky,
coral,
cyan,
invalid,
ivory,
stone,
malibu,
sage,
whiskey,
violet,
darkBackground,
highlightBackground,
background,
tooltipBackground,
selection,
cursor,
};
/// The editor theme styles for One Dark.
export const oneDarkTheme = EditorView.theme(
{
'&': {
color: ivory,
backgroundColor: background,
},
'.cm-content': {
caretColor: cursor,
},
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
{ backgroundColor: selection },
'.cm-panels': { backgroundColor: darkBackground, color: ivory },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
},
'.cm-gutters': {
backgroundColor: background,
color: stone,
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: true },
);
/// The highlighting style for code in the One Dark theme.
export const oneDarkHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: violet },
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
{ tag: [t.definition(t.name), t.separator], color: ivory },
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
{ tag: [t.meta, t.comment], color: stone },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.link, color: stone, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: coral },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
{ tag: t.invalid, color: invalid },
]);
/// Extension to enable the One Dark theme (both the editor theme and
/// the highlight style).
export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'codemirror.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+2 -1
View File
@@ -1,4 +1,5 @@
import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
import { Pattern, getDrawContext, silence, register, pure } from './index.mjs';
import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
const { createParams } = controls;
let clearColor = '#22222210';
+27 -5
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, sequence } from './pattern.mjs';
import { Pattern, register, sequence } from './pattern.mjs';
import { zipWith } from './util.mjs';
const controls = {};
@@ -23,6 +23,15 @@ const generic_params = [
*
*/
[['s', 'n', 'gain'], 'sound'],
/**
* Define a custom webaudio node to use as a sound source.
*
* @name source
* @param {function} getSource
* @synonyms src
*
*/
['source', 'src'],
/**
* Selects the given index from the sample map.
* Numbers too high will wrap around.
@@ -213,6 +222,7 @@ const generic_params = [
*
* @name legato
* @param {number | Pattern} duration between 0 and 1, where 1 is the length of the whole hap time
* @noAutocomplete
* @example
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
*
@@ -740,13 +750,14 @@ const generic_params = [
['val'],
['cps'],
/**
* If set to 1, samples will be cut to the duration of their event.
* In tidal, this would be done with legato, which [is about to land in strudel too](https://github.com/tidalcycles/strudel/issues/111)
* Multiplies the duration with the given number. Also cuts samples off at the end if they exceed the duration.
* In tidal, this would be done with legato, [which has a complicated history in strudel](https://github.com/tidalcycles/strudel/issues/111).
* For now, if you're coming from tidal, just think clip = legato.
*
* @name clip
* @param {number | Pattern} active 1 or 0
* @param {number | Pattern} factor >= 0
* @example
* note("c a f e ~").s("piano").clip(1)
* note("c a f e").s("piano").clip("<.5 1 2>")
*
*/
['clip'],
@@ -801,4 +812,15 @@ generic_params.forEach(([names, ...aliases]) => {
controls.createParams = (...names) =>
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
controls.adsr = register('adsr', (adsr, pat) => {
adsr = !Array.isArray(adsr) ? [adsr] : adsr;
const [attack, decay, sustain, release] = adsr;
return pat.set({ attack, decay, sustain, release });
});
controls.ds = register('ds', (ds, pat) => {
ds = !Array.isArray(ds) ? [ds] : ds;
const [decay, sustain] = ds;
return pat.set({ decay, sustain });
});
export default controls;
+1
View File
@@ -74,6 +74,7 @@ export class Cyclist {
stop() {
logger('[cyclist] stop');
this.clock.stop();
this.lastEnd = 0;
this.setStarted(false);
}
setPattern(pat, autostart = false) {
+96 -2
View File
@@ -1,6 +1,6 @@
/*
draw.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/draw.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.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/>.
*/
@@ -13,7 +13,7 @@ export const getDrawContext = (id = 'test-canvas') => {
canvas.id = id;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0;z-index:5';
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
document.body.prepend(canvas);
}
return canvas.getContext('2d');
@@ -65,3 +65,97 @@ Pattern.prototype.onPaint = function (onPaint) {
this.context = { onPaint };
return this;
};
// const round = (x) => Math.round(x * 1000) / 1000;
// encapsulates starting and stopping animation frames
export class Framer {
constructor(onFrame, onError) {
this.onFrame = onFrame;
this.onError = onError;
}
start() {
const self = this;
let frame = requestAnimationFrame(function updateHighlights(time) {
try {
self.onFrame(time);
} catch (err) {
self.onError(err);
}
frame = requestAnimationFrame(updateHighlights);
});
self.cancel = () => {
cancelAnimationFrame(frame);
};
}
stop() {
if (this.cancel) {
this.cancel();
}
}
}
// syncs animation frames to a cyclist scheduler
// see vite-vanilla-repl-cm6 for an example
export class Drawer {
constructor(onDraw, drawTime) {
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
lookbehind = Math.abs(lookbehind);
this.visibleHaps = [];
this.lastFrame = null;
this.drawTime = drawTime;
this.framer = new Framer(
() => {
if (!this.scheduler) {
console.warn('Drawer: no scheduler');
return;
}
// calculate current frame time (think right side of screen for pianoroll)
const phase = this.scheduler.now() + lookahead;
// first frame just captures the phase
if (this.lastFrame === null) {
this.lastFrame = phase;
return;
}
// query haps from last frame till now. take last 100ms max
const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase);
this.lastFrame = phase;
this.visibleHaps = (this.visibleHaps || [])
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
.filter((h) => h.whole.end >= phase - lookbehind - lookahead)
// add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead;
onDraw(this.visibleHaps, time, this);
},
(err) => {
console.warn('draw error', err);
},
);
}
invalidate(scheduler = this.scheduler) {
if (!scheduler) {
return;
}
this.scheduler = scheduler;
const t = scheduler.now();
let [_, lookahead] = this.drawTime;
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
// remove all future haps
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
// query future haps
const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query..
// append future haps
this.visibleHaps = this.visibleHaps.concat(futureHaps);
}
start(scheduler) {
this.scheduler = scheduler;
this.invalidate();
this.framer.start();
}
stop() {
if (this.framer) {
this.framer.stop();
}
}
}
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,8 @@
# vite-vanilla-repl-cm6
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
```sh
npm i
npm run dev
```
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite Vanilla Strudel REPL</title>
</head>
<body>
<main>
<nav>
<button id="play">eval</button>
<button id="stop">stop</button>
</nav>
<div class="container">
<div id="editor"></div>
<div id="output"></div>
</div>
<canvas id="roll"></canvas>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
@@ -0,0 +1,39 @@
import { StrudelMirror } from '@strudel/codemirror';
import { funk42 } from './tunes';
import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
import './style.css';
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
// init canvas
const canvas = document.getElementById('roll');
canvas.width = canvas.width * 2;
canvas.height = canvas.height * 2;
const drawContext = canvas.getContext('2d');
const drawTime = [-2, 2]; // time window of drawn haps
const editor = new StrudelMirror({
defaultOutput: webaudioOutput,
getTime: () => getAudioContext().currentTime,
transpiler,
root: document.getElementById('editor'),
initialCode: funk42,
drawTime,
onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }),
prebake: async () => {
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
const loadModules = evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
},
});
document.getElementById('play').addEventListener('click', () => editor.evaluate());
document.getElementById('stop').addEventListener('click', () => editor.stop());
@@ -0,0 +1,23 @@
{
"name": "vite-vanilla-repl-cm6",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.3.2"
},
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
}
}
@@ -0,0 +1,31 @@
body,
html {
margin: 0;
height: 100%;
background: #282c34;
}
main {
height: 100%;
display: flex;
flex-direction: column;
}
.container {
flex-grow: 1;
max-height: 100%;
position: relative;
overflow: auto;
}
#editor {
overflow: auto;
}
.cm-editor {
height: 100%;
}
#roll {
height: 300px;
}
@@ -0,0 +1,112 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
// make it 22 edo
.fmap(v => Math.pow(2,v/22))
// mess with the base frequency
.mul("<300 [300@3 200]>/8").freq()
.layer(
// chords
x=>x.div(freq(2)).s("flute").euclidLegato("<3 2>",8)
.shape(.4).lpf(sine.range(800,4000).slow(8)),
// adlibs
x=>x.arp("{0 3 2 [1 3]}%1.5")
.s('xylo').mul(freq(2))
.delay(.5).delayfeedback(.4).juxBy(.5, rev)
.hpf(sine.range(200,3000).slow(8)),
// bass
x=>x.arp("[0 [2 1?]](5,8)").s('sawtooth').div(freq(4))
.lpf(sine.range(400,2000).slow(8)).lpq(8).shape(.4)
.off(1/8, x=>x.mul(freq(2)).degradeBy(.5)).gain(.3)
).clip(1).release(.2)
.stack(
// drums
s("bd sd:<2 1>, [~ hh]*2, [~ rim]").bank('RolandTR909')
.off(1/8, x=>x.speed(2).gain(.4)).sometimes(ply(2)).gain(.8)
.mask("<0@4 1@12>/4")
.reset("<x@15 [x(3,8) x*[4 8]]>")
// wait for it...
).fast(2/3)
//.crush(6) // remove "//" if you dare`;
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', {
m11: ['2M 3m 4P 7m'],
'^7#11': ['3M 4A 5P 7M'],
}, ['C4', 'C6'])
stack(
stack(
"<Bbm11 A^7#11>/2".voicings('hip').note()
.s("gm_epiano1:2")
.arp("[<[0 1 2 3] [3 2 1 0]> ~@5]/2")
.release(2).late(.25).lpf(2000),
"<Bb1 A1>/2".note().s('gm_acoustic_bass'),
n("<0 2 3>(3,8)".off(1/8, add(4)))
.scale("<Bb4:minor A4:lydian>/2")
.s('gm_electric_guitar_jazz')
.decay(sine.range(.05, .2).slow(32)).sustain(0)
.delay(.5).lpf(sine.range(100,5000).slow(64))
.gain(.7).room(.5).pan(sine.range(0,1).slow(11))
).add(perlin.range(0,.25).note()),
stack(
s("bd:1(3,8) rim").bank('RolandTR707').slow(2).room("<0 <.1 .6>>")
.when("<0@7 1>",x=>x.echoWith(3, .0625, (x,i) => x.speed(1+i*.24))),
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
)
)
.late("[0 .05]*2").late(12)
`;
export const funk42 = `// froos - how to funk in 42 lines of code
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5)
let drums = stack(
s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"),
s("hh*4").begin(.2).release(.02).end(.25).release(.02)
.gain(.3).bank('RolandTR707').late(.02).room(.5),
//s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02)
).fast(2)
let wurli = note(\`<
[[a2,g3,[b3 c4],e4] ~ [g3,c4,e4](3,8)@4 ~@2]!3
[[e2,e3,a3,b3,e4]@3 [e2,e3,ab3,b3,e4]@5]>\`)
.s("gm_epiano1:5").decay(.2).sustain("<[1 0@7]!3 1>")
.gain("<[.8@2 .4@14]!3 .7>").room(.3)
let organ = note("<[~@3 [a3,d4,f#4]@2 [[a3,c4,e4]@2 ~] ~@2]!3 ~>".add(12))
.s("gm_percussive_organ:2").gain(.6).lpf(1800).pan(.2).room(.3);
let clav = note(\`<
[~@3 a2 [g3,[b3 c4],e4]@2 ~ a2 [g3,b3,e4] ~@2 [g3,c4,e4] ~@4]!3
[~@3 e3 [[a3 b3],c3,e3]@2 ~ e2 [e3,a3]@3 [b3,e3] ~@2 [b3,e3]@2]>\`)
.s("gm_clavinet:1").decay("<.25!3 [.25 .4]>").sustain(0)
.gain(.7).pan(.8).room(.2);
let bass = note(\`<
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
[a1 [~ [g2 a2]] [e3 d3] [c3 [g3 a3]]]
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
[e2@6 e1@5 e1 [[d2 e3] g1]@4]
>\`).s("gm_electric_bass_pick:1").release(.1)
stack(
drums
,wurli
,organ
,clav
,bass
)`;
@@ -6,5 +6,3 @@ This folder demonstrates how to set up a strudel repl using vite and vanilla JS.
npm i
npm run dev
```
or view it [live on githack](https://rawcdn.githack.com/tidalcycles/strudel/5fb36acb046ead7cd6ad3cd10f532e7f585f536a/packages/core/examples/vite-vanilla-repl/dist/index.html)
@@ -10,7 +10,7 @@
"preview": "vite preview"
},
"devDependencies": {
"vite": "^3.2.0"
"vite": "^4.3.3"
},
"dependencies": {
"@strudel.cycles/core": "workspace:*",
+5 -1
View File
@@ -32,7 +32,11 @@ export class Hap {
}
get duration() {
return this.whole.end.sub(this.whole.begin);
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
}
get endClipped() {
return this.whole.begin.add(this.duration);
}
wholeOrPart() {
+2
View File
@@ -18,11 +18,13 @@ export * from './util.mjs';
export * from './speak.mjs';
export * from './evaluate.mjs';
export * from './repl.mjs';
export * from './cyclist.mjs';
export * from './logger.mjs';
export * from './time.mjs';
export * from './draw.mjs';
export * from './animate.mjs';
export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs';
export { default as gist } from './gist.js';
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.6.8",
"version": "0.8.2",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
@@ -35,7 +35,7 @@
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+35 -10
View File
@@ -1127,6 +1127,8 @@ function _composeOp(a, b, func) {
export const polyrhythm = stack;
export const pr = stack;
export const pm = polymeter;
// methods that create patterns, which are added to patternified Pattern methods
// TODO: remove? this is only used in old transpiler (shapeshifter)
Pattern.prototype.factories = {
@@ -1374,11 +1376,6 @@ export function polymeter(...args) {
return polymeterSteps(0, ...args);
}
// alias
export function pm(...args) {
polymeter(...args);
}
export const mask = curry((a, b) => reify(b).mask(a));
export const struct = curry((a, b) => reify(b).struct(a));
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
@@ -1585,6 +1582,24 @@ export const range2 = register('range2', function (min, max, pat) {
return pat.fromBipolar()._range(min, max);
});
/**
* Allows dividing numbers via list notation using ":".
* Returns a new pattern with just numbers.
* @name ratio
* @memberof Pattern
* @returns Pattern
* @example
* ratio("1, 5:4, 3:2").mul(110).freq().s("piano").slow(2)
*/
export const ratio = register('ratio', (pat) =>
pat.fmap((v) => {
if (!Array.isArray(v)) {
return v;
}
return v.slice(1).reduce((acc, n) => acc / n, v[0]);
}),
);
//////////////////////////////////////////////////////////////////////
// Structural and temporal transformations
@@ -1680,6 +1695,9 @@ export const ply = register('ply', function (factor, pat) {
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
*/
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
if (factor === 0) {
return silence;
}
factor = Fraction(factor);
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
return fastQuery.withHapTime((t) => t.div(factor));
@@ -1706,6 +1724,9 @@ export const hurry = register('hurry', function (r, pat) {
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
*/
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
if (factor === 0) {
return silence;
}
return pat._fast(Fraction(1).div(factor));
});
@@ -2032,7 +2053,7 @@ export const jux = register('jux', function (func, pat) {
* @example
* "<0 [2 4]>"
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
* .scale('C minor').note().legato(.2)
* .scale('C minor').note().clip(.2)
*/
export const { echoWith, echowith, stutWith, stutwith } = register(
['echoWith', 'echowith', 'stutWith', 'stutwith'],
@@ -2182,6 +2203,7 @@ export const velocity = register('velocity', function (velocity, pat) {
/**
*
* Multiplies the hap duration with the given factor.
* With samples, `clip` might be a better function to use ([more info](https://github.com/tidalcycles/strudel/pull/598))
* @name legato
* @memberof Pattern
* @example
@@ -2254,7 +2276,7 @@ const _loopAt = function (factor, pat, cps = 1) {
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
*/
const slice = register(
export const slice = register(
'slice',
function (npat, ipat, opat) {
return npat.innerBind((n) =>
@@ -2282,7 +2304,7 @@ const slice = register(
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
*/
const splice = register(
export const splice = register(
'splice',
function (npat, ipat, opat) {
const sliced = slice(npat, ipat, opat);
@@ -2299,7 +2321,10 @@ const splice = register(
false, // turns off auto-patternification
);
const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
// this function will be redefined in repl.mjs to use the correct cps value.
// It is still here to work in cases where repl.mjs is not used
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
return _loopAt(factor, pat, 1);
});
@@ -2316,6 +2341,6 @@ const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat)
* s("rhodes").loopAtCps(4,1.5).cps(1.5)
*/
// TODO - global cps clock
const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
return _loopAt(factor, pat, cps);
});
+39 -8
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
const scale = (normalized, min, max) => normalized * (max - min) + min;
const getValue = (e) => {
@@ -18,7 +18,7 @@ const getValue = (e) => {
}
note = note ?? n;
if (typeof note === 'string') {
return toMidi(note);
return noteToMidi(note);
}
if (typeof note === 'number') {
return note;
@@ -50,6 +50,7 @@ Pattern.prototype.pianoroll = function ({
timeframe: timeframeProp,
fold = 0,
vertical = 0,
labels = 0,
} = {}) {
const ctx = getDrawContext();
const w = ctx.canvas.width;
@@ -82,12 +83,12 @@ Pattern.prototype.pianoroll = function ({
ctx.fillRect(0, 0, w, h);
}
const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
events.filter(inFrame).forEach((event) => {
const isActive = event.whole.begin <= t && event.whole.end > t;
const isActive = event.whole.begin <= t && event.endClipped > t;
ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
@@ -114,6 +115,14 @@ Pattern.prototype.pianoroll = function ({
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
@@ -181,6 +190,7 @@ export function pianoroll({
timeframe: timeframeProp,
fold = 0,
vertical = 0,
labels = false,
ctx,
} = {}) {
const w = ctx.canvas.width;
@@ -240,7 +250,7 @@ export function pianoroll({
const color = event.value?.color || event.context?.color;
ctx.fillStyle = color || inactive;
ctx.strokeStyle = color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
@@ -267,6 +277,14 @@ export function pianoroll({
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
@@ -284,7 +302,7 @@ export function pianoroll({
return this;
}
function getOptions(drawTime, options = {}) {
export function getDrawOptions(drawTime, options = {}) {
let [lookbehind, lookahead] = drawTime;
lookbehind = Math.abs(lookbehind);
const cycles = lookahead + lookbehind;
@@ -293,5 +311,18 @@ function getOptions(drawTime, options = {}) {
}
Pattern.prototype.punchcard = function (options) {
return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }),
);
};
/* Pattern.prototype.pianoroll = function (options) {
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),
);
}; */
export function drawPianoroll(options) {
const { drawTime, ...rest } = options;
pianoroll({ ...getDrawOptions(drawTime), ...rest });
}
+47 -28
View File
@@ -3,6 +3,7 @@ import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register } from './pattern.mjs';
export function repl({
interval,
@@ -18,46 +19,26 @@ export function repl({
}) {
const scheduler = new Cyclist({
interval,
onTrigger: async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
},
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
onToggle,
});
const setPattern = (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
};
setTime(() => scheduler.now()); // TODO: refactor?
const evaluate = async (code, autostart = true) => {
if (!code) {
throw new Error('no code to evaluate');
}
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
await beforeEval?.({ code });
let { pattern } = await _evaluate(code, transpiler);
logger(`[eval] code updated`);
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
setPattern(pattern, autostart);
afterEval?.({ code, pattern });
return pattern;
} catch (err) {
@@ -70,9 +51,47 @@ export function repl({
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
// the following functions use the cps value, which is why they are defined here..
const loopAt = register('loopAt', (cycles, pat) => {
return pat.loopAtCps(cycles, scheduler.cps);
});
const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: scheduler.cps / hap.whole.duration, // overwrite speed completely?
unit: 'c',
})),
),
);
evalScope({
loopAt,
fit,
setCps,
setcps: setCps,
setCpm,
setcpm: setCpm,
});
return { scheduler, evaluate, start, stop, pause, setCps };
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
}
export const getTrigger =
({ getTime, defaultOutput }) =>
async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
};
+1 -1
View File
@@ -27,7 +27,7 @@ export const isaw2 = isaw.toBipolar();
*
* @return {Pattern}
* @example
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
* @example
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
*
+117
View File
@@ -0,0 +1,117 @@
import { Pattern } from './index.mjs';
// polar coords -> xy
function fromPolar(angle, radius, cx, cy) {
const radians = ((angle - 90) * Math.PI) / 180;
return [cx + Math.cos(radians) * radius, cy + Math.sin(radians) * radius];
}
const xyOnSpiral = (angle, margin, cx, cy, rotate = 0) => fromPolar((angle + rotate) * 360, margin * angle, cx, cy); // TODO: logSpiral
// draw spiral / segment of spiral
function spiralSegment(options) {
let {
ctx,
from = 0,
to = 3,
margin = 50,
cx = 100,
cy = 100,
rotate = 0,
thickness = margin / 2,
color = '#0000ff30',
cap = 'round',
stretch = 1,
fromOpacity = 1,
toOpacity = 1,
} = options;
from *= stretch;
to *= stretch;
rotate *= stretch;
ctx.lineWidth = thickness;
ctx.lineCap = cap;
ctx.strokeStyle = color;
ctx.globalAlpha = fromOpacity;
ctx.beginPath();
let [sx, sy] = xyOnSpiral(from, margin, cx, cy, rotate);
ctx.moveTo(sx, sy);
const increment = 1 / 60;
let angle = from;
while (angle <= to) {
const [x, y] = xyOnSpiral(angle, margin, cx, cy, rotate);
//ctx.lineWidth = angle*thickness;
ctx.globalAlpha = ((angle - from) / (to - from)) * toOpacity;
ctx.lineTo(x, y);
angle += increment;
}
ctx.stroke();
}
Pattern.prototype.spiral = function (options = {}) {
const {
stretch = 1,
size = 80,
thickness = size / 2,
cap = 'butt', // round butt squar,
inset = 3, // start angl,
playheadColor = '#ffffff90',
playheadLength = 0.02,
playheadThickness = thickness,
padding = 0,
steady = 1,
inactiveColor = '#ffffff20',
colorizeInactive = 0,
fade = true,
// logSpiral = true,
} = options;
function spiral({ ctx, time, haps, drawTime }) {
ctx.clearRect(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight);
const [cx, cy] = [ctx.canvas.width / 2, ctx.canvas.height / 2];
const settings = {
margin: size / stretch,
cx,
cy,
stretch,
cap,
thickness,
};
const playhead = {
...settings,
thickness: playheadThickness,
from: inset - playheadLength,
to: inset,
color: playheadColor,
};
const [min] = drawTime;
const rotate = steady * time;
haps.forEach((hap) => {
const isActive = hap.whole.begin <= time && hap.endClipped > time;
const from = hap.whole.begin - time + inset;
const to = hap.endClipped - time + inset - padding;
const { color } = hap.context;
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
spiralSegment({
ctx,
...settings,
from,
to,
rotate,
color: colorizeInactive || isActive ? color : inactiveColor,
fromOpacity: opacity,
toOpacity: opacity,
});
});
spiralSegment({
ctx,
...playhead,
rotate,
});
}
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
};
+40
View File
@@ -0,0 +1,40 @@
/*test for issue 302 support alternative solmization types */
import { sol2note } from '../util.mjs';
import { test } from 'vitest';
import assert from 'assert';
test('solmization - letters', () => {
const result = sol2note(60, 'letters');
const expected = 'C4';
assert.equal(result, expected);
});
test('solmization - solfeggio', () => {
const result = sol2note(60, 'solfeggio');
const expected = 'Do4';
assert.equal(result, expected);
});
test('solmization - indian', () => {
const result = sol2note(60, 'indian');
const expected = 'Sa4';
assert.equal(result, expected);
});
test('solmization - german', () => {
const result = sol2note(60, 'german');
const expected = 'C4';
assert.equal(result, expected);
});
test('solmization - byzantine', () => {
const result = sol2note(60, 'byzantine');
const expected = 'Ni4';
assert.equal(result, expected);
});
test('solmization - japanese', () => {
const result = sol2note(60, 'japanese');
const expected = 'I4';
assert.equal(result, expected);
});
+17 -17
View File
@@ -8,8 +8,8 @@ import { pure } from '../pattern.mjs';
import {
isNote,
tokenizeNote,
toMidi,
fromMidi,
noteToMidi,
midiToFreq,
freqToMidi,
_mod,
compose,
@@ -75,27 +75,27 @@ describe('isNote', () => {
expect(tokenizeNote(123)).toStrictEqual([]);
});
});
describe('toMidi', () => {
describe('noteToMidi', () => {
it('should turn notes into midi', () => {
expect(toMidi('A4')).toEqual(69);
expect(toMidi('C4')).toEqual(60);
expect(toMidi('Db4')).toEqual(61);
expect(toMidi('C3')).toEqual(48);
expect(toMidi('Cb3')).toEqual(47);
expect(toMidi('Cbb3')).toEqual(46);
expect(toMidi('C#3')).toEqual(49);
expect(toMidi('C#3')).toEqual(49);
expect(toMidi('C##3')).toEqual(50);
expect(noteToMidi('A4')).toEqual(69);
expect(noteToMidi('C4')).toEqual(60);
expect(noteToMidi('Db4')).toEqual(61);
expect(noteToMidi('C3')).toEqual(48);
expect(noteToMidi('Cb3')).toEqual(47);
expect(noteToMidi('Cbb3')).toEqual(46);
expect(noteToMidi('C#3')).toEqual(49);
expect(noteToMidi('C#3')).toEqual(49);
expect(noteToMidi('C##3')).toEqual(50);
});
it('should throw an error when given a non-note', () => {
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
});
});
describe('fromMidi', () => {
describe('midiToFreq', () => {
it('should turn midi into frequency', () => {
expect(fromMidi(69)).toEqual(440);
expect(fromMidi(57)).toEqual(220);
expect(midiToFreq(69)).toEqual(440);
expect(midiToFreq(57)).toEqual(220);
});
});
describe('freqToMidi', () => {
+73 -14
View File
@@ -6,12 +6,12 @@ This program is free software: you can redistribute it and/or modify it under th
// returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]?$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9])?$/)?.slice(1) || [];
if (!pc) {
return [];
}
@@ -19,16 +19,16 @@ export const tokenizeNote = (note) => {
};
// turns the given note into its midi number representation
export const toMidi = (note) => {
export const noteToMidi = (note) => {
const [pc, acc, oct = 3] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
}
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1, f: -1 }[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const fromMidi = (n) => {
export const midiToFreq = (n) => {
return Math.pow(2, (n - 69) / 12) * 440;
};
@@ -45,7 +45,7 @@ export const valueToMidi = (value, fallbackValue) => {
return freqToMidi(freq);
}
if (typeof note === 'string') {
return toMidi(note);
return noteToMidi(note);
}
if (typeof note === 'number') {
return note;
@@ -62,18 +62,19 @@ export const valueToMidi = (value, fallbackValue) => {
*/
export const getFreq = (noteOrMidi) => {
if (typeof noteOrMidi === 'number') {
return fromMidi(noteOrMidi);
return midiToFreq(noteOrMidi);
}
return fromMidi(toMidi(noteOrMidi));
return midiToFreq(noteToMidi(noteOrMidi));
};
const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
/**
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
* @deprecated only used in workshop (first-notes)
* @noAutocomplete
*/
export const midi2note = (n) => {
const oct = Math.floor(n / 12) - 1;
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
const pc = pcs[n % 12];
return pc + oct;
};
@@ -91,7 +92,7 @@ export const getPlayableNoteValue = (hap) => {
}
// if value is number => interpret as midi number as long as its not marked as frequency
if (typeof note === 'number' && context.type !== 'frequency') {
note = fromMidi(hap.value);
note = midiToFreq(hap.value);
} else if (typeof note === 'number' && context.type === 'frequency') {
note = hap.value; // legacy workaround.. will be removed in the future
} else if (typeof note !== 'string' || !isNote(note)) {
@@ -110,9 +111,9 @@ export const getFrequency = (hap) => {
return getFreq(value.note || value.n || value.value);
}
if (typeof value === 'number' && context.type !== 'frequency') {
value = fromMidi(hap.value);
value = midiToFreq(hap.value);
} else if (typeof value === 'string' && isNote(value)) {
value = fromMidi(toMidi(hap.value));
value = midiToFreq(noteToMidi(hap.value));
} else if (typeof value !== 'number') {
throw new Error('not a note or frequency: ' + value);
}
@@ -170,7 +171,7 @@ export function parseNumeral(numOrString) {
return asNumber;
}
if (isNote(numOrString)) {
return toMidi(numOrString);
return noteToMidi(numOrString);
}
throw new Error(`cannot parse as numeral: "${numOrString}"`);
}
@@ -212,3 +213,61 @@ export const splitAt = function (index, value) {
};
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
/* solmization, not used yet */
const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/
const indian = [
'Sa',
'Re',
'Ga',
'Ma',
'Pa',
'Dha',
'Ni',
]; /*indian musical notes, seems like they do not use flats or sharps*/
const german = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Hb', 'H']; /*german & dutch musical notes*/
const byzantine = [
'Ni',
'Pab',
'Pa',
'Voub',
'Vou',
'Ga',
'Dib',
'Di',
'Keb',
'Ke',
'Zob',
'Zo',
]; /*byzantine musical notes*/
const japanese = [
'I',
'Ro',
'Ha',
'Ni',
'Ho',
'He',
'To',
]; /*traditional japanese musical notes, seems like they do not use falts or sharps*/
const english = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
export const sol2note = (n, notation = 'letters') => {
const pc =
notation === 'solfeggio'
? solfeggio /*check if its is any of the following*/
: notation === 'indian'
? indian
: notation === 'german'
? german
: notation === 'byzantine'
? byzantine
: notation === 'japanese'
? japanese
: english; /*if not use standard version*/
const note = pc[n % 12]; /*calculating the midi value to the note*/
const oct = Math.floor(n / 12) - 1;
return note + oct;
};
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/csound",
"version": "0.6.2",
"version": "0.8.0",
"description": "csound bindings for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -32,11 +32,11 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@csound/browser": "6.18.5",
"@csound/browser": "6.18.7",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.6.2",
"version": "0.8.0",
"description": "Code evaluator for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -44,7 +44,7 @@
},
"devDependencies": {
"@strudel.cycles/mini": "workspace:*",
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+3 -3
View File
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import * as _WebMidi from 'webmidi';
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { toMidi } from '@strudel.cycles/core';
import { noteToMidi } from '@strudel.cycles/core';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -105,7 +105,7 @@ Pattern.prototype.midi = function (output) {
hap.ensureObjectValue();
// calculate time
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
const timingOffset = WebMidi.time - getAudioContext().getOutputTimestamp().contextTime * 1000;
time = time * 1000 + timingOffset;
// destructure value
@@ -114,7 +114,7 @@ Pattern.prototype.midi = function (output) {
const duration = hap.duration.valueOf() * 1000 - 5;
if (note) {
const midiNumber = toMidi(note);
const midiNumber = noteToMidi(note);
device.playNote(midiNumber, midichan, {
time,
duration,
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.6.0",
"version": "0.8.0",
"description": "Midi API for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -31,9 +31,9 @@
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"webmidi": "^3.0.21"
"webmidi": "^3.1.5"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
+31 -17
View File
@@ -1,4 +1,4 @@
// Generated by Peggy 2.0.1.
// Generated by Peggy 3.0.2.
//
// https://peggyjs.org/
@@ -43,16 +43,19 @@ peg$SyntaxError.prototype.format = function(sources) {
}
}
var s = this.location.start;
var loc = this.location.source + ":" + s.line + ":" + s.column;
var offset_s = (this.location.source && (typeof this.location.source.offset === "function"))
? this.location.source.offset(s)
: s;
var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
if (src) {
var e = this.location.end;
var filler = peg$padEnd("", s.line.toString().length, ' ');
var filler = peg$padEnd("", offset_s.line.toString().length, ' ');
var line = src[s.line - 1];
var last = s.line === e.line ? e.column : line.length + 1;
var hatLen = (last - s.column) || 1;
str += "\n --> " + loc + "\n"
+ filler + " |\n"
+ s.line + " | " + line + "\n"
+ offset_s.line + " | " + line + "\n"
+ filler + " | " + peg$padEnd("", s.column - 1, ' ')
+ peg$padEnd("", hatLen, "^");
} else {
@@ -279,7 +282,7 @@ function peg$parse(input, options) {
var peg$f8 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
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$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
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) {
@@ -289,8 +292,8 @@ function peg$parse(input, options) {
};
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$f16 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } 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 }}};
@@ -422,11 +425,11 @@ function peg$parse(input, options) {
}
}
function peg$computeLocation(startPos, endPos) {
function peg$computeLocation(startPos, endPos, offset) {
var startPosDetails = peg$computePosDetails(startPos);
var endPosDetails = peg$computePosDetails(endPos);
return {
var res = {
source: peg$source,
start: {
offset: startPos,
@@ -439,6 +442,11 @@ function peg$parse(input, options) {
column: endPosDetails.column
}
};
if (offset && peg$source && (typeof peg$source.offset === "function")) {
res.start = peg$source.offset(res.start);
res.end = peg$source.offset(res.end);
}
return res;
}
function peg$fail(expected) {
@@ -1511,18 +1519,20 @@ function peg$parse(input, options) {
}
function peg$parsemini() {
var s0, s1, s2, s3, s4;
var s0, s1, s2, s3, s4, s5, s6;
s0 = peg$currPos;
s1 = peg$parsews();
s2 = peg$parsequote();
if (s2 !== peg$FAILED) {
s3 = peg$parsestack_or_choose();
if (s3 !== peg$FAILED) {
s4 = peg$parsequote();
if (s4 !== peg$FAILED) {
s3 = peg$parsews();
s4 = peg$parsestack_or_choose();
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
s6 = peg$parsequote();
if (s6 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f19(s3);
s0 = peg$f19(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2185,10 +2195,13 @@ function peg$parse(input, options) {
this.location_ = location();
}
var PatternStub = function(source, alignment)
var PatternStub = function(source, alignment, seed)
{
this.type_ = "pattern";
this.arguments_ = { alignment : alignment};
this.arguments_ = { alignment: alignment };
if (seed !== undefined) {
this.arguments_.seed = seed;
}
this.source_ = source;
}
@@ -2214,6 +2227,7 @@ function peg$parse(input, options) {
this.options_ = options;
}
var seed = 0;
peg$result = peg$startRuleFunction();
+10 -6
View File
@@ -19,10 +19,13 @@ This program is free software: you can redistribute it and/or modify it under th
this.location_ = location();
}
var PatternStub = function(source, alignment)
var PatternStub = function(source, alignment, seed)
{
this.type_ = "pattern";
this.arguments_ = { alignment : alignment};
this.arguments_ = { alignment: alignment };
if (seed !== undefined) {
this.arguments_.seed = seed;
}
this.source_ = source;
}
@@ -48,6 +51,7 @@ This program is free software: you can redistribute it and/or modify it under th
this.options_ = options;
}
var seed = 0;
}
start = statement
@@ -137,7 +141,7 @@ op_fast = "*"a:slice
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
op_degrade = "?"a:number?
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) }
op_tail = ":" s:slice
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
@@ -162,12 +166,12 @@ stack_tail = tail:(comma @sequence)+
// a choose is a series of pipe-separated sequence, one of which is
// chosen at random, each cycle
choose_tail = tail:(pipe @sequence)+
{ return { alignment: 'rand', list: tail }; }
{ return { alignment: 'rand', list: tail, seed: seed++ }; }
// if the stack contains only one element, we don't create a stack but return the
// underlying element
stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
polymeter_stack = head:sequence tail:stack_tail?
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
@@ -178,7 +182,7 @@ polymeter_stack = head:sequence tail:stack_tail?
// Experimental haskellish parser begins
// mini-notation = a quoted stack
mini = ws quote sc:stack_or_choose quote
mini = ws quote ws sc:stack_or_choose ws quote
{ return sc; }
// ------------------ operators ---------------------------
+10 -30
View File
@@ -7,12 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import * as krill from './krill-parser.js';
import * as strudel from '@strudel.cycles/core';
/* var _seedState = 0;
const randOffset = 0.0002;
function _nextSeed() {
return _seedState++;
} */
const randOffset = 0.0003;
const applyOptions = (parent, code) => (pat, i) => {
const ast = parent.source_[i];
@@ -44,27 +39,9 @@ const applyOptions = (parent, code) => (pat, i) => {
break;
}
case 'degradeBy': {
// TODO: find out what is right here
// example:
/*
stack(
s("hh*8").degrade(),
s("[ht*8]?")
)
*/
// above example will only be in sync when _degradeBy is used...
// it also seems that the nextSeed will create undeterministic behaviour
// as it uses a global _seedState. This is probably the reason for
// https://github.com/tidalcycles/strudel/issues/245
// this is how it was:
/*
return strudel.reify(pat)._degradeByWith(
strudel.rand.early(randOffset * _nextSeed()).segment(1),
op.arguments_.amount ?? 0.5,
);
*/
pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
pat = strudel
.reify(pat)
._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5);
break;
}
case 'tail': {
@@ -114,9 +91,7 @@ export function patternifyAST(ast, code) {
return strudel.stack(...aligned);
}
if (alignment === 'rand') {
// https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422
// return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
return strudel.chooseCycles(...children);
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
}
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (!weightedChildren && alignment === 'slowcat') {
@@ -215,3 +190,8 @@ export function minify(thing) {
}
return strudel.reify(thing);
}
// calling this function will cause patterns to parse strings as mini notation by default
export function miniAllStrings() {
strudel.setStringParser(mini);
}
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.6.0",
"version": "0.8.2",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
@@ -35,8 +35,8 @@
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"peggy": "^2.0.1",
"vite": "^3.2.2",
"vitest": "^0.25.7"
"peggy": "^3.0.2",
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+19 -5
View File
@@ -140,10 +140,22 @@ 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 multiple independent uses of the random choice operator ("|")', () => {
const numCycles = 1000;
const values = mini('[a|b] [a|b]')
.queryArc(0, numCycles)
.map((e) => e.value);
const observed = { aa: 0, ab: 0, ba: 0, bb: 0 };
for (let i = 0; i < values.length; i += 2) {
const chunk = values.slice(i, i + 2);
observed[chunk.join('')]++;
}
for (const count of Object.values(observed)) {
// Should fall within 99% confidence interval for binomial with p=0.25.
expect(215 <= count && count <= 286).toBe(true);
}
});
/*it('supports the random choice operator ("|") with nesting', () => {
it('supports the random choice operator ("|") with nesting', () => {
const numCycles = 900;
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
@@ -168,6 +180,8 @@ describe('mini', () => {
// 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
// PRNG, this test should succeed
expect(chisq <= 15.086).toBe(true);
// assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
});*/
});
it('supports lists', () => {
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
});
});
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.6.0",
"version": "0.8.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"publishConfig": {
@@ -40,7 +40,7 @@
"osc-js": "^2.4.0"
},
"devDependencies": {
"pkg": "^5.7.0",
"vite": "^3.2.2"
"pkg": "^5.8.1",
"vite": "^4.3.3"
}
}
@@ -15,17 +15,18 @@
"@strudel.cycles/osc": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/react": "workspace:*"
},
"devDependencies": {
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.0.1",
"autoprefixer": "^10.4.8",
"postcss": "^8.4.16",
"tailwindcss": "^3.1.8",
"vite": "^3.0.7"
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"tailwindcss": "^3.3.2",
"vite": "^4.3.3"
}
}
+27 -15
View File
@@ -1,22 +1,34 @@
import { controls, evalScope } from '@strudel.cycles/core';
import { CodeMirror, useHighlighting, useKeydown, useStrudel, flash } from '@strudel.cycles/react';
import { getAudioContext, initAudioOnFirstClick, panic, webaudioOutput } from '@strudel.cycles/webaudio';
import {
getAudioContext,
initAudioOnFirstClick,
panic,
webaudioOutput,
registerSynthSounds,
} from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { useCallback, useState } from 'react';
import './style.css';
// import { prebake } from '../../../../../repl/src/prebake.mjs';
initAudioOnFirstClick();
// TODO: only import stuff when play is pressed?
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
);
async function init() {
// TODO: only import stuff when play is pressed?
const loadModules = evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
}
init();
const defaultTune = `samples({
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
@@ -31,7 +43,7 @@ stack(
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
.add(perlin.range(0,.5)) // random pitch variation
.superimpose(add(.05)) // add second, slightly detuned voice
.n() // wrap in "n"
.note() // wrap in "note"
.decay(.15).sustain(0) // make each note of equal length
.s('sawtooth') // waveform
.gain(.4) // turn down
@@ -40,7 +52,7 @@ stack(
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.note() // wrap in "n"
.s('square') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
@@ -49,7 +61,7 @@ stack(
,"a4 c5 <e6 a6>".struct("x(5,8)")
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.note() // wrap in "note"
.decay(.1).sustain(0) // make notes short
.s('triangle') // waveform
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
@@ -103,7 +115,7 @@ function App() {
}
}
},
[scheduler, evaluate, view],
[scheduler, evaluate, view, code],
),
);
return (
+16 -16
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/react",
"version": "0.6.4",
"version": "0.8.0",
"description": "React components for strudel",
"main": "src/index.js",
"publishConfig": {
@@ -32,18 +32,18 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.4.0",
"@codemirror/lang-javascript": "^6.1.1",
"@codemirror/autocomplete": "^6.6.0",
"@codemirror/lang-javascript": "^6.1.7",
"@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",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@replit/codemirror-emacs": "^6.0.1",
"@replit/codemirror-vim": "^6.0.14",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@uiw/codemirror-themes": "^4.12.4",
"@uiw/react-codemirror": "^4.12.4",
"@uiw/codemirror-themes": "^4.19.16",
"@uiw/react-codemirror": "^4.19.16",
"react-hook-inview": "^4.5.0"
},
"peerDependencies": {
@@ -51,14 +51,14 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
"@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",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.0.24",
"vite": "^3.2.2"
"tailwindcss": "^3.3.2",
"vite": "^4.3.3"
}
}
+34 -16
View File
@@ -2,12 +2,12 @@ import React, { useMemo } from 'react';
import _CodeMirror from '@uiw/react-codemirror';
import { EditorView, Decoration } from '@codemirror/view';
import { StateField, StateEffect } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import { javascript, javascriptLanguage } 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 { strudelAutocomplete } from './Autocomplete';
import { vim } from '@replit/codemirror-vim';
import { emacs } from '@replit/codemirror-emacs';
@@ -58,15 +58,19 @@ const highlightField = StateField.define({
haps
.map((hap) =>
(hap.context.locations || []).map(({ start, end }) => {
// const color = hap.context.color || e.value.color || '#FFCA28';
const color = hap.context.color || e.value.color;
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: 2px solid ${color};` } });
const mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
let mark;
if (color) {
mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
} else {
mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
}
return mark.range(from, to);
}),
)
@@ -84,14 +88,7 @@ const highlightField = StateField.define({
provide: (f) => EditorView.decorations.from(f),
});
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
];
const staticExtensions = [javascript(), highlightField, flashField];
export default function CodeMirror({
value,
@@ -100,6 +97,9 @@ export default function CodeMirror({
onSelectionChange,
theme,
keybindings,
isLineNumbersDisplayed,
isAutoCompletionEnabled,
isLineWrappingEnabled,
fontSize = 18,
fontFamily = 'monospace',
options,
@@ -111,12 +111,14 @@ export default function CodeMirror({
},
[onChange],
);
const handleOnCreateEditor = useCallback(
(view) => {
onViewChanged?.(view);
},
[onViewChanged],
);
const handleOnUpdate = useCallback(
(viewUpdate) => {
if (viewUpdate.selectionSet && onSelectionChange) {
@@ -125,16 +127,31 @@ export default function CodeMirror({
},
[onSelectionChange],
);
const extensions = useMemo(() => {
let _extensions = [...staticExtensions];
let bindings = {
vim,
emacs,
};
if (bindings[keybindings]) {
return [...staticExtensions, bindings[keybindings]()];
_extensions.push(bindings[keybindings]());
}
return staticExtensions;
}, [keybindings]);
if (isAutoCompletionEnabled) {
_extensions.push(javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }));
} else {
_extensions.push(autocompletion({ override: [] }));
}
if (isLineWrappingEnabled) {
_extensions.push(EditorView.lineWrapping);
}
return _extensions;
}, [keybindings, isAutoCompletionEnabled, isLineWrappingEnabled]);
return (
<div style={{ fontSize, fontFamily }} className="w-full">
<_CodeMirror
@@ -144,6 +161,7 @@ export default function CodeMirror({
onCreateEditor={handleOnCreateEditor}
onUpdate={handleOnUpdate}
extensions={extensions}
basicSetup={{ lineNumbers: isLineNumbersDisplayed }}
/>
</div>
);
+61 -49
View File
@@ -18,16 +18,24 @@ export function MiniRepl({
tune,
hideOutsideView = false,
enableKeyboard,
onTrigger,
drawTime,
punchcard,
punchcardLabels,
onPaint,
canvasHeight = 200,
fontSize = 18,
fontFamily,
hideHeader = false,
theme,
keybindings,
isLineNumbersDisplayed,
}) {
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
const evalOnMount = !!drawTime;
const drawContext = useCallback(
!!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
[drawTime],
punchcard ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
[punchcard],
);
const {
code,
@@ -47,7 +55,18 @@ export function MiniRepl({
} = useStrudel({
initialCode: tune,
defaultOutput: webaudioOutput,
editPattern: (pat) => (punchcard ? pat.punchcard() : pat),
editPattern: (pat, id) => {
//pat = pat.withContext((ctx) => ({ ...ctx, id }));
if (onTrigger) {
pat = pat.onTrigger(onTrigger, false);
}
if (onPaint) {
pat = pat.onPaint(onPaint);
} else if (punchcard) {
pat = pat.punchcard({ labels: punchcardLabels });
}
return pat;
},
getTime,
evalOnMount,
drawContext,
@@ -82,7 +101,7 @@ export function MiniRepl({
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === 'Period') {
} else if (e.key === '.' || e.code === 'Period') {
stop();
e.preventDefault();
}
@@ -93,26 +112,6 @@ export function MiniRepl({
),
);
// set active pattern on ctrl+enter
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === 'Period') {
stop();
e.preventDefault();
}
}
};
window.addEventListener('keydown', handleKeyPress, true);
return () => window.removeEventListener('keydown', handleKeyPress, true);
}
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
const [log, setLog] = useState([]);
useLogger(
useCallback((e) => {
@@ -121,7 +120,7 @@ export function MiniRepl({
// const logId = data?.pattern?.meta?.id;
if (logId === replId) {
setLog((l) => {
return l.concat([e.detail]).slice(-10);
return l.concat([e.detail]).slice(-8);
});
}
}, []),
@@ -129,33 +128,46 @@ export function MiniRepl({
return (
<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(
'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>
{!hideHeader && (
<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(
'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>
</div>
{error && <div className="text-right p-1 text-sm text-red-200">{error.message}</div>}
</div>
)}
<div className="overflow-auto relative">
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} theme={theme} />}
{show && (
<CodeMirror6
value={code}
onChange={setCode}
onViewChanged={setView}
theme={theme}
fontFamily={fontFamily}
fontSize={fontSize}
keybindings={keybindings}
isLineNumbersDisplayed={isLineNumbersDisplayed}
/>
)}
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
</div>
{drawTime && (
{punchcard && (
<canvas
id={canvasId}
className="w-full pointer-events-none"
+5 -1
View File
@@ -1,6 +1,6 @@
:root {
--background: #222;
--lineBackground: #22222250;
--lineBackground: #22222299;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
@@ -24,3 +24,7 @@
.cm-theme-light {
width: 100%;
}
footer {
z-index: 0 !important;
}
+1 -1
View File
@@ -17,7 +17,7 @@ function useHighlighting({ view, pattern, active, getTime }) {
const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy
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
highlights.current = highlights.current.filter((hap) => hap.endClipped > 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({ haps: highlights.current }) }); // highlight all still active + new active haps
+4 -3
View File
@@ -29,7 +29,8 @@ function useStrudel({
const [pattern, setPattern] = useState();
const [started, setStarted] = useState(false);
const isDirty = code !== activeCode;
const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
//const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
const shouldPaint = useCallback((pat) => !!pat?.context?.onPaint, []);
// TODO: make sure this hook reruns when scheduler.started changes
const { scheduler, evaluate, start, stop, pause, setCps } = useMemo(
@@ -46,9 +47,9 @@ function useStrudel({
drawContext,
transpiler,
editPattern,
beforeEval: ({ code }) => {
beforeEval: async ({ code }) => {
setCode(code);
beforeEval?.();
await beforeEval?.();
},
afterEval: (res) => {
const { pattern: _pattern, code } = res;
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/serial",
"version": "0.6.0",
"version": "0.8.0",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"publishConfig": {
@@ -32,6 +32,6 @@
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
+46 -7
View File
@@ -1,4 +1,6 @@
import { toMidi } from '@strudel.cycles/core';
import { noteToMidi, freqToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound, getEnvelope } from '@strudel.cycles/webaudio';
import gm from './gm.mjs';
let loadCache = {};
async function loadFont(name) {
@@ -8,7 +10,6 @@ async function loadFont(name) {
const load = async () => {
// TODO: make soundfont source configurable
const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`;
console.log('load font', name, url);
const preset = await fetch(url).then((res) => res.text());
let [_, data] = preset.split('={');
return eval('{' + data);
@@ -17,15 +18,24 @@ async function loadFont(name) {
return loadCache[name];
}
export async function getFontBufferSource(name, pitch, ac) {
if (typeof pitch === 'string') {
pitch = toMidi(pitch);
export async function getFontBufferSource(name, value, ac) {
let { note = 'c3', freq } = value;
let midi;
if (freq) {
midi = freqToMidi(freq);
} else if (typeof note === 'string') {
midi = noteToMidi(note);
} else if (typeof note === 'number') {
midi = note;
} else {
throw new Error(`unexpected "note" type "${typeof note}"`);
}
const { buffer, zone } = await getFontPitch(name, pitch, ac);
const { buffer, zone } = await getFontPitch(name, midi, ac);
const src = ac.createBufferSource();
src.buffer = buffer;
const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune;
const playbackRate = 1.0 * Math.pow(2, (100.0 * pitch - baseDetune) / 1200.0);
const playbackRate = 1.0 * Math.pow(2, (100.0 * midi - baseDetune) / 1200.0);
// src detune?
src.playbackRate.value = playbackRate;
const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd;
@@ -114,3 +124,32 @@ async function getBuffer(zone, audioContext) {
}
}
}
export function registerSoundfonts() {
Object.entries(gm).forEach(([name, fonts]) => {
registerSound(
name,
async (time, value, onended) => {
const { n = 0 } = value;
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
const font = fonts[n % fonts.length];
const ctx = getAudioContext();
const bufferSource = await getFontBufferSource(font, value, ctx);
bufferSource.start(time);
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 0.3, time);
bufferSource.connect(envelope);
const stop = (releaseTime) => {
bufferSource.stop(releaseTime + release);
releaseEnvelope(releaseTime);
};
bufferSource.onended = () => {
bufferSource.disconnect();
envelope.disconnect();
onended();
};
return { node: envelope, stop };
},
{ type: 'soundfont', prebake: true, fonts },
);
});
}
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
import { getFontBufferSource } from './fontloader.mjs';
import { getFontBufferSource, registerSoundfonts } from './fontloader.mjs';
import * as soundfontList from './list.mjs';
import { startPresetNote } from 'sfumato';
import { loadSoundfont } from './sfumato.mjs';
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList };
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts };
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.6.0",
"version": "0.8.2",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -36,7 +36,7 @@
"soundfont2": "^0.4.0"
},
"devDependencies": {
"node-fetch": "^3.2.6",
"vite": "^3.2.2"
"node-fetch": "^3.3.1",
"vite": "^4.3.3"
}
}
+27 -3
View File
@@ -1,5 +1,5 @@
import { Pattern, getPlayableNoteValue, toMidi } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { Pattern, getPlayableNoteValue, noteToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound } from '@strudel.cycles/webaudio';
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
Pattern.prototype.soundfont = function (sf, n = 0) {
@@ -8,7 +8,7 @@ Pattern.prototype.soundfont = function (sf, n = 0) {
const note = getPlayableNoteValue(h);
const preset = sf.presets[n % sf.presets.length];
const deadline = ctx.currentTime + t - ct;
const args = [ctx, preset, toMidi(note), deadline];
const args = [ctx, preset, noteToMidi(note), deadline];
const stop = startPresetNote(...args);
stop(deadline + h.duration);
});
@@ -21,5 +21,29 @@ export function loadSoundfont(url) {
}
const sf = _loadSoundfont(url);
soundfontCache.set(url, sf);
/*sf.then((font) => {
font.presets.forEach((preset) => {
console.log('preset', preset.header.name);
registerSound(
preset.header.name.replaceAll(' ', '_'),
(time, value, onended) => {
const ctx = getAudioContext();
let { note } = value; // freq ?
const p = font.presets.find((p) => p.header.name === preset.header.name);
if (!p) {
throw new Error('preset not found');
}
const deadline = time; // - ctx.currentTime;
const args = [ctx, p, noteToMidi(note), deadline];
const stop = startPresetNote(...args);
return { node: undefined, stop };
},
{ type: 'soundfont' },
);
});
//console.log('f', f);
});*/
return sf;
}
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.6.0",
"version": "0.8.2",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -34,10 +34,10 @@
"@strudel.cycles/core": "workspace:*",
"@tonaljs/tonal": "^4.7.2",
"chord-voicings": "^0.0.1",
"webmidi": "^3.0.21"
"webmidi": "^3.1.5"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+1 -1
View File
@@ -145,7 +145,7 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
export const scale = register('scale', function (scale, pat) {
// Supports ':' list syntax in mininotation
if (Array.isArray(scale)) {
scale = scale.join(' ');
scale = scale.flat().join(' ');
}
return pat.withHap((hap) => {
const isObject = typeof hap.value === 'object';
+44 -1
View File
@@ -6,10 +6,53 @@ This program is free software: you can redistribute it and/or modify it under th
import { stack, register } from '@strudel.cycles/core';
import _voicings from 'chord-voicings';
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
const { dictionaryVoicing, minTopNoteDiff } = _voicings.default || _voicings; // parcel module resolution fuckup
const lefthand = {
m7: ['3m 5P 7m 9M', '7m 9M 10m 12P'],
7: ['3M 6M 7m 9M', '7m 9M 10M 13M'],
'^7': ['3M 5P 7M 9M', '7M 9M 10M 12P'],
69: ['3M 5P 6A 9M'],
m7b5: ['3m 5d 7m 8P', '7m 8P 10m 12d'],
'7b9': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
'7b13': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
o7: ['1P 3m 5d 6M', '5d 6M 8P 10m'],
'7#11': ['7m 9M 11A 13A'],
'7#9': ['3M 7m 9A'],
mM7: ['3m 5P 7M 9M', '7M 9M 10m 12P'],
m6: ['3m 5P 6M 9M', '6M 9M 10m 12P'],
};
const guidetones = {
m7: ['3m 7m', '7m 10m'],
m9: ['3m 7m', '7m 10m'],
7: ['3M 7m', '7m 10M'],
'^7': ['3M 7M', '7M 10M'],
'^9': ['3M 7M', '7M 10M'],
69: ['3M 6M'],
6: ['3M 6M', '6M 10M'],
m7b5: ['3m 7m', '7m 10m'],
'7b9': ['3M 7m', '7m 10M'],
'7b13': ['3M 7m', '7m 10M'],
o7: ['3m 6M', '6M 10m'],
'7#11': ['3M 7m', '7m 10M'],
'7#9': ['3M 7m', '7m 10M'],
mM7: ['3m 7M', '7M 10m'],
m6: ['3m 6M', '6M 10m'],
};
const triads = {
'': ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
M: ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
m: ['1P 3m 5P', '3m 5P 8P', '5P 8P 10m'],
o: ['1P 3m 5d', '3m 5d 8P', '5d 8P 10m'],
aug: ['1P 3m 5A', '3m 5A 8P', '5A 8P 10m'],
};
export const voicingRegistry = {
lefthand: { dictionary: lefthand, range: ['F3', 'A4'] },
triads: { dictionary: triads },
guidetones: { dictionary: guidetones },
};
export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.6.0",
"version": "0.8.0",
"description": "Tone.js API for strudel",
"main": "index.mjs",
"type": "module",
@@ -34,7 +34,7 @@
"tone": "^14.7.77"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/transpiler",
"version": "0.6.0",
"version": "0.8.2",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"publishConfig": {
@@ -36,7 +36,7 @@
"estree-walker": "^3.0.1"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.8"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+82
View File
@@ -0,0 +1,82 @@
# @strudel/web
This package provides an easy to use bundle of multiple strudel packages for the web.
## Usage
Save this code as a `.html` file and double click it:
```html
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
```
With the help of [skypack](https://www.skypack.dev/), you don't need a bundler nor a server.
As soon as you call `initStrudel()`, all strudel functions are made available.
In this case, we are using the `note` function to create a pattern.
To actually play the pattern, you have to append `.play()` to the end.
Note: Due to the [Autoplay policy](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy), you can only play audio in a browser after a click event.
### Via npm
If you're using a bundler, you can install the package via `npm i @strudel/web`, then just import it like:
```js
import { initStrudel } from '@strudel/web';
```
The rest of the code should be the same. Check out [vite](https://vitejs.dev/) for a good bundler / dev server.
### Loading samples
By default, no external samples are loaded, but you can add them like this:
```js
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
document.getElementById('play').addEventListener('click',
() => s("bd sd").play()
)
```
You can learn [more about the `samples` function here](https://strudel.tidalcycles.org/learn/samples#loading-custom-samples).
### Evaluating Code
Instead of creating patterns directly in JS, you might also want to take in user input and turn that into a pattern.
This is called evaluation: Taking a piece of code and executing it on the fly.
To do that, you can use the `evaluate` function:
```js
initStrudel();
document.getElementById('play').addEventListener('click',
() => evaluate('note("c a f e").jux(rev)')
);
document.getElementById('play').addEventListener('stop',
() => hush()
);
```
### Double vs Single Quotes
There is a tiny difference between the [Strudel REPL](https://strudel.tidalcycles.org/) and `@strudel/web`.
In the REPL you can use 'single quotes' for regular JS strings and "double quotes" for mini notation patterns.
In `@strudel/web`, it does not matter which types of quotes you're using.
There will probably be an escapte hatch for that in the future.
## More Examples
Check out the examples folder for more examples, both using plain html and vite!
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
document.getElementById('play').addEventListener('stop', () => hush());
</script>
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<button id="a">A</button>
<button id="b">B</button>
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
click('stop', () => hush());
</script>
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="https://strudel.tidalcycles.org/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@strudel/web REPL Example</title>
</head>
<body>
<div id="app"></div>
<button id="a">A</button>
<button id="b">B</button>
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from '@strudel/web';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
click('stop', () => hush());
</script>
</body>
</html>
@@ -0,0 +1,18 @@
{
"name": "repl-example",
"private": true,
"version": "0.0.0",
"type": "module",
"license": "AGPL-3.0-or-later",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.3.2"
},
"dependencies": {
"@strudel/web": "workspace:*"
}
}
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
document.getElementById('play').addEventListener('click', () => s('[bd sd](3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
+45
View File
@@ -0,0 +1,45 @@
{
"name": "@strudel/web",
"version": "0.8.3",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"main": "web.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": [
"Alex McLean <alex@slab.org>"
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
},
"devDependencies": {
"vite": "^4.3.3"
}
}
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'web.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+66
View File
@@ -0,0 +1,66 @@
export * from '@strudel.cycles/core';
export * from '@strudel.cycles/webaudio';
//export * from '@strudel.cycles/soundfonts';
export * from '@strudel.cycles/transpiler';
export * from '@strudel.cycles/mini';
export * from '@strudel.cycles/tonal';
export * from '@strudel.cycles/webaudio';
import { Pattern, evalScope, controls } from '@strudel.cycles/core';
import { initAudioOnFirstClick, registerSynthSounds, webaudioScheduler } from '@strudel.cycles/webaudio';
// import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { evaluate as _evaluate } from '@strudel.cycles/transpiler';
import { miniAllStrings } from '@strudel.cycles/mini';
// init logic
export async function defaultPrebake() {
const loadModules = evalScope(
evalScope,
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
{ hush, evaluate },
);
await Promise.all([loadModules, registerSynthSounds() /* , registerSoundfonts() */]);
}
// when this function finishes, everything is initialized
let initDone;
let scheduler;
export function initStrudel(options = {}) {
initAudioOnFirstClick();
miniAllStrings();
const { prebake, ...schedulerOptions } = options;
initDone = (async () => {
await defaultPrebake();
await prebake?.();
})();
scheduler = webaudioScheduler(schedulerOptions);
}
window.initStrudel = initStrudel;
// this method will play the pattern on the default scheduler
Pattern.prototype.play = function () {
if (!scheduler) {
throw new Error('.play: no scheduler found. Have you called init?');
}
initDone.then(() => {
scheduler.setPattern(this, true);
});
return this;
};
// stop playback
export function hush() {
scheduler.stop();
}
// evaluate and play the given code using the transpiler
export async function evaluate(code, autoplay = true) {
const { pattern } = await _evaluate(code);
autoplay && pattern.play();
}
+70
View File
@@ -0,0 +1,70 @@
import { getAudioContext } from './webaudio.mjs';
export function gainNode(value) {
const node = getAudioContext().createGain();
node.gain.value = value;
return node;
}
export const getOscillator = ({ s, freq, t }) => {
// make oscillator
const o = getAudioContext().createOscillator();
o.type = s || 'triangle';
o.frequency.value = Number(freq);
o.start(t);
//o.stop(t + duration + release);
const stop = (time) => o.stop(time);
return { node: o, stop };
};
// alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future
export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
// sustain end
return {
node: gainNode,
stop: (t) => {
//if (typeof gainNode.gain.cancelAndHoldAtTime === 'function') {
// gainNode.gain.cancelAndHoldAtTime(t); // this seems to release instantly....
// see https://discord.com/channels/779427371270275082/937365093082079272/1086053607360712735
//} else {
// firefox: this will glitch when the sustain has not been reached yet at the time of release
gainNode.gain.setValueAtTime(sustain * velocity, t);
//}
gainNode.gain.linearRampToValueAtTime(0, t + release);
},
};
};
export const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
// for some reason, using exponential ramping creates little cracklings
/* let t = begin;
gainNode.gain.setValueAtTime(0, t);
gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack));
const sustainGain = Math.max(sustain * velocity, 0.001);
gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay));
if (end - begin < attack + decay) {
gainNode.gain.cancelAndHoldAtTime(end);
} else {
gainNode.gain.setValueAtTime(sustainGain, end);
}
gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */
return gainNode;
};
export const getFilter = (type, frequency, Q) => {
const filter = getAudioContext().createBiquadFilter();
filter.type = type;
filter.frequency.value = frequency;
filter.Q.value = Q;
return filter;
};
+2
View File
@@ -6,3 +6,5 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './webaudio.mjs';
export * from './sampler.mjs';
export * from './helpers.mjs';
export * from './synth.mjs';
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.6.1",
"version": "0.8.2",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
@@ -34,9 +34,10 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*"
"@strudel.cycles/core": "workspace:*",
"nanostores": "^0.8.1"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
+159 -54
View File
@@ -1,5 +1,6 @@
import { logger, toMidi, valueToMidi } from '@strudel.cycles/core';
import { getAudioContext } from './index.mjs';
import { logger, noteToMidi, valueToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound } from './index.mjs';
import { getEnvelope } from './helpers.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer>
const loadCache = {}; // string: Promise<ArrayBuffer>
@@ -20,7 +21,7 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u];
}
export const getSampleBufferSource = async (s, n, note, speed, freq) => {
export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resolveUrl) => {
let transpose = 0;
if (freq !== undefined && note !== undefined) {
logger('[sampler] hap has note and freq. ignoring note', 'warning');
@@ -29,28 +30,11 @@ export const getSampleBufferSource = async (s, n, note, speed, freq) => {
transpose = midi - 36; // C3 is middle C
const ac = getAudioContext();
// is sample from loaded samples(..)
const samples = getLoadedSamples();
if (!samples) {
throw new Error('no samples loaded');
}
const bank = samples?.[s];
if (!bank) {
throw new Error(
`sample not found: "${s}"`,
// , try one of ${Object.keys(samples)
// .map((s) => `"${s}"`)
// .join(', ')}.
);
}
if (typeof bank !== 'object') {
throw new Error('wrong format for sample bank:', s);
}
let sampleUrl;
if (Array.isArray(bank)) {
sampleUrl = bank[n % bank.length];
} else {
const midiDiff = (noteA) => toMidi(noteA) - midi;
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
const closest = Object.keys(bank)
.filter((k) => !k.startsWith('_'))
@@ -61,6 +45,9 @@ export const getSampleBufferSource = async (s, n, note, speed, freq) => {
transpose = -midiDiff(closest); // semitones to repitch
sampleUrl = bank[closest][n % bank[closest].length];
}
if (resolveUrl) {
sampleUrl = await resolveUrl(sampleUrl);
}
let buffer = await loadBuffer(sampleUrl, ac, s, n);
if (speed < 0) {
// should this be cached?
@@ -107,7 +94,45 @@ export const getLoadedBuffer = (url) => {
return bufferCache[url];
};
let sampleCache = { current: undefined };
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
return Object.entries(sampleMap).forEach(([key, value]) => {
if (typeof value === 'string') {
value = [value];
}
if (typeof value !== 'object') {
throw new Error('wrong sample map format for ' + key);
}
baseUrl = value._base || baseUrl;
const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/');
if (Array.isArray(value)) {
//return [key, value.map(replaceUrl)];
value = value.map(replaceUrl);
} else {
// must be object
value = Object.fromEntries(
Object.entries(value).map(([note, samples]) => {
return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)];
}),
);
}
fn(key, value);
});
};
// allows adding a custom url prefix handler
// for example, it is used by the desktop app to load samples starting with '~/music'
let resourcePrefixHandlers = {};
export function registerSamplesPrefix(prefix, resolve) {
resourcePrefixHandlers[prefix] = resolve;
}
// finds a prefix handler for the given url (if any)
function getSamplesPrefixHandler(url) {
const handler = Object.entries(resourcePrefixHandlers).find(([key]) => url.startsWith(key));
if (handler) {
return handler[1];
}
return;
}
/**
* Loads a collection of samples to use with `s`
@@ -123,8 +148,13 @@ let sampleCache = { current: undefined };
*
*/
export const samples = async (sampleMap, baseUrl = sampleMap._base || '') => {
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
if (typeof sampleMap === 'string') {
// check if custom prefix handler
const handler = getSamplesPrefixHandler(sampleMap);
if (handler) {
return handler(sampleMap);
}
if (sampleMap.startsWith('github:')) {
let [_, path] = sampleMap.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
@@ -141,43 +171,118 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '') => {
}
return fetch(sampleMap)
.then((res) => res.json())
.then((json) => samples(json, baseUrl || json._base || base))
.then((json) => samples(json, baseUrl || json._base || base, options))
.catch((error) => {
console.error(error);
throw new Error(`error loading "${sampleMap}"`);
});
}
sampleCache.current = {
...sampleCache.current,
...Object.fromEntries(
Object.entries(sampleMap).map(([key, value]) => {
if (typeof value === 'string') {
value = [value];
}
if (typeof value !== 'object') {
throw new Error('wrong sample map format for ' + key);
}
baseUrl = value._base || baseUrl;
const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/');
if (Array.isArray(value)) {
return [key, value.map(replaceUrl)];
}
// must be object
return [
key,
Object.fromEntries(
Object.entries(value).map(([note, samples]) => {
return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)];
}),
),
];
const { prebake, tag } = options;
processSampleMap(
sampleMap,
(key, value) =>
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: value,
baseUrl,
prebake,
tag,
}),
),
baseUrl,
);
};
const cutGroups = [];
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const {
s,
freq,
unit,
nudge = 0, // TODO: is this in seconds?
cut,
loop,
clip = undefined, // if 1, samples will be cut off when the hap ends
n = 0,
note,
speed = 1, // sample playback speed
begin = 0,
end = 1,
} = value;
// load sample
if (speed === 0) {
// no playback
return;
}
const ac = getAudioContext();
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
//const soundfont = getSoundfontKey(s);
const time = t + nudge;
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl);
// asny stuff above took too long?
if (ac.currentTime > t) {
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
// console.warn('sample still loading:', s, n);
return;
}
if (!bufferSource) {
logger(`[sampler] could not load "${s}:${n}"`, 'error');
return;
}
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 * 1; //cps;
}
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferSource.buffer.duration;
bufferSource.start(time, offset);
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
/*if (loop) {
// TODO: idea for loopBegin / loopEnd
// if one of [loopBegin,loopEnd] is <= 1, interpret it as normlized
// if [loopBegin,loopEnd] is bigger >= 1, interpret it as sample number
// this will simplify perfectly looping things, while still keeping the normalized option
// the only drawback is that looping between samples 0 and 1 is not possible (which is not real use case)
bufferSource.loop = true;
bufferSource.loopStart = offset;
bufferSource.loopEnd = offset + duration;
duration = loop * duration;
}*/
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
bufferSource.connect(envelope);
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
envelope.connect(out);
bufferSource.onended = function () {
bufferSource.disconnect();
envelope.disconnect();
out.disconnect();
onended();
};
};
const stop = (endTime, playWholeBuffer = clip === undefined) => {
let releaseTime = endTime;
if (playWholeBuffer) {
releaseTime = t + (end - begin) * bufferDuration;
}
bufferSource.stop(releaseTime + release);
releaseEnvelope(releaseTime);
};
const handle = { node: out, bufferSource, stop };
export const resetLoadedSamples = () => {
sampleCache.current = undefined;
};
// cut groups
if (cut !== undefined) {
const prev = cutGroups[cut];
if (prev) {
prev.node.gain.setValueAtTime(1, time);
prev.node.gain.linearRampToValueAtTime(0, time + 0.01);
}
cutGroups[cut] = handle;
}
export const getLoadedSamples = () => sampleCache.current;
return handle;
}
+44
View File
@@ -0,0 +1,44 @@
import { midiToFreq, noteToMidi } from '@strudel.cycles/core';
import { registerSound } from './webaudio.mjs';
import { getOscillator, gainNode, getEnvelope } from './helpers.mjs';
export function registerSynthSounds() {
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
registerSound(
wave,
(t, value, onended) => {
// 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 } = value;
let { n, note, freq } = value;
// with synths, n and note are the same thing
n = note || n || 36;
if (typeof n === 'string') {
n = noteToMidi(n); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof n === 'number') {
freq = midiToFreq(n); // + 48);
}
// maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default)
// make oscillator
const { node: o, stop } = getOscillator({ t, s: wave, freq });
const g = gainNode(0.3);
// envelope
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
};
return {
node: o.connect(g).connect(envelope),
stop: (releaseTime) => {
releaseEnvelope(releaseTime);
stop(releaseTime + release);
},
};
},
{ type: 'synth', prebake: true },
);
});
}
+61 -167
View File
@@ -5,15 +5,22 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as strudel from '@strudel.cycles/core';
import { fromMidi, logger, toMidi } from '@strudel.cycles/core';
import './feedbackdelay.mjs';
import './reverb.mjs';
import { getSampleBufferSource } from './sampler.mjs';
const { Pattern } = strudel;
const { Pattern, logger } = strudel;
import './vowel.mjs';
import workletsUrl from './worklets.mjs?url';
import { getFilter, gainNode } from './helpers.mjs';
import { map } from 'nanostores';
// export const getAudioContext = () => Tone.getContext().rawContext;
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
soundMap.setKey(key, { onTrigger, data });
}
export function getSound(s) {
return soundMap.get()[s];
}
export const resetLoadedSounds = () => soundMap.set({});
let audioContext;
export const getAudioContext = () => {
@@ -38,65 +45,6 @@ export const panic = () => {
destination = null;
};
const getFilter = (type, frequency, Q) => {
const filter = getAudioContext().createBiquadFilter();
filter.type = type;
filter.frequency.value = frequency;
filter.Q.value = Q;
return filter;
};
const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
// for some reason, using exponential ramping creates little cracklings
/* let t = begin;
gainNode.gain.setValueAtTime(0, t);
gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack));
const sustainGain = Math.max(sustain * velocity, 0.001);
gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay));
if (end - begin < attack + decay) {
gainNode.gain.cancelAndHoldAtTime(end);
} else {
gainNode.gain.setValueAtTime(sustainGain, end);
}
gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */
return gainNode;
};
const getOscillator = ({ s, freq, t, duration, release }) => {
// make oscillator
const o = getAudioContext().createOscillator();
o.type = s || 'triangle';
o.frequency.value = Number(freq);
o.start(t);
o.stop(t + duration + release);
return o;
};
const getSoundfontKey = (s) => {
if (!globalThis.soundfontList) {
// soundfont package not loaded
return false;
}
if (globalThis.soundfontList?.instruments?.includes(s)) {
return s;
}
// check if s is one of the soundfonts, which are loaded into globalThis, to avoid coupling both packages
const nameIndex = globalThis.soundfontList?.instrumentNames?.indexOf(s);
// convert number nameIndex (0-128) to 3 digit string (001-128)
const name = nameIndex < 10 ? `00${nameIndex}` : nameIndex < 100 ? `0${nameIndex}` : nameIndex;
if (nameIndex !== -1) {
// TODO: indices of instrumentNames do not seem to match instruments
return globalThis.soundfontList.instruments.find((instrument) => instrument.startsWith(name));
}
return;
};
let workletsLoading;
function loadWorklets() {
if (workletsLoading) {
@@ -136,15 +84,13 @@ export async function initAudioOnFirstClick() {
});
}
function gainNode(value) {
const node = getAudioContext().createGain();
node.gain.value = value;
return node;
}
const cutGroups = [];
let delays = {};
const maxfeedback = 0.98;
function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = strudel.clamp(delayfeedback, 0, 0.98);
if (!delays[orbit]) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
@@ -186,15 +132,11 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
// calculate absolute time
let t = ac.currentTime + deadline;
// destructure value
// destructure
let {
freq,
s,
s = 'triangle',
bank,
sf,
clip = 0, // if 1, samples will be cut off when the hap ends
n = 0,
note,
source,
gain = 0.8,
// low pass
cutoff,
@@ -210,110 +152,48 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
crush,
shape,
pan,
speed = 1, // sample playback speed
begin = 0,
end = 1,
vowel,
delay = 0,
delayfeedback = 0.5,
delaytime = 0.25,
unit,
nudge = 0, // TODO: is this in seconds?
cut,
loop,
orbit = 1,
room,
size = 2,
} = hap.value;
const { velocity = 1 } = hap.context;
gain *= velocity; // legacy fix for velocity
// the chain will hold all audio nodes that connect to each other
const chain = [];
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
const onended = () => {
toDisconnect.forEach((n) => n?.disconnect());
};
if (bank && s) {
s = `${bank}_${s}`;
}
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;
// with synths, n and note are the same thing
n = note || n || 36;
if (typeof n === 'string') {
n = toMidi(n); // e.g. c3 => 48
// get source AudioNode
let sourceNode;
if (source) {
sourceNode = source(t, hap.value, hapDuration);
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
const soundHandle = await onTrigger(t, hap.value, onended);
if (soundHandle) {
sourceNode = soundHandle.node;
soundHandle.stop(t + hapDuration);
}
// get frequency
if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48);
}
// make oscillator
const o = getOscillator({ t, s, freq, duration: hapDuration, release });
chain.push(o);
// level down oscillators as they are really loud compared to samples i've tested
chain.push(gainNode(0.3));
// TODO: make adsr work with samples without pops
// envelope
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hapDuration);
chain.push(adsr);
} else {
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = hap.value;
// load sample
if (speed === 0) {
// no playback
return;
}
if (!s) {
console.warn('no sample specified');
return;
}
const soundfont = getSoundfontKey(s);
let bufferSource;
if (soundfont) {
// is soundfont
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac, freq);
} else {
// is sample from loaded samples(..)
bufferSource = await getSampleBufferSource(s, n, note, speed, freq);
}
// asny stuff above took too long?
if (ac.currentTime > t) {
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
// console.warn('sample still loading:', s, n);
return;
}
if (!bufferSource) {
console.warn('no buffer source');
return;
}
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 * 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,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * duration * bufferSource.playbackRate.value;
duration = (end - begin) * duration;
if (loop) {
bufferSource.loop = true;
bufferSource.loopStart = offset;
bufferSource.loopEnd = offset + duration;
duration = loop * duration;
}
t += nudge;
bufferSource.start(t, offset);
if (cut !== undefined) {
cutGroups[cut]?.stop(t); // fade out?
cutGroups[cut] = bufferSource;
}
chain.push(bufferSource);
bufferSource.stop(t + duration + release);
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + duration);
chain.push(adsr);
throw new Error(`sound ${s} not found! Is it loaded?`);
}
if (!sourceNode) {
// if onTrigger does not return anything, we will just silently skip
// this can be used for things like speed(0) in the sampler
return;
}
if (ac.currentTime > t) {
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
return;
}
const chain = []; // audio nodes that will be connected to each other sequentially
chain.push(sourceNode);
// gain stage
chain.push(gainNode(gain));
@@ -357,8 +237,9 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
// disconnect all nodes when source node has ended:
chain[0].onended = () => chain.concat([delaySend, reverbSend]).forEach((n) => n?.disconnect());
// toDisconnect = all the node that should be disconnected in onended callback
// this is crucial for performance
toDisconnect = chain.concat([delaySend, reverbSend]);
};
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps, cps);
@@ -367,3 +248,16 @@ Pattern.prototype.webaudio = function () {
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
return this.onTrigger(webaudioOutputTrigger);
};
export function webaudioScheduler(options = {}) {
options = {
getTime: () => getAudioContext().currentTime,
defaultOutput: webaudioOutput,
...options,
};
const { defaultOutput, getTime } = options;
return new strudel.Cyclist({
...options,
onTrigger: strudel.getTrigger({ defaultOutput, getTime }),
});
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.6.0",
"version": "0.8.0",
"description": "WebDirt integration for Strudel",
"main": "index.mjs",
"type": "module",
@@ -35,7 +35,7 @@
"WebDirt": "github:dktr0/WebDirt"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+4 -3
View File
@@ -1,7 +1,7 @@
import * as strudel from '@strudel.cycles/core';
const { Pattern } = strudel;
import * as WebDirt from 'WebDirt';
import { getLoadedSamples, loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio';
//import { loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio';
let webDirt;
@@ -62,8 +62,9 @@ export function loadWebDirt(config) {
* @noAutocomplete
*/
Pattern.prototype.webdirt = function () {
throw new Error('webdirt support has been dropped..');
// create a WebDirt object and initialize Web Audio context
return this.onTrigger(async (time, e, currentTime) => {
/* return this.onTrigger(async (time, e, currentTime) => {
if (!webDirt) {
throw new Error('WebDirt not initialized!');
}
@@ -92,5 +93,5 @@ Pattern.prototype.webdirt = function () {
webDirt.playSample(msg, deadline);
}
}
});
}); */
};
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/xen",
"version": "0.6.0",
"version": "0.8.0",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -33,7 +33,7 @@
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
File diff suppressed because one or more lines are too long
-597
View File
@@ -1,597 +0,0 @@
[
{
"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
]
]
}
}
]
+818
View File
@@ -0,0 +1,818 @@
<!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>
+57 -52
View File
@@ -8,7 +8,7 @@ author:
affiliation: Then Try This
email: alex@slab.org
abstract: |
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.
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.
bibliography: citations.json
fontsize: 11pt
geometry: margin=2cm
@@ -23,36 +23,37 @@ date: '2022-12-14'
In the following paper, we introduce *Strudel*, an alternative
implementation of the TidalCycles (or 'Tidal' for short) live coding
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
system, using the JavaScript programming language. Strudel is an
attempt to make live coding more accessible, by creating a system that
can run entirely in the browser, while opening Tidal's approach to
algorithmic patterns [@mcleanAlgorithmicPattern2020] up to modern
runs entirely in the browser, while opening Tidal's approach to
algorithmic patterns [@mcleanAlgorithmicPattern2020a] 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 standard JavaScript, it does support some optimisations for simplicity, readability and editability by applying code
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
several other existing projects like TidalVortex
many prior existing projects like TidalVortex
[@mcleanTidalVortexZero2022], Gibber [@robertsGibberLiveCoding2012],
Estuary [@ogbornEstuaryBrowserbasedCollaborative2017], Hydra
[@jackHydra2023], Ocarina [@solomonPurescriptocarina2022] and
Feedforward [@mcleanFeedforward2020]. This paper builds upon a
demo paper presented at Web Audio Conference 2022 [@roosStrudelAlgorithmicPatterns2022].
[@jackHydra2022], Ocarina [@solomonPurescriptocarina2022] and
Feedforward [@mcleanFeedforward2020]. This paper expands the Strudel
Demo paper for the Web Audio Conference 2022 [@StrudelWAC2022].
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 an initial version to the 'npm' JavaScript package database,
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 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
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 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
@@ -60,24 +61,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 different syntactical trade-offs and library ecosystems of
although within the syntactical trade-offs and library ecosystems of
JavaScript and Haskell, some divergence is inevitable and healthy.
Now celebrating the first year of its life, Strudel is now a fully-fledged live
coding environment, porting Tidal's core representation of patterns,
Over the first year of its life, Strudel is now a fully-fledged live
coding environment, porting Tidal's core represention 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 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.
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.
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 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.
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 **querying**.
Example:
@@ -100,7 +101,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 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.
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.
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.
@@ -109,7 +110,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.
@@ -117,29 +118,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 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 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.].
```js
note("c3 [e3 g3]")
```
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.
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.
# Pattern Example
The following example showcases how patterns can be utilised to create musical complexity from simple parts, using repetition and interference:
The following example showcases how patterns can be utilized 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('D3 minor')
.note().s("sawtooth square")
.scale('D minor')
.legato(.25)
.note().s("sawtooth square")
.delay(.8).delaytime(.125)
```
The pattern starts with a rhythmic pattern of numbers expressed in mini-notation, which are later interpreted inside the scale of D minor.
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:
```js
@@ -152,16 +153,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
- `.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
- `.legato(n)`: multiply the duration of all events in a pattern by a factor of n
- `.delay(amount)`: delay effect send amount
- `.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
- `.s(name)`: play back each event with the given sound
- `.delay(wet)`: add delay
- `.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)
@@ -180,11 +181,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 [@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.
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.
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.
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.
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.
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.
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).
@@ -230,13 +231,17 @@ 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 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.
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.
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:
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:
```js
seq(
@@ -250,12 +255,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 is then 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 can be used to highlight active events in real time.
## Scheduling Events
After an instance of `Pattern` is obtained from the user code,
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:
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:
```js
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
@@ -272,7 +277,7 @@ setInterval(() => {
}, interval * 1000); // query each "interval" seconds
```
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).]
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).
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.
@@ -305,7 +310,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 describes the coordinates of a circle.
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.
### Outputs
@@ -323,13 +328,13 @@ function onTrigger(hap, deadline, duration) {
}
```
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.
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.
# 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, 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:
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:
```js
"0 [1 2] 3".add("10 20")
@@ -384,20 +389,20 @@ versus Strudel's simplicity.
To demonstrate this, consider the following Tidal pattern:
```haskell
iter 4 $ every 3 (||+ n "10 20") $ n "20 21 23" # s "triangle" # crush 4
iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4
```
This can be directly translated to the Strudel equivalent:
```js
iter(4, every(3, x => x.add.squeeze(n("10 20")), n("20 21 23").s("triangle").crush(4)))
iter(4, every(3, add.squeeze("10 20"), n("0 1 3").s("triangle").crush(4)))
```
Although for a more canonical Strudel expression, we would reorder it
as:
```js
n("20 21 23").every(3, x => x.add.squeeze(n("10 20"))).iter(4).s("triangle").crush(4)
n("0 1 3").every(3, add.squeeze("10 20")).iter(4).s("triangle").crush(4)
```
The Strudel example uses the `.` method call operator for all
@@ -421,17 +426,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 chaining method calls is a
domain-specific language 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. Tidal's functional reactive approach to pattern-making has in
general translated well to JavaScript, and opportunities and
constraints have traded off to create a very approachable and
usable live coding environment.
constraints have overall traded off to create a very approachable and
useable 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 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.
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.
# Future Outlook
@@ -441,14 +446,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 [@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.
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.
# 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.
# Acknowledgements
# Acknowledgments
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.
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

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