Compare commits

..

220 Commits

Author SHA1 Message Date
alex 185abf1fc1 abandoned experiment 2023-01-21 09:12:41 +00:00
alex 37efdd9ce1 prettier 2023-01-16 21:55:16 +00:00
alex f4039d3666 Merge branch 'main' into composable 2023-01-16 21:47:46 +00:00
Felix Roos 7000a4cead Merge pull request #377 from tidalcycles/draw-fixes
Draw fixes
2023-01-15 23:19:42 +01:00
Felix Roos 5aa983b45b do less work when not drawing 2023-01-15 23:11:49 +01:00
Felix Roos 800989419b catch all query errors by default 2023-01-15 22:50:56 +01:00
Felix Roos 362679954a Merge pull request #372 from tidalcycles/local-draw
improve new draw logic
2023-01-14 11:07:07 +01:00
Felix Roos 2732d64beb fix: another dependency array 2023-01-14 10:58:30 +01:00
Felix Roos 88684d6d9e fix dependency array 2023-01-14 10:48:52 +01:00
Felix Roos 9afddf0850 fix sentence 2023-01-14 01:30:56 +01:00
Felix Roos a2d7d6e81f fix: mini repl could eval before modules are ready 2023-01-14 01:27:22 +01:00
Felix Roos 49bcb5e19f fix minor bugs 2023-01-13 16:35:24 +01:00
Felix Roos 3744ea6218 fix: warnings 2023-01-13 15:17:23 +01:00
Felix Roos b2c6d87633 MiniRepl: add punchcard flag for implicit vis
+ reintroduce editPattern
+ add punchcards to mini-notation.mdx
2023-01-13 15:13:39 +01:00
Felix Roos e4f538b674 fix option handling 2023-01-13 13:00:11 +01:00
Felix Roos ea0e0b4396 reset canvas when pattern changes
+ rename noteroll -> punchcard
2023-01-13 12:57:35 +01:00
Felix Roos 1ac784dc7a localize draw logic 2023-01-13 12:03:34 +01:00
Felix Roos d980c9ca4c Merge branch 'main' into docs 2023-01-13 01:52:36 +01:00
Alex McLean c0d6e36113 Merge branch 'main' into composable 2023-01-12 16:47:47 +00:00
Felix Roos 6a2a60df0d hotfix: tests 2023-01-12 17:43:20 +01:00
Felix Roos ee9b89b6d5 add more synonyms 2023-01-12 17:40:58 +01:00
Alex McLean 8261802078 more hackery, composing seems to work now 2023-01-12 16:36:42 +00:00
Felix Roos 68cc109933 burn down that undocumented list 2023-01-12 17:25:01 +01:00
Felix Roos 1015bb0fdc Merge pull request #369 from tidalcycles/docs
document more functions + change arp join
2023-01-12 14:40:58 +01:00
Felix Roos 7da94dd109 reformat headings
+ add another note about single quotes
2023-01-12 14:22:41 +01:00
Felix Roos 171808cf2c add note about single quote strings 2023-01-12 14:19:18 +01:00
Felix Roos 78d14cdfea automatically print synonyms in doc 2023-01-12 13:10:22 +01:00
Felix Roos 19982a2ffb remove unused package 2023-01-12 13:00:22 +01:00
Felix Roos 79904560fc fix: snapshot 2023-01-12 12:48:05 +01:00
Felix Roos 28aee1c58d fix polymeter doc 2023-01-12 12:43:41 +01:00
Felix Roos d4512f360b fix: double slash 2023-01-12 12:40:20 +01:00
Felix Roos 48d0ffe868 add script that finds all undocumented exports 2023-01-12 12:35:04 +01:00
Felix Roos 62af12f3b9 remove experimental class properties 2023-01-12 12:32:27 +01:00
Felix Roos efa4bfef2f fix snapshot 2023-01-12 02:23:06 +01:00
Felix Roos fc3ead4c01 Merge branch 'main' into docs 2023-01-12 02:20:16 +01:00
Felix Roos ca9c813b40 format 2023-01-12 02:18:09 +01:00
Felix Roos e3b0f47c40 hide internals for now 2023-01-12 02:15:42 +01:00
Felix Roos bf13e42092 update snap 2023-01-12 01:57:14 +01:00
Felix Roos 4345fda9bc more doc 2023-01-12 01:56:46 +01:00
Felix Roos 4059e9faa9 more doc 2023-01-12 00:10:58 +01:00
Felix Roos 95fcce1bcf fix: snapshot 2023-01-11 23:38:40 +01:00
Felix Roos 86cde9716a arpWith example 2023-01-11 23:37:59 +01:00
Felix Roos 6e315a50ed breaking: arp use innerJoin instead of squeezeJoin 2023-01-11 23:28:17 +01:00
alex 6dda6bac66 more composable magic 2023-01-11 22:27:00 +00:00
Felix Roos 500c0b6534 doc: arp + arpWith 2023-01-11 23:26:03 +01:00
Felix Roos db75dca118 doc: split accumulation and conditions 2023-01-11 23:05:17 +01:00
alex d5ab1a3471 actually compose functions together 2023-01-11 21:59:10 +00:00
Felix Roos d5f37e2cbe add internals doc 2023-01-11 22:48:47 +01:00
Felix Roos a120a9895b fix: right sidebar width 2023-01-11 22:48:33 +01:00
Felix Roos e9c1db020f hotfix: remove mocked .out 2023-01-11 22:11:22 +01:00
alex ded90733f6 mark up more function args 2023-01-11 20:30:07 +00:00
alex c64485db99 hack register to support function parameters that can be patterns which compose 2023-01-10 23:28:22 +00:00
alex 08cd319a0b Revert "Merge branch 'main' into main"
This reverts commit 896ec69329, reversing
changes made to 32e64bbb6e.
2023-01-10 19:45:23 +00:00
Alex McLean 896ec69329 Merge branch 'main' into main 2023-01-10 18:23:45 +00:00
alex afa5d6e704 prettier 2023-01-10 11:16:43 +00:00
alex bde7e79a38 failed attempt at composifying 2023-01-10 11:05:51 +00:00
Alex McLean 32e64bbb6e Typo 2023-01-09 23:49:16 +00:00
Felix Roos 0974540e5f fix: typo 2023-01-10 00:21:54 +01:00
Felix Roos e06963e616 Merge pull request #364 from urswilke/patch-2
add https to url
2023-01-10 00:03:46 +01:00
Felix Roos a675815529 hotfix: preview command + build peg without logs 2023-01-09 23:55:45 +01:00
alex 3d48135b32 09 2023-01-09 22:47:25 +00:00
urswilke 8f09e7f097 add https to url 2023-01-09 23:44:49 +01:00
Felix Roos e8b09f1124 Merge pull request #360 from tidalcycles/docs
doc structuring
2023-01-09 23:37:34 +01:00
Felix Roos 57ba353594 snaps 2023-01-09 23:31:30 +01:00
Felix Roos b695e090cd docs: control params 2023-01-09 23:27:30 +01:00
Felix Roos 69ecb7b54f support logs in mini repl
+ pass hap to logger
+ add editPattern hook to repl + useStrudel
+ do not throw when webaudio gets plain values
2023-01-09 23:26:20 +01:00
Alex McLean f57fe182ed Support for multiple mininotation operators WIP (#350)
* support for multiple operators
* fix false test failures by sorting by part
* snapshot
2023-01-09 22:23:27 +00:00
Felix Roos 10ee11c886 docs: improve time modifiers page
+ polymeter + euclidRot
+ rename Factories to Pattern Constructors
2023-01-09 20:39:00 +01:00
Alex McLean 45842b1f05 Fix .out(), renaming webaudio's out() to webaudio() (#361)
* breaking test for out()

* rename webaudio's out() to webaudio(), fixes #359
2023-01-08 23:40:15 +00:00
Felix Roos 9ca5f9ad48 some corrections
+ n should not be recommended for notes as it does not work for samples
2023-01-09 00:19:29 +01:00
Felix Roos bb00036a30 further sidebar reorganizing 2023-01-09 00:17:57 +01:00
Felix Roos 4a2a540500 Update README.md 2023-01-08 23:55:07 +01:00
Felix Roos 09217216dc format 2023-01-08 23:48:32 +01:00
Felix Roos 22a64c4931 update snapshots 2023-01-08 23:46:39 +01:00
Felix Roos 93f520b941 Merge remote-tracking branch 'origin/main' into docs 2023-01-08 23:44:27 +01:00
alex c84714dc3a 08 2023-01-08 13:25:22 +00:00
alex a7aa09a13e 07 2023-01-07 23:15:50 +00:00
alex efa1cdfcb2 07 2023-01-07 23:12:19 +00:00
Felix Roos 70695daae8 remove quotes around inline code snippets 2023-01-07 22:12:53 +01:00
Felix Roos 62fdba0600 pull apart functional docs 2023-01-07 22:12:35 +01:00
Felix Roos 5733e00908 add hideDescription flag to JsDoc 2023-01-07 22:11:47 +01:00
alex e56abaefd7 06 2023-01-07 00:02:39 +00:00
Felix Roos c71e893e8e Merge pull request #356 from tidalcycles/docs
docs: tidal comparison + add global fx + add missing sampler fx
2023-01-06 22:02:31 +01:00
Felix Roos 3992356517 casing 2023-01-06 21:58:35 +01:00
Felix Roos dbb84ef129 write more tidal comparison 2023-01-06 21:56:17 +01:00
Felix Roos 8781fa2e33 add filter synonyms 2023-01-06 21:37:31 +01:00
Felix Roos 605a1365d3 update snapshots 2023-01-06 21:15:32 +01:00
Felix Roos ebb1bbc966 improve effects doc 2023-01-06 21:14:19 +01:00
Felix Roos d4518f7a6e began tidal comparison page 2023-01-06 12:50:35 +01:00
Alex McLean 68c9008019 Fix Bjorklund (#343)
* port Rohan Drape's Bjorklund implementation, add Toussaint's tests
* fix euclidLegato, simplifying a bit now that bjork results should always begin with an 'on'
* migrate euclid numbers in tunes
- 3,4 +1
- 5,8 -1
- 6,8 +3

Co-authored-by: Felix Roos <flix91@gmail.com>
2023-01-06 11:31:32 +00:00
Felix Roos cafde1e910 Merge branch 'main' into docs 2023-01-06 11:49:49 +01:00
alex 41f5f3fe6e 05 2023-01-05 23:58:48 +00:00
alex cf12ecfa34 04 2023-01-04 23:36:40 +00:00
Felix Roos 4fa668927f Merge pull request #345 from tidalcycles/fix-prebake-base-path
Fix prebake base path
2023-01-04 20:29:35 +01:00
Felix Roos 24d2b5b4bd Merge pull request #347 from tidalcycles/fix-midi
fixes #346
2023-01-04 19:55:48 +01:00
Felix Roos 4da2e003e1 fixes #346 2023-01-04 19:52:51 +01:00
Felix Roos 205debde9c more links to fix 2023-01-03 23:40:40 +01:00
Felix Roos ba6c7b0901 fix examples route 2023-01-03 23:32:37 +01:00
Felix Roos 8e5d02fd25 do not include url in base pth 2023-01-03 23:32:28 +01:00
Felix Roos 11d3251c67 add base path + use relative links 2023-01-03 23:02:41 +01:00
alex 9ec5787b4d 03 2023-01-03 21:26:56 +00:00
Felix Roos 027c669ad7 make prebake relative to BASE_URL 2023-01-03 22:17:44 +01:00
Alex McLean 405c12ba3f deploy on push 2023-01-03 10:47:20 +00:00
Alex McLean 64fd53a8e5 tweak 2023-01-03 10:43:54 +00:00
alex 069d63e3cf 02 2023-01-02 23:33:48 +00:00
Alex McLean 66049e0e25 Merge pull request #2 from tidalcycles/main
downstream
2023-01-02 21:07:18 +00:00
Alex McLean db7ef01dbb Patternify euclid, fast, slow and polymeter step parameters in mininotation (#341)
* patternify the mininotation bjorklund, and fast (*) slow (/) and pattern step (%) parameters
* simplify replicate (!), should be closer to tidal now
* highlight atoms rather than elements
* tests
2023-01-02 20:28:07 +00:00
Felix Roos 1a6b649fd6 Merge pull request #340 from tidalcycles/animate
more animate functions + mini repl fix
2023-01-02 00:30:16 +01:00
Felix Roos 548619775e rename .. 2023-01-02 00:28:38 +01:00
Felix Roos 4b79a75f39 fix: mini repls were broken 2023-01-02 00:24:33 +01:00
Felix Roos 28912fff97 animate: rescale + move + zoom 2023-01-01 18:47:45 +01:00
alex 98a83ddcc0 01c 2023-01-01 17:22:16 +00:00
alex e9d965ce7a 01b 2023-01-01 14:22:19 +00:00
alex b7624813cc custom domain 2023-01-01 11:49:20 +00:00
Alex McLean 734e451bd9 Merge pull request #1 from tidalcycles/main
pull from upstream
2023-01-01 11:42:58 +00:00
Alex McLean 7a96436085 move /my-patterns to /swatch (#338) 2023-01-01 11:34:11 +00:00
Felix Roos 670af8fb1b Merge pull request #337 from tidalcycles/animate
animation options
2023-01-01 12:22:55 +01:00
Felix Roos 0abd062e5c fix: smear 0 special case 2023-01-01 12:18:46 +01:00
Felix Roos 1286468d5a animation options
+ do not sync by default
2023-01-01 12:11:39 +01:00
alex 0f3da5be4a 01 2023-01-01 10:05:58 +00:00
Alex McLean 9170269e06 Update README.md 2023-01-01 00:34:53 +00:00
Alex McLean 8bb460701f Tidy parser, implement polymeters (#336)
* rename parser stuff to be more explicit and fit with tidal concepts. qualify all strudel function calls
* Add {,}%n polymeter support, with a few tests
2022-12-31 21:42:49 +00:00
Felix Roos c0a7173ca4 Merge pull request #335 from tidalcycles/animate
animate mvp
2022-12-31 16:51:53 +01:00
Felix Roos 949e15d8c6 controllable smear + sync flag 2022-12-31 16:41:13 +01:00
Felix Roos 0caedeb481 animate mvp 2022-12-31 16:23:45 +01:00
Felix Roos 83ca8d95be Merge pull request #334 from tidalcycles/docs
testing + docs docs
2022-12-30 20:16:09 +01:00
Felix Roos 8483868783 docs: testing + docs 2022-12-30 19:54:02 +01:00
Felix Roos 6961388545 Merge branch 'main' into docs 2022-12-30 17:43:54 +01:00
Felix Roos 8ef1ae5202 Merge pull request #333 from tidalcycles/embed-route
Embed mode improvements
2022-12-29 21:11:15 +01:00
Felix Roos 26b8f93cea roll back hh rhythm 2022-12-29 21:08:11 +01:00
Felix Roos a576523976 simplify meltingsubmarine tune 2022-12-29 21:02:04 +01:00
Felix Roos cf21c4085b simplify embedded mode 2022-12-29 21:01:28 +01:00
Felix Roos 204f655d6d Merge pull request #332 from tidalcycles/fix-mini-multiply-floats
fix: can now multiply floats in mini notation
2022-12-29 14:06:19 +01:00
Felix Roos fb14865785 fix: can now multiply floats in mini notation
- fixes #314
2022-12-29 14:02:50 +01:00
Felix Roos 5cd052cc59 Merge pull request #331 from tidalcycles/my-patterns
improve displaying 's' in pianoroll
2022-12-29 13:50:07 +01:00
Felix Roos af49c4117c add color to some tunes 2022-12-29 13:25:20 +01:00
Felix Roos dd736130a0 improve pianoroll value mapping 2022-12-29 13:03:28 +01:00
Felix Roos cee8bda676 Merge pull request #330 from tidalcycles/my-patterns
my-patterns: fix paths + update readme
2022-12-28 17:35:35 +01:00
Felix Roos 6420a72ceb fix paths + update readme 2022-12-28 17:33:14 +01:00
Felix Roos 380e7c7899 Merge pull request #329 from tidalcycles/my-patterns
my-patterns build + deploy
2022-12-28 17:11:40 +01:00
Felix Roos 16b4058a8b fix build + simplify logic + add deploy note 2022-12-28 17:09:18 +01:00
Felix Roos e3fba0257f Merge pull request #328 from tidalcycles/my-patterns
add my-patterns
2022-12-28 15:43:31 +01:00
Felix Roos cb8b65f6f3 add my-patterns 2022-12-28 15:39:42 +01:00
Felix Roos 3944a8ed54 Merge pull request #327 from tidalcycles/docs
add examples route
2022-12-28 14:47:13 +01:00
Felix Roos 2ceafe6379 Merge pull request #326 from tidalcycles/eval-fix-node
fix: workaround Object.assign globalThis
2022-12-28 14:44:30 +01:00
Felix Roos 9f058bd88a add examples page with prerendered swatches 2022-12-28 14:40:53 +01:00
Felix Roos 1dd0ff13d8 Merge remote-tracking branch 'origin/eval-fix-node' into docs 2022-12-28 14:02:55 +01:00
Felix Roos 6061f10230 fix: workaround Object.assign globalThis
- see comment
2022-12-28 13:57:27 +01:00
Felix Roos ea59fafb52 Merge pull request #324 from tidalcycles/docs
mini repl improvements
2022-12-26 23:29:47 +01:00
Felix Roos 2ff71126de add withCanvas 2022-12-26 23:25:57 +01:00
Felix Roos 6d522e1743 Merge remote-tracking branch 'origin/main' into docs 2022-12-26 23:13:51 +01:00
Felix Roos b37f3c819d remove color magic for now 2022-12-26 23:12:16 +01:00
Felix Roos 1b41994e5f codeformat 2022-12-26 23:01:18 +01:00
Felix Roos 4fd4b0d713 Merge pull request #323 from tidalcycles/notes-without-octave
support notes without octave
2022-12-26 23:00:34 +01:00
Felix Roos 7b43ed702e only start animation loop when canvas is visible
+ add some placeholder files
2022-12-26 22:59:41 +01:00
Felix Roos 56cd77942b remove old graphic 2022-12-26 22:50:10 +01:00
Felix Roos 37dc723b4b react package build... 2022-12-26 22:44:23 +01:00
Felix Roos d7ea37c917 add technical manual to docs 2022-12-26 22:43:02 +01:00
Felix Roos c1d2bf9b9f fix anchor links 2022-12-26 22:16:46 +01:00
Felix Roos 36f837730a better color support 2022-12-26 22:13:41 +01:00
Felix Roos b8f0a1dd82 fix format 2022-12-26 21:04:15 +01:00
Felix Roos 2d1b62a978 begin reimplementing draw logic for parallel use 2022-12-26 20:58:57 +01:00
Felix Roos 0792d0d59d add scheduler.now to get phase starting from 0 2022-12-26 20:55:21 +01:00
Felix Roos 8c6da1a658 Merge branch 'main' into docs 2022-12-26 13:31:06 +01:00
Felix Roos 26be10f937 hotfix: css + sn -> sd 2022-12-26 12:43:08 +01:00
Felix Roos ef4d9a13ed Merge pull request #320 from jarmitage/main
tutorial updates
2022-12-26 12:37:36 +01:00
Jack Armitage 320d987ede wut 2022-12-26 11:32:55 +00:00
Jack Armitage 51a4c4d13b codeformat 2022-12-26 11:30:01 +00:00
Felix Roos aaf3ae5f9c microrhythm experiment 2022-12-26 11:25:27 +01:00
Jack Armitage 858f4f74b3 tutorial updates 2022-12-25 16:21:18 +00:00
Felix Roos 3feed90039 translate parts of tidal "How-tos" 2022-12-24 00:14:26 +01:00
Felix Roos b46330fc7a proper setup minirepl 2022-12-24 00:12:54 +01:00
Felix Roos 5ca7ce1f75 Merge commit 'd1d9b37ec764dd7377754d88e72996ef887beb82' into docs 2022-12-23 23:06:54 +01:00
Felix Roos d1d9b37ec7 supper notes without octave 2022-12-23 23:01:07 +01:00
Felix Roos 823115c4ec Merge pull request #318 from tidalcycles/reference-tab-sort
Reference tab sort
2022-12-23 18:26:30 +01:00
Felix Roos 8fe3fda27f update snapshots 2022-12-23 18:24:40 +01:00
Felix Roos 63fffd7493 docs: envelope functions 2022-12-23 17:57:01 +01:00
Felix Roos 243a350e2b fix: #317 2022-12-23 17:32:25 +01:00
Felix Roos 8c09e7ba36 hotfix: go to repl link 2022-12-23 01:05:17 +01:00
Felix Roos d89502f91f hotfix: make sure jsdoc is built before build 2022-12-23 01:01:45 +01:00
Felix Roos 847602f933 hotfix: add missing npm ci 2022-12-23 00:58:48 +01:00
Felix Roos 8fcf8fe39c Revert "hotfix: build task for ci"
This reverts commit d59eec9296.
2022-12-23 00:57:59 +01:00
Felix Roos d59eec9296 hotfix: build task for ci 2022-12-23 00:54:43 +01:00
Felix Roos b9122ebc34 Merge pull request #315 from tidalcycles/astro-build
Astro build
2022-12-23 00:49:56 +01:00
Felix Roos 38e066ee49 fix import 2022-12-23 00:47:33 +01:00
Felix Roos 232a04a1a2 docs: mobile nav 2022-12-23 00:41:47 +01:00
Felix Roos dcb145635c add tutorial redirect
+ place learn link last in repl header
+ fix all old links to /tutorial
2022-12-22 23:47:11 +01:00
Felix Roos c9b73c68a6 fix pretest task 2022-12-22 23:32:19 +01:00
Felix Roos 08f7925ea9 delete now obsolete repl + tutorial folders 2022-12-22 23:28:34 +01:00
Felix Roos ed79ec7940 copy potentially useful files from old tutorial 2022-12-22 23:22:48 +01:00
Felix Roos 869a44dd04 autolink headings 2022-12-22 23:19:38 +01:00
Felix Roos 6a9e9ebc4c responsiveness 2022-12-22 23:07:14 +01:00
Felix Roos f18a2a11fa make repl and doc styles more similar 2022-12-22 22:49:41 +01:00
Felix Roos 9aaaa755c7 fix import + update build task 2022-12-22 17:23:35 +01:00
Felix Roos 818cd9044b integrate repl into astro website
+ update build and setup tasks + workflow
+ move repl test folder to root
+ move docs and repl to website/src
2022-12-22 17:20:51 +01:00
Felix Roos 006ca12b6d rename folder 2022-12-22 15:36:24 +01:00
Felix Roos b5f9ab7c20 slight layout fixes 2022-12-20 22:47:49 +01:00
Felix Roos 0981f95f16 style left sidebar 2022-12-20 22:26:12 +01:00
Felix Roos f64ef32eaa more styling 2022-12-20 22:19:30 +01:00
Felix Roos 7a19659c9e remove theme.css + cooler bg 2022-12-20 21:57:41 +01:00
Felix Roos b1202fcd55 split tutorial into multiple pages
+ improve layout a bit
2022-12-20 21:38:08 +01:00
Felix Roos b8f991eddb simplify writing mdx files 2022-12-20 20:42:42 +01:00
Felix Roos d0013f55a2 format 2022-12-20 19:50:32 +01:00
Felix Roos 7efaff4f6f basic style for toc + further cleanup 2022-12-20 19:47:28 +01:00
Felix Roos bc7a84e462 basic astro docs 2022-12-19 23:23:31 +01:00
Felix Roos 61657bd1b1 Merge pull request #307 from tidalcycles/obj-support-scale
object support for .scale
2022-12-19 21:02:37 +01:00
Felix Roos fc63f4f89c Merge pull request #312 from tidalcycles/jsdoc-component
Jsdoc component
2022-12-19 21:02:21 +01:00
Felix Roos 2c44c252d7 fixed namespaced anchor links 2022-12-19 20:59:47 +01:00
Felix Roos 613fc6e3ef fix anchor icon color 2022-12-19 20:57:42 +01:00
Felix Roos 0a9604d10e throw out nunjucks 2022-12-19 20:48:57 +01:00
Felix Roos 1cabedc65e Merge pull request #311 from tidalcycles/fix-clipboard
fix: copy share link to clipboard was broken for some browers
2022-12-19 20:31:46 +01:00
Felix Roos 4e5a0d9e02 fix: #309 2022-12-19 20:30:01 +01:00
Alex McLean bf373d894d ICLC2023 paper (#306)
Paper submission for the international conference on live coding
2022-12-19 11:11:24 +00:00
Felix Roos e6bbad84c1 Merge pull request #308 from tidalcycles/pianoroll-freq-support
support freq in pianoroll
2022-12-15 21:23:22 +01:00
Felix Roos 4de477d46c support freq in pianoroll 2022-12-15 21:21:20 +01:00
Felix Roos 264cab3c9b add scale test 2022-12-15 20:35:10 +01:00
Felix Roos 7f9ef59bea Merge branch 'main' into obj-support-scale 2022-12-15 17:49:24 +01:00
Felix Roos 8ebbef4cbb fix core deps after release trouble 2022-12-13 22:46:35 +01:00
Felix Roos 0c98a84242 bump root to 0.5.0 2022-12-13 22:38:58 +01:00
Felix Roos 6679cd60ce scale now supports running on object values with n 2022-12-11 22:45:36 +01:00
310 changed files with 25762 additions and 19101 deletions
+3
View File
@@ -9,6 +9,9 @@ out/**
postcss.config.js
postcss.config.cjs
tailwind.config.js
tailwind.config.cjs
vite.config.js
/**/dist/**/*
!**/*.mjs
**/*.tsx
**/*.ts
+3 -3
View File
@@ -24,10 +24,10 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: "npm"
- name: Install Dependencies
run: npm ci && cd repl && npm ci && cd ../tutorial && npm ci
run: npm ci && cd website && npm ci
- name: Build
run: npm run build
@@ -39,7 +39,7 @@ jobs:
uses: actions/upload-pages-artifact@v1
with:
# Upload entire repository
path: "./out"
path: "./website/dist"
- name: Deploy to GitHub Pages
id: deployment
+1
View File
@@ -6,3 +6,4 @@
**/dist
packages/mini/krill-parser.js
packages/xen/tunejs.js
paper
+2 -1
View File
@@ -5,5 +5,6 @@
],
"yaml.schemas": {
"https://json.schemastore.org/github-workflow.json": "file:///home/felix/projects/strudel/.github/workflows/deploy.yml"
}
},
"testing.automaticallyOpenPeekView": "never"
}
+5 -7
View File
@@ -12,8 +12,8 @@ To get in touch with the contributors, either
## Ask a Question
If you have any questions about strudel, make sure you've read the
[tutorial](https://strudel.tidalcycles.org/tutorial/) to find out if it answers your question.
If you have any questions about strudel, make sure you've glanced through the
[docs](https://strudel.tidalcycles.org/learn/) to find out if it answers your question.
If not, use one of the Communication Channels above!
Don't be afraid to ask! Your question might be of great value for other people too.
@@ -29,12 +29,10 @@ If you made some music with strudel, you can give back some love and share what
Your creation could also be part of the random selection in the REPL if you want.
Use one of the Communication Channels listed above.
## Improve the Tutorial
## Improve the Docs
If you find some weak spots in the [tutorial](https://strudel.tidalcycles.org/),
you are welcome to improve them by editing [this file](https://github.com/tidalcycles/strudel/blob/main/tutorial/tutorial.mdx).
This will even work without setting up a development environment, only a github account is required.
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/learn/getting-started),
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
## Propose a Feature
+1 -1
View File
@@ -5,7 +5,7 @@
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is slowly stabilising, but please continue to tread carefully.
- Try it here: <https://strudel.tidalcycles.org/>
- Tutorial: <https://strudel.tidalcycles.org/tutorial/>
- Docs: <https://strudel.tidalcycles.org/learn/>
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
## Running Locally
+17
View File
@@ -0,0 +1,17 @@
// this barrel export is currently only used to find undocumented exports
export * from './packages/core/index.mjs';
export * from './packages/csound/index.mjs';
export * from './packages/embed/index.mjs';
export * from './packages/eval/index.mjs';
export * from './packages/midi/index.mjs';
export * from './packages/mini/index.mjs';
export * from './packages/osc/index.mjs';
export * from './packages/react/index.mjs';
export * from './packages/serial/index.mjs';
export * from './packages/soundfonts/index.mjs';
export * from './packages/tonal/index.mjs';
export * from './packages/tone/index.mjs';
export * from './packages/transpiler/index.mjs';
export * from './packages/webaudio/index.mjs';
export * from './packages/webdirt/index.mjs';
export * from './packages/xen/index.mjs';
+20
View File
@@ -0,0 +1,20 @@
# my-patterns
This directory can be used to save your own patterns, which then get
made into a pattern swatch.
0. fork and clone the strudel repository
1. run `npm run setup` in the strudel folder
1. Save one or more .txt files in this folder
2. run `npm run repl` in the top-level strudel folder
3. open `http://localhost:3000/swatch/` !
## deploy
1. in your fork, go to settings -> pages and select "Github Actions" as source
2. edit `website/public/CNAME` to contain `<your-username>.github.io/strudel`
3. edit `website/astro.config.mjs` to use site: `https://<your-username>.github.io` and base `/strudel`
4. go to Actions -> `Build and Deploy` and click `Run workflow`
5. view your patterns at `<your-username>.github.io/strudel/swatch/`
Alternatively, github pages allows you to use a custom domain, like https://mycooldomain.org/swatch/. [See their documentation for details](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site).
+1642 -99
View File
File diff suppressed because it is too large Load Diff
+12 -7
View File
@@ -1,27 +1,30 @@
{
"name": "@strudel.cycles/monorepo",
"version": "0.0.4",
"version": "0.5.0",
"private": true,
"description": "Port of tidalcycles to javascript",
"scripts": {
"pretest": "cd tutorial && npm run jsdoc-json",
"pretest": "npm run jsdoc-json",
"prebuild": "npm run jsdoc-json",
"test": "vitest run --version",
"test-ui": "vitest --ui",
"test-coverage": "vitest --coverage",
"bootstrap": "lerna bootstrap",
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
"setup": "npm i && npm run jsdoc-json && npm run bootstrap && cd website && npm i",
"snapshot": "vitest run -u --silent",
"repl": "cd repl && npm run dev",
"repl": "cd website && npm run dev",
"osc": "cd packages/osc && npm run server",
"build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build",
"preview": "npx serve ./out",
"build": "cd website && npm run build",
"preview": "cd website && npm run preview",
"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",
"codeformat": "prettier --write .",
"format-check": "prettier --check .",
"check": "npm run format-check && npm run lint && npm run test"
"report-undocumented": "npm run jsdoc-json && node undocumented.mjs > undocumented.json",
"check": "npm run format-check && npm run lint && npm run test",
"iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf"
},
"workspaces": [
"packages/*"
@@ -46,6 +49,8 @@
"devDependencies": {
"@vitest/ui": "^0.25.7",
"c8": "^7.12.0",
"canvas": "^2.11.0",
"dependency-tree": "^9.0.0",
"eslint": "^8.28.0",
"events": "^3.3.0",
"gh-pages": "^4.0.0",
+67
View File
@@ -0,0 +1,67 @@
import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
const { createParams } = controls;
let clearColor = '#22222210';
Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } = {}) {
window.frame && cancelAnimationFrame(window.frame);
const ctx = getDrawContext();
const { clientWidth: ww, clientHeight: wh } = ctx.canvas;
let smearPart = smear === 0 ? '99' : Number((1 - smear) * 100).toFixed(0);
smearPart = smearPart.length === 1 ? `0${smearPart}` : smearPart;
clearColor = `#200010${smearPart}`;
const render = (t) => {
let frame;
/* if (sync) {
t = scheduler.now();
frame = this.queryArc(t, t);
} else { */
t = Math.round(t);
frame = this.slow(1000).queryArc(t, t);
// }
ctx.fillStyle = clearColor;
ctx.fillRect(0, 0, ww, wh);
frame.forEach((f) => {
let { x, y, w, h, s, r, a = 0, fill = 'darkseagreen' } = f.value;
w *= ww;
h *= wh;
if (r !== undefined && a !== undefined) {
const radians = a * 2 * Math.PI;
const [cx, cy] = [(ww - w) / 2, (wh - h) / 2];
x = cx + Math.cos(radians) * r * cx;
y = cy + Math.sin(radians) * r * cy;
} else {
x *= ww - w;
y *= wh - h;
}
const val = { ...f.value, x, y, w, h };
ctx.fillStyle = fill;
if (s === 'rect') {
ctx.fillRect(x, y, w, h);
} else if (s === 'ellipse') {
ctx.beginPath();
ctx.ellipse(x + w / 2, y + h / 2, w / 2, h / 2, 0, 0, 2 * Math.PI);
ctx.fill();
}
callback && callback(ctx, val, f);
});
window.frame = requestAnimationFrame(render);
};
window.frame = requestAnimationFrame(render);
return silence;
};
export const { x, y, w, h, a, r, fill, smear } = createParams('x', 'y', 'w', 'h', 'a', 'r', 'fill', 'smear');
export const rescale = register('rescale', function (f, pat) {
return pat.mul(x(f).w(f).y(f).h(f));
});
export const moveXY = register('moveXY', function (dx, dy, pat) {
return pat.add(x(dx).y(dy));
});
export const zoomIn = register('zoomIn', function (f, pat) {
const d = pure(1).sub(f).div(2);
return pat.rescale(f).move(d, d);
});
+175
View File
@@ -0,0 +1,175 @@
export const colorMap = {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgreen: '#006400',
darkgrey: '#a9a9a9',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
gold: '#ffd700',
goldenrod: '#daa520',
gray: '#808080',
green: '#008000',
greenyellow: '#adff2f',
grey: '#808080',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavender: '#e6e6fa',
lavenderblush: '#fff0f5',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgreen: '#90ee90',
lightgrey: '#d3d3d3',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370db',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#db7093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
red: '#ff0000',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd32',
};
export function convertColorToNumber(color) {
// Convert color to lowercase for easier matching
color = color.toLowerCase();
// If the color is a hex code, convert it to a number
if (color[0] === '#') {
return convertHexToNumber(color);
}
// If the color is a named color, return the corresponding number
if (colorMap[color] !== undefined) {
return convertHexToNumber(colorMap[color]);
}
// If the color is not recognized, return null
return -1;
}
export function convertHexToNumber(hex) {
// Remove the leading '#' from the hex code
hex = hex.slice(1);
// Convert the hex code to a number
return parseInt(hex, 16);
}
+89 -34
View File
@@ -51,6 +51,7 @@ const generic_params = [
*
* @name accelerate
* @param {number | Pattern} amount acceleration.
* @superdirtOnly
* @example
* s("sax").accelerate("<0 1 2 4 8 16>").slow(2).osc()
*
@@ -75,19 +76,20 @@ const generic_params = [
*
* @name amp
* @param {number | Pattern} amount gain.
* @superdirtOnly
* @example
* s("bd*8").amp(".1*2 .5 .1*2 .5 .1 .5").osc()
*
*/
['f', 'amp', 'like @gain@, but linear.'],
// TODO: find out why 0 does not work, and it generally seems not right
/*
/**
* A pattern of numbers to specify the attack time of an envelope applied to each sample.
* [More info about envelopes](/learn/synths-samples-effects/#envelope)
*
* @name attack
* @param {number | Pattern} attack time in seconds.
* @example
* n("c5 e5").s('superpiano').attack("<0 .1>").osc()
* note("c3 e3").attack("<0 .1 .5>")
*
*/
[
@@ -107,18 +109,36 @@ const generic_params = [
*/
['f', 'bank', 'selects sound bank to use'],
// TODO: find out how this works?
/*
* Envelope decay time = the time it takes after the attack time to reach the sustain level.
/**
* Gain envelope decay time = the time it takes after the attack time to reach the sustain level.
* [More info about envelopes](/learn/synths-samples-effects/#envelope)
*
* @name decay
* @param {number | Pattern} time decay time in seconds
* @example
* s("sax").cut(1).decay("<.1 .2 .3 .4>").sustain(0).osc()
* note("c3 e3").decay("<.1 .2 .3 .4>").sustain(0)
*
*/
['f', 'decay', ''],
/**
* Gain envelope sustain level. [More info about envelopes](/learn/synths-samples-effects/#envelope)
*
* @name sustain
* @param {number | Pattern} gain sustain level between 0 and 1
* @example
* note("c3 e3").decay(.2).sustain("<0 .1 .4 .6 1>")
*
*/
['f', 'sustain', ''],
/**
* Gain envelope release time. [More info about envelopes](/learn/synths-samples-effects/#envelope)
*
* @name release
* @param {number | Pattern} time release time in seconds
* @example
* note("c3 e3 g3 c4").release("<0 .1 .4 .6 1>/2")
*
*/
[
'f',
'release',
@@ -135,22 +155,26 @@ const generic_params = [
*
* @name bandf
* @param {number | Pattern} frequency center frequency
* @synonyms bpf
* @example
* s("bd sd,hh*3").bandf("<1000 2000 4000 8000>")
*
*/
['f', 'bandf', 'A pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
['f', 'bpf', ''],
// TODO: in tidal, it seems to be normalized
/**
* Sets the q-factor of the band-pass filter
*
* @name bandq
* @param {number | Pattern} q q factor
* @synonyms bpq
* @example
* s("bd sd").bandf(500).bandq("<0 1 2 3>")
*
*/
['f', 'bandq', 'a pattern of anumbers from 0 to 1. Sets the q-factor of the band-pass filter.'],
['f', 'bpq', ''],
/**
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
*
@@ -249,7 +273,7 @@ const generic_params = [
* @name cut
* @param {number | Pattern} group cut group number
* @example
* s("bd sax").cut(1).osc()
* s("rd*4").cut(1)
*
*/
[
@@ -262,53 +286,50 @@ const generic_params = [
*
* @name cutoff
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms lpf
* @example
* s("bd sd,hh*3").cutoff("<4000 2000 1000 500 200 100>")
*
*/
// TODO: add lpf synonym
['f', 'cutoff', 'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the low-pass filter.'],
['f', 'lpf'],
/**
* Applies the cutoff frequency of the high-pass filter.
*
* @name hcutoff
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms hpf
* @example
* s("bd sd,hh*4").hcutoff("<4000 2000 1000 500 200 100>")
*
*/
// TODO: add hpf synonym
[
'f',
'hcutoff',
'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the high-pass filter. Also has alias @hpf@',
],
['f', 'hcutoff', ''],
['f', 'hpf', ''],
/**
* Applies the resonance of the high-pass filter.
*
* @name hresonance
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms hpq
* @example
* s("bd sd,hh*4").hcutoff(2000).hresonance("<0 10 20 30>")
*
*/
[
'f',
'hresonance',
'a pattern of numbers from 0 to 1. Applies the resonance of the high-pass filter. Has alias @hpq@',
],
['f', 'hpq', ''],
['f', 'hresonance', ''],
// TODO: add hpq synonym
/**
* Applies the cutoff frequency of the low-pass filter.
*
* @name resonance
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms lpq
* @example
* s("bd sd,hh*4").cutoff(2000).resonance("<0 10 20 30>")
*
*/
['f', 'resonance', 'a pattern of numbers from 0 to 1. Specifies the resonance of the low-pass filter.'],
// TODO: add lpq synonym?
['f', 'lpq'],
['f', 'resonance', ''],
/**
* DJ filter, below 0.5 is low pass filter, above is high pass filter.
*
@@ -321,17 +342,36 @@ const generic_params = [
['f', 'djf', 'DJ filter, below 0.5 is low pass filter, above is high pass filter.'],
// ['f', 'cutoffegint', ''],
// TODO: does not seem to work
/*
/**
* Sets the level of the delay signal.
*
* @name delay
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd").delay("<0 .5 .75 1>").osc()
* s("bd").delay("<0 .25 .5 1>")
*
*/
['f', 'delay', 'a pattern of numbers from 0 to 1. Sets the level of the delay signal.'],
/**
* Sets the level of the signal that is fed back into the delay.
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
* @param {number | Pattern} feedback between 0 and 1
* @example
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>").slow(2)
*
*/
['f', 'delayfeedback', 'a pattern of numbers from 0 to 1. Sets the amount of delay feedback.'],
/**
* Sets the time of the delay effect.
*
* @name delaytime
* @param {number | Pattern} seconds between 0 and Infinity
* @example
* s("bd").delay(.25).delaytime("<.125 .25 .5 1>").slow(2)
*
*/
['f', 'delaytime', 'a pattern of numbers from 0 to 1. Sets the length of the delay.'],
/* // TODO: test
* Specifies whether delaytime is calculated relative to cps.
@@ -484,11 +524,15 @@ const generic_params = [
// ['f', 'ophatdecay', ''],
// TODO: example
/**
* a pattern of numbers. An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share hardware output bus offset and global effects, e.g. reverb and delay. The maximum number of orbits is specified in the superdirt startup, numbers higher than maximum will wrap around.
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
*
* @name orbit
* @param {number | Pattern} number
*
* @example
* stack(
* s("hh*3").delay(.5).delaytime(.25).orbit(1),
* s("~ sd").delay(.5).delaytime(.125).orbit(2)
* )
*/
[
'i',
@@ -570,21 +614,22 @@ const generic_params = [
* @name room
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd sd").room("<0 .2 .4 .6 .8 1>").osc()
* s("bd sd").room("<0 .2 .4 .6 .8 1>")
*
*/
['f', 'room', 'a pattern of numbers from 0 to 1. Sets the level of reverb.'],
/**
* Sets the room size of the reverb, see {@link room}.
*
* @name size
* @param {number | Pattern} size between 0 and 1
* @name roomsize
* @synonyms size
* @param {number | Pattern} size between 0 and 10
* @example
* s("bd sd").room(.8).size("<0 .2 .4 .6 .8 1>").osc()
* s("bd sd").room(.8).roomsize("<0 1 2 4 8>")
*
*/
// TODO: find out why :
// s("bd sd").room(.8).size("<0 .2 .4 .6 .8 [1,0]>").osc()
// s("bd sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc()
// .. does not work. Is it because room is only one effect?
[
'f',
@@ -620,9 +665,9 @@ const generic_params = [
* @name speed
* @param {number | Pattern} speed -inf to inf, negative numbers play the sample backwards.
* @example
* s("bd").speed("<1 2 4 1 -2 -4>").osc()
* s("bd").speed("<1 2 4 1 -2 -4>")
* @example
* speed("1 1.5*2 [2 1.1]").s("sax").cut(1).osc()
* speed("1 1.5*2 [2 1.1]").s("piano").clip(1)
*
*/
[
@@ -775,13 +820,23 @@ const _setter = (func, name) =>
};
generic_params.forEach(([type, name, description]) => {
controls[name] = (...pats) => _name(name, ...pats);
controls[name] = function (...pats) {
const result = _name(name, ...pats);
// Add a function for composing this control with another pattern
result.__as_function = function (pat) {
return pat[name](...pats);
};
return result;
};
Pattern.__registered.push(name);
Pattern.prototype[name] = _setter(controls[name], name);
});
// create custom param
controls.createParam = (name) => {
const func = (...pats) => _name(name, ...pats);
Pattern.__registered.push(name);
Pattern.prototype[name] = _setter(func, name);
return (...pats) => _name(name, ...pats);
};
+6 -6
View File
@@ -8,13 +8,10 @@ import createClock from './zyklus.mjs';
import { logger } from './logger.mjs';
export class Cyclist {
worker;
pattern;
started = false;
cps = 1; // TODO
getTime;
phase = 0;
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
this.started = false;
this.cps = 1; // TODO
this.phase = 0;
this.getTime = getTime;
this.onToggle = onToggle;
this.latency = latency;
@@ -49,6 +46,9 @@ export class Cyclist {
getPhase() {
return this.getTime() - this.origin - this.latency;
}
now() {
return this.getTime() - this.origin + this.clock.minLatency;
}
setStarted(v) {
this.started = v;
this.onToggle?.(v);
+6
View File
@@ -59,3 +59,9 @@ export const cleanupDraw = (clearScreen = true) => {
clearInterval(window.strudelScheduler);
}
};
Pattern.prototype.onPaint = function (onPaint) {
// this is evil! TODO: add pattern.context
this.context = { onPaint };
return this;
};
+102 -29
View File
@@ -1,47 +1,97 @@
/*
euclid.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs>
euclid.mjs - Bjorklund/Euclidean/Diaspora rhythms
Copyright (C) 2023 Rohan Drape and strudel contributors
See <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs> for authors of this file.
The Bjorklund algorithm implementation is ported from the Haskell Music Theory Haskell module by Rohan Drape -
https://rohandrape.net/?t=hmt
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, timeCat } from './pattern.mjs';
import bjork from 'bjork';
import { rotate } from './util.mjs';
import { Pattern, timeCat, register, silence } from './pattern.mjs';
import { rotate, flatten } from './util.mjs';
import Fraction from './fraction.mjs';
const euclid = (pulses, steps, rotation = 0) => {
const b = bjork(steps, pulses);
if (rotation) {
return rotate(b, -rotation);
}
return b;
const splitAt = function (index, value) {
return [value.slice(0, index), value.slice(index)];
};
const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
const left = function (n, x) {
const [ons, offs] = n;
const [xs, ys] = x;
const [_xs, __xs] = splitAt(offs, xs);
return [
[offs, ons - offs],
[zipWith((a, b) => a.concat(b), _xs, ys), __xs],
];
};
const right = function (n, x) {
const [ons, offs] = n;
const [xs, ys] = x;
const [_ys, __ys] = splitAt(ons, ys);
const result = [
[ons, offs - ons],
[zipWith((a, b) => a.concat(b), xs, _ys), __ys],
];
return result;
};
const _bjork = function (n, x) {
const [ons, offs] = n;
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
};
export const bjork = function (ons, steps) {
const offs = steps - ons;
const x = Array(ons).fill([1]);
const y = Array(offs).fill([0]);
const result = _bjork([ons, offs], [x, y]);
return flatten(result[1][0]).concat(flatten(result[1][1]));
};
/**
* Changes the structure of the pattern to form an euclidean rhythm.
* Euclidian rhythms are rhythms obtained using the greatest common divisor of two numbers.
* They were described in 2004 by Godfried Toussaint, a canadian computer scientist.
* Euclidian rhythms are really useful for computer/algorithmic music because they can accurately
* describe a large number of rhythms used in the most important music world traditions.
* Euclidian rhythms are rhythms obtained using the greatest common
* divisor of two numbers. They were described in 2004 by Godfried
* Toussaint, a canadian computer scientist. Euclidian rhythms are
* really useful for computer/algorithmic music because they can
* describe a large number of rhythms with a couple of numbers.
*
* @memberof Pattern
* @name euclid
* @param {number} pulses the number of onsets / beats
* @param {number} steps the number of steps to fill
* @param {number} rotation (optional) offset in steps
* @returns Pattern
* @example
* // The Cuban tresillo pattern.
* note("c3").euclid(3,8)
*/
/**
* Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence.
* @memberof Pattern
* @name euclidRot
* @param {number} pulses the number of onsets / beats
* @param {number} steps the number of steps to fill
* @param {number} rotation offset in steps
* @returns Pattern
* @example
* // A Samba rhythm necklace from Brazil
* note("c3").euclidRot(3,16,14)
*/
/**
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
* note("c3").euclid(2,5)
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
* note("c3").euclid(3,4)
* @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm.
* note("c3").euclid(3,5,2)
* note("c3").euclidRot(3,5,2)
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
* note("c3").euclid(3,7)
* @example // The Cuban tresillo pattern.
@@ -71,34 +121,57 @@ const euclid = (pulses, steps, rotation = 0) => {
* @example // A common West African bell pattern.
* note("c3").euclid(7,12)
* @example // A Samba rhythm necklace from Brazil.
* note("c3").euclid(7,16,14)
* note("c3").euclidRot(7,16,14)
* @example // A rhythm necklace used in the Central African Republic.
* note("c3").euclid(9,16)
* @example // A rhythm necklace of the Aka Pygmies of Central Africa.
* note("c3").euclid(11,24,14)
* note("c3").euclidRot(11,24,14)
* @example // Another rhythm necklace of the Aka Pygmies of the upper Sangha.
* note("c3").euclid(13,24,5)
* note("c3").euclidRot(13,24,5)
*/
Pattern.prototype.euclid = function (pulses, steps, rotation = 0) {
return this.struct(euclid(pulses, steps, rotation));
const _euclidRot = function (pulses, steps, rotation) {
const b = bjork(pulses, steps);
if (rotation) {
return rotate(b, -rotation);
}
return b;
};
export const euclid = register('euclid', function (pulses, steps, pat) {
return pat.struct(_euclidRot(pulses, steps, 0));
});
export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) {
return pat.struct(_euclidRot(pulses, steps, rotation));
});
/**
* Similar to `.euclid`, but each pulse is held until the next pulse, so there will be no gaps.
* Similar to `euclid`, but each pulse is held until the next pulse,
* so there will be no gaps.
* @name euclidLegato
* @memberof Pattern
* @example
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
*/
Pattern.prototype.euclidLegato = function (pulses, steps, rotation = 0) {
const bin_pat = euclid(pulses, steps, rotation);
const firstOne = bin_pat.indexOf(1);
const gapless = rotate(bin_pat, firstOne)
const _euclidLegato = function (pulses, steps, rotation, pat) {
if (pulses < 1) {
return silence;
}
const bin_pat = _euclidRot(pulses, steps, rotation);
const gapless = bin_pat
.join('')
.split('1')
.slice(1)
.map((s) => [s.length + 1, true]);
return this.struct(timeCat(...gapless)).late(Fraction(firstOne).div(steps));
return pat.struct(timeCat(...gapless));
};
export default euclid;
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {
return _euclidLegato(pulses, steps, 0, pat);
});
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
return _euclidLegato(pulses, steps, rotation, pat);
});
+8 -1
View File
@@ -19,7 +19,14 @@ export const evalScope = async (...args) => {
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
}
});
Object.assign(globalThis, ...modules);
// Object.assign(globalThis, ...modules);
// below is a fix for above commented out line
// same error as https://github.com/vitest-dev/vitest/issues/1807 when running this on astro server
modules.forEach((module) => {
Object.entries(module).forEach(([name, value]) => {
globalThis[name] = value;
});
});
};
function safeEval(str, options = {}) {
+1
View File
@@ -21,6 +21,7 @@ export * from './repl.mjs';
export * from './logger.mjs';
export * from './time.mjs';
export * from './draw.mjs';
export * from './animate.mjs';
export * from './pianoroll.mjs';
export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs';
+1 -1
View File
@@ -2,7 +2,7 @@ export const logKey = 'strudel.log';
export function logger(message, type, data = {}) {
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
if (typeof CustomEvent !== 'undefined') {
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
document.dispatchEvent(
new CustomEvent(logKey, {
detail: {
-1
View File
@@ -25,7 +25,6 @@
},
"homepage": "https://strudel.tidalcycles.org",
"dependencies": {
"bjork": "^0.0.1",
"fraction.js": "^4.2.0"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2"
+403 -121
View File
@@ -23,24 +23,40 @@ export const setStringParser = (parser) => (stringParser = parser);
/** @class Class representing a pattern. */
export class Pattern {
_Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another Pattern
/**
* Create a pattern. As an end user, you will most likely not create a Pattern directly.
*
* @param {function} query - The function that maps a {@link State} to an array of {@link Hap}.
*/
constructor(query) {
constructor(query, as_function) {
this.query = query;
this._Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another Pattern
this.__as_function = as_function;
}
// TODO - would a default 'as_function' be useful?
// /**
// * Accessor for pattern-as-function
// */
// get as_function() {
// if (!this.__as_function) {
// // TODO - add other alignments
// this.__as_function = x => x.set(this);
// }
// return this.__as_function;
// }
//////////////////////////////////////////////////////////////////////
// Haskell-style functor, applicative and monadic operations
/**
* Returns a new pattern, with the function applied to the value of
* each hap. It has the alias {@link Pattern#fmap}.
* @param {Function} func
* @synonyms fmap
* @param {Function} func to to apply to the value
* @returns Pattern
* @example
* "0 1 2".withValue(v => v + 10).log()
*/
withValue(func) {
return new Pattern((state) => this.query(state).map((hap) => hap.withValue(func)));
@@ -53,10 +69,19 @@ export class Pattern {
return this.withValue(func);
}
applyValue(func) {
return this.withValue((x) => x(func));
}
/**
* Assumes 'this' is a pattern of functions, and given a function to
* resolve wholes, applies a given pattern of values to that
* pattern of functions.
* @param {Function} whole_func
* @param {Function} func
* @returns Pattern
*/
appWhole(whole_func, pat_val) {
// Assumes 'this' is a pattern of functions, and given a function to
// resolve wholes, applies a given pattern of values to that
// pattern of functions.
const pat_func = this;
const query = function (state) {
const hap_funcs = pat_func.query(state);
@@ -326,7 +351,12 @@ export class Pattern {
* silence
*/
queryArc(begin, end) {
return this.query(new State(new TimeSpan(begin, end)));
try {
return this.query(new State(new TimeSpan(begin, end)));
} catch (err) {
logger(`[query]: ${err.message}`, 'error');
return [];
}
}
/**
@@ -676,9 +706,10 @@ export class Pattern {
// Methods without corresponding toplevel functions
/**
* Layers the result of the given function(s). Like {@link superimpose}, but without the original pattern:
* Layers the result of the given function(s). Like {@link Pattern.superimpose}, but without the original pattern:
* @name layer
* @memberof Pattern
* @synonyms apply
* @returns Pattern
* @example
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
@@ -712,7 +743,7 @@ export class Pattern {
* @memberof Pattern
* @example
* s("hh*2").stack(
* n("c2(3,8)")
* note("c2(3,8)")
* )
*/
stack(...pats) {
@@ -724,12 +755,13 @@ export class Pattern {
}
/**
* Appends the given pattern(s) to the current pattern. Synonyms: .sequence .fastcat
* Appends the given pattern(s) to the current pattern.
* @name seq
* @memberof Pattern
* @synonyms sequence, fastcat
* @example
* s("hh*2").seq(
* n("c2(3,8)")
* note("c2(3,8)")
* )
*/
seq(...pats) {
@@ -737,12 +769,13 @@ export class Pattern {
}
/**
* Appends the given pattern(s) to the next cycle. Synonym: .slowcat
* Appends the given pattern(s) to the next cycle.
* @name cat
* @memberof Pattern
* @synonyms slowcat
* @example
* s("hh*2").cat(
* n("c2(3,8)")
* note("c2(3,8)")
* )
*/
cat(...pats) {
@@ -776,8 +809,10 @@ export class Pattern {
);
}
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`) {
return this.onTrigger((...args) => logger(func(...args)), false);
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) {
return this.onTrigger((...args) => {
logger(func(...args), undefined, getData(...args));
}, false);
}
logValues(func = id) {
@@ -820,15 +855,25 @@ Pattern.prototype.collect = function () {
);
};
// applies func to each array of congruent haps
/**
* Selects indices in in stacked notes.
* @example
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arpWith(haps => haps[2])
* */
Pattern.prototype.arpWith = function (func) {
return this.collect()
.fmap((v) => reify(func(v)))
.squeezeJoin()
.innerJoin()
.withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value)));
};
// applies pattern of indices to each array of congruent haps
/**
* Selects indices in in stacked notes.
* @example
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arp("0 [0,2] 1 [0,2]").slow(2)
* */
Pattern.prototype.arp = function (pat) {
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
};
@@ -988,9 +1033,6 @@ function _composeOp(a, b, func) {
/**
* Applies the given structure to the pattern:
*
* @name struct
* @memberof Pattern
* @returns Pattern
* @example
* note("c3,eb3,g3")
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
@@ -1002,18 +1044,37 @@ function _composeOp(a, b, func) {
Pattern.prototype.structAll = function (...args) {
return this.keep.out(...args);
};
/**
* Returns silence when mask is 0 or "~"
*
* @example
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>").slow(2)
*/
Pattern.prototype.mask = function (...args) {
return this.keepif.in(...args);
};
Pattern.prototype.maskAll = function (...args) {
return this.keep.in(...args);
};
/**
* Resets the pattern to the start of the cycle for each onset of the reset pattern.
*
* @example
* s("<bd lt> sd, hh*4").reset("<x@3 x(3,8)>")
*/
Pattern.prototype.reset = function (...args) {
return this.keepif.trig(...args);
};
Pattern.prototype.resetAll = function (...args) {
return this.keep.trig(...args);
};
/**
* Restarts the pattern for each onset of the restart pattern.
* While reset will only reset the current cycle, restart will start from cycle 0.
*
* @example
* s("<bd lt> sd, hh*4").restart("<x@3 x(3,8)>")
*/
Pattern.prototype.restart = function (...args) {
return this.keepif.trigzero(...args);
};
@@ -1027,6 +1088,7 @@ export const polyrhythm = stack;
export const pr = stack;
// methods that create patterns, which are added to patternified Pattern methods
// TODO: remove? this is only used in old transpiler (shapeshifter)
Pattern.prototype.factories = {
pure,
stack,
@@ -1045,7 +1107,12 @@ Pattern.prototype.factories = {
// Elemental patterns
// Nothing
/**
* Does absolutely nothing..
* @name silence
* @example
* silence // "~"
*/
export const silence = new Pattern(() => []);
/** A discrete value that repeats once per cycle.
@@ -1091,6 +1158,7 @@ export function reify(thing) {
/** The given items are played at the same time at the same length.
*
* @return {Pattern}
* @synonyms polyrhythm, pr
* @example
* stack(g3, b3, [e4, d4]).note() // "g3,b3,[e4,d4]".note()
*/
@@ -1145,24 +1213,10 @@ export function slowcatPrime(...pats) {
return new Pattern(query).splitQueries();
}
/** Concatenation: as with {@link slowcat}, but squashes a cycle from each pattern into one cycle
*
* Synonyms: {@link seq}, {@link sequence}
*
* @param {...any} items - The items to concatenate
* @return {Pattern}
* @example
* fastcat(e5, b4, [d5, c5])
* // sequence(e5, b4, [d5, c5])
* // seq(e5, b4, [d5, c5])
*/
export function fastcat(...pats) {
return slowcat(...pats)._fast(pats.length);
}
/** The given items are con**cat**enated, where each one takes one cycle. Synonym: slowcat
/** The given items are con**cat**enated, where each one takes one cycle.
*
* @param {...any} items - The items to concatenate
* @synonyms slowcat
* @return {Pattern}
* @example
* cat(e5, b4, [d5, c5]).note() // "<e5 b4 [d5 c5]>".note()
@@ -1172,7 +1226,7 @@ export function cat(...pats) {
return slowcat(...pats);
}
/** Like {@link seq}, but each step has a length, relative to the whole.
/** Like {@link Pattern.seq}, but each step has a length, relative to the whole.
* @return {Pattern}
* @example
* timeCat([3,e3],[1, g3]).note() // "e3@3 g3".note()
@@ -1189,12 +1243,17 @@ export function timeCat(...timepats) {
return stack(...pats);
}
export function fastcat(...pats) {
return slowcat(...pats)._fast(pats.length);
}
/** See {@link fastcat} */
export function sequence(...pats) {
return fastcat(...pats);
}
/** Like **cat**, but the items are crammed into one cycle. Synonyms: fastcat, sequence
/** Like **cat**, but the items are crammed into one cycle.
* @synonyms fastcat, sequence
* @example
* seq(e5, b4, [d5, c5]).note() // "e5 b4 [d5 c5]".note()
*
@@ -1215,7 +1274,17 @@ function _sequenceCount(x) {
}
return [reify(x), 1];
}
/**
* Aligns one or more given sequences to the given number of steps per cycle.
*
* @name polymeterSteps
* @param {number} steps how many items are placed in one cycle
* @param {any[]} sequences one or more arrays of Patterns / values
* @example
* polymeterSteps(2, ["c", "d", "e", "f", "g", "f", "e", "d"])
* .note().stack(s("bd")) // 1 cycle = 1 bd = 2 notes
* // note("{c d e f g f e d}%2").stack(s("bd"))
*/
export function polymeterSteps(steps, ...args) {
const seqs = args.map((a) => _sequenceCount(a));
if (seqs.length == 0) {
@@ -1238,6 +1307,14 @@ export function polymeterSteps(steps, ...args) {
return stack(...pats);
}
/**
* Combines the given lists of patterns with the same pulse. This will create so called polymeters when different sized sequences are used.
* @synonyms pm
* @example
* polymeter(["c", "eb", "g"], ["c2", "g2"]).note()
* // "{c eb g, c2 g2}".note()
*
*/
export function polymeter(...args) {
return polymeterSteps(0, ...args);
}
@@ -1278,11 +1355,18 @@ export const and = curry((a, b) => reify(b).and(a));
export const or = curry((a, b) => reify(b).or(a));
export const func = curry((a, b) => reify(b).func(a));
export function register(name, func) {
/**
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
*
* @param {string} name name of the function
* @param {function} func function with 1 or more params, where last is the current pattern
*
*/
export function register(name, func, f_params = null) {
if (Array.isArray(name)) {
const result = {};
for (const name_item of name) {
result[name_item] = register(name_item, func);
result[name_item] = register(name_item, func, f_params);
}
return result;
}
@@ -1306,9 +1390,32 @@ export function register(name, func) {
return func(...args, pat);
};
mapFn = curry(mapFn, null, arity - 1);
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
// Don't use applicative for arguments that a) have a '__as_function' function and b) are
// marked as being a higher order function parameter
function app(acc, p, i) {
if (f_params != null && f_params.length > i + 1 && f_params[i + 1] && '__as_function' in p) {
return acc.applyValue(p.__as_function);
}
return acc.appLeft(p);
}
var start;
// Do the same check for the first parameter.. a bit repetitive
if (f_params != null && f_params.length > 0 && f_params[0] && '__as_function' in left) {
start = pure(mapFn(left.__as_function));
} else {
start = left.fmap(mapFn);
}
return right.reduce(app, start).innerJoin();
};
if (!Pattern.__registered) {
Pattern.__registered = [];
}
Pattern.__registered.push(name);
Pattern.prototype[name] = function (...args) {
args = args.map(reify);
// For methods that take a single argument (plus 'this'), allow
@@ -1318,7 +1425,19 @@ export function register(name, func) {
} else if (arity !== args.length + 1) {
throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`);
}
return pfunc(...args, this);
const result = pfunc(...args, this);
// speed(2,3).__as_function(pat) = pat.speed(2,3)
// and so
// speed(2,3).fast(2).__as_function(pat) = pat.speed(2,3).fast(2)
if ('__as_function' in this) {
const pata = this;
result.__as_function = function (patb) {
return pata.__as_function(patb)[name](...args);
};
}
return result;
};
if (arity > 1) {
@@ -1354,7 +1473,11 @@ export const round = register('round', function (pat) {
* Assumes a numerical pattern. Returns a new pattern with all values set to
* their mathematical floor. E.g. `3.7` replaced with to `3`, and `-4.2`
* replaced with `-5`.
* @name floor
* @memberof Pattern
* @returns Pattern
* @example
* "42 42.1 42.5 43".floor().note()
*/
export const floor = register('floor', function (pat) {
return pat.asNumber().fmap((v) => Math.floor(v));
@@ -1364,7 +1487,11 @@ export const floor = register('floor', function (pat) {
* Assumes a numerical pattern. Returns a new pattern with all values set to
* their mathematical ceiling. E.g. `3.2` replaced with `4`, and `-4.2`
* replaced with `-4`.
* @name ceil
* @memberof Pattern
* @returns Pattern
* @example
* "42 42.1 42.5 43".ceil().note()
*/
export const ceil = register('ceil', function (pat) {
return pat.asNumber().fmap((v) => Math.ceil(v));
@@ -1402,23 +1529,27 @@ export const range = register('range', function (min, max, pat) {
});
/**
* Assumes a numerical pattern, containing unipolar values in the range 0 ..
* 1. Returns a new pattern with values scaled to the given min/max range,
* Assumes a numerical pattern, containing unipolar values in the range 0 .. 1
* Returns a new pattern with values scaled to the given min/max range,
* following an exponential curve.
* @param {Number} min
* @param {Number} max
* @name rangex
* @memberof Pattern
* @returns Pattern
* @example
* s("bd sd,hh*4").cutoff(sine.rangex(500,2000).slow(4))
*/
export const rangex = register('rangex', function (min, max, pat) {
return pat._range(Math.log(min), Math.log(max)).fmap(Math.exp);
});
/**
* Assumes a numerical pattern, containing bipolar values in the range -1 ..
* 1. Returns a new pattern with values scaled to the given min/max range.
* @param {Number} min
* @param {Number} max
* Assumes a numerical pattern, containing bipolar values in the range -1 .. 1
* Returns a new pattern with values scaled to the given min/max range.
* @name range2
* @memberof Pattern
* @returns Pattern
* @example
* s("bd sd,hh*4").cutoff(sine2.range2(500,2000).slow(4))
*/
export const range2 = register('range2', function (min, max, pat) {
return pat.fromBipolar()._range(min, max);
@@ -1427,8 +1558,16 @@ export const range2 = register('range2', function (min, max, pat) {
//////////////////////////////////////////////////////////////////////
// Structural and temporal transformations
// Compress each cycle into the given timespan, leaving a gap
/** Compress each cycle into the given timespan, leaving a gap
* @example
* cat(
* s("bd sd").compress(.25,.75),
* s("~ bd sd ~")
* )
*/
export const compress = register('compress', function (b, e, pat) {
b = Fraction(b);
e = Fraction(e);
if (b.gt(e) || b.gt(1) || e.gt(1) || b.lt(0) || e.lt(0)) {
return silence;
}
@@ -1439,6 +1578,13 @@ export const { compressSpan, compressspan } = register(['compressSpan', 'compres
return pat._compress(span.begin, span.end);
});
/**
* speeds up a pattern like fast, but rather than it playing multiple times as fast would it instead leaves a gap in the remaining space of the cycle. For example, the following will play the sound pattern "bd sn" only once but compressed into the first half of the cycle, i.e. twice as fast.
* @name fastGap
* @synonyms fastgap
* @example
* s("bd sd").fastGap(2)
*/
export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (factor, pat) {
// A bit fiddly, to drop zero-width queries at the start of the next cycle
const qf = function (span) {
@@ -1469,10 +1615,14 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f
return pat.withQuerySpanMaybe(qf).withHap(ef).splitQueries();
});
// Similar to compress, but doesn't leave gaps, and the 'focus' can be
// bigger than a cycle
/**
* Similar to compress, but doesn't leave gaps, and the 'focus' can be bigger than a cycle
* @example
* s("bd hh sd hh").focus(1/4, 3/4)
*/
export const focus = register('focus', function (b, e, pat) {
b = Fraction(b);
e = Fraction(e);
return pat._fast(Fraction(1).div(e.sub(b))).late(b.cyclePos());
});
@@ -1480,6 +1630,10 @@ export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], fun
return pat._focus(span.begin, span.end);
});
/** The ply function repeats each event the given number of times.
* @example
* s("bd ~ sd cp").ply("<1 2 3>")
*/
export const ply = register('ply', function (factor, pat) {
return pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin();
});
@@ -1488,6 +1642,7 @@ export const ply = register('ply', function (factor, pat) {
* Speed up a pattern by the given factor. Used by "*" in mini notation.
*
* @name fast
* @synonyms density
* @memberof Pattern
* @param {number | Pattern} factor speed up factor
* @returns Pattern
@@ -1504,6 +1659,7 @@ export const { fast, density } = register(['fast', 'density'], function (factor,
* Slow down a pattern over the given number of cycles. Like the "/" operator in mini notation.
*
* @name slow
* @synonyms sparsity
* @memberof Pattern
* @param {number | Pattern} factor slow down factor
* @returns Pattern
@@ -1514,15 +1670,33 @@ export const { slow, sparsity } = register(['slow', 'sparsity'], function (facto
return pat._fast(Fraction(1).div(factor));
});
// Should these really be in alphabetical order? a shame to split
// fast/slow, inside/outside..
export const inside = register('inside', function (factor, f, pat) {
return f(pat._slow(factor))._fast(factor);
});
/**
* Carries out an operation 'inside' a cycle.
* @example
* "0 1 2 3 4 3 2 1".inside(4, rev).scale('C major').note()
* // "0 1 2 3 4 3 2 1".slow(4).rev().fast(4).scale('C major').note()
*/
export const inside = register(
'inside',
function (factor, f, pat) {
return f(pat._slow(factor))._fast(factor);
},
[false, true],
);
export const outside = register('outside', function (factor, f, pat) {
return f(pat._fast(factor))._slow(factor);
});
/**
* Carries out an operation 'outside' a cycle.
* @example
* "<[0 1] 2 [3 4] 5>".outside(4, rev).scale('C major').note()
* // "<[0 1] 2 [3 4] 5>".fast(4).rev().slow(4).scale('C major').note()
*/
export const outside = register(
'outside',
function (factor, f, pat) {
return f(pat._fast(factor))._slow(factor);
},
[false, true],
);
/**
* Applies the given function every n cycles, starting from the last cycle.
@@ -1534,11 +1708,16 @@ export const outside = register('outside', function (factor, f, pat) {
* @example
* note("c3 d3 e3 g3").lastOf(4, x=>x.rev())
*/
export const lastOf = register('lastOf', function (n, func, pat) {
const pats = Array(n - 1).fill(pat);
pats.push(func(pat));
return slowcatPrime(...pats);
});
export const lastOf = register(
'lastOf',
function (n, func, pat) {
const pats = Array(n - 1).fill(pat);
pats.push(func(pat));
return slowcatPrime(...pats);
},
// second parameter is a function
[false, true],
);
/**
* Applies the given function every n cycles, starting from the first cycle.
@@ -1561,11 +1740,16 @@ export const lastOf = register('lastOf', function (n, func, pat) {
* @example
* note("c3 d3 e3 g3").every(4, x=>x.rev())
*/
export const { firstOf, every } = register(['firstOf', 'every'], function (n, func, pat) {
const pats = Array(n - 1).fill(pat);
pats.unshift(func(pat));
return slowcatPrime(...pats);
});
export const { firstOf, every } = register(
['firstOf', 'every'],
function (n, func, pat) {
const pats = Array(n - 1).fill(pat);
pats.unshift(func(pat));
return slowcatPrime(...pats);
},
// second parameter is a function
[false, true],
);
/**
* Like layer, but with a single function:
@@ -1574,11 +1758,20 @@ export const { firstOf, every } = register(['firstOf', 'every'], function (n, fu
* @example
* "<c3 eb3 g3>".scale('C minor').apply(scaleTranspose("0,2,4")).note()
*/
export const apply = register('apply', function (func, pat) {
return func(pat);
});
// TODO: remove or dedupe with layer?
export const apply = register(
'apply',
function (func, pat) {
return func(pat);
},
[true],
);
// cpm = cycles per minute
/**
* Plays the pattern at the given cycles per minute.
* @example
* s("<bd sd>,hh*2").cpm(90) // = 90 bpm
*/
// TODO - global clock
export const cpm = register('cpm', function (cpm, pat) {
return pat._fast(cpm / 60);
@@ -1614,6 +1807,13 @@ export const late = register('late', function (offset, pat) {
return pat._early(Fraction(0).sub(offset));
});
/**
* Plays a portion of a pattern, specified by the beginning and end of a time span. The new resulting pattern is played over the time period of the original pattern:
*
* @example
* s("bd*2 hh*3 [sd bd]*2 perc").zoom(0.25, 0.75)
* // s("hh*3 [sd bd]*2") // equivalent
*/
export const zoom = register('zoom', function (s, e, pat) {
e = Fraction(e);
s = Fraction(s);
@@ -1628,6 +1828,12 @@ export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a
return pat.zoom(a.begin, a.end);
});
/**
* Selects the given fraction of the pattern and repeats that part to fill the remainder of the cycle.
* @param {number} fraction fraction to select
* @example
* s("lt ht mt cp, [hh oh]*2").linger("<1 .5 .25 .125>")
*/
export const linger = register('linger', function (t, pat) {
if (t == 0) {
return silence;
@@ -1637,10 +1843,23 @@ export const linger = register('linger', function (t, pat) {
return pat._zoom(0, t)._slow(t);
});
/**
* Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one.
* @param {number} segments number of segments per cycle
* @example
* note(saw.range(0,12).segment(24)).add(40)
*/
export const segment = register('segment', function (rate, pat) {
return pat.struct(pure(true)._fast(rate));
});
/**
* Swaps 1s and 0s in a binary pattern.
* @name invert
* @synonyms inv
* @example
* s("bd").struct("1 0 0 1 0 0 1 0".lastOf(4, invert))
*/
export const { invert, inv } = register(['invert', 'inv'], function (pat) {
// Swap true/false in a binary pattern
return pat.fmap((x) => !x);
@@ -1656,9 +1875,13 @@ export const { invert, inv } = register(['invert', 'inv'], function (pat) {
* @example
* "c3 eb3 g3".when("<0 1>/2", x=>x.sub(5)).note()
*/
export const when = register('when', function (on, func, pat) {
return on ? func(pat) : pat;
});
export const when = register(
'when',
function (on, func, pat) {
return on ? func(pat) : pat;
},
[false, true],
);
/**
* Superimposes the function result on top of the original pattern, delayed by the given time.
@@ -1670,9 +1893,13 @@ export const when = register('when', function (on, func, pat) {
* @example
* "c3 eb3 g3".off(1/8, x=>x.add(7)).note()
*/
export const off = register('off', function (time_pat, func, pat) {
return stack(pat, func(pat.late(time_pat)));
});
export const off = register(
'off',
function (time_pat, func, pat) {
return stack(pat, func(pat.late(time_pat)));
},
[false, true],
);
/**
* Returns a new pattern where every other cycle is played once, twice as
@@ -1712,45 +1939,69 @@ export const rev = register('rev', function (pat) {
return new Pattern(query).splitQueries();
});
/**
* Silences a pattern.
* @example
* stack(
* s("bd").hush(),
* s("hh*3")
* )
*/
export const hush = register('hush', function (pat) {
return silence;
});
/**
* Applies `rev` to a pattern every other cycle, so that the pattern alternates between forwards and backwards.
* @example
* note("c d e g").palindrome()
*/
export const palindrome = register('palindrome', function (pat) {
return pat.every(2, rev);
});
export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, pat) {
by /= 2;
const elem_or = function (dict, key, dflt) {
if (key in dict) {
return dict[key];
}
return dflt;
};
const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by }));
const right = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by }));
/**
* Jux with adjustable stereo width. 0 = mono, 1 = full stereo.
* @name juxBy
* @synonyms juxby
* @example
* s("lt ht mt ht hh").juxBy("<0 .5 1>/2", rev)
*/
export const { juxBy, juxby } = register(
['juxBy', 'juxby'],
function (by, func, pat) {
by /= 2;
const elem_or = function (dict, key, dflt) {
if (key in dict) {
return dict[key];
}
return dflt;
};
const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by }));
const right = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by }));
return stack(left, func(right));
});
return stack(left, func(right));
},
[false, true],
);
export const jux = register('jux', function (func, pat) {
return pat._juxBy(1, func, pat);
});
export const { stutWith, stutwith } = register(['stutWith', 'stutwith'], function (times, time, func, pat) {
return stack(...listRange(0, times - 1).map((i) => func(pat.late(Fraction(time).mul(i)), i)));
});
export const stut = register('stut', function (times, feedback, time, pat) {
return pat._stutWith(times, time, (pat, i) => pat.velocity(Math.pow(feedback, i)));
});
/**
* The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.
* @example
* s("lt ht mt ht hh").jux(rev)
*/
export const jux = register(
'jux',
function (func, pat) {
return pat._juxBy(1, func, pat);
},
[true],
);
/**
* Superimpose and offset multiple times, applying the given function each time.
* @name echoWith
* @memberof Pattern
* @returns Pattern
* @synonyms echowith, stutWith, stutwith
* @param {number} times how many times to repeat
* @param {number} time cycle offset between iterations
* @param {function} func function to apply, given the pattern and the iteration index
@@ -1759,9 +2010,13 @@ export const stut = register('stut', function (times, feedback, time, pat) {
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
* .scale('C minor').note().legato(.2)
*/
export const { echoWith, echowith } = register(['echoWith', 'echowith'], function (times, time, func, pat) {
return stack(...listRange(0, times - 1).map((i) => func(pat.late(Fraction(time).mul(i)), i)));
});
export const { echoWith, echowith, stutWith, stutwith } = register(
['echoWith', 'echowith', 'stutWith', 'stutwith'],
function (times, time, func, pat) {
return stack(...listRange(0, times - 1).map((i) => func(pat.late(Fraction(time).mul(i)), i)));
},
[false, false, true],
);
/**
* Superimpose and offset multiple times, gradually decreasing the velocity
@@ -1778,6 +2033,19 @@ export const echo = register('echo', function (times, time, feedback, pat) {
return pat._echoWith(times, time, (pat, i) => pat.velocity(Math.pow(feedback, i)));
});
/**
* Deprecated. Like echo, but the last 2 parameters are flipped.
* @name stut
* @param {number} times how many times to repeat
* @param {number} feedback velocity multiplicator for each iteration
* @param {number} time cycle offset between iterations
* @example
* s("bd sd").stut(3, .8, 1/6)
*/
export const stut = register('stut', function (times, feedback, time, pat) {
return pat._echoWith(times, time, (pat, i) => pat.velocity(Math.pow(feedback, i)));
});
/**
* Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played.
* @name iter
@@ -1803,6 +2071,7 @@ export const iter = register('iter', function (times, pat) {
/**
* Like `iter`, but plays the subdivisions in reverse order. Known as iter' in tidalcycles
* @name iterBack
* @synonyms iterback
* @memberof Pattern
* @returns Pattern
* @example
@@ -1827,26 +2096,35 @@ const _chunk = function (n, func, pat, back = false) {
return pat.when(binary_pat, func);
};
export const chunk = register('chunk', function (n, func, pat) {
return _chunk(n, func, pat, false);
});
export const chunk = register(
'chunk',
function (n, func, pat) {
return _chunk(n, func, pat, false);
},
[false, true],
);
/**
* Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles
* @name chunkBack
* @synonyms chunkback
* @memberof Pattern
* @returns Pattern
* @example
* "0 1 2 3".chunkBack(4, x=>x.add(7)).scale('A minor').note()
*/
export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], function (n, func, pat) {
return _chunk(n, func, pat, true);
});
export const { chunkBack, chunkback } = register(
['chunkBack', 'chunkback'],
function (n, func, pat) {
return _chunk(n, func, pat, true);
},
[false, true],
);
// TODO - redefine elsewhere in terms of mask
export const bypass = register('bypass', function (on, pat) {
on = Boolean(parseInt(on));
return on ? silence : this;
return on ? silence : pat;
});
// sets absolute duration of haps
@@ -1855,7 +2133,10 @@ export const duration = register('duration', function (value, pat) {
return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
});
// TODO - make control?
/**
* Sets the color of the hap in visualizations like pianoroll or highlighting.
*/
// TODO: move this to controls https://github.com/tidalcycles/strudel/issues/288
export const { color, colour } = register(['color', 'colour'], function (color, pat) {
return pat.withContext((context) => ({ ...context, color }));
});
@@ -1883,6 +2164,7 @@ export const velocity = register('velocity', function (velocity, pat) {
*/
// TODO - fix
export const legato = register('legato', function (value, pat) {
value = Fraction(value);
return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value))));
});
+158 -8
View File
@@ -4,13 +4,27 @@ 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 } from './index.mjs';
import { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
const scale = (normalized, min, max) => normalized * (max - min) + min;
const getValue = (e) => {
let value = typeof e.value === 'object' ? e.value.note ?? e.value.n : e.value;
if (typeof value === 'string') {
value = toMidi(value);
let { value } = e;
if (typeof e.value !== 'object') {
value = { value };
}
let { note, n, freq, s } = value;
if (freq) {
return freqToMidi(freq);
}
note = note ?? n;
if (typeof note === 'string') {
return toMidi(note);
}
if (typeof note === 'number') {
return note;
}
if (s) {
return '_' + s;
}
return value;
};
@@ -48,9 +62,6 @@ Pattern.prototype.pianoroll = function ({
from = 0;
to = timeframeProp;
}
if (!autorange && fold) {
console.warn('disabling autorange has no effect when fold is enabled');
}
const timeAxis = vertical ? h : w;
const valueAxis = vertical ? w : h;
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
@@ -138,10 +149,149 @@ Pattern.prototype.pianoroll = function ({
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
foldValues = values.sort((a, b) => a - b);
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
},
},
);
return this;
};
// this function allows drawing a pianoroll without ties to Pattern.prototype
// it will probably replace the above in the future
export function pianoroll({
time,
haps,
cycles = 4,
playhead = 0.5,
flipTime = 0,
flipValues = 0,
hideNegative = false,
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
minMidi = 10,
maxMidi = 90,
autorange = 0,
timeframe: timeframeProp,
fold = 0,
vertical = 0,
ctx,
} = {}) {
const w = ctx.canvas.width;
const h = ctx.canvas.height;
let from = -cycles * playhead;
let to = cycles * (1 - playhead);
if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead');
from = 0;
to = timeframeProp;
}
const timeAxis = vertical ? h : w;
const valueAxis = vertical ? w : h;
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
let foldValues = [];
flipTime && timeRange.reverse();
flipValues && valueRange.reverse();
// onQuery
const { min, max, values } = haps.reduce(
({ min, max, values }, e) => {
const v = getValue(e);
return {
min: v < min ? v : min,
max: v > max ? v : max,
values: values.includes(v) ? values : [...values, v],
};
},
{ min: Infinity, max: -Infinity, values: [] },
);
if (autorange) {
minMidi = min;
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
// foldValues = values.sort((a, b) => a - b);
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
/* const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
haps
// .filter(inFrame)
.forEach((event) => {
const isActive = event.whole.begin <= time && event.whole.end > time;
const color = event.value?.color || event.context?.color;
ctx.fillStyle = color || inactive;
ctx.strokeStyle = color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valuePx = scale(
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
...valueRange,
);
let margin = 0;
const offset = scale(time / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
ctx.strokeStyle = playheadColor;
ctx.beginPath();
if (vertical) {
ctx.moveTo(0, playheadPosition);
ctx.lineTo(valueAxis, playheadPosition);
} else {
ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
return this;
}
function getOptions(drawTime, options = {}) {
let [lookbehind, lookahead] = drawTime;
lookbehind = Math.abs(lookbehind);
const cycles = lookahead + lookbehind;
const playhead = lookbehind / cycles;
return { fold: 1, ...options, cycles, playhead };
}
Pattern.prototype.punchcard = function (options) {
return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
};
+5 -2
View File
@@ -13,6 +13,7 @@ export function repl({
getTime,
transpiler,
onToggle,
editPattern,
}) {
const scheduler = new Cyclist({
interval,
@@ -34,15 +35,17 @@ export function repl({
getTime,
onToggle,
});
setTime(() => scheduler.getPhase()); // TODO: refactor?
setTime(() => scheduler.now()); // TODO: refactor?
const evaluate = async (code, autostart = true) => {
if (!code) {
throw new Error('no code to evaluate');
}
try {
beforeEval?.({ code });
const { pattern } = await _evaluate(code, transpiler);
let { pattern } = await _evaluate(code, transpiler);
logger(`[eval] code updated`);
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
afterEval?.({ code, pattern });
return pattern;
+5
View File
@@ -154,6 +154,11 @@ describe('Pattern', () => {
).toBe(7);
});
});
describe('out()', () => {
it('is an alias for set.out()', () => {
sameFirst(sequence(1, 2).out(5, 6, 7, 8), sequence(1, 2).set.out(5, 6, 7, 8));
});
});
describe('add()', () => {
it('works as toplevel function', () => {
expect(add(pure(4), pure(5)).query(st(0, 1))[0].value).toBe(9);
+5 -1
View File
@@ -33,9 +33,12 @@ describe('isNote', () => {
expect(isNote(note)).toBe(true);
});
});
it('should recognize notes without octave', () => {
expect(isNote('C')).toBe(true);
expect(isNote('Bb')).toBe(true);
});
it('should not recognize invalid notes', () => {
expect(isNote('H5')).toBe(false);
expect(isNote('C')).toBe(false);
expect(isNote('X')).toBe(false);
expect(isNote(1)).toBe(false);
});
@@ -189,6 +192,7 @@ describe('parseNumeral', () => {
expect(parseNumeral(1.5)).toBe(1.5);
});
it('should parse notes', () => {
expect(parseNumeral('c')).toBe(48);
expect(parseNumeral('c4')).toBe(60);
expect(parseNumeral('c#4')).toBe(61);
expect(parseNumeral('db4')).toBe(61);
+24 -2
View File
@@ -4,8 +4,11 @@ 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 } from './pattern.mjs';
// returns true if the given string is a note
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
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 tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
@@ -19,7 +22,7 @@ export const tokenizeNote = (note) => {
// turns the given note into its midi number representation
export const toMidi = (note) => {
const [pc, acc, oct] = tokenizeNote(note);
const [pc, acc, oct = 3] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
}
@@ -148,6 +151,25 @@ export function curry(func, overload, arity = func.length) {
const partial = function (...args2) {
return curried.apply(this, args.concat(args2));
};
if (args.length == arity - 1) {
// The penultimate arg.. so add some composition magic
// TODO - To make this useful, we also need to add stub functions for every pattern method to
// do the actual composing
for (const r of Pattern.__registered) {
partial[r] = function (...args) {
const result = new Pattern(() => []);
result.__compose = function (pat) {
return partial(pat)[r](...args);
};
return result;
};
}
partial.__compose = function (pat) {
return partial(pat);
};
}
if (overload) {
overload(partial, args);
}
+1 -1
View File
@@ -44,6 +44,6 @@ function createClock(
};
const getPhase = () => phase;
// setCallback
return { setDuration, start, stop, pause, duration, getPhase };
return { setDuration, start, stop, pause, duration, getPhase, minLatency };
}
export default createClock;
+1 -1
View File
@@ -28,7 +28,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.4.1",
"@strudel.cycles/core": "^0.5.0",
"estraverse": "^5.3.0",
"shift-ast": "^7.0.0",
"shift-codegen": "^8.1.0",
+34 -20
View File
@@ -11,10 +11,14 @@ import { getAudioContext } from '@strudel.cycles/webaudio';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
function supportsMidi() {
return typeof navigator.requestMIDIAccess === 'function';
}
export function enableWebMidi(options = {}) {
const { onReady, onConnected, onDisconnected } = options;
if (typeof navigator.requestMIDIAccess !== 'function') {
if (!supportsMidi()) {
throw new Error('Your Browser does not support WebMIDI.');
}
return new Promise((resolve, reject) => {
@@ -42,23 +46,43 @@ export function enableWebMidi(options = {}) {
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
const outputByName = (name) => WebMidi.getOutputByName(name);
let midiReady;
// output?: string | number, outputs: typeof WebMidi.outputs
function getDevice(output, outputs) {
if (!outputs.length) {
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
}
if (typeof output === 'number') {
return outputs[output];
}
if (typeof output === 'string') {
return outputByName(output);
}
return outputs[0];
}
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.midi = async function (output, channel = 1) {
await enableWebMidi({
Pattern.prototype.midi = function (output, channel = 1) {
if (!supportsMidi()) {
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
}
/* await */ enableWebMidi({
onConnected: ({ outputs }) =>
logger(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
onDisconnected: ({ outputs }) =>
logger(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
onReady: ({ outputs }) => {
const chosenOutput = output ?? outputs[0];
const device = getDevice(output, outputs);
const otherOutputs = outputs
.filter((o) => o.name !== chosenOutput.name)
.filter((o) => o.name !== device.name)
.map((o) => `'${o.name}'`)
.join(' | ');
logger(`Midi connected! Using "${chosenOutput.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
midiReady = true;
logger(`Midi connected! Using "${device.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
},
});
if (isPattern(output?.constructor?.name)) {
if (isPattern(output)) {
throw new Error(
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
@@ -71,20 +95,10 @@ Pattern.prototype.midi = async function (output, channel = 1) {
if (!isNote(note)) {
throw new Error('not a note: ' + note);
}
if (!WebMidi.enabled) {
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
}
if (!WebMidi.outputs.length) {
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
}
let device;
if (typeof output === 'number') {
device = WebMidi.outputs[output];
} else if (typeof output === 'string') {
device = outputByName(output);
} else {
device = WebMidi.outputs[0];
if (!midiReady) {
return;
}
const device = getDevice(output, WebMidi.outputs);
if (!device) {
throw new Error(
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
+1 -1
View File
@@ -32,7 +32,7 @@ yields:
## Mini Notation API
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/learn/mini-notation)
## Building the Parser
File diff suppressed because it is too large Load Diff
+80 -56
View File
@@ -5,13 +5,20 @@ This program is free software: you can redistribute it and/or modify it under th
*/
// Some terminology:
// a sequence = a serie of elements placed between quotes
// a stack = a serie of vertically aligned slices sharing the same overall length
// a slice = a serie of horizontally aligned elements
// a choose = a serie of elements, one of which is chosen at random
// mini(notation) = a series of elements placed between quotes
// a stack = a series of vertically aligned slices sharing the same overall length
// a sequence = a series of horizontally aligned elements
// a choose = a series of elements, one of which is chosen at random
{
var AtomStub = function(source)
{
this.type_ = "atom";
this.source_ = source;
this.location_ = location();
}
var PatternStub = function(source, alignment)
{
this.type_ = "pattern";
@@ -90,82 +97,99 @@ quote = '"' / "'"
// single step definition (e.g bd)
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
step = ws chars:step_char+ ws { return chars.join("") }
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
// define a sub cycle e.g. [1 2, 3 [4]]
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s}
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s }
// define a timeline e.g <1 3 [3 5]>. We simply defer to a stack and change the alignement
timeline = ws "<" ws sc:single_cycle ws ">" ws
{ sc.arguments_.alignment = "t"; return sc;}
// define a polymeter e.g. {1 2, 3 4 5}
polymeter = ws "{" ws s:polymeter_stack ws "}" stepsPerCycle:polymeter_steps? ws
{ s.arguments_.stepsPerCycle = stepsPerCycle ; return s; }
polymeter_steps = "%"a:slice
{ return a }
// define a step-per-cycle timeline e.g <1 3 [3 5]>. We simply defer to a sequence and
// change the alignment to slowcat
slow_sequence = ws "<" ws s:sequence ws ">" ws
{ s.arguments_.alignment = 'slowcat'; return s; }
// a slice is either a single step or a sub cycle
slice = step / sub_cycle / timeline
slice = step / sub_cycle / polymeter / slow_sequence
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
// at this point, we assume we can represent them as regular sequence operators
slice_modifier = slice_weight / slice_bjorklund / slice_slow / slice_fast / slice_fixed_step / slice_replicate / slice_degrade
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade
slice_weight = "@" a:number
{ return { weight: a} }
op_weight = "@" a:number
{ return x => x.options_['weight'] = a }
slice_replicate = "!"a:number
{ return { replicate: a } }
op_replicate = "!"a:number
{ return x => x.options_['reps'] = a }
slice_bjorklund = "(" ws p:number ws comma ws s:number ws comma? ws r:number? ws ")"
{ return { operator : { type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r || 0 } } } }
op_bjorklund = "(" ws p:slice_with_ops ws comma ws s:slice_with_ops ws comma? ws r:slice_with_ops? ws ")"
{ return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }
slice_slow = "/"a:number
{ return { operator : { type_: "stretch", arguments_ :{ amount:a } } } }
op_slow = "/"a:slice
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) }
slice_fast = "*"a:number
{ return { operator : { type_: "stretch", arguments_ :{ amount:"1/"+a } } } }
op_fast = "*"a:slice
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
slice_fixed_step = "%"a:number
{ return { operator : { type_: "fixed-step", arguments_ :{ amount:a } } } }
slice_degrade = "?"a:number?
{ return { operator : { type_: "degradeBy", arguments_ :{ amount:(a? a : 0.5) } } } }
op_degrade = "?"a:number?
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
slice_with_modifier = s:slice o:slice_modifier?
{ return new ElementStub(s, o);}
slice_with_ops = s:slice ops:slice_op*
{ const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
for (const op of ops) {
op(result);
}
return result;
}
// a single cycle is a combination of one or more successive slices (as an array). If we
// have only one element, we skip the array and return the element itself
single_cycle = s:(slice_with_modifier)+
{ return new PatternStub(s,"h"); }
// a sequence is a combination of one or more successive slices (as an array)
sequence = s:(slice_with_ops)+
{ return new PatternStub(s, 'fastcat'); }
// a stack is a serie of vertically aligned single cycles, separated by a comma
stack_tail = tail:(comma @single_cycle)+
{ return { alignment: 'v', list: tail }; }
// a stack is a series of vertically aligned sequence, separated by a comma
stack_tail = tail:(comma @sequence)+
{ return { alignment: 'stack', list: tail }; }
// a choose is a serie of pipe-separated single cycles, one of which is chosen
// at random each time through the pattern
choose_tail = tail:(pipe @single_cycle)+
{ return { alignment: 'r', list: tail }; }
// 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 }; }
// if the stack contains only one element, we don't create a stack but return the
// underlying element
stack_or_choose = head:single_cycle tail:(stack_tail / choose_tail)?
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; } }
// a sequence is a quoted stack
sequence = ws quote sc:stack_or_choose quote
polymeter_stack = head:sequence tail:stack_tail?
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
// Mini-notation innards ends
// ---------->8---------->8---------->8---------->8---------->8----------
// Experimental haskellish parser begins
// mini-notation = a quoted stack
mini = ws quote sc:stack_or_choose quote
{ return sc; }
// ------------------ operators ---------------------------
operator = scale / slow / fast / target / bjorklund / struct / rotR / rotL
struct = "struct" ws s:sequence_or_operator
{ return { name: "struct", args: { sequence:s }}}
struct = "struct" ws s:mini_or_operator
{ return { name: "struct", args: { mini:s }}}
target = "target" ws quote s:step quote
{ return { name: "target", args : { name:s}}}
bjorklund = "euclid" ws p:int ws s:int ws r:int?
{ return { name: "bjorklund", args :{ pulse: parseInt(p), step:parseInt(s) }}}
{ return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}}
slow = "slow" ws a:number
{ return { name: "stretch", args :{ amount: a}}}
@@ -189,27 +213,27 @@ comment = '//' p:([^\n]*)
group_operator = cat
// cat is another form of timeline
cat = "cat" ws "[" ws s:sequence_or_operator ss:(comma v:sequence_or_operator { return v})* ws "]"
{ ss.unshift(s); return new PatternStub(ss,"t"); }
cat = "cat" ws "[" ws s:mini_or_operator ss:(comma v:mini_or_operator { return v})* ws "]"
{ ss.unshift(s); return new PatternStub(ss, 'slowcat'); }
// ------------------ high level sequence ---------------------------
// ------------------ high level mini ---------------------------
sequence_or_group =
mini_or_group =
group_operator /
sequence
mini
sequence_or_operator =
sg:sequence_or_group ws (comment)*
mini_or_operator =
sg:mini_or_group ws (comment)*
{return sg}
/ o:operator ws "$" ws soc:sequence_or_operator
/ o:operator ws "$" ws soc:mini_or_operator
{ return new OperatorStub(o.name,o.args,soc)}
sequ_or_operator_or_comment =
sc: sequence_or_operator
sc: mini_or_operator
{ return sc }
/ comment
sequence_definition = s:sequ_or_operator_or_comment
mini_definition = s:sequ_or_operator_or_comment
// ---------------------- statements ----------------------------
@@ -227,4 +251,4 @@ hush = "hush"
// ---------------------- statements ----------------------------
statement = sequence_definition / command
statement = mini_definition / command
+103 -103
View File
@@ -7,8 +7,6 @@ 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';
const { pure, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel;
/* var _seedState = 0;
const randOffset = 0.0002;
@@ -16,148 +14,150 @@ function _nextSeed() {
return _seedState++;
} */
const applyOptions = (parent) => (pat, i) => {
const applyOptions = (parent, code) => (pat, i) => {
const ast = parent.source_[i];
const options = ast.options_;
const operator = options?.operator;
if (operator) {
switch (operator.type_) {
case 'stretch': {
const speed = Fraction(operator.arguments_.amount).inverse();
return reify(pat).fast(speed);
}
case 'bjorklund':
return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation);
case 'degradeBy':
// TODO: find out what is right here
// example:
/*
const ops = options?.ops;
if (ops) {
for (const op of ops) {
switch (op.type_) {
case 'stretch': {
const legalTypes = ['fast', 'slow'];
const { type, amount } = op.arguments_;
if (!legalTypes.includes(type)) {
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
}
pat = strudel.reify(pat)[type](patternifyAST(amount, code));
break;
}
case 'bjorklund': {
if (op.arguments_.rotation) {
pat = pat.euclidRot(
patternifyAST(op.arguments_.pulse, code),
patternifyAST(op.arguments_.step, code),
patternifyAST(op.arguments_.rotation, code),
);
} else {
pat = pat.euclid(patternifyAST(op.arguments_.pulse, code), patternifyAST(op.arguments_.step, code));
}
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
// 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 reify(pat)._degradeByWith(
// this is how it was:
/*
return strudel.reify(pat)._degradeByWith(
strudel.rand.early(randOffset * _nextSeed()).segment(1),
operator.arguments_.amount ?? 0.5,
op.arguments_.amount ?? 0.5,
);
*/
return reify(pat)._degradeBy(operator.arguments_.amount ?? 0.5);
// TODO: case 'fixed-step': "%"
pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
break;
}
default: {
console.warn(`operator "${op.type_}" not implemented`);
}
}
}
console.warn(`operator "${operator.type_}" not implemented`);
}
if (options?.weight) {
// weight is handled by parent
return pat;
}
// TODO: bjorklund e.g. "c3(5,8)"
const unimplemented = Object.keys(options || {}).filter((key) => key !== 'operator');
if (unimplemented.length) {
console.warn(
`option${unimplemented.length > 1 ? 's' : ''} ${unimplemented.map((o) => `"${o}"`).join(', ')} not implemented`,
);
}
return pat;
};
function resolveReplications(ast) {
// the general idea here: x!3 = [x*3]@3
// could this be made easier?!
ast.source_ = ast.source_.map((child) => {
const { replicate, ...options } = child.options_ || {};
if (replicate) {
return {
...child,
options_: { ...options, weight: replicate },
source_: {
type_: 'pattern',
arguments_: {
alignment: 'h',
},
source_: [
{
type_: 'element',
source_: child.source_,
location_: child.location_,
options_: {
operator: {
type_: 'stretch',
arguments_: { amount: Fraction(replicate).inverse().toString() },
},
},
},
],
},
};
}
return child;
});
ast.source_ = strudel.flatten(
ast.source_.map((child) => {
const { reps } = child.options_ || {};
if (!reps) {
return [child];
}
delete child.options_.reps;
return Array(reps).fill(child);
}),
);
}
export function patternifyAST(ast, code) {
switch (ast.type_) {
case 'pattern': {
resolveReplications(ast);
const children = ast.source_.map((child) => patternifyAST(child, code)).map(applyOptions(ast));
const children = ast.source_.map((child) => patternifyAST(child, code)).map(applyOptions(ast, code));
const alignment = ast.arguments_.alignment;
if (alignment === 'v') {
return stack(...children);
if (alignment === 'stack') {
return strudel.stack(...children);
}
if (alignment === 'r') {
if (alignment === 'polymeter') {
// polymeter
const stepsPerCycle = ast.arguments_.stepsPerCycle
? patternifyAST(ast.arguments_.stepsPerCycle, code).fmap((x) => strudel.Fraction(x))
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
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);
}
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (!weightedChildren && alignment === 't') {
return slowcat(...children);
if (!weightedChildren && alignment === 'slowcat') {
return strudel.slowcat(...children);
}
if (weightedChildren) {
const pat = timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
if (alignment === 't') {
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
if (alignment === 'slowcat') {
return pat._slow(weightSum); // timecat + slow
}
pat.__weight = weightSum;
return pat;
}
return sequence(...children);
const pat = strudel.sequence(...children);
pat.__weight = children.length;
return pat;
}
case 'element': {
return patternifyAST(ast.source_, code);
}
case 'atom': {
if (ast.source_ === '~') {
return silence;
return strudel.silence;
}
if (typeof ast.source_ !== 'object') {
if (!ast.location_) {
console.warn('no location for', ast);
return ast.source_;
}
const { start, end } = ast.location_;
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
// the following line expects the shapeshifter append .withMiniLocation
// because location_ is only relative to the mini string, but we need it relative to whole code
// make sure whitespaces are not part of the highlight:
const actual = code?.split('').slice(start.offset, end.offset).join('');
const [offsetStart = 0, offsetEnd = 0] = actual
? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length)
: [];
return pure(value).withLocation(
if (!ast.location_) {
console.warn('no location for', ast);
return ast.source_;
}
const { start, end } = ast.location_;
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
// the following line expects the shapeshifter append .withMiniLocation
// because location_ is only relative to the mini string, but we need it relative to whole code
// make sure whitespaces are not part of the highlight:
const actual = code?.split('').slice(start.offset, end.offset).join('');
const [offsetStart = 0, offsetEnd = 0] = actual
? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length)
: [];
return strudel
.pure(value)
.withLocation(
[start.line, start.column + offsetStart, start.offset + offsetStart],
[start.line, end.column - offsetEnd, end.offset - offsetEnd],
);
}
return patternifyAST(ast.source_, code);
}
case 'stretch':
return patternifyAST(ast.source_, code).slow(ast.arguments_.amount);
return patternifyAST(ast.source_, code).slow(patternifyAST(ast.arguments_.amount, code));
/* case 'scale':
let [tonic, scale] = Scale.tokenize(ast.arguments_.scale);
const intervals = Scale.get(scale).intervals;
@@ -179,10 +179,10 @@ export function patternifyAST(ast, code) {
}); */
/* case 'struct':
// TODO:
return silence; */
return strudel.silence; */
default:
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
return silence;
return strudel.silence;
}
}
@@ -193,7 +193,7 @@ export const mini = (...strings) => {
const ast = krill.parse(code);
return patternifyAST(ast, code);
});
return sequence(...pats);
return strudel.sequence(...pats);
};
// includes haskell style (raw krill parsing)
@@ -207,5 +207,5 @@ export function minify(thing) {
if (typeof thing === 'string') {
return mini(thing);
}
return reify(thing);
return strudel.reify(thing);
}
+1 -1
View File
@@ -26,7 +26,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.4.1",
"@strudel.cycles/core": "^0.5.0",
"@strudel.cycles/eval": "^0.5.0",
"@strudel.cycles/tone": "^0.5.0"
},
+65 -2
View File
@@ -9,8 +9,8 @@ import '@strudel.cycles/core/euclid.mjs';
import { describe, expect, it } from 'vitest';
describe('mini', () => {
const minV = (v) => mini(v).firstCycleValues;
const minS = (v) => mini(v).showFirstCycle;
const minV = (v) => mini(v).sortHapsByPart().firstCycleValues;
const minS = (v) => mini(v).sortHapsByPart().showFirstCycle;
it('supports single elements', () => {
expect(minV('a')).toEqual(['a']);
});
@@ -21,6 +21,21 @@ describe('mini', () => {
expect(minS('a b')).toEqual(['a: 0 - 1/2', 'b: 1/2 - 1']);
expect(minS('a b c')).toEqual(['a: 0 - 1/3', 'b: 1/3 - 2/3', 'c: 2/3 - 1']);
});
it('supports fast', () => {
expect(minS('a*3 b')).toEqual(minS('[a a a] b'));
});
it('supports patterned fast', () => {
expect(minS('[a*<3 5>]*2')).toEqual(minS('[a a a] [a a a a a]'));
});
it('supports slow', () => {
expect(minS('[a a a]/3 b')).toEqual(minS('a b'));
});
it('supports patterned slow', () => {
expect(minS('[a a a a a a a a]/[2 4]')).toEqual(minS('[a a] a'));
});
it('supports patterned fast', () => {
expect(minS('[a*<3 5>]*2')).toEqual(minS('[a a a] [a a a a a]'));
});
it('supports slowcat', () => {
expect(minV('<a b>')).toEqual(['a']);
});
@@ -36,6 +51,16 @@ describe('mini', () => {
expect(minS('c3 [d3 e3]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 1']);
expect(minS('c3 [d3 [e3 f3]]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 7/8', 'f3: 7/8 - 1']);
});
it('supports curly brackets', () => {
expect(minS('{a b, c d e}*3')).toEqual(minS('[a b a b a b, c d e c d e]'));
expect(minS('{a b, c [d e] f}*3')).toEqual(minS('[a b a b a b, c [d e] f c [d e] f]'));
expect(minS('{a b c, d e}*2')).toEqual(minS('[a b c a b c, d e d e d e]'));
});
it('supports curly brackets with explicit step-per-cycle', () => {
expect(minS('{a b, c d e}%3')).toEqual(minS('[a b a, c d e]'));
expect(minS('{a b, c d e}%5')).toEqual(minS('[a b a b a, c d e c d]'));
expect(minS('{a b, c d e}%6')).toEqual(minS('[a b a b a b, c d e c d e]'));
});
it('supports commas', () => {
expect(minS('c3,e3,g3')).toEqual(['c3: 0 - 1', 'e3: 0 - 1', 'g3: 0 - 1']);
expect(minS('[c3,e3,g3] f3')).toEqual(['c3: 0 - 1/2', 'e3: 0 - 1/2', 'g3: 0 - 1/2', 'f3: 1/2 - 1']);
@@ -46,10 +71,48 @@ describe('mini', () => {
});
it('supports replication', () => {
expect(minS('a!3 b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
expect(minS('[<a b c>]!3 d')).toEqual(minS('<a b c> <a b c> <a b c> d'));
});
it('supports euclidean rhythms', () => {
expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
});
it('supports patterning euclidean rhythms', () => {
expect(minS('[a(<3 5>, <8 16>)]*2')).toEqual(minS('a(3,8) a(5,16)'));
});
it("reproduces Toussaint's example euclidean algorithms", () => {
const checkEuclid = function (spec, target) {
expect(minS(`x(${spec[0]},${spec[1]})`)).toEqual(minS(target));
};
checkEuclid([1, 2], 'x ~');
checkEuclid([1, 3], 'x ~ ~');
checkEuclid([1, 4], 'x ~ ~ ~');
checkEuclid([4, 12], 'x ~ ~ x ~ ~ x ~ ~ x ~ ~');
checkEuclid([2, 5], 'x ~ x ~ ~');
// checkEuclid([3, 4], "x ~ x x"); // Toussaint is wrong..
checkEuclid([3, 4], 'x x x ~'); // correction
checkEuclid([3, 5], 'x ~ x ~ x');
checkEuclid([3, 7], 'x ~ x ~ x ~ ~');
checkEuclid([3, 8], 'x ~ ~ x ~ ~ x ~');
checkEuclid([4, 7], 'x ~ x ~ x ~ x');
checkEuclid([4, 9], 'x ~ x ~ x ~ x ~ ~');
checkEuclid([4, 11], 'x ~ ~ x ~ ~ x ~ ~ x ~');
// checkEuclid([5, 6], "x ~ x x x x"); // Toussaint is wrong..
checkEuclid([5, 6], 'x x x x x ~'); // correction
checkEuclid([5, 7], 'x ~ x x ~ x x');
checkEuclid([5, 8], 'x ~ x x ~ x x ~');
checkEuclid([5, 9], 'x ~ x ~ x ~ x ~ x');
checkEuclid([5, 11], 'x ~ x ~ x ~ x ~ x ~ ~');
checkEuclid([5, 12], 'x ~ ~ x ~ x ~ ~ x ~ x ~');
// checkEuclid([5, 16], "x ~ ~ x ~ ~ x ~ ~ x ~ ~ x ~ ~ ~ ~"); // Toussaint is wrong..
checkEuclid([5, 16], 'x ~ ~ x ~ ~ x ~ ~ x ~ ~ x ~ ~ ~'); // correction
// checkEuclid([7, 8], "x ~ x x x x x x"); // Toussaint is wrong..
checkEuclid([7, 8], 'x x x x x x x ~'); // Correction
checkEuclid([7, 12], 'x ~ x x ~ x ~ x x ~ x ~');
checkEuclid([7, 16], 'x ~ ~ x ~ x ~ x ~ ~ x ~ x ~ x ~');
checkEuclid([9, 16], 'x ~ x x ~ x ~ x ~ x x ~ x ~ x ~');
checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~');
checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~');
});
it('supports the ? operator', () => {
expect(
mini('a?')
+1 -1
View File
@@ -36,4 +36,4 @@ s("<bd sd> hh").osc()
or just [click here](http://localhost:3000/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.tidalcycles.org/tutorial/#superdirt-api)
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.tidalcycles.org/learn/outputs#superdirt-api)
+1 -1
View File
File diff suppressed because one or more lines are too long
+359 -237
View File
@@ -1,15 +1,15 @@
import n, { useCallback as _, useRef as H, useEffect as L, useMemo as V, useState as w, useLayoutEffect as j } from "react";
import X from "@uiw/react-codemirror";
import { Decoration as E, EditorView as U } from "@codemirror/view";
import { StateEffect as $, StateField as G } from "@codemirror/state";
import { javascript as Y } from "@codemirror/lang-javascript";
import { tags as r } from "@lezer/highlight";
import { createTheme as Z } from "@uiw/codemirror-themes";
import { useInView as ee } from "react-hook-inview";
import { webaudioOutput as te, getAudioContext as re } from "@strudel.cycles/webaudio";
import { repl as oe } from "@strudel.cycles/core";
import { transpiler as ne } from "@strudel.cycles/transpiler";
const ae = Z({
import d, { useCallback as E, useRef as A, useEffect as k, useMemo as Q, useState as _, useLayoutEffect as te } from "react";
import ue from "@uiw/react-codemirror";
import { Decoration as M, EditorView as re } from "@codemirror/view";
import { StateEffect as ne, StateField as oe } from "@codemirror/state";
import { javascript as de } from "@codemirror/lang-javascript";
import { tags as u } from "@lezer/highlight";
import { createTheme as fe } from "@uiw/codemirror-themes";
import { webaudioOutput as me, getAudioContext as he } from "@strudel.cycles/webaudio";
import { useInView as ge } from "react-hook-inview";
import { repl as pe, logger as ve } from "@strudel.cycles/core";
import { transpiler as be } from "@strudel.cycles/transpiler";
const Ee = fe({
theme: "dark",
settings: {
background: "#222",
@@ -22,299 +22,421 @@ const ae = Z({
gutterForeground: "#8a919966"
},
styles: [
{ tag: r.keyword, color: "#c792ea" },
{ tag: r.operator, color: "#89ddff" },
{ tag: r.special(r.variableName), color: "#eeffff" },
{ tag: r.typeName, color: "#c3e88d" },
{ tag: r.atom, color: "#f78c6c" },
{ tag: r.number, color: "#c3e88d" },
{ tag: r.definition(r.variableName), color: "#82aaff" },
{ tag: r.string, color: "#c3e88d" },
{ tag: r.special(r.string), color: "#c3e88d" },
{ tag: r.comment, color: "#7d8799" },
{ tag: r.variableName, color: "#c792ea" },
{ tag: r.tagName, color: "#c3e88d" },
{ tag: r.bracket, color: "#525154" },
{ tag: r.meta, color: "#ffcb6b" },
{ tag: r.attributeName, color: "#c792ea" },
{ tag: r.propertyName, color: "#c792ea" },
{ tag: r.className, color: "#decb6b" },
{ tag: r.invalid, color: "#ffffff" }
{ tag: u.keyword, color: "#c792ea" },
{ tag: u.operator, color: "#89ddff" },
{ tag: u.special(u.variableName), color: "#eeffff" },
{ tag: u.typeName, color: "#c3e88d" },
{ tag: u.atom, color: "#f78c6c" },
{ tag: u.number, color: "#c3e88d" },
{ tag: u.definition(u.variableName), color: "#82aaff" },
{ tag: u.string, color: "#c3e88d" },
{ tag: u.special(u.string), color: "#c3e88d" },
{ tag: u.comment, color: "#7d8799" },
{ tag: u.variableName, color: "#c792ea" },
{ tag: u.tagName, color: "#c3e88d" },
{ tag: u.bracket, color: "#525154" },
{ tag: u.meta, color: "#ffcb6b" },
{ tag: u.attributeName, color: "#c792ea" },
{ tag: u.propertyName, color: "#c792ea" },
{ tag: u.className, color: "#decb6b" },
{ tag: u.invalid, color: "#ffffff" }
]
});
const B = $.define(), se = G.define({
const X = ne.define(), ye = oe.define({
create() {
return E.none;
return M.none;
},
update(e, t) {
try {
for (let o of t.effects)
if (o.is(B))
if (o.value) {
const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } });
e = E.set([a.range(0, t.newDoc.length)]);
for (let r of t.effects)
if (r.is(X))
if (r.value) {
const n = M.mark({ attributes: { style: "background-color: #FFCA2880" } });
e = M.set([n.range(0, t.newDoc.length)]);
} else
e = E.set([]);
e = M.set([]);
return e;
} catch (o) {
return console.warn("flash error", o), e;
} catch (r) {
return console.warn("flash error", r), e;
}
},
provide: (e) => U.decorations.from(e)
}), ce = (e) => {
e.dispatch({ effects: B.of(!0) }), setTimeout(() => {
e.dispatch({ effects: B.of(!1) });
provide: (e) => re.decorations.from(e)
}), we = (e) => {
e.dispatch({ effects: X.of(!0) }), setTimeout(() => {
e.dispatch({ effects: X.of(!1) });
}, 200);
}, z = $.define(), ie = G.define({
}, B = ne.define(), ke = oe.define({
create() {
return E.none;
return M.none;
},
update(e, t) {
try {
for (let o of t.effects)
if (o.is(z)) {
const a = o.value.map(
(s) => (s.context.locations || []).map(({ start: u, end: d }) => {
const f = s.context.color || "#FFCA28";
let c = t.newDoc.line(u.line).from + u.column, i = t.newDoc.line(d.line).from + d.column;
const m = t.newDoc.length;
return c > m || i > m ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${f};` } }).range(c, i);
for (let r of t.effects)
if (r.is(B)) {
const n = r.value.map(
(l) => (l.context.locations || []).map(({ start: m, end: f }) => {
const c = l.context.color || "#FFCA28";
let s = t.newDoc.line(m.line).from + m.column, g = t.newDoc.line(f.line).from + f.column;
const b = t.newDoc.length;
return s > b || g > b ? void 0 : M.mark({ attributes: { style: `outline: 1.5px solid ${c};` } }).range(s, g);
})
).flat().filter(Boolean) || [];
e = E.set(a, !0);
e = M.set(n, !0);
}
return e;
} catch {
return E.set([]);
return M.set([]);
}
},
provide: (e) => U.decorations.from(e)
}), le = [Y(), ae, ie, se];
function de({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: u }) {
const d = _(
(i) => {
t?.(i);
provide: (e) => re.decorations.from(e)
}), Fe = [de(), Ee, ke, ye];
function _e({ value: e, onChange: t, onViewChanged: r, onSelectionChange: n, options: l, editorDidMount: m }) {
const f = E(
(g) => {
t?.(g);
},
[t]
), f = _(
(i) => {
o?.(i);
), c = E(
(g) => {
r?.(g);
},
[o]
), c = _(
(i) => {
i.selectionSet && a && a?.(i.state.selection);
[r]
), s = E(
(g) => {
g.selectionSet && n && n?.(g.state.selection);
},
[a]
[n]
);
return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(X, {
return /* @__PURE__ */ d.createElement(d.Fragment, null, /* @__PURE__ */ d.createElement(ue, {
value: e,
onChange: d,
onCreateEditor: f,
onUpdate: c,
extensions: le
onChange: f,
onCreateEditor: c,
onUpdate: s,
extensions: Fe
}));
}
function K(...e) {
function T(...e) {
return e.filter(Boolean).join(" ");
}
function ue({ view: e, pattern: t, active: o, getTime: a }) {
const s = H([]), u = H();
L(() => {
function Me({ view: e, pattern: t, active: r, getTime: n }) {
const l = A([]), m = A(0);
k(() => {
if (e)
if (t && o) {
let d = requestAnimationFrame(function f() {
if (t && r) {
m.current = 0;
let f = requestAnimationFrame(function c() {
try {
const c = a(), m = [Math.max(u.current || c, c - 1 / 10, 0), c + 1 / 60];
u.current = m[1], s.current = s.current.filter((g) => g.whole.end > c);
const h = t.queryArc(...m).filter((g) => g.hasOnset());
s.current = s.current.concat(h), e.dispatch({ effects: z.of(s.current) });
const s = n(), b = [Math.max(m.current ?? s, s - 1 / 10, -0.01), s + 1 / 60];
m.current = b[1], l.current = l.current.filter((h) => h.whole.end > s);
const i = t.queryArc(...b).filter((h) => h.hasOnset());
l.current = l.current.concat(i), e.dispatch({ effects: B.of(l.current) });
} catch {
e.dispatch({ effects: z.of([]) });
e.dispatch({ effects: B.of([]) });
}
d = requestAnimationFrame(f);
f = requestAnimationFrame(c);
});
return () => {
cancelAnimationFrame(d);
cancelAnimationFrame(f);
};
} else
s.current = [], e.dispatch({ effects: z.of([]) });
}, [t, o, e]);
l.current = [], e.dispatch({ effects: B.of([]) });
}, [t, r, e]);
}
const fe = "_container_3i85k_1", me = "_header_3i85k_5", ge = "_buttons_3i85k_9", pe = "_button_3i85k_9", he = "_buttonDisabled_3i85k_17", be = "_error_3i85k_21", ve = "_body_3i85k_25", v = {
container: fe,
header: me,
buttons: ge,
button: pe,
buttonDisabled: he,
error: be,
body: ve
};
function O({ type: e }) {
return /* @__PURE__ */ n.createElement("svg", {
function Ae(e, t = !1) {
const r = A(), n = A(), l = (c) => {
if (n.current !== void 0) {
const s = c - n.current;
e(c, s);
}
n.current = c, r.current = requestAnimationFrame(l);
}, m = () => {
r.current = requestAnimationFrame(l);
}, f = () => {
r.current && cancelAnimationFrame(r.current), delete r.current;
};
return k(() => {
r.current && (f(), m());
}, [e]), k(() => (t && m(), f), []), {
start: m,
stop: f
};
}
function Ne({ pattern: e, started: t, getTime: r, onDraw: n, drawTime: l = [-2, 2] }) {
let [m, f] = l;
m = Math.abs(m);
let c = A([]), s = A(null);
k(() => {
if (e && t) {
const i = r(), h = e.queryArc(Math.max(i, 0), i + f + 0.1);
c.current = c.current.filter((p) => p.whole.begin < i), c.current = c.current.concat(h);
}
}, [e, t]);
const { start: g, stop: b } = Ae(
E(() => {
const i = r() + f;
if (s.current === null) {
s.current = i;
return;
}
const h = e.queryArc(Math.max(s.current, i - 1 / 10), i);
s.current = i, c.current = (c.current || []).filter((p) => p.whole.end >= i - m - f).concat(h.filter((p) => p.hasOnset())), n(e, i - f, c.current, l);
}, [e])
);
return k(() => {
t ? g() : (c.current = [], b());
}, [t]), {
clear: () => {
c.current = [];
}
};
}
function Ce(e) {
return k(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), E((t) => window.postMessage(t, "*"), []);
}
function De({
defaultOutput: e,
interval: t,
getTime: r,
evalOnMount: n = !1,
initialCode: l = "",
autolink: m = !1,
beforeEval: f,
afterEval: c,
editPattern: s,
onEvalError: g,
onToggle: b,
canvasId: i,
drawContext: h,
drawTime: p = [-2, 2]
}) {
const D = Q(() => Re(), []);
i = i || `canvas-${D}`;
const [P, R] = _(), [z, H] = _(), [y, S] = _(l), [V, q] = _(), [x, I] = _(), [N, O] = _(!1), K = y !== V, L = E((a) => !!(a?.context?.onPaint && h), [h]), { scheduler: C, evaluate: o, start: v, stop: j, pause: U } = Q(
() => pe({
interval: t,
defaultOutput: e,
onSchedulerError: R,
onEvalError: (a) => {
H(a), g?.(a);
},
getTime: r,
drawContext: h,
transpiler: be,
editPattern: s,
beforeEval: ({ code: a }) => {
S(a), f?.();
},
afterEval: ({ pattern: a, code: w }) => {
q(w), I(a), H(), R(), m && (window.location.hash = "#" + encodeURIComponent(btoa(w))), c?.();
},
onToggle: (a) => {
O(a), b?.(a);
}
}),
[e, t, r]
), ce = Ce(({ data: { from: a, type: w } }) => {
w === "start" && a !== D && j();
}), Y = E(
async (a = !0) => {
const w = await o(y, a);
return ce({ type: "start", from: D }), w;
},
[o, y]
), W = E(
(a, w, G, J) => {
const { onPaint: ie } = a.context || {}, le = typeof h == "function" ? h(i) : h;
ie?.(le, w, G, J);
},
[h, i]
), $ = E(
(a) => {
if (L(a)) {
const [w, G] = p, J = a.queryArc(0, G);
W(a, -1e-3, J, p);
}
},
[p, W, L]
), Z = A();
k(() => {
!Z.current && n && y && (Z.current = !0, o(y, !1).then((a) => $(a)));
}, [n, y, o, $]), k(() => () => {
C.stop();
}, [C]);
const ae = async () => {
N ? (C.stop(), $(x)) : await Y();
}, se = P || z;
return Ne({
pattern: x,
started: L(x) && N,
getTime: () => C.now(),
drawTime: p,
onDraw: W
}), {
id: D,
canvasId: i,
code: y,
setCode: S,
error: se,
schedulerError: P,
scheduler: C,
evalError: z,
evaluate: o,
activateCode: Y,
activeCode: V,
isDirty: K,
pattern: x,
started: N,
start: v,
stop: j,
pause: U,
togglePlay: ae
};
}
function Re() {
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
}
function ee({ type: e }) {
return /* @__PURE__ */ d.createElement("svg", {
xmlns: "http://www.w3.org/2000/svg",
className: "sc-h-5 sc-w-5",
viewBox: "0 0 20 20",
fill: "currentColor"
}, {
refresh: /* @__PURE__ */ n.createElement("path", {
refresh: /* @__PURE__ */ d.createElement("path", {
fillRule: "evenodd",
d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",
clipRule: "evenodd"
}),
play: /* @__PURE__ */ n.createElement("path", {
play: /* @__PURE__ */ d.createElement("path", {
fillRule: "evenodd",
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",
clipRule: "evenodd"
}),
pause: /* @__PURE__ */ n.createElement("path", {
pause: /* @__PURE__ */ d.createElement("path", {
fillRule: "evenodd",
d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",
clipRule: "evenodd"
}),
stop: /* @__PURE__ */ d.createElement("path", {
fillRule: "evenodd",
d: "M2 10a8 8 0 1116 0 8 8 0 01-16 0zm5-2.25A.75.75 0 017.75 7h4.5a.75.75 0 01.75.75v4.5a.75.75 0 01-.75.75h-4.5a.75.75 0 01-.75-.75v-4.5z",
clipRule: "evenodd"
})
}[e]);
}
function Ee(e) {
return L(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), _((t) => window.postMessage(t, "*"), []);
}
function we({
defaultOutput: e,
interval: t,
getTime: o,
evalOnMount: a = !1,
initialCode: s = "",
autolink: u = !1,
beforeEval: d,
afterEval: f,
onEvalError: c,
onToggle: i
}) {
const m = V(() => ye(), []), [h, g] = w(), [C, N] = w(), [p, y] = w(s), [M, S] = w(), [k, D] = w(), [F, x] = w(!1), b = p !== M, { scheduler: A, evaluate: T, start: J, stop: q, pause: Q } = V(
() => oe({
interval: t,
defaultOutput: e,
onSchedulerError: g,
onEvalError: (l) => {
N(l), c?.(l);
},
getTime: o,
transpiler: ne,
beforeEval: ({ code: l }) => {
y(l), d?.();
},
afterEval: ({ pattern: l, code: P }) => {
S(P), D(l), N(), g(), u && (window.location.hash = "#" + encodeURIComponent(btoa(P))), f?.();
},
onToggle: (l) => {
x(l), i?.(l);
}
}),
[e, t, o]
), W = Ee(({ data: { from: l, type: P } }) => {
P === "start" && l !== m && q();
}), R = _(
async (l = !0) => {
await T(p, l), W({ type: "start", from: m });
},
[T, p]
), I = H();
return L(() => {
!I.current && a && p && (I.current = !0, R());
}, [R, a, p]), L(() => () => {
A.stop();
}, [A]), {
code: p,
setCode: y,
error: h || C,
schedulerError: h,
scheduler: A,
evalError: C,
evaluate: T,
activateCode: R,
activeCode: M,
isDirty: b,
pattern: k,
started: F,
start: J,
stop: q,
pause: Q,
togglePlay: async () => {
F ? A.pause() : await R();
}
};
}
function ye() {
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
}
const ke = () => re().currentTime;
function Se({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) {
const {
const xe = "_container_3i85k_1", Le = "_header_3i85k_5", Pe = "_buttons_3i85k_9", qe = "_button_3i85k_9", ze = "_buttonDisabled_3i85k_17", He = "_error_3i85k_21", Se = "_body_3i85k_25", F = {
container: xe,
header: Le,
buttons: Pe,
button: qe,
buttonDisabled: ze,
error: He,
body: Se
}, Ve = () => he().currentTime;
function Ze({ tune: e, hideOutsideView: t = !1, enableKeyboard: r, drawTime: n, punchcard: l, canvasHeight: m = 200 }) {
n = n || (l ? [0, 4] : void 0);
const f = !!n, c = E(
n ? (o) => document.querySelector("#" + o)?.getContext("2d") : null,
[n]
), {
code: s,
setCode: u,
evaluate: d,
activateCode: f,
error: c,
isDirty: i,
activeCode: m,
pattern: h,
started: g,
scheduler: C,
togglePlay: N,
stop: p
} = we({
setCode: g,
evaluate: b,
activateCode: i,
error: h,
isDirty: p,
activeCode: D,
pattern: P,
started: R,
scheduler: z,
togglePlay: H,
stop: y,
canvasId: S,
id: V
} = De({
initialCode: e,
defaultOutput: te,
getTime: ke
}), [y, M] = w(), [S, k] = ee({
defaultOutput: me,
editPattern: (o) => l ? o.punchcard() : o,
getTime: Ve,
evalOnMount: f,
drawContext: c,
drawTime: n
}), [q, x] = _(), [I, N] = ge({
threshold: 0.01
}), D = H(), F = V(() => ((k || !t) && (D.current = !0), k || D.current), [k, t]);
return ue({
view: y,
pattern: h,
active: g && !m?.includes("strudel disable-highlighting"),
getTime: () => C.getPhase()
}), j(() => {
if (a) {
const x = async (b) => {
(b.ctrlKey || b.altKey) && (b.code === "Enter" ? (b.preventDefault(), ce(y), await f()) : b.code === "Period" && (p(), b.preventDefault()));
}), O = A(), K = Q(() => ((N || !t) && (O.current = !0), N || O.current), [N, t]);
Me({
view: q,
pattern: P,
active: R && !D?.includes("strudel disable-highlighting"),
getTime: () => z.now()
}), te(() => {
if (r) {
const o = async (v) => {
(v.ctrlKey || v.altKey) && (v.code === "Enter" ? (v.preventDefault(), we(q), await i()) : v.code === "Period" && (y(), v.preventDefault()));
};
return window.addEventListener("keydown", x, !0), () => window.removeEventListener("keydown", x, !0);
return window.addEventListener("keydown", o, !0), () => window.removeEventListener("keydown", o, !0);
}
}, [a, h, s, d, p, y]), /* @__PURE__ */ n.createElement("div", {
className: v.container,
ref: S
}, /* @__PURE__ */ n.createElement("div", {
className: v.header
}, /* @__PURE__ */ n.createElement("div", {
className: v.buttons
}, /* @__PURE__ */ n.createElement("button", {
className: K(v.button, g ? "sc-animate-pulse" : ""),
onClick: () => N()
}, /* @__PURE__ */ n.createElement(O, {
type: g ? "pause" : "play"
})), /* @__PURE__ */ n.createElement("button", {
className: K(i ? v.button : v.buttonDisabled),
onClick: () => f()
}, /* @__PURE__ */ n.createElement(O, {
}, [r, P, s, b, y, q]);
const [L, C] = _([]);
return Oe(
E((o) => {
const { data: v } = o.detail;
v?.hap?.context?.id === V && C((U) => U.concat([o.detail]).slice(-10));
}, [])
), /* @__PURE__ */ d.createElement("div", {
className: F.container,
ref: I
}, /* @__PURE__ */ d.createElement("div", {
className: F.header
}, /* @__PURE__ */ d.createElement("div", {
className: F.buttons
}, /* @__PURE__ */ d.createElement("button", {
className: T(F.button, R ? "sc-animate-pulse" : ""),
onClick: () => H()
}, /* @__PURE__ */ d.createElement(ee, {
type: R ? "stop" : "play"
})), /* @__PURE__ */ d.createElement("button", {
className: T(p ? F.button : F.buttonDisabled),
onClick: () => i()
}, /* @__PURE__ */ d.createElement(ee, {
type: "refresh"
}))), c && /* @__PURE__ */ n.createElement("div", {
className: v.error
}, c.message)), /* @__PURE__ */ n.createElement("div", {
className: v.body
}, F && /* @__PURE__ */ n.createElement(de, {
}))), h && /* @__PURE__ */ d.createElement("div", {
className: F.error
}, h.message)), /* @__PURE__ */ d.createElement("div", {
className: F.body
}, K && /* @__PURE__ */ d.createElement(_e, {
value: s,
onChange: u,
onViewChanged: M
})));
onChange: g,
onViewChanged: x
})), n && /* @__PURE__ */ d.createElement("canvas", {
id: S,
className: "w-full pointer-events-none",
height: m,
ref: (o) => {
o && o.width !== o.clientWidth && (o.width = o.clientWidth);
}
}), !!L.length && /* @__PURE__ */ d.createElement("div", {
className: "sc-bg-gray-800 sc-rounded-md sc-p-2"
}, L.map(({ message: o }, v) => /* @__PURE__ */ d.createElement("div", {
key: v
}, o))));
}
const Te = (e) => j(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
function Oe(e) {
Be(ve.key, e);
}
function Be(e, t, r = !1) {
k(() => (document.addEventListener(e, t, r), () => {
document.removeEventListener(e, t, r);
}), [t]);
}
const Te = (e) => te(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
export {
de as CodeMirror,
Se as MiniRepl,
K as cx,
ce as flash,
ue as useHighlighting,
_e as CodeMirror,
Ze as MiniRepl,
T as cx,
we as flash,
Me as useHighlighting,
Te as useKeydown,
Ee as usePostMessage,
we as useStrudel
Ce as usePostMessage,
De as useStrudel
};
+1 -1
View File
@@ -1 +1 @@
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:18px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto}
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:18px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}.sc-rounded-md{border-radius:.375rem}.sc-bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}.sc-p-2{padding:.5rem}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto}
+1 -1
View File
@@ -39,7 +39,7 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/lang-javascript": "^6.1.1",
"@strudel.cycles/core": "^0.4.1",
"@strudel.cycles/core": "^0.5.0",
"@strudel.cycles/tone": "^0.5.0",
"@strudel.cycles/transpiler": "^0.5.0",
"@strudel.cycles/webaudio": "^0.5.0",
+7
View File
@@ -26,6 +26,13 @@ export function Icon({ type }) {
clipRule="evenodd"
/>
),
stop: (
<path
fillRule="evenodd"
d="M2 10a8 8 0 1116 0 8 8 0 01-16 0zm5-2.25A.75.75 0 017.75 7h4.5a.75.75 0 01.75.75v4.5a.75.75 0 01-.75.75h-4.5a.75.75 0 01-.75-.75v-4.5z"
clipRule="evenodd"
/>
),
}[type]
}
</svg>
+72 -13
View File
@@ -1,18 +1,25 @@
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import React, { useLayoutEffect, useMemo, useRef, useState, useCallback, useEffect } from 'react';
import { useInView } from 'react-hook-inview';
import 'tailwindcss/tailwind.css';
import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
import 'tailwindcss/tailwind.css';
import './style.css';
import styles from './MiniRepl.module.css';
import { Icon } from './Icon';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import useStrudel from '../hooks/useStrudel.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
import { Icon } from './Icon';
import styles from './MiniRepl.module.css';
import './style.css';
import { logger } from '@strudel.cycles/core';
const getTime = () => getAudioContext().currentTime;
export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }) {
export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTime, punchcard, canvasHeight = 200 }) {
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
const evalOnMount = !!drawTime;
const drawContext = useCallback(
!!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
[drawTime],
);
const {
code,
setCode,
@@ -26,14 +33,18 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
scheduler,
togglePlay,
stop,
canvasId,
id: replId,
} = useStrudel({
initialCode: tune,
defaultOutput: webaudioOutput,
editPattern: (pat) => (punchcard ? pat.punchcard() : pat),
getTime,
evalOnMount,
drawContext,
drawTime,
});
/* useEffect(() => {
init && activateCode();
}, [init, activateCode]); */
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01,
@@ -49,7 +60,7 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
view,
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.getPhase(),
getTime: () => scheduler.now(),
});
// set active pattern on ctrl+enter
@@ -72,12 +83,26 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
}
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
const [log, setLog] = useState([]);
useLogger(
useCallback((e) => {
const { data } = e.detail;
const logId = data?.hap?.context?.id;
// const logId = data?.pattern?.meta?.id;
if (logId === replId) {
setLog((l) => {
return l.concat([e.detail]).slice(-10);
});
}
}, []),
);
return (
<div className={styles.container} ref={ref}>
<div className={styles.header}>
<div className={styles.buttons}>
<button className={cx(styles.button, started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
<Icon type={started ? 'pause' : 'play'} />
<Icon type={started ? 'stop' : 'play'} />
</button>
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
<Icon type="refresh" />
@@ -88,6 +113,40 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
<div className={styles.body}>
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</div>
{drawTime && (
<canvas
id={canvasId}
className="w-full pointer-events-none"
height={canvasHeight}
ref={(el) => {
if (el && el.width !== el.clientWidth) {
el.width = el.clientWidth;
}
}}
></canvas>
)}
{!!log.length && (
<div className="sc-bg-gray-800 sc-rounded-md sc-p-2">
{log.map(({ message }, i) => (
<div key={i}>{message}</div>
))}
</div>
)}
</div>
);
}
// TODO: dedupe
function useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
}
// TODO: dedupe
function useEvent(name, onTrigger, useCapture = false) {
useEffect(() => {
document.addEventListener(name, onTrigger, useCapture);
return () => {
document.removeEventListener(name, onTrigger, useCapture);
};
}, [onTrigger]);
}
+43
View File
@@ -0,0 +1,43 @@
import { useEffect, useRef } from 'react';
function useFrame(callback, autostart = false) {
const requestRef = useRef();
const previousTimeRef = useRef();
const animate = (time) => {
if (previousTimeRef.current !== undefined) {
const deltaTime = time - previousTimeRef.current;
callback(time, deltaTime);
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
};
const start = () => {
requestRef.current = requestAnimationFrame(animate);
};
const stop = () => {
requestRef.current && cancelAnimationFrame(requestRef.current);
delete requestRef.current;
};
useEffect(() => {
if (requestRef.current) {
stop();
start();
}
}, [callback]);
useEffect(() => {
if (autostart) {
start();
}
return stop;
}, []); // Make sure the effect only runs once
return {
start,
stop,
};
}
export default useFrame;
+3 -2
View File
@@ -3,16 +3,17 @@ import { setHighlights } from '../components/CodeMirror6';
function useHighlighting({ view, pattern, active, getTime }) {
const highlights = useRef([]);
const lastEnd = useRef();
const lastEnd = useRef(0);
useEffect(() => {
if (view) {
if (pattern && active) {
lastEnd.current = 0;
let frame = requestAnimationFrame(function updateHighlights() {
try {
const audioTime = getTime();
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
// see https://github.com/tidalcycles/strudel/issues/108
const begin = Math.max(lastEnd.current || audioTime, audioTime - 1 / 10, 0); // negative time seems buggy
const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy
const span = [begin, audioTime + 1 / 60];
lastEnd.current = span[1];
highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
@@ -0,0 +1,48 @@
import { useCallback, useEffect, useRef } from 'react';
import 'tailwindcss/tailwind.css';
import useFrame from '../hooks/useFrame.mjs';
function usePatternFrame({ pattern, started, getTime, onDraw, drawTime = [-2, 2] }) {
let [lookbehind, lookahead] = drawTime;
lookbehind = Math.abs(lookbehind);
let visibleHaps = useRef([]);
let lastFrame = useRef(null);
useEffect(() => {
if (pattern && started) {
const t = getTime();
const futureHaps = pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +0.1 = workaround for weird holes in query..
visibleHaps.current = visibleHaps.current.filter((h) => h.whole.begin < t);
visibleHaps.current = visibleHaps.current.concat(futureHaps);
}
}, [pattern, started]);
const { start: startFrame, stop: stopFrame } = useFrame(
useCallback(() => {
const phase = getTime() + lookahead;
if (lastFrame.current === null) {
lastFrame.current = phase;
return;
}
const haps = pattern.queryArc(Math.max(lastFrame.current, phase - 1 / 10), phase);
lastFrame.current = phase;
visibleHaps.current = (visibleHaps.current || [])
.filter((h) => h.whole.end >= phase - lookbehind - lookahead) // in frame
.concat(haps.filter((h) => h.hasOnset()));
onDraw(pattern, phase - lookahead, visibleHaps.current, drawTime);
}, [pattern]),
);
useEffect(() => {
if (started) {
startFrame();
} else {
visibleHaps.current = [];
stopFrame();
}
}, [started]);
return {
clear: () => {
visibleHaps.current = [];
},
};
}
export default usePatternFrame;
+47 -4
View File
@@ -1,6 +1,7 @@
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
import { repl } from '@strudel.cycles/core';
import { transpiler } from '@strudel.cycles/transpiler';
import usePatternFrame from './usePatternFrame';
import usePostMessage from './usePostMessage.mjs';
function useStrudel({
@@ -12,10 +13,15 @@ function useStrudel({
autolink = false,
beforeEval,
afterEval,
editPattern,
onEvalError,
onToggle,
canvasId,
drawContext,
drawTime = [-2, 2],
}) {
const id = useMemo(() => s4(), []);
canvasId = canvasId || `canvas-${id}`;
// scheduler
const [schedulerError, setSchedulerError] = useState();
const [evalError, setEvalError] = useState();
@@ -24,6 +30,7 @@ function useStrudel({
const [pattern, setPattern] = useState();
const [started, setStarted] = useState(false);
const isDirty = code !== activeCode;
const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
// TODO: make sure this hook reruns when scheduler.started changes
const { scheduler, evaluate, start, stop, pause } = useMemo(
@@ -37,7 +44,9 @@ function useStrudel({
onEvalError?.(err);
},
getTime,
drawContext,
transpiler,
editPattern,
beforeEval: ({ code }) => {
setCode(code);
beforeEval?.();
@@ -67,19 +76,41 @@ function useStrudel({
});
const activateCode = useCallback(
async (autostart = true) => {
await evaluate(code, autostart);
const res = await evaluate(code, autostart);
broadcast({ type: 'start', from: id });
return res;
},
[evaluate, code],
);
const onDraw = useCallback(
(pattern, time, haps, drawTime) => {
const { onPaint } = pattern.context || {};
const ctx = typeof drawContext === 'function' ? drawContext(canvasId) : drawContext;
onPaint?.(ctx, time, haps, drawTime);
},
[drawContext, canvasId],
);
const drawFirstFrame = useCallback(
(pat) => {
if (shouldPaint(pat)) {
const [_, lookahead] = drawTime;
const haps = pat.queryArc(0, lookahead);
// draw at -0.001 to avoid activating haps at 0
onDraw(pat, -0.001, haps, drawTime);
}
},
[drawTime, onDraw, shouldPaint],
);
const inited = useRef();
useEffect(() => {
if (!inited.current && evalOnMount && code) {
inited.current = true;
activateCode();
evaluate(code, false).then((pat) => drawFirstFrame(pat));
}
}, [activateCode, evalOnMount, code]);
}, [evalOnMount, code, evaluate, drawFirstFrame]);
// this will stop the scheduler when hot reloading in development
useEffect(() => {
@@ -90,13 +121,25 @@ function useStrudel({
const togglePlay = async () => {
if (started) {
scheduler.pause();
scheduler.stop();
drawFirstFrame(pattern);
} else {
await activateCode();
}
};
const error = schedulerError || evalError;
usePatternFrame({
pattern,
started: shouldPaint(pattern) && started,
getTime: () => scheduler.now(),
drawTime,
onDraw,
});
return {
id,
canvasId,
code,
setCode,
error,
+1 -1
View File
@@ -22,7 +22,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.4.1",
"@strudel.cycles/core": "^0.5.0",
"@strudel.cycles/webaudio": "^0.5.0",
"sfumato": "^0.1.2",
"soundfont2": "^0.4.0"
+1 -1
View File
@@ -31,4 +31,4 @@ yields:
## Tonal API
See "Tonal API" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
See "Tonal API" in the [Strudel Tutorial](https://strudel.tidalcycles.org/learn/tonal)
+1 -1
View File
@@ -25,7 +25,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.4.1",
"@strudel.cycles/core": "^0.5.0",
"@tonaljs/tonal": "^4.7.2",
"chord-voicings": "^0.0.1",
"webmidi": "^3.0.21"
+17 -1
View File
@@ -7,11 +7,27 @@ This program is free software: you can redistribute it and/or modify it under th
// import { strict as assert } from 'assert';
import '../tonal.mjs'; // need to import this to add prototypes
import { pure } from '@strudel.cycles/core';
import { pure, controls, seq } from '@strudel.cycles/core';
import { describe, it, expect } from 'vitest';
const { n } = controls;
describe('tonal', () => {
it('Should run tonal functions ', () => {
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
});
it('scale with plain values', () => {
expect(
seq(0, 1, 2)
.scale('C major')
.note()
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with n values', () => {
expect(
n(0, 1, 2)
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
});
+3 -2
View File
@@ -141,13 +141,14 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
export const scale = register('scale', function (scale /* : string */, pat) {
return pat.withHap((hap) => {
let note = hap.value;
const isObject = typeof hap.value === 'object';
let note = isObject ? hap.value.n : hap.value;
const asNumber = Number(note);
if (!isNaN(asNumber)) {
let [tonic, scaleName] = Scale.tokenize(scale);
const { pc, oct = 3 } = Note.get(tonic);
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
}
return hap.withValue(() => note).setContext({ ...hap.context, scale });
return hap.withValue(() => (isObject ? { ...hap.value, note } : note)).setContext({ ...hap.context, scale });
});
});
+2 -8
View File
@@ -1,5 +1,7 @@
# @strudel.cycles/tone
Note: This package still works but is no longer maintained in favor of `@strudel.cycles/webaudio`.
This package adds Tone.js functions to strudel Patterns.
## Install
@@ -31,11 +33,3 @@ document.getElementById('play').addEventListener('click', async () => {
[open in codesandbox](https://codesandbox.io/s/strudel-tone-example-5ph2te?file=/src/index.js:0-708)
## API
See "Tone API" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
## Dev Notes
- `@tonejs/piano` has peer dependency on `webmidi@^2.5.1`! A newer version of webmidi will break.
- If you use Tone in another package, make sure to `import { Tone } from '@strudel.cycles/tone`, to ensure you get the same AudioContext.
+1 -1
View File
@@ -22,7 +22,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.4.1",
"@strudel.cycles/core": "^0.5.0",
"tone": "^14.7.77"
}
}
+1 -1
View File
@@ -24,7 +24,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.4.1",
"@strudel.cycles/core": "^0.5.0",
"acorn": "^8.8.1",
"escodegen": "^2.0.0",
"estree-walker": "^3.0.1"
+3 -3
View File
@@ -1,7 +1,7 @@
import escodegen from 'escodegen';
import { parse } from 'acorn';
import { walk } from 'estree-walker';
import { isNote } from '@strudel.cycles/core';
import { isNoteWithOctave } from '@strudel.cycles/core';
export function transpiler(input, options = {}) {
const { wrapAsync = false, addReturn = true, simpleLocs = false } = options;
@@ -25,11 +25,11 @@ export function transpiler(input, options = {}) {
this.skip();
return this.replace(miniWithLocation(value, node, simpleLocs));
}
if (node.type === 'Identifier' && isNote(node.name)) {
// TODO: remove pseudo note variables?
if (node.type === 'Identifier' && isNoteWithOctave(node.name)) {
this.skip();
return this.replace({ type: 'Literal', value: node.name });
}
// TODO:
},
leave(node, parent, prop, index) {},
});
+1 -1
View File
@@ -30,6 +30,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.4.1"
"@strudel.cycles/core": "^0.5.0"
}
}
+2 -1
View File
@@ -124,7 +124,8 @@ let sampleCache = { current: undefined };
export const samples = async (sampleMap, baseUrl = sampleMap._base || '') => {
if (typeof sampleMap === 'string') {
if (sampleMap.startsWith('github:')) {
const [_, path] = sampleMap.split('github:');
let [_, path] = sampleMap.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
sampleMap = `https://raw.githubusercontent.com/${path}/strudel.json`;
}
if (typeof fetch !== 'function') {
+24 -9
View File
@@ -195,9 +195,14 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
hap.value = { note: hap.value };
} */
if (typeof hap.value !== 'object') {
throw new Error(
`hap.value ${hap.value} is not supported by webaudio output. Hint: append .note() or .s() to the end`,
logger(
`hap.value "${hap.value}" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
'error',
);
/* throw new Error(
`hap.value "${hap.value}"" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
); */
return;
}
// calculate correct time (tone.js workaround)
let t = ac.currentTime + deadline;
@@ -211,12 +216,22 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
n = 0,
note,
gain = 0.8,
cutoff,
resonance = 1,
hcutoff,
hresonance = 1,
bandf,
bandq = 1,
// low pass
lpf,
cutoff = lpf,
lpq = 1,
resonance = lpq,
// high pass
hpf,
hcutoff = hpf,
hpq = 1,
hresonance = hpq,
// band pass
bpf,
bandf = bpf,
bpq = 1,
bandq = bpq,
//
coarse,
crush,
shape,
@@ -381,7 +396,7 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps);
Pattern.prototype.out = function () {
Pattern.prototype.webaudio = function () {
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
return this.onTrigger(webaudioOutputTrigger);
};
+1 -1
View File
@@ -22,7 +22,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.4.1",
"@strudel.cycles/core": "^0.5.0",
"WebDirt": "github:dktr0/WebDirt"
}
}
+1 -1
View File
@@ -24,6 +24,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.4.1"
"@strudel.cycles/core": "^0.5.0"
}
}
+16
View File
@@ -0,0 +1,16 @@
all: iclc2023.pdf iclc2023.html
clean:
rm iclc2023.pdf iclc2023.html
iclc2023.html: iclc2023.md citations.json
pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html
iclc2023.pdf: iclc2023.md citations.json pandoc/iclc.latex pandoc/iclc.sty
pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf
iclc2023.docx: iclc2023.md citations.json
pandoc --citeproc --number-sections iclc2023.md -o iclc2023.docx
iclc2023x.pdf: iclc2023.md citations.json pandoc/iclc.latex pandoc/iclc.sty
pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md --pdf-engine=xelatex -o iclc2023x.pdf
+19 -5
View File
@@ -1,8 +1,22 @@
# paper
## iclc2023
from the strudel project root:
```sh
npm run iclc
npm run iclc-nocite # try this if you get an error
```
## old
Work in progress on a paper about strudel
To build you will need
* pandoc
* pandoc-url2cite (`npm install -g pandoc-url2cite`)
* latex/xelatex
* python
* pandocfilters (`pip3 install pandocfilters`)
- pandoc
- pandoc-url2cite (`npm install -g pandoc-url2cite`)
- latex/xelatex
- python
- pandocfilters (`pip3 install pandocfilters`)
+3 -1
View File
@@ -926,5 +926,7 @@
{"id":"zhangDynamicFMRIStudy2006","abstract":"Functional MRI (fMRI) combined with the paired-stimuli paradigms (referred as dynamic fMRI) was used to study the illusory double-flash effect on brain activity in the human visual cortex. Three experiments were designed. The first two experiments aimed to examine the cross-modal neural interaction between the visual and auditory sensory systems caused by the illusory double-flash effect using combined auditory (beep sound) and visual (light flash) stimuli. The fMRI signal in the visual cortex was significantly increased in response to the illusory double flashes compared to the physical single flash when the inter-stimuli delay between the auditory and visual stimuli was 25 ms. This increase disappeared when the delay was prolonged to 300 ms. These results reveal that the illusory double-flash effect can significantly affect the brain activity in the visual cortex, and the degree of this effect is dynamically sensitive to the inter-stimuli delay. The third experiment was to address the spatial differentiation of brain activation in the visual cortex in response to the illusory double-flash stimulation. It was found that the illusory double-flash effect in the human visual cortex is much stronger in the periphery than the fovea. This finding suggests that the periphery may be involved in high-level brain processing beyond the retinotopic visual perception. The behavioral measures conducted in this study indicate an excellent correlation between the fMRI results and behavioral performance. Finally, this work demonstrates a unique merit of fMRI for providing both temporal and spatial information regarding cross-modal neural interaction between different sensory systems.","author":[{"family":"Zhang","given":"Nanyin"},{"family":"Chen","given":"Wei"}],"citation-key":"zhangDynamicFMRIStudy2006","container-title":"Experimental Brain Research","DOI":"10.1007/s00221-005-0304-7","ISSN":"0014-4819","issue":"1","issued":{"date-parts":[[2006,6]]},"page":"5766","title":"A dynamic fMRI study of illusory double-flash effect on human visual cortex","type":"article-journal","URL":"http://dx.doi.org/10.1007/s00221-005-0304-7","volume":"172"},
{"id":"zimmermannMiningVersionHistories2005","abstract":"We apply data mining to version histories in order to guide programmers along related changes: \"Programmers who changed these functions also changed....\" Given a set of existing changes, the mined association rules 1) suggest and predict likely further changes, 2) show up item coupling that is undetectable by program analysis, and 3) can prevent errors due to incomplete changes. After an initial change, our ROSE prototype can correctly predict further locations to be changed; the best predictive power is obtained for changes to existing software. In our evaluation based on the history of eight popular open source projects, ROSE's topmost three suggestions contained a correct location with a likelihood of more than 70 percent.","author":[{"family":"Zimmermann","given":"T."},{"family":"Zeller","given":"A."},{"family":"Weissgerber","given":"P."},{"family":"Diehl","given":"S."}],"citation-key":"zimmermannMiningVersionHistories2005","container-title":"Software Engineering, IEEE Transactions on","container-title-short":"Software Engineering, IEEE Transactions on","DOI":"10.1109/tse.2005.72","ISSN":"0098-5589","issue":"6","issued":{"date-parts":[[2005,6]]},"page":"429445","title":"Mining version histories to guide software changes","type":"article-journal","URL":"http://dx.doi.org/10.1109/tse.2005.72","volume":"31"},
{"id":"zittrainFutureInternetHow2009","author":[{"family":"Zittrain","given":"Jonathan"}],"citation-key":"zittrainFutureInternetHow2009","ISBN":"0-300-15124-1","issued":{"date-parts":[[2009,3]]},"note":"Published: Paperback","publisher":"Yale University Press","title":"The Future of the InternetAnd How to Stop It","type":"book","URL":"http://www.worldcat.org/isbn/0300151241"},
{"id":"zivanovicDevelopmentCyberneticSculptor2005","abstract":"Edward Ihnatowicz (1926-1988) built one of the world's first computer-controlled robotic sculptures, The Senster, in 1968-70. Rather than concentrate entirely on this groundbreaking and influential piece of work, this paper describes the stages he went through in developing his ideas, as an illustration of how a conventional artist became a cybernetic sculptor.","author":[{"family":"Zivanovic","given":"Aleksandar"}],"citation-key":"zivanovicDevelopmentCyberneticSculptor2005","container-title":"C&amp;C '05: Proceedings of the 5th conference on Creativity &amp; cognition","DOI":"10.1145/1056224.1056240","event-place":"London, United Kingdom","ISBN":"1-59593-025-6","issued":{"date-parts":[[2005]]},"page":"102108","publisher":"ACM","publisher-place":"London, United Kingdom","title":"The development of a cybernetic sculptor: Edward Ihnatowicz and the senster","type":"paper-conference","URL":"http://dx.doi.org/10.1145/1056224.1056240"}
{"id":"zivanovicDevelopmentCyberneticSculptor2005","abstract":"Edward Ihnatowicz (1926-1988) built one of the world's first computer-controlled robotic sculptures, The Senster, in 1968-70. Rather than concentrate entirely on this groundbreaking and influential piece of work, this paper describes the stages he went through in developing his ideas, as an illustration of how a conventional artist became a cybernetic sculptor.","author":[{"family":"Zivanovic","given":"Aleksandar"}],"citation-key":"zivanovicDevelopmentCyberneticSculptor2005","container-title":"C&amp;C '05: Proceedings of the 5th conference on Creativity &amp; cognition","DOI":"10.1145/1056224.1056240","event-place":"London, United Kingdom","ISBN":"1-59593-025-6","issued":{"date-parts":[[2005]]},"page":"102108","publisher":"ACM","publisher-place":"London, United Kingdom","title":"The development of a cybernetic sculptor: Edward Ihnatowicz and the senster","type":"paper-conference","URL":"http://dx.doi.org/10.1145/1056224.1056240"},
{"id":"CsoundWebAssembly","abstract":"This paper describes WebAssembly AudioWorklet (WAAW)\r\nCsound, one of the implementations of Web Audio Csound.\r\nWe begin by introducing the background to this current implementation, stemming from the two first ports of Csound\r\nto the web platform using Native Clients and asm.js. The\r\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\r\nWAAW Csound. We complement this discussion by considering the overarching question of support for multiple platforms, which implement different versions of Web Audio.\r\nSome initial examples of the system are presented to illustrate various potential applications. Finally, we complement\r\nthe paper by discussing current issues that are fundamental\r\nfor this project and others that rely on the development of\r\na robust support for WASM-based audio computing.","author":[{"family":"Yi","given":"Steven"},{"family":"Lazzarini","given":"Victor"},{"family":"Costello","given":"Edward"}],"citation-key":"CsoundWebAssembly","event-place":"Berlin, Germany","issued":{"date-parts":[[2018]]},"publisher-place":"Berlin, Germany","title":"WebAssembly AudioWorklet Csound","type":"paper-conference","URL":"https://mural.maynoothuniversity.ie/16018/"},
{"id":"StrudelWAC2022","publisher":"Zenodo","DOI":"10.5281/zenodo.6768844","language":"eng","title":"Strudel: Algorithmic Patterns for the Web","issued":{"date-parts":[[2022,6,28]]},"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).","author":[{"family":"Roos", "given":"Felix "},{"family":"McLean","given":"Alex"}],"note":"Demo paper","event-place":"Cannes, France","type":"paper-conference","event":"Web Audio Conference 2022 (WAC 2022)"}
]
+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>
+460
View File
@@ -0,0 +1,460 @@
---
title: 'Strudel: live coding patterns on the Web'
author:
- name: Felix Roos
affiliation: Unaffiliated
email: flix91@gmail.com
- name: Alex McLean
affiliation: Then Try This
email: alex@slab.org
abstract: |
This paper introduces Strudel, which brings the TidalCycles approach to live coding algorithmic patterns to native JavaScript and the web. We begin by giving a little background of the first year of development, before sharing some detail about its implementation and examples of use. We go on to outline the wide range of synthesis and other outputs available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), WebSerial and CSound, and introduce Strudel's REPL live editor, including its built-in visualisations. We then compare Strudel with Tidal, the trade-offs involved between JavaScript and Haskell, and the unique capabilities offered by Strudel for aligning patterns, before concluding with some thoughts about the future.
bibliography: citations.json
fontsize: 11pt
geometry: margin=2cm
fontfamily: libertine
fontfamily: inconsolata
mainfont: Linux Libertine O
monofont: Inconsolata
date: '2022-12-14'
---
# Introduction
In the following paper, we introduce *Strudel*, an alternative
implementation of the TidalCycles (or 'Tidal' for short) live coding
system, using the JavaScript programming language. Strudel is an
attempt to make live coding more accessible, by creating a system that
runs entirely in the browser, while opening Tidal's approach to
algorithmic patterns [@mcleanAlgorithmicPattern2020a] up to modern
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
[@mcleanTidalVortexZero2022], Gibber [@robertsGibberLiveCoding2012],
Estuary [@ogbornEstuaryBrowserbasedCollaborative2017], Hydra
[@jackHydra2022], Ocarina [@solomonPurescriptocarina2022] and
Feedforward [@mcleanFeedforward2020]. This paper expands the Strudel
Demo paper for the Web Audio Conference 2022 [@StrudelWAC2022].
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 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
to jump between developing both Strudel and Tidal. Aspects of
Strudel's development have therefore fed back into TidalCycles, and
both systems have maintained a shared conceptual underpinning. We plan
to continue working towards feature parity between these systems,
although within the syntactical trade-offs and library ecosystems of
JavaScript and Haskell, some divergence is inevitable and healthy.
Over the first year of its life, Strudel is now a fully-fledged live
coding environment, porting Tidal's core represention of patterns,
pattern transformations, and mini-notation for polymetric sequences,
combined with a wealth of features for synthesising and visualising
those patterns.
# From Tidal to Strudel and back
As mentioned above, the original Tidal is implemented as a domain specific language (DSL) embedded in the Haskell pure functional programming language, and takes advantage of Haskell's terse syntax and advanced, 'strong' type system. JavaScript on the other hand, is a multi-paradigm programming language, with a dynamic type system. Because Tidal leans heavily on many of Haskell's more unique features, it was not always clear that it could meaningfully be ported to a multi-paradigm scripting language. However, this possibility was already demonstrated with an earlier port to Python [TidalVortex; @mcleanTidalVortexZero2022], and we have now successfully implemented Tidal's pure functional representation of patterns in Strudel, including partial application, currying, and the functor, applicative and monadic structures that underlie Tidal's expressive pattern transformations. The result is a terse and highly composable system, where everything is either a pattern, or a function for combining and manipulating patterns, offering a rich creative ground for exploration.
This development process has been far from a one-way port, however. The process of porting Tidal's concepts has also opened up new possibilities, some just from revisiting every design decision, and some from the particular affordances and constraints offered by JavaScript. This has lead to new features (and indeed bugfixes) that have found their way back to Tidal where appropriate, and ongoing work that we will return to in the conclusion of this paper.
# Representing Patterns
Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern can output a set of events that happen within that time span. It depends on the structure of the Pattern how the events are located in time.
From now on, this process of generating events from a time span will be called **querying**.
Example:
```js
const pattern = sequence(c3, [e3, g3])
const events = pattern.queryArc(0, 1)
console.log(events.map(e => e.show()))
```
In this example, we create a pattern using the `sequence` function and **query** it for the time span from `0` to `1`.
Those numbers represent units of time called **cycles**. The length of one cycle depends on the tempo, which defaults to one cycle per second.
The resulting events are:
```js
["[ 0/1 -> 1/2 | c3 ]",
"[ 1/2 -> 3/4 | e3 ]",
"[ 3/4 -> 1/1 | g3 ]"
]
```
Each event has a value, a begin time and an end time, where time is represented as a fraction. In the above case, the events are placed in sequential order, where c3 takes the first half, and e3 and g3 together take the second half. This temporal placement is the result of the `sequence` function, which divides its arguments equally over one cycle. If an argument is an array, the same rule applies to that part of the cycle. In the example, e3 and g3 are divided equally over the second half of the whole cycle.
Note that the query function is not just a way to access a pattern, but true to the principles of functional programming, is the pattern itself. This means that in theory there is no way to change a pattern, it is opaque as a pure function. In practice though, Strudel and Tidal are all about transforming patterns, so how is this done? The answer is, by replacing the pattern with a new one, that calls the old one. This new one is only able to manipulate the query before passing it to the old pattern, and manipulate the results from it before returning them to caller. But, this is enough to support all the temporal and structural manipulations provided by Strudel (and Tidal's) extensive library of functions.
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.
![Screenshot of the Strudel 'REPL' live coding editor, including piano-roll visualisation.](images/strudel-screenshot2.png){ width=60% }
# 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.
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.
```js
note(c3, [e3, g3])
```
Perhaps more often, they will use the mini-notation for even terser notation of rhythmic sequences: ^[This last example is also valid Tidal code, albeit the parenthesis is not required in its Haskell syntax in this case. Tidal does not support passing sequences as lists directly to the `note` function, however.].
```js
note("c3 [e3 g3]")
```
Such sequences are often treated only as a starting point for manipulation, where they then undergo pattern transformations such as repetition, symmetry, interference/combination or randomisation, potentially at multiple timescales. Because Strudel patterns are represented as pure functions of time rather than as data structures, very long and complex generative results can be represented and manipulated without having to store the resulting sequences in memory.
# Pattern Example
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('D minor')
.legato(.25)
.note().s("sawtooth square")
.delay(.8).delaytime(.125)
```
The pattern starts with a rhythm of numbers in mini-notation, which are later interpreted inside the scale of D minor.
The first line could also be expressed without mini-notation:
```js
cat(0, 2, [4, 6].euclid(3, 4, 1), 3)
```
These numbers then undergo various pattern transformations. Here is a short description of all the functions used:
- `cat`: play elements sequentially, where each lasts one cycle
- `brackets`: elements inside brackets are divided equally over the time of their parent
- `.euclid(p, s, o)`: place p pulses evenly over s steps, with offset o [@toussaintEuclideanAlgorithmGenerates2005]
- `.off(n, f)`: layers a pattern on top of itself, with the new layer offset by n cycles, and with function f applied
- `.legato(n)`: multiply the duration of all events in a pattern by a factor of n
- `.echo(t, n, v)`: copy each event t times, with n cycles in between each copy, decreasing velocity by v
- `.note()`: interpretes values as notes
- `.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. -->
# Ways to make Sound (and other events)
To generate sound, Strudel supports bindings for different outputs:
- Tone.js (deprecated)
- Web Audio API
- WebDirt, a js recreation of Tidal's *Dirt* sample engine (deprecated)
- OSC via osc-js, compatible with superdirt
- Csound via the Csound WebAssembly build
- MIDI via WebMIDI
- Serial via WebSerial
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 *fire-and-forget* approach, creating a lot of Tone.js instruments and effects causes performance issues quickly. For that reason, we chose to search for alternatives.
Strudel's new default output uses the Web Audio API to create a new audio graph for each event. It currently supports basic oscillators, sample playback, various effects and an experimental support for soundfonts.
WebDirt [@ogbornDktr0WebDirt2022] was created as part of the Estuary Live Coding System [@ogbornEstuaryBrowserbasedCollaborative2017], and proved to be a solid choice for handling samples in Strudel as well. We are however focused on working more directly with the Web Audio API to be able to integrate new features more tightly.
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 [@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).
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.
# The Strudel REPL
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, highlighting 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.
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:
1. The user writes and updates code. Each update transpiles and evaluates it to create a `Pattern` instance
2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span.
3. For each scheduling tick, all generated `Events` are triggered by calling their `onTrigger` method, which is set by the output.
![REPL control flow](images/strudelflow.png){ width=43% }
## User Code
To create a `Pattern` from the user code, two steps are needed:
1. Transpile the JS input code to make it functional
2. Evaluate the transpiled code
### Transpilation & Evaluation
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 `babel` will transpile code that contains unsupported language features into a version of the code without those features.
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:
```js
"c3 [e3 g3]*2"
```
is transpiled to:
```js
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`.
### Mini-notation
While the transpilation allows JavaScript to express Patterns in a less verbose way, it is still preferable to use the mini-notation as a more compact way to express rhythm. Strudel aims to provide the same mini-notation features and syntax as used in Tidal.
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(
reify('c3').withLocation([1,1,1], [1,4,4]),
seq(
reify('e3').withLocation([1,5,5], [1,8,8]),
reify('g3').withLocation([1,8,8], [1,10,10]),
).fast(2)
)
```
### Highlighting Locations
As seen in the examples above, both the JS and the mini-notation parser add source code locations using `withMiniLocation` and `withLocation` methods. While the JS parser adds locations relative to the user code as a whole, the mini-notation adds locations relative to the position of the mini-notation string. The absolute location of elements within mini-notation can be calculated by simply adding both locations together. This absolute location can be used to highlight active events in real time.
## Scheduling Events
After an instance of `Pattern` is obtained from the user code,
it is used by the scheduler to get queried for events. Once started, the scheduler runs at a fixed interval to query the active pattern for events within the current interval's time span. A simplified implementation looks like this:
```js
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
let interval = 0.5; // query interval in seconds
let time = 0; // beginning of current time span
let minLatency = .1; // min time before a hap should trigger
setInterval(() => {
const haps = pattern.queryArc(time, time + interval);
time += interval; // increment time
haps.forEach((hap) => {
const deadline = hap.whole.begin - time + minLatency;
onTrigger(hap, deadline, duration);
});
}, interval * 1000); // query each "interval" seconds
```
Note that the above code is simplified for illustrative purposes. The actual implementation has to work around imprecise callbacks of `setInterval`. More about the implementation details can be read in [this blog post](https://loophole-letters.vercel.app/web-audio-scheduling).
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.
The latency between the time the pattern is evaluated and the change is heard is between `minLatency` and `interval + minLatency`, 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.
## Output
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.
### Control Parameters
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:
```js
note("c3 e3").cutoff(1000).s('sawtooth')
.queryArc(0, 1).map(hap => hap.value)
/* [
{ note: 'c3', cutoff: 1000, s: 'sawtooth' }
{ note: 'e3', cutoff: 1000, s: 'sawtooth' }
] */
```
Here, the control parameter functions `note`, `cutoff` and `s` 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 `Pattern`, or a `Pattern`. In the example, `note` gets a `Pattern` from a mini-notation expression (double quoted), while `cutoff` and `s` are given a `Number` and a (single quoted) `String` respectively.
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 parameters for any purpose:
```js
const { x, y } = createParams('x', 'y')
x(sine.range(0, 200)).y(cosine.range(0,200))
```
This example creates the custom control parameters `x` and `y` which are then used to form a pattern that descibes the coordinates of a circle.
### Outputs
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 `onTrigger` function which is used to implement the output. A very simple version of the web audio output could look like this:
```js
function onTrigger(hap, deadline, duration) {
const { note } = hap.value;
const time = getAudioContext().currentTime + deadline;
const o = getAudioContext().createOscillator();
o.frequency.value = getFreq(note);
o.start(time);
o.stop(time + event.duration);
o.connect(getAudioContext().destination);
}
```
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, 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")
"10 [11 22] 23"
```
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:
```js
"0 1 2".add("10 20")
"10 [11 21] 20"
```
They are similar to the previous example in that the number `1` is split in two, with its two halves added to `10` and `20` respectively. However, the `11` 'remembers' that it is a fragment of that original `1` 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 `21` is also a fragment of that original `1` event, but a fragment of its second half. Because the start of its event is missing, it wouldn't actually trigger a sound (unless it underwent further pattern transformations/combinations).
In practice, the effect of this default, implicit method for combining two patterns is that the second pattern is added *in* to the first one, and indeed this can be made explicit:
```js
"0 1 2".add.in("10 20")
```
This makes way for other ways to align the pattern, and several are already defined, in particular:
* `in` - as explained above, aligns cycles, and applies values from the pattern on the right *in* to the pattern on the left.
* `out` - as with `in`, but values are applied *out* of the pattern on the left (i.e. *in* to the one on the right).
* `mix` - structures from both patterns are combined, so that the new events are not fragments but are created at intersections of events from both sides.
* `squeeze` - cycles from the pattern on the right are squeezed into events on the left. So that e.g. `"0 1 2".add.squeeze("10 20")` is equivalent to `"[10 20] [11 21] [12 22]"`.
* `squeezeout` - as with `squeeze`, but cycles from the left are squeezed into events on the right. So, `"0 1 2".add.squeezeout("10 20")` is equivalent to `[10 11 12] [20 21 22]`.
* `trig` is similar to `squeezeout` 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 `"0 1 2 3 4 5 6 7".add.trig("10 [20 30]")` would be equivalent to `10 11 12 13 20 21 30 31`. In effect, events on the right 'trigger' cycles on the left.
* `trigzero` is similar to `trig`, but the pattern is 'triggered' from its very first cycle, rather than from the current cycle. `trig` and `trigzero` therefore only give different results where the leftmost pattern differs from one cycle to the next.
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.
# Comparing Strudel and Haskell in use
Unlike Haskell, JavaScript lacks the ability to define custom infix
operators, or change the meaning of existing ones. So the above
Strudel example of `"0 1 2".add.out("10 20")` is equivalent to the
Tidal expression `"0 1 2" +| "10 20"`, where the vertical bar in the
operator `+|` stands for `out` (where `a |+ b` would be equivalent of
`a.add.in(b)`).
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 `.`
operator. This then is the design trade-off of Tidal's tersity,
versus Strudel's simplicity.
To demonstrate this, consider the following Tidal pattern:
```haskell
iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4
```
This can be directly translated to the Strudel equivalent:
```js
iter(4, every(3, add.squeeze("10 20"), n("0 1 3").s("triangle").crush(4)))
```
Although for a more canonical Strudel expression, we would reorder it
as:
```js
n("0 1 3").every(3, add.squeeze("10 20")).iter(4).s("triangle").crush(4)
```
The Strudel example uses the `.` method call operator for all
operations and combinations, whereas the Tidal example has `#` for the
default method for combining patterns and uses infix operators for
other methods. The relative lack of parenthesis in the Tidal example is partly
due to the way that arguments are applied to Haskell's functions, and
partly due to the use of the `$` operator as an alternative way to
establish precedence and control the order of evaluation.
Considering the above, we hypothesise that the Haskell syntax is a little
cleaner, but that the Strudel syntax is easier to learn. Our informal
observation is that while Haskell's dollar `$` 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 largely be mitigated by
reordering expressions, and further mitigated by supporting editor
features.
With Strudel, we have little choice but to embrace the affordances and
constraints offered by JavaScript, and while designing a
domain-specific language based on method calls is a
challenge, through creative adoption of functional programming
techniques like partial application, we are so far very happy with the
results. Tidal's functional reactive approach to pattern-making has in
general translated well to JavaScript, and opportunities and
constraints have overall traded off to create a very approachable and
useable live coding environment.
## The trade-off of flexible typing
We have identified one problem with porting Tidal to JavaScript where we have missed Haskell's strict typing and type inference. In both Tidal and Strudel, time is rational, where any point in time is represented as the ratio of two integers. This allows representation of musical ratios such that are impossible to represent accurately using the more common floating point numbers. However while libraries are available that support rational numbers in JavaScript, the lack of strict typing means that it is easy to implement pattern methods where computationally expensive conversion from floating point to rational numbers are performed late, and therefore often enough to overload the CPUs, due to the large number of iterative calculations required to estimate a ratio for a given floating point number. To mitigate this problem, we might consider moving to TypeScript in the future.
# Future Outlook
The project is still young, with many features on the horizon. As general guiding principles, Strudel aims to be
1. accessible
2. consistent with Tidal's approach to pattern
3. modular and extensible
While Haskell's type system makes it a great language for the ongoing development of Tidal's inner representation of pattern, JavaScript's vibrant ecosystem, flexibility and accessibility makes it a great host for more ad-hoc experiments, including interface design. For the future, it is planned to integrate additional alternative sound engines such as Glicol [@lanChaosprintGlicol2022] and Faust [@FaustProgrammingLanguage2022]. Strudel is already approaching feature parity with Tidal, but there are more Tidal functions to be ported, and work to be done to improve compatibility with Tidal's mini-notation. Tidal version 2.0 is under development, which brings a new representation for sequences to its patterns, which will then be brought to Strudel. Besides sound, other ways to render events are being explored, such as graphical, and choreographic output. We are also looking into alternative ways of editing patterns, including multi-user editing for network music, parsing a novel syntax to escape the constraints of JavaScript, and developing hardware/e-textile interfaces. In summary, there is a lot of fun ahead.
# Links
The Strudel REPL is available at <https://strudel.tidalcycles.org>, including an interactive tutorial.
The repository is at <https://github.com/tidalcycles/strudel>, all the code is open source under the AGPL-3.0 License.
# Acknowledgments
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].
# References
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

+92
View File
@@ -0,0 +1,92 @@
% Copyright 2014 Michael Sharpe
% Based initially on Karl Berry's inconsolata.sty.
% You may freely use, modify and/or distribute this file.
\def\fileversion{1.05}
\def\filedate{2014/06/22}
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{inconsolata}[\filedate\space v\fileversion]
\message{`inconsolata-zi4' v\fileversion, \filedate\space Text macros for Inconsolata (msharpe)}
\RequirePackage{textcomp}
\RequirePackage{keyval}
\newcount\zifour@ocount
\newif\ifzifour@altzero
\newif\ifzifour@noupq
\define@key{zifour}{scaled}[1.0]{\def\zifour@scaled{s*[#1]}}
\DeclareOption*{%
\begingroup
\edef\x{\endgroup
\noexpand\setkeys{zifour}{\CurrentOption}}%
\x}
% by default, change \tt to mean zi4.
\newcommand*{\zifour@default}{%
\renewcommand*{\ttdefault}{zi4}%
}
% option [nott] to avoid changing tt.
\DeclareOption{nott}{%
\renewcommand*{\zifour@default}{}%
}
% option [noupquote] to prevent loading upquote.
\DeclareOption{noupquote}{%
\zifour@noupqtrue}%
% option var0---use unslashed zero (slashed is default)
\DeclareOption{var0}{%
\zifour@altzerotrue\advance\zifour@ocount \tw@ %
}
\DeclareOption{varl}{%
\advance\zifour@ocount \@ne %
}
\DeclareOption{varqu}{%
\advance\zifour@ocount 4\relax %
}
\ProcessOptions*
\zifour@default
\edef\zifour@opt{\the\zifour@ocount}
\ifzifour@altzero
\advance\zifour@ocount -\tw@
\else
\advance\zifour@ocount \tw@
\fi
\edef\zifour@altopt{\the\zifour@ocount}
% define an \altzero macro which flips to slashed, unslashed
\def\altzero{{\fontfamily{zi4}%
\fontshape{scit}%
\selectfont 0}}
\def\zifour@T@ne@nc{T1}
\def\zifour@OT@ne@nc{OT1}
\def\zifour@LY@ne@nc{LY1}
\def\zifour@QX@nc{QX}
\def\zifour@TQS{%
\UndeclareTextCommand{\textquotesingle}{\encodingdefault}
\DeclareTextSymbol{\textquotesingle}{TS1}{39}}
\ifzifour@noupq% do nothing
% Try to correct for wrong slots for QX
\ifx\encodingdefault\zifour@QX@nc
\zifour@TQS
\else
\ifx\encodingdefault\zifour@LY@ne@nc
\zifour@TQS
\fi
\fi
\else
\AtBeginDocument{%
\ifx\encodingdefault\zifour@T@ne@nc % do nothing
\else
\ifx\encodingdefault\zifour@OT@ne@nc % do nothing
\else
\zifour@TQS
\fi
\fi
\usepackage{upquote}}
\fi
\endinput
+80
View File
@@ -0,0 +1,80 @@
$if(false)$
This is a pandoc template and should not be edited.
$endif$
<!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"$if(lang)$ lang="$lang$" xml:lang="$lang$"$endif$>
<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" />
$for(author-meta)$
<meta name="author" content="$author-meta$" />
$endfor$
$if(date-meta)$
<meta name="date" content="$date-meta$" />
$endif$
<title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
<style type="text/css">code{white-space: pre;}</style>
$if(quotes)$
<style type="text/css">q { quotes: "“" "”" "" ""; }</style>
$endif$
$if(highlighting-css)$
<style type="text/css">
$highlighting-css$
</style>
$endif$
<link rel="stylesheet" href="css/iclc.css" $if(html5)$$else$type="text/css" $endif$/>
$for(css)$
<link rel="stylesheet" href="$css$" $if(html5)$$else$type="text/css" $endif$/>
$endfor$
$if(math)$
$math$
$endif$
$for(header-includes)$
$header-includes$
$endfor$
</head>
<body>
$for(include-before)$
$include-before$
$endfor$
$if(title)$
<div id="$idprefix$header">
<h1 class="title">$title$</h1>
$if(subtitle)$
<h1 class="subtitle">$subtitle$</h1>
$endif$
<ul id="authorlist">
$for(author)$
<li>$author$</li>
$endfor$
</ul>
$if(date)$
<h3 class="date">$date$</h3>
$endif$
</div>
$endif$
$if(toc)$
<div id="$idprefix$TOC">
$toc$
</div>
$endif$
<h2 class="abstract">Abstract</h2>
<div id="abstract">
$if(abstract)$
$abstract$
$else$
Please provide an abstract in the metadata block at the top of your
markdown document. Refer to template.txt for details.
$endif$
</div>
$body$
$for(include-after)$
$include-after$
$endfor$
</body>
</html>
+224
View File
@@ -0,0 +1,224 @@
$if(false)$
This is a pandoc template and should not be edited.
$endif$
\documentclass[$if(fontsize)$$fontsize$,$endif$$if(lang)$$lang$,$endif$$if(papersize)$$papersize$,$endif$$for(classoption)$$classoption$$sep$,$endfor$]{$documentclass$}
\usepackage{pandoc/iclc}
$if(linestretch)$
\usepackage{setspace}
\setstretch{$linestretch$}
$endif$
\usepackage{amssymb,amsmath}
\usepackage{ifxetex,ifluatex}
\usepackage{fixltx2e} % provides \textsubscript
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
$if(fontfamily)$
\usepackage{$fontfamily$}
\usepackage{inconsolata}
$else$
\usepackage{lmodern}
$endif$
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
$if(euro)$
\usepackage{eurosym}
$endif$
\else % if luatex or xelatex
\ifxetex
\usepackage{mathspec}
\usepackage{xltxtra,xunicode}
\else
\usepackage{fontspec}
\fi
\defaultfontfeatures{Mapping=tex-text,Scale=MatchLowercase}
\newcommand{\euro}{€}
$if(mainfont)$
\setmainfont{$mainfont$}
$endif$
$if(sansfont)$
\setsansfont{$sansfont$}
$endif$
$if(monofont)$
\setmonofont[Mapping=tex-ansi]{$monofont$}
$endif$
$if(mathfont)$
\setmathfont(Digits,Latin,Greek){$mathfont$}
$endif$
\fi
% use upquote if available, for straight quotes in verbatim environments
\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
% use microtype if available
\IfFileExists{microtype.sty}{%
\usepackage{microtype}
\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
}{}
$if(geometry)$
\usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry}
$endif$
$if(lang)$
\ifxetex
\usepackage{polyglossia}
\setmainlanguage{$mainlang$}
\else
\usepackage[shorthands=off,$lang$]{babel}
\fi
$endif$
$if(natbib)$
\usepackage{natbib}
\bibliographystyle{$if(biblio-style)$$biblio-style$$else$plainnat$endif$}
$endif$
$if(biblatex)$
\usepackage{biblatex}
$if(biblio-files)$
\bibliography{$biblio-files$}
$endif$
$endif$
$if(listings)$
\usepackage{listings}
$endif$
$if(lhs)$
\lstnewenvironment{code}{\lstset{language=Haskell,basicstyle=\small\ttfamily}}{}
$endif$
$if(highlighting-macros)$
$highlighting-macros$
$endif$
$if(verbatim-in-note)$
\usepackage{fancyvrb}
\VerbatimFootnotes
$endif$
$if(tables)$
\usepackage{longtable,booktabs}
$endif$
$if(csl-refs)$
\newlength{\cslhangindent}
\setlength{\cslhangindent}{1.5em}
\newenvironment{CSLReferences}%
{$if(csl-hanging-indent)$\setlength{\parindent}{0pt}%
\everypar{\setlength{\hangindent}{\cslhangindent}}\ignorespaces$endif$}%
{\par}
$endif$
$if(graphics)$
\usepackage{graphicx}
\makeatletter
\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}
\def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi}
\makeatother
% Scale images if necessary, so that they will not overflow the page
% margins by default, and it is still possible to overwrite the defaults
% using explicit options in \includegraphics[width, height, ...]{}
\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio}
$endif$
\ifxetex
\usepackage[setpagesize=false, % page size defined by xetex
unicode=false, % unicode breaks when used with xetex
xetex]{hyperref}
\else
\usepackage[unicode=true]{hyperref}
\fi
\hypersetup{breaklinks=true,
bookmarks=true,
pdfauthor={$author-meta$},
pdftitle={$title-meta$},
colorlinks=true,
citecolor=$if(citecolor)$$citecolor$$else$blue$endif$,
urlcolor=$if(urlcolor)$$urlcolor$$else$blue$endif$,
linkcolor=$if(linkcolor)$$linkcolor$$else$magenta$endif$,
pdfborder={0 0 0}}
\urlstyle{same} % don't use monospace font for urls
$if(links-as-notes)$
% Make links footnotes instead of hotlinks:
\renewcommand{\href}[2]{#2\footnote{\url{#1}}}
$endif$
$if(strikeout)$
\usepackage[normalem]{ulem}
% avoid problems with \sout in headers with hyperref:
\pdfstringdefDisableCommands{\renewcommand{\sout}{}}
$endif$
\setlength{\parindent}{0pt}
\setlength{\parskip}{6pt plus 2pt minus 1pt}
\setlength{\emergencystretch}{3em} % prevent overfull lines
\providecommand{\tightlist}{%/
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
$if(numbersections)$
\setcounter{secnumdepth}{5}
$else$
\setcounter{secnumdepth}{0}
$endif$
$if(verbatim-in-note)$
\VerbatimFootnotes % allows verbatim text in footnotes
$endif$
$if(title)$
\title{$title$$if(subtitle)$\\\vspace{0.5em}{\large $subtitle$}$endif$}
$endif$
$if(author)$
\author{
$for(author)$
$author.name$ \\
$author.affiliation$\\
\href{mailto:$author.email$}{$author.email$}
$sep$ \and
$endfor$
}
$endif$
\date{$date$}
$for(header-includes)$
$header-includes$
$endfor$
\begin{document}
$if(title)$
\maketitle
$endif$
\begin{abstract}
$if(abstract)$
$abstract$
$else$
Please provide an abstract in the metadata block at the top of the
markdown document. Refer to template.txt for details. $endif$
\end{abstract}
$for(include-before)$
$include-before$
$endfor$
$if(toc)$
{
\hypersetup{linkcolor=black}
\setcounter{tocdepth}{$toc-depth$}
\tableofcontents
}
$endif$
$if(lot)$
\listoftables
$endif$
$if(lof)$
\listoffigures
$endif$
$body$
$if(natbib)$
$if(biblio-files)$
$if(biblio-title)$
$if(book-class)$
\renewcommand\bibname{$biblio-title$}
$else$
\renewcommand\refname{$biblio-title$}
$endif$
$endif$
\bibliography{$biblio-files$}
$endif$
$endif$
$if(biblatex)$
\printbibliography$if(biblio-title)$[title=$biblio-title$]$endif$
$endif$
$for(include-after)$
$include-after$
$endfor$
\end{document}
+54
View File
@@ -0,0 +1,54 @@
\def\Hline{\noalign{\hrule height 0.4mm}}
%\newcommand{\bm}[1]{\mbox{\boldmath{$#1$}}}
\newcommand{\figbox}[1]{\fbox{\parbox{\columnwidth}{\centering{ #1 }}}}
\newcommand{\range}[2]{{#1,\cdots,#2\;}}
\newcommand{\secref}[1]{\mbox{Section~\ref{#1}}}
\newcommand{\tabref}[1]{\mbox{Table~\ref{#1}}}
\newcommand{\figref}[1]{\mbox{Figure~\ref{#1}}}
\newcommand{\eqnref}[1]{\mbox{Eq.~(\ref{#1})}}
\renewcommand{\sfdefault}{phv}
\renewcommand{\rmdefault}{ptm}
\renewcommand{\ttdefault}{pcr}
\setlength{\paperheight}{297mm}
\setlength{\paperwidth}{210mm}
\setlength{\textheight}{252mm}
\setlength{\textwidth}{172mm}
\setlength{\columnsep}{8mm}
\setlength{\headheight}{0mm}
\setlength{\voffset}{-12mm}
\setlength{\hoffset}{0mm}
\setlength{\marginparwidth}{0mm}
\setlength{\parindent}{2mm} %1pc
\setlength{\topmargin}{-5mm}
\setlength{\oddsidemargin}{-6mm}
\setlength{\evensidemargin}{-6mm}
\setlength\normallineskip{1\p@}
\setlength\parskip{0\p@ \@plus \p@}
%\def\baselinestretch{0.98}
\def\normalsize{\@setsize\normalsize{12pt}\xpt\@xpt}
\def\small{\@setsize\small{10pt}\ixpt\@ixpt}
\def\footnotesize{\@setsize\footnotesize{8pt}\viiipt\@viiipt}
\def\scriptsize{\@setsize\scriptsize{8pt}\viipt\@viipt}
\def\tiny{\@setsize\tiny{7pt}\vipt\@vipt}
\def\large{\@setsize\large{14pt}\xiipt\@xiipt}
\def\Large{\@setsize\Large{16pt}\xivpt\@xivpt}
\def\LARGE{\@setsize\LARGE{20pt}\xviipt\@xviipt}
\def\huge{\@setsize\huge{23pt}\xxpt\@xxpt}
\def\Huge{\@setsize\Huge{28pt}\xxvpt\@xxvpt}
\pagestyle{empty}
\def\abstract{
\begin{center}{
\bf ABSTRACT
}
\end{center}
}
\def\endabstract{\par}
\flushbottom
-27
View File
@@ -1,27 +0,0 @@
# 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?
oldtunes.mjs
public/samples/EMU World/
-26
View File
@@ -1,26 +0,0 @@
# Strudel REPL
This is the REPL for Strudel. REPL stands for
- Read
- Evaluate
- Play!
- Loop
The REPL is deployed at [strudel.tidalcycles.org](https://strudel.tidalcycles.org/).
## Run REPL locally
```bash
# from project root
npm run setup
npm run repl
```
## Build REPL
```bash
cd repl
npm run build # <- builds repl + tutorial to ../docs
npm run static # <- test static build
```
-18
View File
@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/src/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Strudel REPL" />
<!-- TODO: add manifest images -->
<!-- <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> -->
<title>Strudel REPL</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
-4463
View File
File diff suppressed because it is too large Load Diff
-36
View File
@@ -1,36 +0,0 @@
{
"name": "@strudel.cycles/repl",
"private": true,
"version": "0.0.0",
"scripts": {
"render-jsdoc": "cd ${PWD}/../tutorial/ && npm run render",
"predev": "npm run render-jsdoc",
"prebuild": "npm run render-jsdoc",
"dev": "vite --host",
"start": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run --reporter verbose -v --no-isolate",
"add-license": "cat etc/agpl-header.txt ../docs/static/js/*LICENSE.txt > /tmp/strudel-license.txt && cp /tmp/strudel-license.txt ../docs/static/js/*LICENSE.txt",
"predeploy": "npm run build",
"deploy": "gh-pages -d ../docs",
"static": "npx serve ../docs",
"dbdump": "node src/test/dbdump.js > src/test/dbdump.json"
},
"dependencies": {
"@heroicons/react": "^2.0.13",
"@supabase/supabase-js": "^1.35.3",
"nanoid": "^4.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.2",
"@vitejs/plugin-react": "^1.3.0",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.13",
"rollup-plugin-visualizer": "^5.8.1",
"tailwindcss": "^3.0.24",
"vite": "^3.2.2"
}
}
-12
View File
@@ -1,12 +0,0 @@
/*
postcss.config.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/postcss.config.js>
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/>.
*/
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
View File
-11
View File
@@ -1,11 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
-11
View File
@@ -1,11 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root'),
);
-33
View File
@@ -1,33 +0,0 @@
import { Pattern, toMidi, valueToMidi } from '@strudel.cycles/core';
import { samples } from '@strudel.cycles/webaudio';
export async function prebake({ isMock = false, baseDir = '.' } = {}) {
if (!isMock) {
// https://archive.org/details/SalamanderGrandPianoV3
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
return await Promise.all([
samples('piano.json', `${baseDir}/piano/`),
// https://github.com/sgossner/VCSL/
// https://api.github.com/repositories/126427031/contents/
// LICENSE: CC0 general-purpose
samples('vcsl.json', 'github:sgossner/VCSL/master/'),
samples('tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/'),
samples('EmuSP12.json', `${baseDir}/EmuSP12/`),
]);
}
}
const maxPan = toMidi('C8');
const panwidth = (pan, width) => pan * width + (1 - width) / 2;
Pattern.prototype.piano = function () {
return this.clip(1)
.s('piano')
.release(0.1)
.fmap((value) => {
const midi = valueToMidi(value);
// pan by pitch
const pan = panwidth(Math.min(Math.round(midi) / maxPan, 1), 0.5);
return { ...value, pan: (value.pan || 1) * pan };
});
};
-11
View File
@@ -1,11 +0,0 @@
// this script will render all example tunes and log them to the console.
// it is intended to be written to tunes.snapshot.mjs using `npm run snapshot`
import * as tunes from './tunes.mjs';
import { queryCode, testCycles } from './runtime.mjs';
Object.entries(tunes).forEach(([key, code]) => {
queryCode(code, testCycles[key] || 1).then((haps) => {
console.log(`export const ${key} = ${JSON.stringify(haps)}`);
});
});
-63
View File
@@ -1,63 +0,0 @@
/*
static.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/static.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Tone } from '@strudel.cycles/tone';
import { State, TimeSpan } from '@strudel.cycles/core';
import { evaluate } from '@strudel.cycles/eval';
import { webaudioOutputTrigger } from '@strudel.cycles/webaudio';
// this is a test to play back events with as less runtime code as possible..
// the code asks for the number of seconds to prequery
// after the querying is done, the events are scheduled
// after the scheduling is done, the transport is started
// nothing happens while tone.js runs except the schedule callback, which is a thin wrapper around triggerAttackRelease calls
// so all glitches that appear here should have nothing to do with strudel and or the repl
async function playStatic(code) {
Tone.getTransport().cancel();
Tone.getTransport().stop();
let start, took;
const seconds = Number(prompt('How many seconds to run?')) || 60;
start = performance.now();
console.log('evaluating..');
const { pattern: pat } = await evaluate(code);
took = performance.now() - start;
console.log('evaluate took', took, 'ms');
console.log('querying..');
start = performance.now();
const events = pat
?.query(new State(new TimeSpan(0, seconds)))
?.filter((event) => event.part.begin.equals(event.whole.begin))
?.map((event) => ({
time: event.whole.begin.valueOf(),
duration: event.whole.end.sub(event.whole.begin).valueOf(),
value: event.value,
context: event.context,
}));
took = performance.now() - start;
console.log('query took', took, 'ms');
console.log('scheduling..');
start = performance.now();
events.forEach((event) => {
Tone.getTransport().schedule((time) => {
try {
const { onTrigger = webaudioOutputTrigger } = event.context;
onTrigger(time, event);
} catch (err) {
console.warn(err);
err.message = 'unplayable event: ' + err?.message;
console.error(err);
}
}, event.time);
});
took = performance.now() - start;
console.log('scheduling took', took, 'ms');
console.log('now starting!');
Tone.getTransport().start('+0.5');
}
export default playStatic;
-29
View File
@@ -1,29 +0,0 @@
/*
tailwind.config.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/tailwind.config.js>
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/>.
*/
module.exports = {
// TODO: find out if leaving out tutorial path works now
content: ['./src/**/*.{js,jsx,ts,tsx}', './tutorial/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
colors: {
primary: '#c792ea',
secondary: '#c3e88d',
tertiary: '#82aaff',
highlight: '#ffcc00',
linegray: '#8a91991a',
lineblack: '#00000095',
bg: '#222222',
// header: '#8a91991a',
// footer: '#8a91991a',
header: '#00000050',
// header: 'transparent',
footer: '#00000050',
},
},
},
plugins: [require('@tailwindcss/typography')],
};
-15
View File
@@ -1,15 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
build: {
outDir: '../out',
sourcemap: true,
rollupOptions: {
plugins: [visualizer({ template: 'treemap' })],
},
},
});
@@ -1,6 +1,6 @@
import { queryCode } from '../../repl/src/runtime.mjs';
import { queryCode } from './runtime.mjs';
import { describe, it } from 'vitest';
import doc from '../../doc.json';
import doc from '../doc.json';
describe('runs examples', () => {
const { docs } = doc;
+2 -5
View File
@@ -30,8 +30,7 @@ import '@strudel.cycles/xen/xen.mjs';
// import '@strudel.cycles/webaudio/webaudio.mjs';
// import '@strudel.cycles/serial/serial.mjs';
// import controls from '@strudel.cycles/core/controls.mjs';
import { prebake } from './prebake.mjs';
import '../website/src/repl/prebake';
class MockedNode {
chain() {
@@ -117,7 +116,7 @@ strudel.Pattern.prototype.filter = function () {
strudel.Pattern.prototype.adsr = function () {
return this;
};
strudel.Pattern.prototype.out = function () {
strudel.Pattern.prototype.webaudio = function () {
return this;
};
strudel.Pattern.prototype.soundfont = function () {
@@ -136,8 +135,6 @@ const uiHelpersMocked = {
backgroundImage: id,
};
prebake({ isMock: true });
const canvasCtx = {
clearRect: () => {},
fillText: () => {},
@@ -1,5 +1,5 @@
import { queryCode, testCycles } from '../runtime.mjs';
import * as tunes from '../tunes.mjs';
import { queryCode, testCycles } from './runtime.mjs';
import * as tunes from '../website/src/repl/tunes.mjs';
import { describe, it } from 'vitest';
const tuneKeys = Object.keys(tunes);
-26
View File
@@ -1,26 +0,0 @@
import { evalScope, controls } from '@strudel.cycles/core';
import { MiniRepl as _MiniRepl } from '@strudel.cycles/react';
import { samples } from '@strudel.cycles/webaudio';
import { prebake } from '../repl/src/prebake.mjs';
fetch('https://strudel.tidalcycles.org/EmuSP12.json')
.then((res) => res.json())
.then((json) => samples(json, 'https://strudel.tidalcycles.org/EmuSP12/'));
evalScope(
controls,
import('@strudel.cycles/core'),
// import('@strudel.cycles/tone'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
);
// prebake();
export function MiniRepl({ tune }) {
return <_MiniRepl tune={tune} hideOutsideView={true} />;
}
-41
View File
@@ -1,41 +0,0 @@
/*
Tutorial.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/tutorial/Tutorial.js>
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 React from 'react';
import ReactDOM from 'react-dom';
import Tutorial from './tutorial.rendered.mdx';
// import ApiDoc from './ApiDoc';
import './style.scss';
import '@strudel.cycles/react/dist/style.css';
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
initAudioOnFirstClick();
ReactDOM.render(
<React.StrictMode>
<div className="min-h-screen bg-slate-900">
<header className="flex-none flex justify-start sticky top-0 z-[2] w-full h-16 px-2 items-center border-b border-slate-500 text-white bg-slate-900 z-[100]">
<div className="p-4 w-full flex justify-between items-center">
<div className="flex items-center space-x-2">
<img src={'https://tidalcycles.org/img/logo.svg'} className="Tidal-logo w-10 h-10" alt="logo" />
<h1 className="text-xl cursor-pointer" onClick={() => window.scrollTo(0, 0)}>
Strudel Tutorial
</h1>
</div>
{!window.location.href.includes('localhost') && (
<div className="flex space-x-4 text-slate-200">
<a href="../">go to REPL</a>
</div>
)}
</div>
</header>
<main className="p-4 pl-6 max-w-3xl prose prose-invert">
<Tutorial />
</main>
</div>
</React.StrictMode>,
document.getElementById('root'),
);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

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