Compare commits
761 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cbe7aaacfb | |||
| 35014b5190 | |||
| 9ff0449ca3 | |||
| 8398385dcd | |||
| 629c2b9301 | |||
| 5cc93996ce | |||
| 7cef558440 | |||
| c261fb6dd6 | |||
| 28056deb66 | |||
| 7071dec775 | |||
| 3d55567445 | |||
| 39e2221c29 | |||
| b8ce920f50 | |||
| ccb5ec1685 | |||
| b09230fe90 | |||
| 8f7b439dce | |||
| c823a37c93 | |||
| 0c9f535ff5 | |||
| fb5884ab81 | |||
| ea91d30ce4 | |||
| f0bd5bffaa | |||
| ac37b6a029 | |||
| a986688874 | |||
| 4ab8c256b2 | |||
| 06ea5a36e2 | |||
| 98f5a56ef6 | |||
| 46af6ed9ef | |||
| 752940b3ab | |||
| c727534d29 | |||
| 20e5fdedfb | |||
| 188f0e9098 | |||
| c2a5562bad | |||
| 57f2a97b7b | |||
| 72423c3a25 | |||
| 105db2a4ca | |||
| be61d08cc6 | |||
| 6e58c973af | |||
| 2473a5391d | |||
| 4e17cfbdd6 | |||
| 83c7e63432 | |||
| 909e0154fe | |||
| ef4e21ac40 | |||
| 857c82e260 | |||
| ba93bd1bc5 | |||
| 3c1a8c8bdb | |||
| b7e941c649 | |||
| ded91fab7d | |||
| f40d079543 | |||
| d5833dd475 | |||
| e3658ec30e | |||
| 72eaf96b4f | |||
| 9c5c71c31a | |||
| af53bab259 | |||
| 0065db8569 | |||
| 7eebf3a466 | |||
| 6f703e48e7 | |||
| 64f16c4f33 | |||
| ffbbc43c89 | |||
| 086596c47c | |||
| fdde05e751 | |||
| 694162a7b1 | |||
| cbcb25b063 | |||
| 1ae773acc4 | |||
| 53c504f260 | |||
| 0d08225983 | |||
| 00872bd1de | |||
| 120f89d57f | |||
| d496919fe5 | |||
| f369de13d7 | |||
| 908a703b7f | |||
| 99e14dae5c | |||
| ac582b4d40 | |||
| b7d98dd370 | |||
| 0ecacae71e | |||
| 8cd497abb2 | |||
| 070055d2ef | |||
| e500ebe268 | |||
| 72298c5f03 | |||
| c885d84785 | |||
| a14bf4d97b | |||
| c5f1085bd9 | |||
| ccbf28f2db | |||
| 3a7ec18d46 | |||
| a2b8407fbb | |||
| ac1fd88d8d | |||
| 9f8143e062 | |||
| cdc5e5f3f3 | |||
| bad498a1e5 | |||
| 4de0c11f8c | |||
| 97af50222f | |||
| f360d394a7 | |||
| 48718daea8 | |||
| 8d45016ccf | |||
| ec69f6fd77 | |||
| d94f145649 | |||
| bf3fe605d9 | |||
| 54ebc97ddc | |||
| c689874bb4 | |||
| 23d4bfa9d9 | |||
| 91eb50f1c1 | |||
| 5838ade740 | |||
| 917d01c47d | |||
| 0a51718df3 | |||
| 774372a339 | |||
| 626a99ba5b | |||
| 8c3ee6db6b | |||
| c5c05ebadc | |||
| a8918d55fb | |||
| 07e3ddbe40 | |||
| a9957b45e5 | |||
| 4bd103e612 | |||
| 466234bad6 | |||
| 97beaec25a | |||
| 2b3b638943 | |||
| e1629e8383 | |||
| 266cb614a8 | |||
| dfc4a0818c | |||
| 236ff577a6 | |||
| 66a32ac45d | |||
| 8b2c35b7a3 | |||
| f8f42565ae | |||
| 96981c3c1d | |||
| bd8d207a3d | |||
| e3741ae8fa | |||
| ef87f65bef | |||
| b3537a9acb | |||
| d3d9f23c3c | |||
| c474b8c92e | |||
| da81ba00fb | |||
| f0669ce3ba | |||
| 28c94efaa9 | |||
| 0633954e24 | |||
| cedf09168f | |||
| 3373225f53 | |||
| 1eb9dc73fa | |||
| fdcdc3aaa0 | |||
| da926805b3 | |||
| 82893ffc22 | |||
| beafd9c700 | |||
| c5bd2a7487 | |||
| b4d57f12fe | |||
| 118086aff5 | |||
| 452827630b | |||
| 86248328cc | |||
| 8833d623f7 | |||
| 3fff4609a0 | |||
| 98a1573e27 | |||
| 7080490c68 | |||
| e2b5b92a0c | |||
| 1c3e07afd3 | |||
| c54fa7d266 | |||
| a25f763796 | |||
| 91ad3d729f | |||
| f51ad2cb7e | |||
| 4cb0488839 | |||
| 663229ecde | |||
| cdb623cb66 | |||
| 28d7e0e489 | |||
| aba594b72a | |||
| 5d7c6c3e4b | |||
| 820744dc27 | |||
| 5445f0812f | |||
| 49868fc119 | |||
| a68c784753 | |||
| f2b2f9f959 | |||
| 53dd53553a | |||
| 89b7eac789 | |||
| 9cdba1a50e | |||
| 98b7003504 | |||
| cb2dc6304f | |||
| edb6c0db2e | |||
| a46a055246 | |||
| 9a8f8a051c | |||
| 3eb40ee7d3 | |||
| 4cc453c640 | |||
| 74e27ca94f | |||
| 7dfd7dbcdc | |||
| 2cba1dcb31 | |||
| 48a6a12648 | |||
| 71dd45cd4b | |||
| 6bbf7cbe32 | |||
| 9f36b73081 | |||
| 6ebe84e096 | |||
| a7e485992e | |||
| cb011c8bb0 | |||
| c8875b29c1 | |||
| 978616833a | |||
| ab74ccfe7c | |||
| 92b2013cf6 | |||
| 2e97fb7be2 | |||
| df1934f87b | |||
| d3472feb5c | |||
| addb6db1c7 | |||
| 566d590453 | |||
| e57bd98da0 | |||
| f5c4373f99 | |||
| e762291bb7 | |||
| 9da027d377 | |||
| 96dccdc38b | |||
| d6254294b0 | |||
| 339c1bb573 | |||
| 9782795761 | |||
| 2d2b238da9 | |||
| f0839e02eb | |||
| a9f7d825c8 | |||
| 709b654ed3 | |||
| c265595536 | |||
| 1670aebad8 | |||
| 39460eb100 | |||
| cd659ac2a1 | |||
| ed182d4b3f | |||
| 90614612e2 | |||
| 6b6d4b86a6 | |||
| 0b67473bd3 | |||
| d87ce960d2 | |||
| 69b034349a | |||
| ed00686804 | |||
| e10c469749 | |||
| 5f7e10733a | |||
| d81a214c74 | |||
| e57f240e70 | |||
| e18296e760 | |||
| c145355ccc | |||
| 352a3c39d7 | |||
| c85a4d4fc1 | |||
| 7676641c9e | |||
| 260d2e499e | |||
| a46617f3f0 | |||
| d164b5e7f5 | |||
| c0bf86d11e | |||
| 6f06683d70 | |||
| e3409e52d0 | |||
| 41d6f1b5da | |||
| aca4601734 | |||
| 2a00fcdf5d | |||
| 9200a67a60 | |||
| 80fe5b8140 | |||
| 085e839bfe | |||
| 7ff7e8852b | |||
| 637d714ab2 | |||
| c42d00f048 | |||
| 702e558d1a | |||
| bcc2e8f759 | |||
| 27339715b7 | |||
| 5e2e6411e5 | |||
| 9848740e18 | |||
| 51b150b829 | |||
| 0fac16e769 | |||
| d0ce82e3cd | |||
| e82629de2e | |||
| 38ce593c60 | |||
| 8a91549a7b | |||
| cc27d482cd | |||
| 37e640470b | |||
| 5e5e7730c1 | |||
| 89e7d52308 | |||
| 99bb227cf4 | |||
| 70e776e799 | |||
| 66aad0004a | |||
| a968545458 | |||
| 5414bbe85d | |||
| e7e80bfd83 | |||
| c57f5bb429 | |||
| f2608e712b | |||
| 34b41fc26f | |||
| f6e171fed8 | |||
| 2511dcc09e | |||
| 798eb22d9a | |||
| 977420e74d | |||
| 9c108862ef | |||
| 2fbdc9fdd7 | |||
| 0a96fa5896 | |||
| 46a45b4596 | |||
| 1205573bc6 | |||
| 9b1c23d49f | |||
| 42ab65abdb | |||
| bb982e6b9b | |||
| 3e0e903d38 | |||
| 591c3fe08f | |||
| 56b1ea3fa5 | |||
| c199b51645 | |||
| 8d2a368da9 | |||
| 052d09e892 | |||
| 439a7dc5e6 | |||
| 06ebcbff8c | |||
| 4e2e790864 | |||
| 4f3b40bd7b | |||
| 142160d79a | |||
| ff2c59a2a6 | |||
| 9b9176325b | |||
| b8e1b8d7ac | |||
| e84c39837f | |||
| 6527c68cda | |||
| 6a09f54b25 | |||
| dc6b766ab7 | |||
| 9eb3c9410d | |||
| 189e650a73 | |||
| fe46e1da53 | |||
| d1869c18ba | |||
| 02cd79a6d9 | |||
| a5886bb9d4 | |||
| 877bc95a58 | |||
| 18b1739b89 | |||
| 022ac95bbd | |||
| b3e25cfc52 | |||
| 3d25aa1b91 | |||
| b54139376e | |||
| d64a0ef0eb | |||
| 659071a4ee | |||
| 64f7bc4442 | |||
| 8c97f2e2d0 | |||
| b8c98736c5 | |||
| b8c46d6b26 | |||
| 79453ac2c3 | |||
| b08890bd9e | |||
| 938c92fda2 | |||
| a0fc52b1ec | |||
| 9a9fe83f9c | |||
| 0693a32736 | |||
| 42a903ecc0 | |||
| da283eb55a | |||
| b12707316a | |||
| 66aa3ac1da | |||
| bf5d9917ab | |||
| 4f15d681b0 | |||
| d81b1ab65c | |||
| 8465517c76 | |||
| 215ab87809 | |||
| e5fe998327 | |||
| 6f75a0a62b | |||
| 0464845262 | |||
| e490774294 | |||
| 34e8a57472 | |||
| 59c8d70714 | |||
| ce7cff2c3b | |||
| 32fe73aba2 | |||
| d6ee10e05c | |||
| 36441e7b73 | |||
| 189daa3942 | |||
| d009b9592e | |||
| f7f1bd63e8 | |||
| 7d24e45692 | |||
| cf55d4c8d1 | |||
| f17a4d045f | |||
| e7e636886d | |||
| 8e1e83f3cf | |||
| 2d64b538e7 | |||
| b717615558 | |||
| e9aef9e4d4 | |||
| 8c4f10464b | |||
| 018649c4f3 | |||
| 5ff59a878d | |||
| a20468b85f | |||
| 9648e35de2 | |||
| cd344cefe0 | |||
| 1f6a313f02 | |||
| 5cbeb227d8 | |||
| c8c6a2ce08 | |||
| 2ab2cc5fc8 | |||
| 3dd667e825 | |||
| 482ebb695a | |||
| 218952b8b6 | |||
| 2e8a8b50df | |||
| 612efad3eb | |||
| e7839a09a1 | |||
| 3292f88810 | |||
| a4c040e101 | |||
| 7046c1f546 | |||
| a4ab8e1b06 | |||
| f22f89347e | |||
| de15d79edf | |||
| 23e5358ae4 | |||
| 3b6777e259 | |||
| ed5bf3b3fc | |||
| b089328da3 | |||
| 784c711870 | |||
| 34b10b6c84 | |||
| 450f9685f3 | |||
| 03090e399c | |||
| ed58d26da6 | |||
| e3605590e5 | |||
| d55ef8e843 | |||
| efc2333360 | |||
| 92626e7c20 | |||
| ad113b3888 | |||
| 39c45bc2b2 | |||
| 899af88bd9 | |||
| c2b8cc4b82 | |||
| 40ed7b5382 | |||
| 5d829419ba | |||
| e26da854c0 | |||
| a28b7a2d5a | |||
| 83c776d70f | |||
| 051efdc13b | |||
| ef3cd56b0d | |||
| 9017085fa3 | |||
| cdbbe079db | |||
| d6045c943b | |||
| 6ee9dea2ff | |||
| 03cbe4b6d4 | |||
| 784ee576da | |||
| ee3e1217b4 | |||
| 30c80119e9 | |||
| 973ae4c7da | |||
| 4d13b981f1 | |||
| 7691403fd0 | |||
| 3b6e3624be | |||
| 51f0bab1f8 | |||
| dae1560c40 | |||
| 82f0dbb750 | |||
| fb7d76a2ab | |||
| 26225e4ea7 | |||
| ab12c6ae0a | |||
| 4a894b0be2 | |||
| 4ad305550e | |||
| cf5c502a05 | |||
| 2415210be6 | |||
| fe18aa770d | |||
| 2eda047108 | |||
| c8cf1dc712 | |||
| e136e95375 | |||
| a26559fd3d | |||
| 004c0baa59 | |||
| 5f44ae9c4b | |||
| 66828e17d3 | |||
| b2f90ecb3b | |||
| ccf2a6de52 | |||
| 9416f317d4 | |||
| 75b6f59212 | |||
| 84238c8999 | |||
| 2d855cf5e8 | |||
| dd04d49c75 | |||
| 34874862ab | |||
| 5448fc6077 | |||
| 06fafbd8eb | |||
| 10156f0067 | |||
| 5632d83afb | |||
| a32473d570 | |||
| 0361bc629f | |||
| 1fdd87a4f3 | |||
| 16e9aca222 | |||
| 4a3d5ca44f | |||
| 6c0e8a0833 | |||
| 7336ff0ded | |||
| 735f57e276 | |||
| eb4f1f69f6 | |||
| e7e321fe03 | |||
| bf3de368f0 | |||
| d645cc6fe5 | |||
| 6fe11be218 | |||
| cf7fc23981 | |||
| bfdfab0a00 | |||
| edb347e85a | |||
| 63fcce79e0 | |||
| 8defdc424c | |||
| 16b2fc0632 | |||
| d0a544d7a7 | |||
| 9b3c6b9f94 | |||
| 6327e17feb | |||
| 15fe2734e9 | |||
| 1dd1ce254f | |||
| 3ea91adfd6 | |||
| ce5f425142 | |||
| 671a22fc22 | |||
| 976955dae3 | |||
| a6ad58a375 | |||
| 681eed3d0e | |||
| 81a9811803 | |||
| 72b582c605 | |||
| 2f0891d4e5 | |||
| a7a283a99e | |||
| d9e44dcda6 | |||
| e87bb4376f | |||
| 0722cf7ded | |||
| 37fde8f3e8 | |||
| acd5661106 | |||
| b0b2b6a120 | |||
| 664d76cf1b | |||
| a901e2e387 | |||
| 82b14b5292 | |||
| 05bcd5e32f | |||
| 077556e7cb | |||
| 4cc2de9b7e | |||
| 8655c5b73c | |||
| 31c1450edc | |||
| cbb4207bab | |||
| b0c0a393a1 | |||
| 168269a839 | |||
| 34771f03d4 | |||
| f5ae50c3ce | |||
| 504fe9ed40 | |||
| caf6686d01 | |||
| c063fb6b1c | |||
| e3680b96de | |||
| 688e275e0c | |||
| 2e33569786 | |||
| 46fe6d3c4d | |||
| 9220b95677 | |||
| 5b27b19972 | |||
| 5a72ea94b1 | |||
| eac6f27ca4 | |||
| f2330a2307 | |||
| db413881db | |||
| 77bab433e0 | |||
| f8cc1c2022 | |||
| e0bf9ea4d6 | |||
| 374661eb23 | |||
| df06248c54 | |||
| 27073d6c17 | |||
| f44caf9096 | |||
| ff5b11f5ed | |||
| 719c70a598 | |||
| 7a53f6c021 | |||
| 09d05abd70 | |||
| 3acc364a03 | |||
| 2dd528134a | |||
| fef50e654c | |||
| b1aba294ad | |||
| 63ac80eb79 | |||
| 82e3f9b837 | |||
| e19fd24447 | |||
| fe9a91d1e4 | |||
| 860bb58d61 | |||
| d58b3e490b | |||
| 0003ce174e | |||
| 8e0f23e081 | |||
| 71e60ed8de | |||
| e2a29914c7 | |||
| a9a3d491a2 | |||
| a750764f88 | |||
| 5b397009ab | |||
| e68df9e789 | |||
| 4d3bbd5057 | |||
| 15f3321b04 | |||
| 67bfedb317 | |||
| c0184dd827 | |||
| 09fdc45ff2 | |||
| c01b4c651c | |||
| 634eed1d82 | |||
| 01810f379f | |||
| f3c4afaa54 | |||
| a8757fecc8 | |||
| a1d181d609 | |||
| 440f1cb081 | |||
| 95193b0bfd | |||
| 66673d211e | |||
| 64ab3c9ba7 | |||
| 6c4aa12c60 | |||
| a8f2537e31 | |||
| 7e6876c282 | |||
| 1cc15c7e8d | |||
| 10ac7c353c | |||
| 9412bf60bf | |||
| c751bb7e5f | |||
| 28cd7c1681 | |||
| 011139eff8 | |||
| 184cb746f3 | |||
| 5efcd14f40 | |||
| 351314ccdf | |||
| e52017c0d8 | |||
| b253d437d3 | |||
| bebbe30b50 | |||
| 489d3f8d4d | |||
| cc4cad05b2 | |||
| 2cb22e94b5 | |||
| ee7d783099 | |||
| 9becd0a3f0 | |||
| 9738c90b82 | |||
| f7b58f2e3e | |||
| d856e567c1 | |||
| a7f044e1aa | |||
| 1f7d78c6ce | |||
| 4137f39495 | |||
| 0163c8ba25 | |||
| 912b3270aa | |||
| 2bc3d69fb0 | |||
| 8051e56d13 | |||
| aa20526963 | |||
| 47de9e45ff | |||
| 0c193238c5 | |||
| 3428e18e7d | |||
| 8fba92f447 | |||
| 38e7fe606c | |||
| 59e7584bf7 | |||
| 7f50bcebd2 | |||
| d7b83e200c | |||
| db33707e52 | |||
| 84efa664ca | |||
| 94746b5fae | |||
| 239987af0b | |||
| 8363ba5a41 | |||
| 30f7127a92 | |||
| 14193abe03 | |||
| 3caf08e199 | |||
| 5cbd38a3e8 | |||
| bbd7ed0f27 | |||
| 43504d16b6 | |||
| 1b7fbecf50 | |||
| 7e8206dcbc | |||
| 3963b807eb | |||
| e99adffc56 | |||
| c9f494d865 | |||
| 117cf7e18a | |||
| e8254735bb | |||
| 3d30007756 | |||
| aeaed50446 | |||
| ae15bb7274 | |||
| 3ac0fdf619 | |||
| d1b6b6be85 | |||
| 1081fc69e9 | |||
| 73ef43dd2c | |||
| 181e51ef0e | |||
| 1167fdb910 | |||
| ec9109dd89 | |||
| 55aef18851 | |||
| 356d4360ec | |||
| 0f2aa9569b | |||
| ac7ee376e5 | |||
| ab052ce1ac | |||
| 5988e3d3b4 | |||
| 2f2bf5bf29 | |||
| b000f2297d | |||
| 77463a199a | |||
| e54449f5f7 | |||
| d743570546 | |||
| 1dac69635d | |||
| 49f38aeddb | |||
| 43c05cb504 | |||
| ccc90d547c | |||
| 998a010247 | |||
| e4cd5dc552 | |||
| 6cf0b6f651 | |||
| 794bf86904 | |||
| 9ed690ce45 | |||
| 623bf93368 | |||
| 0cc18a6468 | |||
| 87598e10d1 | |||
| ccec7e725a | |||
| f0e6c5483c | |||
| 2cbd35f6d2 | |||
| 930dabe4d6 | |||
| ff4b7ca171 | |||
| 5129fa6677 | |||
| 5336569c25 | |||
| 483843bd0a | |||
| 1b3b07842e | |||
| 98efd72ab4 | |||
| 1eb5f6a995 | |||
| 9b10dc8535 | |||
| 2951a60fac | |||
| 94257e81ee | |||
| 006ce9d1da | |||
| 607a6121bc | |||
| 4208c79c09 | |||
| 7565354274 | |||
| ffe831d24b | |||
| 60dd8bd763 | |||
| 29e5833c8c | |||
| e3a6444f41 | |||
| 30fd4c5dec | |||
| 2da03ab46f | |||
| 243adda3dd | |||
| 7fc21d55d7 | |||
| d5ef686fe0 | |||
| d4733a30ab | |||
| 989bd0990e | |||
| 60d09adb44 | |||
| b2588e8c93 | |||
| 39d2784441 | |||
| aa70246afc | |||
| f6ffdd6ae6 | |||
| 077aac1888 | |||
| 94a3a60e49 | |||
| f2372e7a41 | |||
| a0fb8fb37f | |||
| b4027fd92a | |||
| c9cafa37fd | |||
| 2e017e46f9 | |||
| c9f58220a3 | |||
| e9de45993e | |||
| 6b2b8b5124 | |||
| 64a6dacc1e | |||
| 1738e4d538 | |||
| abc7a1e48d | |||
| 7716fdb98e | |||
| 2938bfd88f | |||
| 222e479e30 | |||
| 92b5b6538f | |||
| 3a19e23473 | |||
| dd5743ab8c | |||
| 51e3aa257c | |||
| 5eabcf0618 | |||
| 6d21456423 | |||
| bca16cdf99 | |||
| 492271d786 | |||
| f71db4aeed | |||
| 5d4ef46ac7 | |||
| bd93e44154 | |||
| 6fd89ddee7 | |||
| cf1f4ec35c | |||
| 12bb82d295 | |||
| c33cfc78c5 | |||
| ac6472a43a | |||
| 989fdfa20b | |||
| 05ae31838e | |||
| c691916cbf | |||
| e16295623d | |||
| 9626f3cc9a | |||
| 432e8dcccc | |||
| d2e93a99f1 | |||
| 3505732afa | |||
| 55a5f1d62a | |||
| 3657e2fd65 | |||
| 65a7b3030e | |||
| 208706fb52 | |||
| e04f250adb | |||
| 642ddcdb59 | |||
| 88ca54461e | |||
| 129fd7d6b0 | |||
| 7db52b3dc2 | |||
| 40f6489111 | |||
| 118b619b74 | |||
| 2dd445c102 | |||
| 3c3832dbce | |||
| 9b8761bc45 | |||
| 77ade0758e | |||
| 902012759a | |||
| ea61627963 | |||
| 95526ac99c | |||
| b0da353115 | |||
| fae59a6bcd | |||
| 11196fb1e6 | |||
| e782dc09dd | |||
| e3df504423 | |||
| b71b1354c3 | |||
| dbe3915368 | |||
| 62705da3de | |||
| 81ca199a96 | |||
| bee8c81254 | |||
| 8ad48e1633 | |||
| 4a181d5b33 | |||
| d5ab6b7211 | |||
| 4ba9d47bff | |||
| c6c5a3035c | |||
| e024ae2142 | |||
| 19df19375d | |||
| cba96049a3 | |||
| 1509ff1793 | |||
| 6a33032da7 | |||
| 6c05a4eb7b | |||
| 8d8b8ab99e | |||
| a4f10d5339 | |||
| 16472b59dd | |||
| 33a73c37bc | |||
| 4e6d39666a | |||
| a5b6022506 | |||
| 7df663e87d | |||
| 3159f66560 | |||
| 0d900c0134 | |||
| 66a27a609c | |||
| 496d60dc1c |
@@ -0,0 +1,37 @@
|
||||
name: Build and Deploy to beta (warm.strudel.cc)
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
# cache: "pnpm"
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Deploy
|
||||
run: |
|
||||
eval $(ssh-agent -s)
|
||||
echo "$SSH_PRIVATE_KEY" | ssh-add -
|
||||
apt update && apt install -y rsync
|
||||
mkdir ~/.ssh
|
||||
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
||||
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/warm.strudel.cc
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Build and Deploy to live (strudel.cc)
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
# cache: "pnpm"
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Deploy
|
||||
run: |
|
||||
eval $(ssh-agent -s)
|
||||
echo "$SSH_PRIVATE_KEY" | ssh-add -
|
||||
apt update && apt install -y rsync
|
||||
mkdir ~/.ssh
|
||||
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
||||
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
|
||||
@@ -11,13 +11,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: apt install ztd
|
||||
run: apt update && apt install -y zstd
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache: "pnpm"
|
||||
- run: pnpm install
|
||||
- run: pnpm run format-check
|
||||
- run: pnpm run lint
|
||||
@@ -1,49 +0,0 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
deployments: write
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v2
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
# Upload entire repository
|
||||
path: "./website/dist"
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -45,6 +45,20 @@ tidal-drum-machines
|
||||
webaudiofontdata
|
||||
src-tauri/target
|
||||
|
||||
|
||||
|
||||
### BEGIN Visual Studio Code ###
|
||||
# Blanket, recursive exclude for .vscode directory and files
|
||||
.vscode/**/*
|
||||
# Unexclude specific files and directories within .vscode
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
### END Visual Studio Code ###
|
||||
|
||||
|
||||
|
||||
# BEGIN JetBrains -> END JetBrains
|
||||
|
||||
# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
@@ -130,3 +144,4 @@ fabric.properties
|
||||
|
||||
samples/*
|
||||
!samples/README.md
|
||||
.idea/
|
||||
|
||||
@@ -17,7 +17,7 @@ git remote set-url origin git@codeberg.org:uzu/strudel.git
|
||||
|
||||
To get in touch with the contributors, either
|
||||
|
||||
- [join the Tidal Discord Channel](https://discord.gg/remJ6gQA) and go to the #strudel channel
|
||||
- [join the Tidal Discord Channel](https://discord.com/invite/HGEdXmRkzT) and go to the #strudel channel
|
||||
- Find related discussions on the [tidal club forum](https://club.tidalcycles.org/)
|
||||
|
||||
## Ask a Question
|
||||
@@ -150,6 +150,7 @@ Important: Always publish with `pnpm`, as `npm` does not support overriding main
|
||||
|
||||
|
||||
## useful commands
|
||||
|
||||
```sh
|
||||
#regenerate the test snapshots (ex: when updating or creating new pattern functions)
|
||||
pnpm snapshot
|
||||
@@ -160,6 +161,81 @@ pnpm run osc
|
||||
#build the standalone version
|
||||
pnpm tauri build
|
||||
```
|
||||
|
||||
## version tag patching
|
||||
|
||||
here's a little guide on how to patch patterns in the database to prevent breaking old patterns due to breaking changes in newer versions.
|
||||
|
||||
the general tactic is to use `// @version x.y` to tag a pattern with a specific strudel version. when a pattern is evaluated, this metadata will de-activate any breaking changes that came after the specified version.
|
||||
for example, in version 1.1, the default value for `fanchor` was changed from `0.5` to `0`.
|
||||
if play a pattern that was made before that change, sounds that use filter evenlopes can sound very different, so by adding `// @version 1.0` will make it sound like it used to.
|
||||
before releasing a new version with breaking changes, we can edit all patterns in the database, inserting the version tag they were created under:
|
||||
|
||||
as an example, to release version 1.2, do the following:
|
||||
|
||||
1. get date range
|
||||
|
||||
```sh
|
||||
# get date of last version:
|
||||
git log -1 --format=%aI @strudel/core@1.1.0
|
||||
# 2024-05-31T23:07:26+02:00
|
||||
|
||||
# get date of current version:
|
||||
git log -1 --format=%aI @strudel/core@1.2.0
|
||||
# 2025-05-01T12:39:24+02:00
|
||||
# might also use todays timestamp if version is not yet released
|
||||
```
|
||||
|
||||
now we know, all patterns between these 2 dates have to receive a version tag (unless they already have one).
|
||||
|
||||
2. get patterns in question
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM code_v1
|
||||
WHERE code NOT LIKE '%@version%'
|
||||
AND created_at > '2024-05-31T23:07:26+02:00'
|
||||
AND created_at < '2025-05-01T12:39:24+02:00'
|
||||
ORDER BY created_at ASC;
|
||||
```
|
||||
|
||||
this gives us all unversioned patterns that were saved between 1.1.0 and 1.2.0. in this case, it's 9373 patterns!
|
||||
|
||||
3. insert version tags
|
||||
|
||||
we are now ready to insert the version tag to these patterns.
|
||||
before updating thousands of patterns, it's probably a good idea to test if a single one gets udpated:
|
||||
|
||||
```sql
|
||||
UPDATE code_v1
|
||||
SET code = code || E'\n// @version 1.1'
|
||||
WHERE hash = 'Ns2sMB40yIw4';
|
||||
```
|
||||
|
||||
after [verifying](https://strudel.cc/?Ns2sMB40yIw4) that the version tag has been added, let's insert it everywhere:
|
||||
|
||||
```sql
|
||||
UPDATE code_v1
|
||||
SET code = code || E'\n// @version 1.1'
|
||||
WHERE code NOT LIKE '%@version%'
|
||||
AND created_at > '2024-05-31T23:07:26+02:00'
|
||||
AND created_at < '2025-05-01T12:39:24+02:00'
|
||||
```
|
||||
|
||||
4. verify
|
||||
|
||||
we can verify that the edits worked by querying all patterns that contain the new version tag:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM code_v1
|
||||
WHERE code LIKE '%@version 1.1%'
|
||||
AND created_at > '2024-05-31T23:07:26+02:00'
|
||||
AND created_at < '2025-05-01T12:39:24+02:00'
|
||||
ORDER BY created_at ASC;
|
||||
```
|
||||
|
||||
|
||||
## Have Fun
|
||||
|
||||
Remember to have fun, and that this project is driven by the passion of volunteers!
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
FROM node:24
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm install pnpm --global
|
||||
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY packages/ ./packages/
|
||||
COPY examples/ ./examples/
|
||||
RUN mkdir -p website/public
|
||||
COPY website/package.json ./website/
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 4321
|
||||
|
||||
CMD ["pnpm", "dev"]
|
||||
@@ -1,8 +1,7 @@
|
||||
# strudel
|
||||
|
||||
[](https://github.com/tidalcycles/strudel/actions) [](https://doi.org/10.5281/zenodo.6659278)
|
||||
|
||||
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is a bit more stable now, but please continue to tread carefully.
|
||||
Live coding patterns on the web
|
||||
https://strudel.cc/
|
||||
|
||||
- Try it here: <https://strudel.cc>
|
||||
- Docs: <https://strudel.cc/learn>
|
||||
@@ -31,19 +30,13 @@ This project is organized into many [packages](./packages), which are also avail
|
||||
|
||||
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start).
|
||||
|
||||
You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE.md). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details.
|
||||
You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details.
|
||||
|
||||
Licensing info for the default sound banks can be found over on the [dough-samples](https://github.com/felixroos/dough-samples/blob/main/README.md) repository.
|
||||
|
||||
## Contributing
|
||||
|
||||
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
|
||||
|
||||
<a href="https://codeberg.org/uzu/strudel/activity/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=tidalcycles/strudel" />
|
||||
</a>
|
||||
|
||||
Made with [contrib.rocks](https://contrib.rocks).
|
||||
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). You can find the full list of contributors [here](https://codeberg.org/uzu/strudel/activity/contributors).
|
||||
|
||||
## Community
|
||||
|
||||
@@ -52,3 +45,5 @@ There is a #strudel channel on the TidalCycles discord: <https://discord.com/inv
|
||||
You can also ask questions and find related discussions on the tidal club forum: <https://club.tidalcycles.org/>
|
||||
|
||||
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
|
||||
|
||||
We also have a mastodon account: <a rel="me" href="https://social.toplap.org/@strudel">social.toplap.org/@strudel</a>
|
||||
|
||||
@@ -201,7 +201,7 @@ interfaces.
|
||||
|
||||
The Strudel REPL is available at <https://strudel.cc>,
|
||||
including an interactive tutorial. The repository is at
|
||||
<https://github.com/tidalcycles/strudel>, all the code is open source
|
||||
<https://codeberg.org/uzu/strudel/releases>, all the code is open source
|
||||
under the GPL-3.0 License.
|
||||
|
||||
# Acknowledgments
|
||||
@@ -128,7 +128,7 @@ For the future, it is planned to integrate alternative sound engines such as Gli
|
||||
# Links
|
||||
|
||||
The Strudel REPL is available at <https://strudel.cc>, including an interactive tutorial.
|
||||
The repository is at <https://github.com/tidalcycles/strudel>, all the code is open source under the GPL-3.0 License.
|
||||
The repository is at <https://codeberg.org/uzu/strudel/releases>, all the code is open source under the GPL-3.0 License.
|
||||
|
||||
# Acknowledgments
|
||||
|
||||
@@ -0,0 +1,954 @@
|
||||
<!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 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.
|
||||
</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 Tidal’s
|
||||
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 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.
|
||||
</p>
|
||||
<p>
|
||||
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 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 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;
|
||||
<span class="citation" data-cites="mcleanTidalVortexZero2022">McLean et al. (2022)</span>], 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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</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">=></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">'c3'</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">'e3'</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">'g3'</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%"
|
||||
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 Strudel’s 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">"c3 [e3 g3]"</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">"<0 2 [4 6](3,4,1) 3>"</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">'D minor'</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">"sawtooth square"</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 Tidal’s <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>
|
||||
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.
|
||||
</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 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
|
||||
<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%"
|
||||
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 & 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">"c3 [e3 g3]*2"</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">"c3 [e3 g3]*2"</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">'c3'</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">'e3'</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">'g3'</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 interval’s 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">'c3'</span><span class="op">,</span> [<span class="st">'e3'</span><span class="op">,</span> <span class="st">'g3'</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">=></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">=></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 "interval" 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">"c3 e3"</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">'sawtooth'</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">=></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: 'c3', cutoff: 1000, s: 'sawtooth' }</span></span>
|
||||
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a><span class="co"> { note: 'e3', cutoff: 1000, s: 'sawtooth' }</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">'x'</span><span class="op">,</span> <span class="st">'y'</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">"0 [1 2] 3"</span><span class="op">.</span><span class="fu">add</span>(<span class="st">"10 20"</span>)</span>
|
||||
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a><span class="st">"10 [11 22] 23"</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">"0 1 2"</span><span class="op">.</span><span class="fu">add</span>(<span class="st">"10 20"</span>)</span>
|
||||
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a><span class="st">"10 [11 21] 20"</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
|
||||
wouldn’t 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">"0 1 2"</span><span class="op">.</span><span class="at">add</span><span class="op">.</span><span class="fu">in</span>(<span class="st">"10 20"</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 Tidal’s tersity, versus Strudel’s simplicity.
|
||||
</p>
|
||||
<p>To demonstrate this, consider the following Tidal pattern:</p>
|
||||
<pre
|
||||
class="tidal"
|
||||
><code>iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # 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">"10 20"</span>)<span class="op">,</span> <span class="fu">n</span>(<span class="st">"0 1 3"</span>)<span class="op">.</span><span class="fu">s</span>(<span class="st">"triangle"</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">"0 1 3"</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">"10 20"</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">"triangle"</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
|
||||
Haskell’s 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 Haskell’s 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.
|
||||
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.
|
||||
</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 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.
|
||||
</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 Tidal’s approach to pattern</li>
|
||||
<li>modular and extensible</li>
|
||||
</ol>
|
||||
<p>
|
||||
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 <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 Tidal’s 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.cc" class="uri">https://strudel.cc</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 McLean’s 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>, 47–56.
|
||||
<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>
|
||||
@@ -451,7 +451,7 @@ While Haskell's type system makes it a great language for the ongoing developmen
|
||||
# Links
|
||||
|
||||
The Strudel REPL is available at <https://strudel.cc>, 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.
|
||||
The repository is at <https://codeberg.org/uzu/strudel>, all the code is open source under the AGPL-3.0 License.
|
||||
|
||||
# Acknowledgments
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
@@ -0,0 +1,63 @@
|
||||
$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>
|
||||
@@ -1,4 +1,8 @@
|
||||
This document introduces you to Strudel in a technical sense. If you just want to *use* Strudel, have a look at the [Tutorial](https://strudel.tidalcycles.org/tutorial/).
|
||||
This document introduces you to Strudel in a technical sense.
|
||||
|
||||
It is rather out of date, but there might still be useful info below.
|
||||
|
||||
If you just want to *use* Strudel, have a look at the [Tutorial](https://strudel.tidalcycles.org/tutorial/).
|
||||
|
||||
## Strudel Packages
|
||||
|
||||
@@ -17,11 +21,11 @@ More info in the [REPL README](https://codeberg.org/uzu/strudel/src/branch/main/
|
||||
|
||||
# High Level Overview
|
||||
|
||||
<img src="https://github.com/tidalcycles/strudel/blob/talk/talk/public/strudelflow.png?raw=true" width="600" />
|
||||
<img src="strudelflow.png" width="600" />
|
||||
|
||||
## 1. End User Code
|
||||
|
||||
The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://codeberg.org/uzu/strudel/src/branch/main/packages/eval#strudelcycleseval) evaluates the user code
|
||||
The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://github.com/tidalcycles/strudel/tree/main/packages/eval#strudelcycleseval) evaluates the user code
|
||||
after a transpilation step, which resolves the syntax sugar. If you don't want the syntax sugar, you can omit the eval package and call the native javascript `eval` instead.
|
||||
|
||||
### 🍭 Syntax Sugar
|
||||
@@ -54,13 +58,13 @@ This is how it works:
|
||||
- The AST is transformed to resolve the syntax sugar
|
||||
- The AST is used to generate code again (shift-codegen)
|
||||
|
||||
Shift will most likely be replaced with acorn in the future, see https://codeberg.org/uzu/strudel/issues/174
|
||||
Shift will most likely be replaced with acorn in the future, see https://github.com/tidalcycles/strudel/issues/174
|
||||
|
||||
### Mini Notation
|
||||
|
||||
Another important part of the user code is the mini notation, which allows to express rhythms in a short manner.
|
||||
|
||||
- the mini notation is [implemented as a PEG grammar](https://codeberg.org/uzu/strudel/src/branch/main/packages/mini/krill.pegjs), living in the [mini package](https://codeberg.org/uzu/strudel/src/branch/main/packages/mini)
|
||||
- the mini notation is [implemented as a PEG grammar](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), living in the [mini package](https://github.com/tidalcycles/strudel/tree/main/packages/mini)
|
||||
- it is based on [krill](https://github.com/Mdashdotdashn/krill) by Mdashdotdashn
|
||||
- the peg grammar is used to generate a parser with [peggyjs](https://peggyjs.org/)
|
||||
- the generated parser takes a mini notation string and outputs an AST
|
||||
@@ -171,7 +175,7 @@ Here is an example Hap value with different properties:
|
||||
```js
|
||||
{ note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 }
|
||||
```
|
||||
<img src="https://github.com/tidalcycles/strudel/blob/talk/talk/public/waa-nodes.png?raw=true" width="600" />
|
||||
<img src="waa-nodes.png" width="600" />
|
||||
|
||||
<div className="text-left">
|
||||
|
||||
@@ -182,12 +186,12 @@ Here is an example Hap value with different properties:
|
||||
|
||||
At the time of writing this doc, the following outputs are supported:
|
||||
|
||||
- Web Audio API `.out()` see [/webaudio](https://codeberg.org/uzu/strudel/src/branch/main/packages/webaudio)
|
||||
- MIDI `.midi()` see [/midi](https://codeberg.org/uzu/strudel/src/branch/main/packages/midi)
|
||||
- OSC `.osc()` see [/osc](https://codeberg.org/uzu/strudel/src/branch/main/packages/osc)
|
||||
- Serial `.serial()` see [/serial](https://codeberg.org/uzu/strudel/src/branch/main/packages/serial)
|
||||
- Tone.js `.tone()` (deprecated?) [/tone](https://codeberg.org/uzu/strudel/src/branch/main/packages/tone)
|
||||
- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://codeberg.org/uzu/strudel/src/branch/main/packages/webdirt)
|
||||
- Speech `.speak()` (experimental) part of [/core](https://codeberg.org/uzu/strudel/src/branch/main/packages/core)
|
||||
- Web Audio API `.out()` see [/webaudio](https://github.com/tidalcycles/strudel/tree/main/packages/webaudio)
|
||||
- MIDI `.midi()` see [/midi](https://github.com/tidalcycles/strudel/tree/main/packages/midi)
|
||||
- OSC `.osc()` see [/osc](https://github.com/tidalcycles/strudel/tree/main/packages/osc)
|
||||
- Serial `.serial()` see [/serial](https://github.com/tidalcycles/strudel/tree/main/packages/serial)
|
||||
- Tone.js `.tone()` (deprecated?) [/tone](https://github.com/tidalcycles/strudel/tree/main/packages/tone)
|
||||
- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://github.com/tidalcycles/strudel/tree/main/packages/webdirt)
|
||||
- Speech `.speak()` (experimental) part of [/core](https://github.com/tidalcycles/strudel/tree/main/packages/core)
|
||||
|
||||
These could change, so make sure to check the [packages folder](https://codeberg.org/uzu/strudel/src/branch/main/packages).
|
||||
These could change, so make sure to check the [packages folder](https://github.com/tidalcycles/strudel/tree/main/packages).
|
||||
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 49 KiB |
@@ -42,6 +42,7 @@ export default [
|
||||
'**/hydra.mjs',
|
||||
'**/jsdoc-synonyms.js',
|
||||
'packages/hs2js/src/hs2js.mjs',
|
||||
'packages/supradough/dough-export.mjs',
|
||||
'**/samples',
|
||||
],
|
||||
},
|
||||
@@ -83,4 +84,14 @@ export default [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Properties provided by AudioWorkletGlobalScope
|
||||
files: ['packages/superdough/worklets.mjs'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
currentTime: 'readonly',
|
||||
sampleRate: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
jsdoc-synonyms.js - Add support for @synonym tag
|
||||
jsdoc-synonyms.js - Add support for @synonyms tag
|
||||
Copyright (C) 2023 Strudel contributors - see <https://codeberg.org/uzu/strudel/activity/contributors>
|
||||
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/>.
|
||||
*/
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
Each folder represents one of the @strudel/* packages [published to npm](https://www.npmjs.com/org/strudel).
|
||||
|
||||
To understand how those pieces connect, refer to the [Technical Manual](https://codeberg.org/uzu/strudel/src/branch/main/technical-manual.md) or the individual READMEs.
|
||||
To understand how those pieces connect, refer to the [Technical Manual](https://codeberg.org/uzu/strudel/wiki/Technical-Manual) or the individual READMEs.
|
||||
|
||||
@@ -1,84 +1,460 @@
|
||||
import jsdoc from '../../doc.json';
|
||||
// import { javascriptLanguage } from '@codemirror/lang-javascript';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { h } from './html';
|
||||
import { Scale } from '@tonaljs/tonal';
|
||||
import { soundMap } from 'superdough';
|
||||
import { complex } from '@strudel/tonal';
|
||||
|
||||
function plaintext(str) {
|
||||
const escapeHtml = (str) => {
|
||||
const div = document.createElement('div');
|
||||
div.innerText = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
};
|
||||
|
||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||
const getInnerText = (html) => {
|
||||
var div = document.createElement('div');
|
||||
const stripHtml = (html) => {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
return div.textContent || div.innerText || '';
|
||||
};
|
||||
|
||||
export function Autocomplete({ doc, label }) {
|
||||
return h`<div class="prose dark:prose-invert max-h-[400px] overflow-auto p-2">
|
||||
<h1 class="pt-0 mt-0">${label || getDocLabel(doc)}</h1>
|
||||
${doc.description}
|
||||
<ul>
|
||||
${doc.params?.map(
|
||||
({ name, type, description }) =>
|
||||
`<li>${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}</li>`,
|
||||
)}
|
||||
</ul>
|
||||
<div>
|
||||
${doc.examples?.map((example) => `<div><pre>${plaintext(example)}</pre></div>`)}
|
||||
</div>
|
||||
</div>`[0];
|
||||
/*
|
||||
<pre
|
||||
className="cursor-pointer"
|
||||
onMouseDown={(e) => {
|
||||
console.log('ola!');
|
||||
navigator.clipboard.writeText(example);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{example}
|
||||
</pre>
|
||||
*/
|
||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||
|
||||
const buildParamsList = (params) =>
|
||||
params?.length
|
||||
? `
|
||||
<div class="autocomplete-info-params-section">
|
||||
<h4 class="autocomplete-info-section-title">Parameters</h4>
|
||||
<ul class="autocomplete-info-params-list">
|
||||
${params
|
||||
.map(
|
||||
({ name, type, description }) => `
|
||||
<li class="autocomplete-info-param-item">
|
||||
<span class="autocomplete-info-param-name">${name}</span>
|
||||
<span class="autocomplete-info-param-type">${type.names?.join(' | ')}</span>
|
||||
${description ? `<div class="autocomplete-info-param-desc">${stripHtml(description)}</div>` : ''}
|
||||
</li>
|
||||
`,
|
||||
)
|
||||
.join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`
|
||||
: '';
|
||||
|
||||
const buildExamples = (examples) =>
|
||||
examples?.length
|
||||
? `
|
||||
<div class="autocomplete-info-examples-section">
|
||||
<h4 class="autocomplete-info-section-title">Examples</h4>
|
||||
${examples
|
||||
.map(
|
||||
(example) => `
|
||||
<pre class="autocomplete-info-example-code">${escapeHtml(example)}</pre>
|
||||
`,
|
||||
)
|
||||
.join('')}
|
||||
</div>
|
||||
`
|
||||
: '';
|
||||
|
||||
export const Autocomplete = (doc) =>
|
||||
h`
|
||||
<div class="autocomplete-info-container">
|
||||
<div class="autocomplete-info-tooltip">
|
||||
<h3 class="autocomplete-info-function-name">${getDocLabel(doc)}</h3>
|
||||
${doc.synonyms_text ? `<div class="autocomplete-info-function-synonyms">Synonyms: ${doc.synonyms_text}</div>` : ''}
|
||||
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
|
||||
${buildParamsList(doc.params)}
|
||||
${buildExamples(doc.examples)}
|
||||
</div>
|
||||
</div>
|
||||
`[0];
|
||||
|
||||
const isValidDoc = (doc) => {
|
||||
const label = getDocLabel(doc);
|
||||
return label && !label.startsWith('_') && !['package'].includes(doc.kind);
|
||||
};
|
||||
|
||||
const hasExcludedTags = (doc) =>
|
||||
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
||||
|
||||
export function bankCompletions() {
|
||||
const soundDict = soundMap.get();
|
||||
const banks = new Set();
|
||||
for (const key of Object.keys(soundDict)) {
|
||||
const [bank, suffix] = key.split('_');
|
||||
if (suffix && bank) banks.add(bank);
|
||||
}
|
||||
return Array.from(banks)
|
||||
.sort()
|
||||
.map((name) => ({ label: name, type: 'bank' }));
|
||||
}
|
||||
|
||||
const jsdocCompletions = jsdoc.docs
|
||||
.filter(
|
||||
(doc) =>
|
||||
getDocLabel(doc) &&
|
||||
!getDocLabel(doc).startsWith('_') &&
|
||||
!['package'].includes(doc.kind) &&
|
||||
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
|
||||
)
|
||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
||||
.map((doc) /*: Completion */ => ({
|
||||
label: getDocLabel(doc),
|
||||
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
||||
info: () => Autocomplete({ doc }),
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
}));
|
||||
// Attempt to get all scale names from Tonal
|
||||
let scaleCompletions = [];
|
||||
try {
|
||||
scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
|
||||
} catch (e) {
|
||||
console.warn('[autocomplete] Could not load scale names from Tonal:', e);
|
||||
}
|
||||
|
||||
export const strudelAutocomplete = (context /* : CompletionContext */) => {
|
||||
let word = context.matchBefore(/\w*/);
|
||||
if (word.from == word.to && !context.explicit) return null;
|
||||
// Valid mode values for voicing
|
||||
const modeCompletions = [
|
||||
{ label: 'below', type: 'mode' },
|
||||
{ label: 'above', type: 'mode' },
|
||||
{ label: 'duck', type: 'mode' },
|
||||
{ label: 'root', type: 'mode' },
|
||||
];
|
||||
|
||||
// Valid chord symbols from ireal dictionary plus empty string for major triads
|
||||
const chordSymbols = ['', ...Object.keys(complex)].sort();
|
||||
const chordSymbolCompletions = chordSymbols.map((symbol) => {
|
||||
if (symbol === '') {
|
||||
return {
|
||||
label: 'major',
|
||||
apply: '',
|
||||
type: 'chord-symbol',
|
||||
};
|
||||
}
|
||||
return {
|
||||
from: word.from,
|
||||
options: jsdocCompletions,
|
||||
/* options: [
|
||||
{ label: 'match', type: 'keyword' },
|
||||
{ label: 'hello', type: 'variable', info: '(World)' },
|
||||
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
|
||||
], */
|
||||
label: symbol,
|
||||
apply: symbol,
|
||||
type: 'chord-symbol',
|
||||
};
|
||||
});
|
||||
|
||||
export const getSynonymDoc = (doc, synonym) => {
|
||||
const synonyms = doc.synonyms || [];
|
||||
const docLabel = getDocLabel(doc);
|
||||
// Swap `doc.name` in for `s` in the list of synonyms
|
||||
const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
|
||||
return {
|
||||
...doc,
|
||||
name: synonym,
|
||||
longname: synonym,
|
||||
synonyms: synonymsWithDoc,
|
||||
synonyms_text: synonymsWithDoc.join(', '),
|
||||
};
|
||||
};
|
||||
|
||||
export function isAutoCompletionEnabled(on) {
|
||||
return on
|
||||
? [
|
||||
autocompletion({ override: [strudelAutocomplete] }),
|
||||
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
|
||||
]
|
||||
: []; // autocompletion({ override: [] })
|
||||
const jsdocCompletions = (() => {
|
||||
const seen = new Set(); // avoid repetition
|
||||
const completions = [];
|
||||
for (const doc of jsdoc.docs) {
|
||||
if (!isValidDoc(doc) || hasExcludedTags(doc)) continue;
|
||||
const docLabel = getDocLabel(doc);
|
||||
// Remove duplicates
|
||||
const synonyms = doc.synonyms || [];
|
||||
let labels = [docLabel, ...synonyms];
|
||||
for (const label of labels) {
|
||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
||||
if (label && !seen.has(label)) {
|
||||
seen.add(label);
|
||||
completions.push({
|
||||
label,
|
||||
info: () => Autocomplete(getSynonymDoc(doc, label)),
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return completions;
|
||||
})();
|
||||
|
||||
// --- Handler functions for each context ---
|
||||
const pitchNames = [
|
||||
'C',
|
||||
'C#',
|
||||
'Db',
|
||||
'D',
|
||||
'D#',
|
||||
'Eb',
|
||||
'E',
|
||||
'E#',
|
||||
'Fb',
|
||||
'F',
|
||||
'F#',
|
||||
'Gb',
|
||||
'G',
|
||||
'G#',
|
||||
'Ab',
|
||||
'A',
|
||||
'A#',
|
||||
'Bb',
|
||||
'B',
|
||||
'B#',
|
||||
'Cb',
|
||||
];
|
||||
|
||||
// Cached regex patterns for scaleHandler
|
||||
const SCALE_NO_QUOTES_REGEX = /scale\(\s*$/;
|
||||
const SCALE_AFTER_COLON_REGEX = /scale\(\s*['"][^'"]*:[^'"]*$/;
|
||||
const SCALE_PRE_COLON_REGEX = /scale\(\s*['"][^'"]*$/;
|
||||
const SCALE_PITCH_MATCH_REGEX = /([A-Ga-g][#b]*)?$/;
|
||||
const SCALE_SPACES_TO_COLON_REGEX = /\s+/g;
|
||||
|
||||
function scaleHandler(context) {
|
||||
// First check for scale context without quotes - block with empty completions
|
||||
let scaleNoQuotesContext = context.matchBefore(SCALE_NO_QUOTES_REGEX);
|
||||
if (scaleNoQuotesContext) {
|
||||
return {
|
||||
from: scaleNoQuotesContext.to,
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Check for after-colon context first (more specific)
|
||||
let scaleAfterColonContext = context.matchBefore(SCALE_AFTER_COLON_REGEX);
|
||||
if (scaleAfterColonContext) {
|
||||
const text = scaleAfterColonContext.text;
|
||||
const colonIdx = text.lastIndexOf(':');
|
||||
if (colonIdx !== -1) {
|
||||
const fragment = text.slice(colonIdx + 1);
|
||||
const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment));
|
||||
const options = filteredScales.map((s) => ({
|
||||
...s,
|
||||
apply: s.label.replace(SCALE_SPACES_TO_COLON_REGEX, ':'),
|
||||
}));
|
||||
const from = scaleAfterColonContext.from + colonIdx + 1;
|
||||
return {
|
||||
from,
|
||||
options,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Then check for pre-colon context
|
||||
let scalePreColonContext = context.matchBefore(SCALE_PRE_COLON_REGEX);
|
||||
if (scalePreColonContext) {
|
||||
if (!scalePreColonContext.text.includes(':')) {
|
||||
if (context.explicit) {
|
||||
const text = scalePreColonContext.text;
|
||||
const match = text.match(SCALE_PITCH_MATCH_REGEX);
|
||||
const fragment = match ? match[0] : '';
|
||||
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
|
||||
const from = scalePreColonContext.to - fragment.length;
|
||||
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
|
||||
return { from, options };
|
||||
} else {
|
||||
return { from: scalePreColonContext.to, options: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cached regex patterns for soundHandler
|
||||
const SOUND_NO_QUOTES_REGEX = /(s|sound)\(\s*$/;
|
||||
const SOUND_WITH_QUOTES_REGEX = /(s|sound)\(\s*['"][^'"]*$/;
|
||||
const SOUND_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w]*)$/;
|
||||
|
||||
function soundHandler(context) {
|
||||
// First check for sound context without quotes - block with empty completions
|
||||
let soundNoQuotesContext = context.matchBefore(SOUND_NO_QUOTES_REGEX);
|
||||
if (soundNoQuotesContext) {
|
||||
return {
|
||||
from: soundNoQuotesContext.to,
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Then check for sound context with quotes - provide completions
|
||||
let soundContext = context.matchBefore(SOUND_WITH_QUOTES_REGEX);
|
||||
if (!soundContext) return null;
|
||||
|
||||
const text = soundContext.text;
|
||||
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
||||
if (quoteIdx === -1) return null;
|
||||
const inside = text.slice(quoteIdx + 1);
|
||||
const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX);
|
||||
const fragment = fragMatch ? fragMatch[1] : inside;
|
||||
const soundNames = Object.keys(soundMap.get()).sort();
|
||||
const filteredSounds = soundNames.filter((name) => name.includes(fragment));
|
||||
let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
|
||||
const from = soundContext.to - fragment.length;
|
||||
return {
|
||||
from,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
// Cached regex patterns for bankHandler
|
||||
const BANK_NO_QUOTES_REGEX = /bank\(\s*$/;
|
||||
const BANK_WITH_QUOTES_REGEX = /bank\(\s*['"][^'"]*$/;
|
||||
|
||||
function bankHandler(context) {
|
||||
// First check for bank context without quotes - block with empty completions
|
||||
let bankNoQuotesContext = context.matchBefore(BANK_NO_QUOTES_REGEX);
|
||||
if (bankNoQuotesContext) {
|
||||
return {
|
||||
from: bankNoQuotesContext.to,
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Then check for bank context with quotes - provide completions
|
||||
let bankMatch = context.matchBefore(BANK_WITH_QUOTES_REGEX);
|
||||
if (!bankMatch) return null;
|
||||
|
||||
const text = bankMatch.text;
|
||||
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
||||
if (quoteIdx === -1) return null;
|
||||
const inside = text.slice(quoteIdx + 1);
|
||||
const fragment = inside;
|
||||
let banks = bankCompletions();
|
||||
const filteredBanks = banks.filter((b) => b.label.startsWith(fragment));
|
||||
const from = bankMatch.to - fragment.length;
|
||||
return {
|
||||
from,
|
||||
options: filteredBanks,
|
||||
};
|
||||
}
|
||||
|
||||
// Cached regex patterns for modeHandler
|
||||
const MODE_NO_QUOTES_REGEX = /mode\(\s*$/;
|
||||
const MODE_AFTER_COLON_REGEX = /mode\(\s*['"][^'"]*:[^'"]*$/;
|
||||
const MODE_PRE_COLON_REGEX = /mode\(\s*['"][^'"]*$/;
|
||||
const MODE_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w:]*)$/;
|
||||
|
||||
function modeHandler(context) {
|
||||
// First check for mode context without quotes - block with empty completions
|
||||
let modeNoQuotesContext = context.matchBefore(MODE_NO_QUOTES_REGEX);
|
||||
if (modeNoQuotesContext) {
|
||||
return {
|
||||
from: modeNoQuotesContext.to,
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Check for after-colon context first (more specific)
|
||||
let modeAfterColonContext = context.matchBefore(MODE_AFTER_COLON_REGEX);
|
||||
if (modeAfterColonContext) {
|
||||
const text = modeAfterColonContext.text;
|
||||
const colonIdx = text.lastIndexOf(':');
|
||||
if (colonIdx !== -1) {
|
||||
const fragment = text.slice(colonIdx + 1);
|
||||
// For anchor after colon, we can suggest pitch names
|
||||
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
|
||||
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
|
||||
const from = modeAfterColonContext.from + colonIdx + 1;
|
||||
return {
|
||||
from,
|
||||
options,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Then check for pre-colon context
|
||||
let modeContext = context.matchBefore(MODE_PRE_COLON_REGEX);
|
||||
if (!modeContext) return null;
|
||||
|
||||
const text = modeContext.text;
|
||||
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
||||
if (quoteIdx === -1) return null;
|
||||
const inside = text.slice(quoteIdx + 1);
|
||||
const fragMatch = inside.match(MODE_FRAGMENT_MATCH_REGEX);
|
||||
const fragment = fragMatch ? fragMatch[1] : inside;
|
||||
const filteredModes = modeCompletions.filter((m) => m.label.startsWith(fragment));
|
||||
const from = modeContext.to - fragment.length;
|
||||
return {
|
||||
from,
|
||||
options: filteredModes,
|
||||
};
|
||||
}
|
||||
|
||||
// Cached regex patterns for chordHandler
|
||||
const CHORD_NO_QUOTES_REGEX = /chord\(\s*$/;
|
||||
const CHORD_WITH_QUOTES_REGEX = /chord\(\s*['"][^'"]*$/;
|
||||
const CHORD_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w#b+^:-]*)$/;
|
||||
|
||||
function chordHandler(context) {
|
||||
// First check for chord context without quotes - block with empty completions
|
||||
let chordNoQuotesContext = context.matchBefore(CHORD_NO_QUOTES_REGEX);
|
||||
if (chordNoQuotesContext) {
|
||||
return {
|
||||
from: chordNoQuotesContext.to,
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Then check for chord context with quotes - provide completions
|
||||
let chordContext = context.matchBefore(CHORD_WITH_QUOTES_REGEX);
|
||||
if (!chordContext) return null;
|
||||
|
||||
const text = chordContext.text;
|
||||
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
||||
if (quoteIdx === -1) return null;
|
||||
const inside = text.slice(quoteIdx + 1);
|
||||
|
||||
// Use same fragment matching as sound/mode for expressions like "<G Am>"
|
||||
const fragMatch = inside.match(CHORD_FRAGMENT_MATCH_REGEX);
|
||||
const fragment = fragMatch ? fragMatch[1] : inside;
|
||||
|
||||
// Check if fragment contains any pitch name at start (for root + symbol)
|
||||
let rootMatch = null;
|
||||
let symbolFragment = fragment;
|
||||
for (const pitch of pitchNames) {
|
||||
if (fragment.toLowerCase().startsWith(pitch.toLowerCase())) {
|
||||
rootMatch = pitch;
|
||||
symbolFragment = fragment.slice(pitch.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (rootMatch) {
|
||||
// We have a root, now complete chord symbols
|
||||
const filteredSymbols = chordSymbolCompletions.filter((s) =>
|
||||
s.label.toLowerCase().startsWith(symbolFragment.toLowerCase()),
|
||||
);
|
||||
|
||||
// Create completions that replace the entire chord, not just the symbol part
|
||||
const options = filteredSymbols;
|
||||
|
||||
const from = chordContext.to - symbolFragment.length;
|
||||
return { from, options };
|
||||
} else {
|
||||
// No root yet, complete with pitch names
|
||||
const filteredPitches = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
|
||||
const options = filteredPitches.map((p) => ({ label: p, type: 'pitch' }));
|
||||
const from = chordContext.to - fragment.length;
|
||||
return { from, options };
|
||||
}
|
||||
}
|
||||
|
||||
// Cached regex patterns for fallbackHandler
|
||||
const FALLBACK_WORD_REGEX = /\w*/;
|
||||
|
||||
function fallbackHandler(context) {
|
||||
const word = context.matchBefore(FALLBACK_WORD_REGEX);
|
||||
if (word && word.from === word.to && !context.explicit) return null;
|
||||
if (word) {
|
||||
return {
|
||||
from: word.from,
|
||||
options: jsdocCompletions,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlers = [
|
||||
soundHandler,
|
||||
bankHandler,
|
||||
chordHandler,
|
||||
scaleHandler,
|
||||
modeHandler,
|
||||
// this handler *must* be last
|
||||
fallbackHandler,
|
||||
];
|
||||
|
||||
export const strudelAutocomplete = (context) => {
|
||||
for (const handler of handlers) {
|
||||
const result = handler(context);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const isAutoCompletionEnabled = (enabled) =>
|
||||
enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : [];
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
keymap,
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
highlightActiveLine,
|
||||
dropCursor,
|
||||
rectangularSelection,
|
||||
crosshairCursor,
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter,
|
||||
} from '@codemirror/view';
|
||||
import {
|
||||
defaultHighlightStyle,
|
||||
syntaxHighlighting,
|
||||
bracketMatching,
|
||||
foldGutter,
|
||||
foldKeymap,
|
||||
} from '@codemirror/language';
|
||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete';
|
||||
|
||||
// Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
|
||||
|
||||
export const basicSetup = (() => [
|
||||
// lineNumbers(),
|
||||
// highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
// foldGutter(),
|
||||
// drawSelection(),
|
||||
dropCursor(),
|
||||
// EditorState.allowMultipleSelections.of(true),
|
||||
// indentOnInput(),
|
||||
// syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
// autocompletion(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
// highlightActiveLine(),
|
||||
// highlightSelectionMatches(),
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
// ...searchKeymap,
|
||||
...historyKeymap,
|
||||
// ...foldKeymap,
|
||||
// ...completionKeymap,
|
||||
]),
|
||||
])();
|
||||
|
||||
/// A minimal set of extensions to create a functional editor. Only
|
||||
/// includes [the default keymap](#commands.defaultKeymap), [undo
|
||||
/// history](#commands.history), [special character
|
||||
/// highlighting](#view.highlightSpecialChars), [custom selection
|
||||
/// drawing](#view.drawSelection), and [default highlight
|
||||
/// style](#language.defaultHighlightStyle).
|
||||
export const minimalSetup = (() => [
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
drawSelection(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
keymap.of([...defaultKeymap, ...historyKeymap]),
|
||||
])();
|
||||
@@ -1,8 +1,8 @@
|
||||
import { closeBrackets } from '@codemirror/autocomplete';
|
||||
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
|
||||
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { history } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { indentWithTab } from '@codemirror/commands';
|
||||
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
||||
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
|
||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||
import {
|
||||
@@ -24,6 +24,7 @@ import { initTheme, activateTheme, theme } from './themes.mjs';
|
||||
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||
import { widgetPlugin, updateWidgets } from './widget.mjs';
|
||||
import { persistentAtom } from '@nanostores/persistent';
|
||||
import { basicSetup } from './basicSetup.mjs';
|
||||
|
||||
const extensions = {
|
||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||
@@ -37,6 +38,14 @@ const extensions = {
|
||||
isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
|
||||
isFlashEnabled,
|
||||
keybindings,
|
||||
isTabIndentationEnabled: (on) => (on ? keymap.of([indentWithTab]) : []),
|
||||
isMultiCursorEnabled: (on) =>
|
||||
on
|
||||
? [
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
|
||||
]
|
||||
: [],
|
||||
};
|
||||
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||
|
||||
@@ -51,6 +60,8 @@ export const defaultSettings = {
|
||||
isFlashEnabled: true,
|
||||
isTooltipEnabled: false,
|
||||
isLineWrappingEnabled: false,
|
||||
isTabIndentationEnabled: false,
|
||||
isMultiCursorEnabled: false,
|
||||
theme: 'strudelTheme',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 18,
|
||||
@@ -62,7 +73,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
|
||||
});
|
||||
|
||||
// https://codemirror.net/docs/guide/
|
||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root }) {
|
||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) {
|
||||
const settings = codemirrorSettings.get();
|
||||
const initialSettings = Object.keys(compartments).map((key) =>
|
||||
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
||||
@@ -75,13 +86,17 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
/* search(),
|
||||
highlightSelectionMatches(), */
|
||||
...initialSettings,
|
||||
javascript(),
|
||||
basicSetup,
|
||||
mondo ? [] : javascript(),
|
||||
javascriptLanguage.data.of({
|
||||
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||
}),
|
||||
sliderPlugin,
|
||||
widgetPlugin,
|
||||
// indentOnInput(), // works without. already brought with javascript extension?
|
||||
// bracketMatching(), // does not do anything
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
history(),
|
||||
EditorView.updateListener.of((v) => onChange(v)),
|
||||
drawSelection({ cursorBlinkRate: 0 }),
|
||||
Prec.highest(
|
||||
@@ -209,6 +224,7 @@ export class StrudelMirror {
|
||||
},
|
||||
onEvaluate: () => this.evaluate(),
|
||||
onStop: () => this.stop(),
|
||||
mondo: replOptions.mondo,
|
||||
});
|
||||
const cmEditor = this.root.querySelector('.cm-editor');
|
||||
if (cmEditor) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
||||
import { RangeSetBuilder, StateEffect, StateField, Prec } from '@codemirror/state';
|
||||
import { Decoration, EditorView } from '@codemirror/view';
|
||||
|
||||
export const setMiniLocations = StateEffect.define();
|
||||
@@ -134,5 +134,5 @@ export const isPatternHighlightingEnabled = (on, config) => {
|
||||
setTimeout(() => {
|
||||
updateMiniLocations(config.editor, config.miniLocations);
|
||||
}, 100);
|
||||
return on ? highlightExtension : [];
|
||||
return on ? Prec.highest(highlightExtension) : [];
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
|
||||
export let html = (string) => {
|
||||
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = string.trim();
|
||||
return template.content.childNodes;
|
||||
};
|
||||
let parseChunk = (chunk) => {
|
||||
if (Array.isArray(chunk)) return chunk.flat().join('');
|
||||
|
||||
@@ -3,8 +3,9 @@ import { keymap, ViewPlugin } from '@codemirror/view';
|
||||
// import { searchKeymap } from '@codemirror/search';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
// import { vim } from './vim_test.mjs';
|
||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
|
||||
const vscodePlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
@@ -21,11 +22,11 @@ const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
||||
const keymaps = {
|
||||
vim,
|
||||
emacs,
|
||||
codemirror: () => keymap.of(defaultKeymap),
|
||||
vscode: vscodeExtension,
|
||||
};
|
||||
|
||||
export function keybindings(name) {
|
||||
const active = keymaps[name];
|
||||
return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []];
|
||||
// keymap.of(searchKeymap),
|
||||
return [active ? Prec.high(active()) : []];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/codemirror",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.6",
|
||||
"description": "Codemirror Extensions for Strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -42,12 +42,15 @@
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@nanostores/persistent": "^0.10.2",
|
||||
"@replit/codemirror-emacs": "^6.1.0",
|
||||
"@replit/codemirror-vim": "^6.2.1",
|
||||
"@replit/codemirror-vim": "^6.3.0",
|
||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/draw": "workspace:*",
|
||||
"@strudel/tonal": "workspace:*",
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
"nanostores": "^0.11.3"
|
||||
"@tonaljs/tonal": "^4.10.0",
|
||||
"nanostores": "^0.11.3",
|
||||
"superdough": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
|
||||
@@ -8,6 +8,11 @@ import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs';
|
||||
import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs';
|
||||
import redText, { settings as redTextSettings } from './themes/red-text.mjs';
|
||||
import greenText, { settings as greenTextSettings } from './themes/green-text.mjs';
|
||||
import archBtw, { settings as archBtwSettings } from './themes/archBtw.mjs';
|
||||
import fruitDaw, { settings as fruitDawSettings } from './themes/fruitDaw.mjs';
|
||||
|
||||
import bluescreenlight, { settings as bluescreenlightsettings } from './themes/bluescreenlight.mjs';
|
||||
|
||||
import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
|
||||
import atomone, { settings as atomOneSettings } from './themes/atomone.mjs';
|
||||
import aura, { settings as auraSettings } from './themes/aura.mjs';
|
||||
@@ -39,17 +44,20 @@ import { setTheme } from '@strudel/draw';
|
||||
export const themes = {
|
||||
strudelTheme,
|
||||
algoboy,
|
||||
archBtw,
|
||||
androidstudio,
|
||||
atomone,
|
||||
aura,
|
||||
bbedit,
|
||||
blackscreen,
|
||||
bluescreen,
|
||||
bluescreenlight,
|
||||
CutiePi,
|
||||
darcula,
|
||||
dracula,
|
||||
duotoneDark,
|
||||
eclipse,
|
||||
fruitDaw,
|
||||
githubDark,
|
||||
githubLight,
|
||||
greenText,
|
||||
@@ -78,10 +86,12 @@ export const themes = {
|
||||
export const settings = {
|
||||
strudelTheme: strudelThemeSettings,
|
||||
bluescreen: bluescreenSettings,
|
||||
bluescreenlight: bluescreenlightsettings,
|
||||
blackscreen: blackscreenSettings,
|
||||
whitescreen: whitescreenSettings,
|
||||
teletext: teletextSettings,
|
||||
algoboy: algoboySettings,
|
||||
archBtw: archBtwSettings,
|
||||
androidstudio: androidstudioSettings,
|
||||
atomone: atomOneSettings,
|
||||
aura: auraSettings,
|
||||
@@ -92,9 +102,11 @@ export const settings = {
|
||||
eclipse: eclipseSettings,
|
||||
CutiePi: CutiePiSettings,
|
||||
sonicPink: sonicPinkSettings,
|
||||
fruitDaw: fruitDawSettings,
|
||||
githubLight: githubLightSettings,
|
||||
githubDark: githubDarkSettings,
|
||||
greenText: greenTextSettings,
|
||||
|
||||
gruvboxDark: gruvboxDarkSettings,
|
||||
gruvboxLight: gruvboxLightSettings,
|
||||
materialDark: materialDarkSettings,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Arch Btw
|
||||
* Modern terminal inspired theme
|
||||
* made by Jade
|
||||
*/
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
const hex = ['rgb(0, 0, 0)', 'rgb(82, 208, 250)', 'rgba(113, 208, 250, .4)', 'rgba(113, 208, 250, .15)'];
|
||||
|
||||
export const settings = {
|
||||
background: hex[0],
|
||||
lineBackground: 'transparent',
|
||||
foreground: hex[1],
|
||||
selection: hex[2],
|
||||
selectionMatch: hex[0],
|
||||
gutterBackground: hex[0],
|
||||
gutterForeground: hex[2],
|
||||
gutterBorder: 'transparent',
|
||||
lineHighlight: hex[0],
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{
|
||||
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||
color: hex[1],
|
||||
},
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
|
||||
{ tag: [t.comment, t.brace, t.bracket], color: hex[2] },
|
||||
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[1] },
|
||||
{ tag: [t.attributeName, t.number], color: hex[1] },
|
||||
{ tag: t.keyword, color: hex[1] },
|
||||
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* A lighter blue screen theme
|
||||
* made by Jade
|
||||
*/
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
const hex = ['rgb(75, 130, 247)', 'rgb(47, 108, 246)', 'rgb(255, 255, 255)', 'rgba(255, 255, 255,.3)'];
|
||||
|
||||
export const settings = {
|
||||
background: hex[0],
|
||||
lineBackground: 'transparent',
|
||||
foreground: hex[2],
|
||||
selection: hex[3],
|
||||
selectionMatch: hex[0],
|
||||
gutterBackground: hex[0],
|
||||
gutterForeground: hex[2],
|
||||
gutterBorder: 'transparent',
|
||||
lineHighlight: hex[1],
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{
|
||||
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||
color: hex[2],
|
||||
},
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[2] },
|
||||
{ tag: [t.comment, t.bracket, t.brace, t.compareOperator], color: hex[3] },
|
||||
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
|
||||
{ tag: [t.attributeName, t.number], color: hex[2] },
|
||||
{ tag: t.keyword, color: hex[2] },
|
||||
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Fruit Daw
|
||||
* made by Jade
|
||||
*/
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
const hex = [
|
||||
'rgb(84, 93, 98)',
|
||||
'rgb(255, 255, 255)',
|
||||
'rgba(255, 255, 255, .25)',
|
||||
'rgb(67, 76, 81)',
|
||||
'rgb(186, 230, 115)',
|
||||
'rgb(252, 184, 67)',
|
||||
'rgb(124, 206, 254)',
|
||||
'rgb(83, 101, 102)',
|
||||
'rgba(46, 62, 72,.5)',
|
||||
'rgb(94, 100, 108)',
|
||||
'rgb(167, 216, 177)',
|
||||
];
|
||||
|
||||
export const settings = {
|
||||
background: hex[0],
|
||||
lineBackground: 'transparent',
|
||||
foreground: hex[10],
|
||||
selection: hex[8],
|
||||
selectionMatch: hex[0],
|
||||
gutterBackground: hex[3],
|
||||
gutterForeground: hex[2],
|
||||
gutterBorder: 'transparent',
|
||||
lineHighlight: hex[3],
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{
|
||||
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||
color: hex[1],
|
||||
},
|
||||
{ tag: [t.bool, t.special(t.variableName)], color: hex[1] },
|
||||
{ tag: [t.comment, t.brace, t.bracket], color: hex[2] },
|
||||
{ tag: [t.variableName], color: hex[1] },
|
||||
{ tag: [t.labelName, t.propertyName, t.self, t.atom], color: hex[5] },
|
||||
{ tag: [t.attributeName, t.number], color: hex[6] },
|
||||
{ tag: t.keyword, color: hex[5] },
|
||||
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[4] },
|
||||
],
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { hoverTooltip } from '@codemirror/view';
|
||||
import jsdoc from '../../doc.json';
|
||||
import { Autocomplete } from './autocomplete.mjs';
|
||||
import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
|
||||
|
||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||
|
||||
@@ -52,10 +52,11 @@ export const strudelTooltip = hoverTooltip(
|
||||
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
||||
if (!entry) {
|
||||
// Try for synonyms
|
||||
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||
if (!entry) {
|
||||
const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
entry = getSynonymDoc(doc, word);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip(
|
||||
create(view) {
|
||||
let dom = document.createElement('div');
|
||||
dom.className = 'strudel-tooltip';
|
||||
const ac = Autocomplete({ doc: entry, label: word });
|
||||
const ac = Autocomplete(entry);
|
||||
dom.appendChild(ac);
|
||||
return { dom };
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, bench } from 'vitest';
|
||||
|
||||
import { calculateTactus, sequence, stack } from '../index.mjs';
|
||||
import { calculateSteps, sequence, stack } from '../index.mjs';
|
||||
|
||||
const pat64 = sequence(...Array(64).keys());
|
||||
|
||||
describe('steps', () => {
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
bench(
|
||||
'+tactus',
|
||||
() => {
|
||||
@@ -14,7 +14,7 @@ describe('steps', () => {
|
||||
{ time: 1000 },
|
||||
);
|
||||
|
||||
calculateTactus(false);
|
||||
calculateSteps(false);
|
||||
bench(
|
||||
'-tactus',
|
||||
() => {
|
||||
@@ -25,7 +25,7 @@ describe('steps', () => {
|
||||
});
|
||||
|
||||
describe('stack', () => {
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
bench(
|
||||
'+tactus',
|
||||
() => {
|
||||
@@ -34,7 +34,7 @@ describe('stack', () => {
|
||||
{ time: 1000 },
|
||||
);
|
||||
|
||||
calculateTactus(false);
|
||||
calculateSteps(false);
|
||||
bench(
|
||||
'-tactus',
|
||||
() => {
|
||||
@@ -43,4 +43,4 @@ describe('stack', () => {
|
||||
{ time: 1000 },
|
||||
);
|
||||
});
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
|
||||
@@ -4,13 +4,14 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Pattern, register, sequence } from './pattern.mjs';
|
||||
import { Pattern, register, reify } from './pattern.mjs';
|
||||
|
||||
export function createParam(names) {
|
||||
let isMulti = Array.isArray(names);
|
||||
names = !isMulti ? [names] : names;
|
||||
const name = names[0];
|
||||
|
||||
// todo: make this less confusing
|
||||
const withVal = (xs) => {
|
||||
let bag;
|
||||
// check if we have an object with an unnamed control (.value)
|
||||
@@ -35,25 +36,34 @@ export function createParam(names) {
|
||||
}
|
||||
};
|
||||
|
||||
const func = (...pats) => sequence(...pats).withValue(withVal);
|
||||
|
||||
const setter = function (...pats) {
|
||||
if (!pats.length) {
|
||||
return this.fmap(withVal);
|
||||
// todo: make this less confusing
|
||||
const func = function (value, pat) {
|
||||
if (!pat) {
|
||||
return reify(value).withValue(withVal);
|
||||
}
|
||||
return this.set(func(...pats));
|
||||
if (typeof value === 'undefined') {
|
||||
return pat.fmap(withVal);
|
||||
}
|
||||
return pat.set(reify(value).withValue(withVal));
|
||||
};
|
||||
Pattern.prototype[name] = function (value) {
|
||||
return func(value, this);
|
||||
};
|
||||
Pattern.prototype[name] = setter;
|
||||
return func;
|
||||
}
|
||||
|
||||
// maps control alias names to the "main" control name
|
||||
const controlAlias = new Map();
|
||||
|
||||
export function isControlName(name) {
|
||||
return controlAlias.has(name);
|
||||
}
|
||||
|
||||
export function registerControl(names, ...aliases) {
|
||||
const name = Array.isArray(names) ? names[0] : names;
|
||||
let bag = {};
|
||||
bag[name] = createParam(names);
|
||||
controlAlias.set(name, name);
|
||||
aliases.forEach((alias) => {
|
||||
bag[alias] = bag[name];
|
||||
controlAlias.set(alias, name);
|
||||
@@ -77,10 +87,244 @@ export function registerControl(names, ...aliases) {
|
||||
*/
|
||||
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
||||
|
||||
/**
|
||||
* Position in the wavetable of the wavetable oscillator
|
||||
*
|
||||
* @name wt
|
||||
* @param {number | Pattern} position Position in the wavetable from 0 to 1
|
||||
* @synonyms wavetablePosition
|
||||
* @example
|
||||
* s("squelch").bank("wt_digital").seg(8).note("F1").wt("0 0.25 0.5 0.75 1")
|
||||
*/
|
||||
export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePosition');
|
||||
|
||||
/**
|
||||
* Amount of envelope applied wavetable oscillator's position envelope
|
||||
*
|
||||
* @name wtenv
|
||||
* @param {number | Pattern} amount between 0 and 1
|
||||
*/
|
||||
export const { wtenv } = registerControl('wtenv');
|
||||
/**
|
||||
* Attack time of the wavetable oscillator's position envelope
|
||||
*
|
||||
* @name wtattack
|
||||
* @synonyms wtatt
|
||||
* @param {number | Pattern} time attack time in seconds
|
||||
*/
|
||||
export const { wtattack, wtatt } = registerControl('wtattack', 'wtatt');
|
||||
|
||||
/**
|
||||
* Decay time of the wavetable oscillator's position envelope
|
||||
*
|
||||
* @name wtdecay
|
||||
* @synonyms wtdec
|
||||
* @param {number | Pattern} time decay time in seconds
|
||||
*/
|
||||
export const { wtdecay, wtdec } = registerControl('wtdecay', 'wtdec');
|
||||
|
||||
/**
|
||||
* Sustain time of the wavetable oscillator's position envelope
|
||||
*
|
||||
* @name wtsustain
|
||||
* @synonyms wtsus
|
||||
* @param {number | Pattern} gain sustain level (0 to 1)
|
||||
*/
|
||||
export const { wtsustain, wtsus } = registerControl('wtsustain', 'wtsus');
|
||||
|
||||
/**
|
||||
* Release time of the wavetable oscillator's position envelope
|
||||
*
|
||||
* @name wtrelease
|
||||
* @synonyms wtrel
|
||||
* @param {number | Pattern} time release time in seconds
|
||||
*/
|
||||
export const { wtrelease, wtrel } = registerControl('wtrelease', 'wtrel');
|
||||
|
||||
/**
|
||||
* Rate of the LFO for the wavetable oscillator's position
|
||||
*
|
||||
* @name wtrate
|
||||
* @param {number | Pattern} rate rate in hertz
|
||||
*/
|
||||
export const { wtrate } = registerControl('wtrate');
|
||||
/**
|
||||
* cycle synced rate of the LFO for the wavetable oscillator's position
|
||||
*
|
||||
* @name wtsync
|
||||
* @param {number | Pattern} rate rate in cycles
|
||||
*/
|
||||
export const { wtsync } = registerControl('wtsync');
|
||||
|
||||
/**
|
||||
* Depth of the LFO for the wavetable oscillator's position
|
||||
*
|
||||
* @name wtdepth
|
||||
* @param {number | Pattern} depth depth of modulation
|
||||
*/
|
||||
export const { wtdepth } = registerControl('wtdepth');
|
||||
|
||||
/**
|
||||
* Shape of the LFO for the wavetable oscillator's position
|
||||
*
|
||||
* @name wtshape
|
||||
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
|
||||
*/
|
||||
export const { wtshape } = registerControl('wtshape');
|
||||
|
||||
/**
|
||||
* DC offset of the LFO for the wavetable oscillator's position
|
||||
*
|
||||
* @name wtdc
|
||||
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
|
||||
*/
|
||||
export const { wtdc } = registerControl('wtdc');
|
||||
|
||||
/**
|
||||
* Skew of the LFO for the wavetable oscillator's position
|
||||
*
|
||||
* @name wtskew
|
||||
* @param {number | Pattern} skew How much to bend the LFO shape
|
||||
*/
|
||||
export const { wtskew } = registerControl('wtskew');
|
||||
|
||||
/**
|
||||
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator
|
||||
*
|
||||
* @name warp
|
||||
* @param {number | Pattern} amount Warp of the wavetable from 0 to 1
|
||||
* @synonyms wavetableWarp
|
||||
* @example
|
||||
* s("basique").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
|
||||
* .warpmode("spin")
|
||||
*/
|
||||
export const { warp, wavetableWarp } = registerControl('warp', 'wavetableWarp');
|
||||
|
||||
/**
|
||||
* Attack time of the wavetable oscillator's warp envelope
|
||||
*
|
||||
* @name warpattack
|
||||
* @synonyms warpatt
|
||||
* @param {number | Pattern} time attack time in seconds
|
||||
*/
|
||||
export const { warpattack, warpatt } = registerControl('warpattack', 'warpatt');
|
||||
|
||||
/**
|
||||
* Decay time of the wavetable oscillator's warp envelope
|
||||
*
|
||||
* @name warpdecay
|
||||
* @synonyms warpdec
|
||||
* @param {number | Pattern} time decay time in seconds
|
||||
*/
|
||||
export const { warpdecay, warpdec } = registerControl('warpdecay', 'warpdec');
|
||||
|
||||
/**
|
||||
* Sustain time of the wavetable oscillator's warp envelope
|
||||
*
|
||||
* @name warpsustain
|
||||
* @synonyms warpsus
|
||||
* @param {number | Pattern} gain sustain level (0 to 1)
|
||||
*/
|
||||
export const { warpsustain, warpsus } = registerControl('warpsustain', 'warpsus');
|
||||
|
||||
/**
|
||||
* Release time of the wavetable oscillator's warp envelope
|
||||
*
|
||||
* @name warprelease
|
||||
* @synonyms warprel
|
||||
* @param {number | Pattern} time release time in seconds
|
||||
*/
|
||||
export const { warprelease, warprel } = registerControl('warprelease', 'warprel');
|
||||
|
||||
/**
|
||||
* Rate of the LFO for the wavetable oscillator's warp
|
||||
*
|
||||
* @name warprate
|
||||
* @param {number | Pattern} rate rate in hertz
|
||||
*/
|
||||
export const { warprate } = registerControl('warprate');
|
||||
|
||||
/**
|
||||
* Depth of the LFO for the wavetable oscillator's warp
|
||||
*
|
||||
* @name warpdepth
|
||||
* @param {number | Pattern} depth depth of modulation
|
||||
*/
|
||||
export const { warpdepth } = registerControl('warpdepth');
|
||||
|
||||
/**
|
||||
* Shape of the LFO for the wavetable oscillator's warp
|
||||
*
|
||||
* @name warpshape
|
||||
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
|
||||
*/
|
||||
export const { warpshape } = registerControl('warpshape');
|
||||
|
||||
/**
|
||||
* DC offset of the LFO for the wavetable oscillator's warp
|
||||
*
|
||||
* @name warpdc
|
||||
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
|
||||
*/
|
||||
export const { warpdc } = registerControl('warpdc');
|
||||
|
||||
/**
|
||||
* Skew of the LFO for the wavetable oscillator's warp
|
||||
*
|
||||
* @name warpskew
|
||||
* @param {number | Pattern} skew How much to bend the LFO shape
|
||||
*/
|
||||
export const { warpskew } = registerControl('warpskew');
|
||||
|
||||
/**
|
||||
* Type of warp (alteration of the waveform) to apply to the wavetable oscillator.
|
||||
*
|
||||
* The current options are: none, asym, bendp, bendm, bendmp, sync, quant, fold, pwm, orbit,
|
||||
* spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip
|
||||
*
|
||||
* @name warpmode
|
||||
* @param {number | string | Pattern} mode Warp mode
|
||||
* @synonyms wavetableWarpMode
|
||||
* @example
|
||||
* s("morgana").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
|
||||
* .warpmode("<asym bendp spin logistic sync wormhole brownian>*2")
|
||||
*
|
||||
*/
|
||||
export const { warpmode, wavetableWarpMode } = registerControl('warpmode', 'wavetableWarpMode');
|
||||
|
||||
/**
|
||||
* Amount of randomness of the initial phase of the wavetable oscillator.
|
||||
*
|
||||
* @name wtphaserand
|
||||
* @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random)
|
||||
* @synonyms wavetablePhaseRand
|
||||
* @example
|
||||
* s("basique").bank("wt_digital").seg(16).wtphaserand("<0 1>")
|
||||
*
|
||||
*/
|
||||
export const { wtphaserand, wavetablePhaseRand } = registerControl('wtphaserand', 'wavetablePhaseRand');
|
||||
|
||||
/**
|
||||
* Amount of envelope applied wavetable oscillator's position envelope
|
||||
*
|
||||
* @name warpenv
|
||||
* @param {number | Pattern} amount between 0 and 1
|
||||
*/
|
||||
export const { warpenv } = registerControl('warpenv');
|
||||
|
||||
/**
|
||||
* cycle synced rate of the LFO for the wavetable warp position
|
||||
*
|
||||
* @name warpsync
|
||||
* @param {number | Pattern} rate rate in cycles
|
||||
*/
|
||||
export const { warpsync } = registerControl('warpsync');
|
||||
|
||||
/**
|
||||
* Define a custom webaudio node to use as a sound source.
|
||||
*
|
||||
* @name source
|
||||
* @synonyms src
|
||||
* @param {function} getSource
|
||||
* @synonyms src
|
||||
*
|
||||
@@ -103,7 +347,7 @@ export const { n } = registerControl('n');
|
||||
*
|
||||
* - a letter (a-g or A-G)
|
||||
* - optional accidentals (b or #)
|
||||
* - optional octave number (0-9). Defaults to 3
|
||||
* - optional (possibly negative) octave number (0-9). Defaults to 3
|
||||
*
|
||||
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
|
||||
*
|
||||
@@ -116,6 +360,8 @@ export const { n } = registerControl('n');
|
||||
* note("c4 a4 f4 e4")
|
||||
* @example
|
||||
* note("60 69 65 64")
|
||||
* @example
|
||||
* note("fbb1 a#0 cbbb-1 e##-2").sound("saw")
|
||||
*/
|
||||
export const { note } = registerControl(['note', 'n']);
|
||||
|
||||
@@ -131,8 +377,8 @@ export const { note } = registerControl(['note', 'n']);
|
||||
*/
|
||||
export const { accelerate } = registerControl('accelerate');
|
||||
/**
|
||||
*
|
||||
* Sets the velocity from 0 to 1. Is multiplied together with gain.
|
||||
*
|
||||
* @name velocity
|
||||
* @example
|
||||
* s("hh*8")
|
||||
@@ -242,6 +488,20 @@ export const { fmenv } = registerControl('fmenv');
|
||||
*
|
||||
*/
|
||||
export const { fmattack } = registerControl('fmattack');
|
||||
|
||||
/**
|
||||
* Waveform of the fm modulator
|
||||
*
|
||||
* @name fmwave
|
||||
* @param {number | Pattern} wave waveform
|
||||
* @example
|
||||
* n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("<sine square sawtooth crackle>").fm(4).fmh(2.01)
|
||||
* @example
|
||||
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6)
|
||||
*
|
||||
*/
|
||||
export const { fmwave } = registerControl('fmwave');
|
||||
|
||||
/**
|
||||
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
|
||||
*
|
||||
@@ -285,6 +545,17 @@ export const { fmvelocity } = registerControl('fmvelocity');
|
||||
*/
|
||||
export const { bank } = registerControl('bank');
|
||||
|
||||
/**
|
||||
* mix control for the chorus effect
|
||||
*
|
||||
* @name chorus
|
||||
* @param {string | Pattern} chorus mix amount between 0 and 1
|
||||
* @example
|
||||
* note("d d a# a").s("sawtooth").chorus(.5)
|
||||
*
|
||||
*/
|
||||
export const { chorus } = registerControl('chorus');
|
||||
|
||||
// analyser node send amount 0 - 1 (used by scope)
|
||||
export const { analyze } = registerControl('analyze');
|
||||
// fftSize of analyser
|
||||
@@ -296,6 +567,7 @@ export const { fft } = registerControl('fft');
|
||||
*
|
||||
* @name decay
|
||||
* @param {number | Pattern} time decay time in seconds
|
||||
* @synonyms dec
|
||||
* @example
|
||||
* note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0)
|
||||
*
|
||||
@@ -352,7 +624,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
|
||||
// ['bpq'],
|
||||
export const { bandq, bpq } = registerControl('bandq', '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.
|
||||
* 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.
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name begin
|
||||
@@ -413,7 +685,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb');
|
||||
*/
|
||||
export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
|
||||
/**
|
||||
* bit crusher effect.
|
||||
* Bit crusher effect.
|
||||
*
|
||||
* @name crush
|
||||
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
|
||||
@@ -424,7 +696,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
|
||||
// ['clhatdecay'],
|
||||
export const { crush } = registerControl('crush');
|
||||
/**
|
||||
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
|
||||
* Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
|
||||
*
|
||||
* @name coarse
|
||||
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
|
||||
@@ -435,7 +707,80 @@ export const { crush } = registerControl('crush');
|
||||
export const { coarse } = registerControl('coarse');
|
||||
|
||||
/**
|
||||
* filter overdrive for supported filter types
|
||||
* Modulate the amplitude of a sound with a continuous waveform
|
||||
*
|
||||
* @name tremolo
|
||||
* @synonyms trem
|
||||
* @param {number | Pattern} speed modulation speed in HZ
|
||||
* @example
|
||||
* note("d d d# d".fast(4)).s("supersaw").tremolo("<3 2 100> ").tremoloskew("<.5>")
|
||||
*
|
||||
*/
|
||||
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
|
||||
|
||||
/**
|
||||
* Modulate the amplitude of a sound with a continuous waveform
|
||||
*
|
||||
* @name tremolosync
|
||||
* @synonyms tremsync
|
||||
* @param {number | Pattern} cycles modulation speed in cycles
|
||||
* @example
|
||||
* note("d d d# d".fast(4)).s("supersaw").tremolosync("4").tremoloskew("<1 .5 0>")
|
||||
*
|
||||
*/
|
||||
export const { tremolosync } = registerControl(
|
||||
['tremolosync', 'tremolodepth', 'tremoloskew', 'tremolophase'],
|
||||
'tremsync',
|
||||
);
|
||||
|
||||
/**
|
||||
* Depth of amplitude modulation
|
||||
*
|
||||
* @name tremolodepth
|
||||
* @synonyms tremdepth
|
||||
* @param {number | Pattern} depth
|
||||
* @example
|
||||
* note("a1 a1 a#1 a1".fast(4)).s("pulse").tremsync(4).tremolodepth("<1 2 .7>")
|
||||
*
|
||||
*/
|
||||
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
|
||||
/**
|
||||
* Alter the shape of the modulation waveform
|
||||
*
|
||||
* @name tremoloskew
|
||||
* @synonyms tremskew
|
||||
* @param {number | Pattern} amount between 0 & 1, the shape of the waveform
|
||||
* @example
|
||||
* note("{f a c e}%16").s("sawtooth").tremsync(4).tremoloskew("<.5 0 1>")
|
||||
*
|
||||
*/
|
||||
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
|
||||
|
||||
/**
|
||||
* Alter the phase of the modulation waveform
|
||||
*
|
||||
* @name tremolophase
|
||||
* @synonyms tremphase
|
||||
* @param {number | Pattern} offset the offset in cycles of the modulation
|
||||
* @example
|
||||
* note("{f a c e}%16").s("sawtooth").tremsync(4).tremolophase("<0 .25 .66>")
|
||||
*
|
||||
*/
|
||||
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
||||
|
||||
/**
|
||||
* Shape of amplitude modulation
|
||||
*
|
||||
* @name tremoloshape
|
||||
* @synonyms tremshape
|
||||
* @param {number | Pattern} shape tri | square | sine | saw | ramp
|
||||
* @example
|
||||
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
|
||||
*
|
||||
*/
|
||||
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
||||
/**
|
||||
* Filter overdrive for supported filter types
|
||||
*
|
||||
* @name drive
|
||||
* @param {number | Pattern} amount
|
||||
@@ -445,6 +790,92 @@ export const { coarse } = registerControl('coarse');
|
||||
*/
|
||||
export const { drive } = registerControl('drive');
|
||||
|
||||
/**
|
||||
* Modulate the amplitude of an orbit to create a "sidechain" like effect.
|
||||
*
|
||||
* Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")`
|
||||
*
|
||||
* @name duckorbit
|
||||
* @synonyms duck
|
||||
* @param {number | Pattern} orbit target orbit
|
||||
* @example
|
||||
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)
|
||||
* @example
|
||||
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||
* $: s("hh*16").orbit(3)
|
||||
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth(1)
|
||||
*
|
||||
*/
|
||||
export const { duck } = registerControl('duckorbit', 'duck');
|
||||
|
||||
/**
|
||||
* The amount of ducking applied to target orbit
|
||||
*
|
||||
* Can vary across orbits with the ':' mininotation, e.g. `duckdepth("0.3:0.1")`.
|
||||
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
|
||||
*
|
||||
* @name duckdepth
|
||||
* @param {number | Pattern} depth depth of modulation from 0 to 1
|
||||
* @example
|
||||
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>"))
|
||||
* @example
|
||||
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||
* $: s("hh*16").orbit(3)
|
||||
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth("1:0.5")
|
||||
*
|
||||
*/
|
||||
export const { duckdepth } = registerControl('duckdepth');
|
||||
|
||||
/**
|
||||
* The time required for the ducked signal(s) to reach their lowest volume.
|
||||
* Can be used to prevent clicking or for creative rhythmic effects.
|
||||
*
|
||||
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
|
||||
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
|
||||
*
|
||||
* @name duckonset
|
||||
* @synonyms duckons
|
||||
*
|
||||
* @param {number | Pattern} time The onset time in seconds
|
||||
* @example
|
||||
* // Clicks
|
||||
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
|
||||
* duckerWithClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0).postgain(0)
|
||||
* @example
|
||||
* // No clicks
|
||||
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
|
||||
* duckerWithoutClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0.01).postgain(0)
|
||||
* @example
|
||||
* // Rhythmic
|
||||
* noise: s("pink").distort("2:1").orbit(4) // used rhythmically with 0.3 onset below
|
||||
* hhat: s("hh*16").orbit(7)
|
||||
* ducker: s("bd*4").bank("tr909").duckorbit("4:7").duckonset("0.3:0.003").duckattack(0.25)
|
||||
*
|
||||
*/
|
||||
export const { duckonset } = registerControl('duckonset', 'duckons');
|
||||
|
||||
/**
|
||||
* The time required for the ducked signal(s) to return to their normal volume.
|
||||
*
|
||||
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
|
||||
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
|
||||
*
|
||||
* @name duckattack
|
||||
* @synonyms duckatt
|
||||
*
|
||||
* @param {number | Pattern} time The attack time in seconds
|
||||
* @example
|
||||
* sound: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
|
||||
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1)
|
||||
* @example
|
||||
* moreduck: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
|
||||
* lessduck: s("hh*16").orbit(5)
|
||||
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1")
|
||||
*
|
||||
*/
|
||||
export const { duckattack } = registerControl('duckattack', 'duckatt');
|
||||
|
||||
/**
|
||||
* Create byte beats with custom expressions
|
||||
*
|
||||
@@ -485,7 +916,7 @@ export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime',
|
||||
export const { channels, ch } = registerControl('channels', 'ch');
|
||||
|
||||
/**
|
||||
* controls the pulsewidth of the pulse oscillator
|
||||
* Controls the pulsewidth of the pulse oscillator
|
||||
*
|
||||
* @name pw
|
||||
* @param {number | Pattern} pulsewidth
|
||||
@@ -497,7 +928,7 @@ export const { channels, ch } = registerControl('channels', 'ch');
|
||||
export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
|
||||
|
||||
/**
|
||||
* controls the lfo rate for the pulsewidth of the pulse oscillator
|
||||
* Controls the lfo rate for the pulsewidth of the pulse oscillator
|
||||
*
|
||||
* @name pwrate
|
||||
* @param {number | Pattern} rate
|
||||
@@ -509,7 +940,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
|
||||
export const { pwrate } = registerControl('pwrate');
|
||||
|
||||
/**
|
||||
* controls the lfo sweep for the pulsewidth of the pulse oscillator
|
||||
* Controls the lfo sweep for the pulsewidth of the pulse oscillator
|
||||
*
|
||||
* @name pwsweep
|
||||
* @param {number | Pattern} sweep
|
||||
@@ -550,7 +981,7 @@ export const { phaserrate, ph, phaser } = registerControl(
|
||||
export const { phasersweep, phs } = registerControl('phasersweep', 'phs');
|
||||
|
||||
/**
|
||||
* The center frequency of the phaser in HZ. Defaults to 1000
|
||||
* The center frequency of the phaser in HZ. Defaults to 1000
|
||||
*
|
||||
* @name phasercenter
|
||||
* @synonyms phc
|
||||
@@ -567,7 +998,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
|
||||
* The amount the signal is affected by the phaser effect. Defaults to 0.75
|
||||
*
|
||||
* @name phaserdepth
|
||||
* @synonyms phd
|
||||
* @synonyms phd, phasdp
|
||||
* @param {number | Pattern} depth number between 0 and 1
|
||||
* @example
|
||||
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
|
||||
@@ -578,7 +1009,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
|
||||
export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp');
|
||||
|
||||
/**
|
||||
* choose the channel the pattern is sent to in superdirt
|
||||
* Choose the channel the pattern is sent to in superdirt
|
||||
*
|
||||
* @name channel
|
||||
* @param {number | Pattern} channel channel number
|
||||
@@ -930,7 +1361,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq');
|
||||
* @name djf
|
||||
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
|
||||
* @example
|
||||
* n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc()
|
||||
* n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>")
|
||||
*
|
||||
*/
|
||||
export const { djf } = registerControl('djf');
|
||||
@@ -964,26 +1395,55 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
|
||||
*
|
||||
*/
|
||||
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @synonyms delayfb, dfb
|
||||
* @example
|
||||
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>")
|
||||
*
|
||||
*/
|
||||
export const { delayspeed } = registerControl('delayspeed');
|
||||
/**
|
||||
* Sets the time of the delay effect.
|
||||
*
|
||||
* @name delaytime
|
||||
* @param {number | Pattern} seconds between 0 and Infinity
|
||||
* @name delayspeed
|
||||
* @param {number | Pattern} delayspeed controls the pitch of the delay feedback
|
||||
* @synonyms delayt, dt
|
||||
* @example
|
||||
* s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")
|
||||
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
|
||||
*
|
||||
*/
|
||||
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
||||
/* // TODO: test
|
||||
|
||||
/**
|
||||
* Sets the time of the delay effect in cycles.
|
||||
*
|
||||
* @name delaysync
|
||||
* @param {number | Pattern} cycles delay length in cycles
|
||||
* @synonyms delayt, dt
|
||||
* @example
|
||||
* s("bd bd").delay(.25).delaysync("<1 2 3 5>".div(8))
|
||||
*
|
||||
*/
|
||||
export const { delaysync } = registerControl('delaysync');
|
||||
|
||||
/**
|
||||
* Specifies whether delaytime is calculated relative to cps.
|
||||
*
|
||||
* @name lock
|
||||
* @param {number | Pattern} enable When set to 1, delaytime is a direct multiple of a cycle.
|
||||
* @superdirtOnly
|
||||
* @example
|
||||
* s("sd").delay().lock(1).osc()
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
export const { lock } = registerControl('lock');
|
||||
/**
|
||||
* Set detune for stacked voices of supported oscillators
|
||||
@@ -1033,6 +1493,7 @@ export const { dry } = registerControl('dry');
|
||||
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
|
||||
*
|
||||
* @name fadeTime
|
||||
* @synonyms fadeOutTime
|
||||
* @param {number | Pattern} time between 0 and 1
|
||||
* @example
|
||||
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
||||
@@ -1367,6 +1828,29 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade');
|
||||
*
|
||||
*/
|
||||
export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse');
|
||||
|
||||
/**
|
||||
* Sets speed of the sample for the impulse response.
|
||||
* @name irspeed
|
||||
* @param {string | Pattern} speed
|
||||
* @example
|
||||
* samples('github:switchangel/pad')
|
||||
* $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.2).irspeed("<2 1 .5>/2").irbegin(.5).roomsize(.5)
|
||||
*
|
||||
*/
|
||||
export const { irspeed } = registerControl('irspeed');
|
||||
|
||||
/**
|
||||
* Sets the beginning of the IR response sample
|
||||
* @name irbegin
|
||||
* @param {string | Pattern} begin between 0 and 1
|
||||
* @synonyms ir
|
||||
* @example
|
||||
* samples('github:switchangel/pad')
|
||||
* $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.65).irspeed("-2").irbegin("<0 .5 .75>/2").roomsize(.6)
|
||||
*
|
||||
*/
|
||||
export const { irbegin } = registerControl('irbegin');
|
||||
/**
|
||||
* Sets the room size of the reverb, see `room`.
|
||||
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
|
||||
@@ -1403,19 +1887,52 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size',
|
||||
export const { shape } = registerControl(['shape', 'shapevol']);
|
||||
/**
|
||||
* Wave shaping distortion. CAUTION: it can get loud.
|
||||
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output.
|
||||
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. Third option sets the waveshaping type.
|
||||
* Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)
|
||||
*
|
||||
* @name distort
|
||||
* @synonyms dist
|
||||
* @param {number | Pattern} distortion
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
* @param {number | string | Pattern} type type of distortion to apply
|
||||
* @example
|
||||
* s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>")
|
||||
* @example
|
||||
* note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4")
|
||||
* @example
|
||||
* s("bd:4*4").bank("tr808").distort("3:0.5:diode")
|
||||
*
|
||||
*/
|
||||
export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dist');
|
||||
export const { distort, dist } = registerControl(['distort', 'distortvol', 'distorttype'], 'dist');
|
||||
|
||||
/**
|
||||
* Postgain for waveshaping distortion.
|
||||
*
|
||||
* @name distortvol
|
||||
* @synonyms distvol
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
* @example
|
||||
* s("bd*4").bank("tr909").distort(2).distortvol(0.8)
|
||||
*/
|
||||
export const { distortvol } = registerControl('distortvol', 'distvol');
|
||||
|
||||
/**
|
||||
* Type of waveshaping distortion to apply.
|
||||
*
|
||||
* @name distorttype
|
||||
* @synonyms disttype
|
||||
* @param {number | string | Pattern} type type of distortion to apply
|
||||
* @example
|
||||
* s("bd*4").bank("tr909").distort(2).distorttype("<0 1 2>")
|
||||
*
|
||||
* @example
|
||||
* s("sine").note("F1*2").release(1)
|
||||
* .penv(24).pdecay(0.05)
|
||||
* .distort(rand.range(1, 8))
|
||||
* .distorttype("<fold chebyshev scurve diode asym sinefold>")
|
||||
*/
|
||||
export const { distorttype } = registerControl('distorttype', 'disttype');
|
||||
|
||||
/**
|
||||
* Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")`
|
||||
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
|
||||
@@ -1535,18 +2052,6 @@ export const { density } = registerControl('density');
|
||||
// ['modwheel'],
|
||||
export const { expression } = registerControl('expression');
|
||||
export const { sustainpedal } = registerControl('sustainpedal');
|
||||
/* // TODO: doesn't seem to do anything
|
||||
*
|
||||
* Tremolo Audio DSP effect
|
||||
*
|
||||
* @name tremolodepth
|
||||
* @param {number | Pattern} depth between 0 and 1
|
||||
* @example
|
||||
* n("0,4,7").tremolodepth("<0 .3 .6 .9>").osc()
|
||||
*
|
||||
*/
|
||||
export const { tremolodepth, tremdp } = registerControl('tremolodepth', 'tremdp');
|
||||
export const { tremolorate, tremr } = registerControl('tremolorate', 'tremr');
|
||||
|
||||
export const { fshift } = registerControl('fshift');
|
||||
export const { fshiftnote } = registerControl('fshiftnote');
|
||||
@@ -1623,7 +2128,6 @@ export const { zmod } = registerControl('zmod');
|
||||
// like crush but scaled differently
|
||||
export const { zcrush } = registerControl('zcrush');
|
||||
export const { zdelay } = registerControl('zdelay');
|
||||
export const { tremolo } = registerControl('tremolo');
|
||||
export const { zzfx } = registerControl('zzfx');
|
||||
|
||||
/**
|
||||
@@ -1816,6 +2320,24 @@ export const { miditouch } = registerControl('miditouch');
|
||||
// TODO: what is this?
|
||||
export const { polyTouch } = registerControl('polyTouch');
|
||||
|
||||
/**
|
||||
* The host to send open sound control messages to. Requires running the OSC bridge.
|
||||
* @name oschost
|
||||
* @param {string | Pattern} oschost e.g. 'localhost'
|
||||
* @example
|
||||
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
|
||||
*/
|
||||
export const { oschost } = registerControl('oschost');
|
||||
|
||||
/**
|
||||
* The port to send open sound control messages to. Requires running the OSC bridge.
|
||||
* @name oscport
|
||||
* @param {number | Pattern} oscport e.g. 57120
|
||||
* @example
|
||||
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
|
||||
*/
|
||||
export const { oscport } = registerControl('oscport');
|
||||
|
||||
export const getControlName = (alias) => {
|
||||
if (controlAlias.has(alias)) {
|
||||
return controlAlias.get(alias);
|
||||
|
||||
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import createClock from './zyklus.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { errorLogger, logger } from './logger.mjs';
|
||||
|
||||
export class Cyclist {
|
||||
constructor({
|
||||
@@ -57,7 +57,7 @@ export class Cyclist {
|
||||
}
|
||||
|
||||
// query the pattern for events
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
|
||||
|
||||
haps.forEach((hap) => {
|
||||
if (hap.hasOnset()) {
|
||||
@@ -67,6 +67,7 @@ export class Cyclist {
|
||||
// the following line is dumb and only here for backwards compatibility
|
||||
// see https://codeberg.org/uzu/strudel/pulls/1004
|
||||
const deadline = targetTime - phase;
|
||||
// this onTrigger has another signature
|
||||
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
|
||||
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
||||
this.cps = hap.value.cps;
|
||||
@@ -75,7 +76,7 @@ export class Cyclist {
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger(`[cyclist] error: ${e.message}`);
|
||||
errorLogger(e);
|
||||
onError?.(e);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,9 +10,9 @@ 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, register, silence } from './pattern.mjs';
|
||||
import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs';
|
||||
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
import Fraction, { lcm } from './fraction.mjs';
|
||||
|
||||
const left = function (n, x) {
|
||||
const [ons, offs] = n;
|
||||
@@ -42,29 +42,26 @@ const _bjork = function (n, x) {
|
||||
|
||||
export const bjork = function (ons, steps) {
|
||||
const inverted = ons < 0;
|
||||
ons = Math.abs(ons);
|
||||
const offs = steps - ons;
|
||||
const x = Array(ons).fill([1]);
|
||||
const y = Array(offs).fill([0]);
|
||||
const result = _bjork([ons, offs], [x, y]);
|
||||
const p = flatten(result[1][0]).concat(flatten(result[1][1]));
|
||||
if (inverted) {
|
||||
return p.map((x) => (x === 0 ? 1 : 0));
|
||||
}
|
||||
return p;
|
||||
const absOns = Math.abs(ons);
|
||||
const offs = steps - absOns;
|
||||
const ones = Array(absOns).fill([1]);
|
||||
const zeros = Array(offs).fill([0]);
|
||||
const result = _bjork([absOns, offs], [ones, zeros]);
|
||||
const pattern = flatten(result[1][0]).concat(flatten(result[1][1]));
|
||||
return inverted ? pattern.map((x) => 1 - x) : pattern;
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes the structure of the pattern to form an euclidean rhythm.
|
||||
* Euclidian rhythms are rhythms obtained using the greatest common
|
||||
* Changes the structure of the pattern to form an Euclidean rhythm.
|
||||
* Euclidean 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
|
||||
* Toussaint, a Canadian computer scientist. Euclidean 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} pulses the number of onsets/beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @returns Pattern
|
||||
* @example
|
||||
@@ -76,7 +73,7 @@ export const bjork = function (ons, steps) {
|
||||
* 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} pulses the number of onsets/beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param {number} rotation offset in steps
|
||||
* @returns Pattern
|
||||
@@ -86,13 +83,13 @@ export const bjork = function (ons, steps) {
|
||||
*/
|
||||
|
||||
/**
|
||||
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
|
||||
* @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").euclidRot(3,5,2)
|
||||
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
|
||||
* @example // A Ruchenitza rhythm used in a Bulgarian folk dance.
|
||||
* note("c3").euclid(3,7)
|
||||
* @example // The Cuban tresillo pattern.
|
||||
* note("c3").euclid(3,8)
|
||||
@@ -142,6 +139,14 @@ export const euclid = register('euclid', function (pulses, steps, pat) {
|
||||
return pat.struct(_euclidRot(pulses, steps, 0));
|
||||
});
|
||||
|
||||
export const e = register('e', function (euc, pat) {
|
||||
if (!Array.isArray(euc)) {
|
||||
euc = [euc];
|
||||
}
|
||||
const [pulses, steps = pulses, rot = 0] = euc;
|
||||
return pat.struct(_euclidRot(pulses, steps, rot));
|
||||
});
|
||||
|
||||
export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) {
|
||||
return pat.struct(_euclidRot(pulses, steps, rotation));
|
||||
});
|
||||
@@ -151,8 +156,10 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
|
||||
* so there will be no gaps.
|
||||
* @name euclidLegato
|
||||
* @memberof Pattern
|
||||
* @param {number} pulses the number of onsets / beats
|
||||
* @param {number} pulses the number of onsets/beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param rotation offset in steps
|
||||
* @param pat
|
||||
* @example
|
||||
* note("c3").euclidLegato(3,8)
|
||||
*/
|
||||
@@ -161,13 +168,13 @@ const _euclidLegato = function (pulses, steps, rotation, pat) {
|
||||
if (pulses < 1) {
|
||||
return silence;
|
||||
}
|
||||
const bin_pat = _euclidRot(pulses, steps, rotation);
|
||||
const bin_pat = _euclidRot(pulses, steps, 0);
|
||||
const gapless = bin_pat
|
||||
.join('')
|
||||
.split('1')
|
||||
.slice(1)
|
||||
.map((s) => [s.length + 1, true]);
|
||||
return pat.struct(timeCat(...gapless));
|
||||
return pat.struct(timeCat(...gapless)).late(Fraction(rotation).div(steps));
|
||||
};
|
||||
|
||||
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {
|
||||
@@ -180,7 +187,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
|
||||
* the resulting sequence
|
||||
* @name euclidLegatoRot
|
||||
* @memberof Pattern
|
||||
* @param {number} pulses the number of onsets / beats
|
||||
* @param {number} pulses the number of onsets/beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param {number} rotation offset in steps
|
||||
* @example
|
||||
@@ -189,3 +196,26 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
|
||||
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
||||
return _euclidLegato(pulses, steps, rotation, pat);
|
||||
});
|
||||
|
||||
/**
|
||||
* A 'euclid' variant with an additional parameter that morphs the resulting
|
||||
* rhythm from 0 (no morphing) to 1 (completely 'even'). For example
|
||||
* `sound("bd").euclidish(3,8,0)` would be the same as
|
||||
* `sound("bd").euclid(3,8)`, and `sound("bd").euclidish(3,8,1)` would be the
|
||||
* same as `sound("bd bd bd")`. `sound("bd").euclidish(3,8,0.5)` would have a
|
||||
* groove somewhere between.
|
||||
* Inspired by the work of Malcom Braff.
|
||||
* @name euclidish
|
||||
* @synonyms eish
|
||||
* @memberof Pattern
|
||||
* @param {number} pulses the number of onsets
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse)
|
||||
* @example
|
||||
* sound("hh").euclidish(7,12,sine.slow(8))
|
||||
* .pan(sine.slow(8))
|
||||
*/
|
||||
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
|
||||
const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc);
|
||||
return pat.struct(morphed).setSteps(steps);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const strudelScope = {};
|
||||
|
||||
export const evalScope = async (...args) => {
|
||||
const results = await Promise.allSettled(args);
|
||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||
@@ -18,6 +20,7 @@ export const evalScope = async (...args) => {
|
||||
modules.forEach((module) => {
|
||||
Object.entries(module).forEach(([name, value]) => {
|
||||
globalThis[name] = value;
|
||||
strudelScope[name] = value;
|
||||
});
|
||||
});
|
||||
return modules;
|
||||
|
||||
@@ -126,6 +126,8 @@ export const lcm = (...fractions) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const isFraction = (x) => x instanceof Fraction;
|
||||
|
||||
fraction._original = Fraction;
|
||||
|
||||
export default fraction;
|
||||
|
||||
@@ -4,6 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import Fraction from './fraction.mjs';
|
||||
import { stringifyValues } from './util.mjs';
|
||||
|
||||
export class Hap {
|
||||
/*
|
||||
@@ -148,13 +149,7 @@ export class Hap {
|
||||
}
|
||||
|
||||
showWhole(compact = false) {
|
||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
|
||||
typeof this.value === 'object'
|
||||
? compact
|
||||
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||
: JSON.stringify(this.value)
|
||||
: this.value
|
||||
}`;
|
||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`;
|
||||
}
|
||||
|
||||
combineContext(b) {
|
||||
|
||||
@@ -4,6 +4,13 @@ let debounce = 1000,
|
||||
lastMessage,
|
||||
lastTime;
|
||||
|
||||
export function errorLogger(e, origin = 'cyclist') {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error(e);
|
||||
}
|
||||
logger(`[${origin}] error: ${e.message}`);
|
||||
}
|
||||
|
||||
export function logger(message, type, data = {}) {
|
||||
let t = performance.now();
|
||||
if (lastMessage === message && t - lastTime < debounce) {
|
||||
|
||||
@@ -11,7 +11,6 @@ export class NeoCyclist {
|
||||
constructor({ onTrigger, onToggle, getTime }) {
|
||||
this.started = false;
|
||||
this.cps = 0.5;
|
||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||
this.getTime = getTime; // get absolute time
|
||||
this.time_at_last_tick_message = 0;
|
||||
// the clock of the worker and the audio context clock can drift apart over time
|
||||
@@ -39,8 +38,7 @@ export class NeoCyclist {
|
||||
if (this.started === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' });
|
||||
haps.forEach((hap) => {
|
||||
if (hap.hasOnset()) {
|
||||
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/core",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.5",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import TimeSpan from './timespan.mjs';
|
||||
import Fraction, { lcm } from './fraction.mjs';
|
||||
import Fraction, { isFraction, lcm } from './fraction.mjs';
|
||||
import Hap from './hap.mjs';
|
||||
import State from './state.mjs';
|
||||
import { unionWithObj } from './value.mjs';
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
numeralArgs,
|
||||
parseNumeral,
|
||||
pairs,
|
||||
zipWith,
|
||||
stringifyValues,
|
||||
} from './util.mjs';
|
||||
import drawLine from './drawLine.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
@@ -96,10 +98,7 @@ export class Pattern {
|
||||
|
||||
// runs func on query state
|
||||
withState(func) {
|
||||
return this.withHaps((haps, state) => {
|
||||
func(state);
|
||||
return haps;
|
||||
});
|
||||
return new Pattern((state) => this.query(func(state)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -852,14 +851,29 @@ export class Pattern {
|
||||
);
|
||||
}
|
||||
|
||||
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) {
|
||||
/**
|
||||
* Writes the content of the current event to the console (visible in the side menu).
|
||||
* @name log
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("bd sd").log()
|
||||
*/
|
||||
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
|
||||
return this.onTrigger((...args) => {
|
||||
logger(func(...args), undefined, getData(...args));
|
||||
}, false);
|
||||
}
|
||||
|
||||
logValues(func = id) {
|
||||
return this.log((_, hap) => func(hap.value));
|
||||
/**
|
||||
* A simplified version of `log` which writes all "values" (various configurable parameters)
|
||||
* within the event to the console (visible in the side menu).
|
||||
* @name logValues
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("bd sd").gain("0.25 0.5 1").n("2 1 0").logValues()
|
||||
*/
|
||||
logValues(func = (value) => `[hap] ${stringifyValues(value, true)}`) {
|
||||
return this.log((hap) => func(hap.value));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
@@ -869,6 +883,31 @@ export class Pattern {
|
||||
console.log(drawLine(this));
|
||||
return this;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// methods relating to breaking patterns into subcycles
|
||||
|
||||
// Breaks a pattern into a pattern of patterns, according to the structure of the given binary pattern.
|
||||
unjoin(pieces, func = id) {
|
||||
return pieces.withHap((hap) =>
|
||||
hap.withValue((v) => (v ? func(this.ribbon(hap.whole.begin, hap.whole.duration)) : this)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaks a pattern into pieces according to the structure of a given pattern.
|
||||
* True values in the given pattern cause the corresponding subcycle of the
|
||||
* source pattern to be looped, and for an (optional) given function to be
|
||||
* applied. False values result in the corresponding part of the source pattern
|
||||
* to be played unchanged.
|
||||
* @name into
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* sound("bd sd ht lt").into("1 0", hurry(2))
|
||||
*/
|
||||
into(pieces, func) {
|
||||
return this.unjoin(pieces, func).innerJoin();
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
@@ -904,12 +943,13 @@ Pattern.prototype.collect = function () {
|
||||
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
|
||||
* .arpWith(haps => haps[2])
|
||||
* */
|
||||
Pattern.prototype.arpWith = function (func) {
|
||||
return this.collect()
|
||||
export const arpWith = register('arpWith', (func, pat) => {
|
||||
return pat
|
||||
.collect()
|
||||
.fmap((v) => reify(func(v)))
|
||||
.innerJoin()
|
||||
.withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value)));
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Selects indices in in stacked notes.
|
||||
@@ -917,9 +957,11 @@ Pattern.prototype.arpWith = function (func) {
|
||||
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
|
||||
* .arp("0 [0,2] 1 [0,2]")
|
||||
* */
|
||||
Pattern.prototype.arp = function (pat) {
|
||||
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
|
||||
};
|
||||
export const arp = register(
|
||||
'arp',
|
||||
(indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])),
|
||||
false,
|
||||
);
|
||||
|
||||
/*
|
||||
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
|
||||
@@ -954,7 +996,7 @@ addToPrototype('weaveWith', function (t, ...funcs) {
|
||||
// compose matrix functions
|
||||
|
||||
function _nonArrayObject(x) {
|
||||
return !Array.isArray(x) && typeof x === 'object';
|
||||
return !Array.isArray(x) && typeof x === 'object' && !isFraction(x);
|
||||
}
|
||||
function _composeOp(a, b, func) {
|
||||
if (_nonArrayObject(a) || _nonArrayObject(b)) {
|
||||
@@ -1201,7 +1243,8 @@ export const silence = gap(1);
|
||||
/* Like silence, but with a 'steps' (relative duration) of 0 */
|
||||
export const nothing = gap(0);
|
||||
|
||||
/** A discrete value that repeats once per cycle.
|
||||
/**
|
||||
* A discrete value that repeats once per cycle.
|
||||
*
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
@@ -1254,13 +1297,14 @@ export function sequenceP(pats) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The given items are played at the same time at the same length.
|
||||
/**
|
||||
* 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()
|
||||
* // "g3,b3,[e4 d4]".note()
|
||||
*
|
||||
* @example
|
||||
* // As a chained function:
|
||||
@@ -1337,11 +1381,11 @@ export function stackBy(by, ...pats) {
|
||||
.setSteps(steps);
|
||||
}
|
||||
|
||||
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
|
||||
*
|
||||
* synonyms: `cat`
|
||||
/**
|
||||
* Concatenation: combines a list of patterns, switching between them successively, one per cycle.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @synonyms cat
|
||||
* @example
|
||||
* slowcat("e5", "b4", ["d5", "c5"])
|
||||
*
|
||||
@@ -1541,7 +1585,7 @@ export const func = curry((a, b) => reify(b).func(a));
|
||||
/**
|
||||
* 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 {string | string[]} name name of the function, or an array of names to be used as synonyms
|
||||
* @param {function} func function with 1 or more params, where last is the current pattern
|
||||
* @noAutocomplete
|
||||
*
|
||||
@@ -1981,6 +2025,7 @@ export const apply = register('apply', function (func, pat) {
|
||||
|
||||
/**
|
||||
* Plays the pattern at the given cycles per minute.
|
||||
* @deprecated
|
||||
* @example
|
||||
* s("<bd sd>,hh*2").cpm(90) // = 90 bpm
|
||||
*/
|
||||
@@ -2351,6 +2396,57 @@ export const stut = register('stut', function (times, feedback, time, pat) {
|
||||
return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
|
||||
});
|
||||
|
||||
export const applyN = register('applyN', function (n, func, p) {
|
||||
let result = p;
|
||||
for (let i = 0; i < n; i++) {
|
||||
result = func(result);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* The plyWith function repeats each event the given number of times, applying the given function to each event.\n
|
||||
* @name plyWith
|
||||
* @synonyms plywith
|
||||
* @param {number} factor how many times to repeat
|
||||
* @param {function} func function to apply, given the pattern
|
||||
* @example
|
||||
* "<0 [2 4]>"
|
||||
* .plyWith(4, (p) => p.add(2))
|
||||
* .scale("C:minor").note()
|
||||
*/
|
||||
export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) {
|
||||
const result = pat
|
||||
.fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor))
|
||||
.squeezeJoin();
|
||||
if (__steps) {
|
||||
result._steps = Fraction(factor).mulmaybe(pat._steps);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* The plyForEach function repeats each event the given number of times, applying the given function to each event.
|
||||
* This version of ply uses the iteration index as an argument to the function, similar to echoWith.
|
||||
* @name plyForEach
|
||||
* @synonyms plyforeach
|
||||
* @param {number} factor how many times to repeat
|
||||
* @param {function} func function to apply, given the pattern and the iteration index
|
||||
* @example
|
||||
* "<0 [2 4]>"
|
||||
* .plyForEach(4, (p,n) => p.add(n*2))
|
||||
* .scale("C:minor").note()
|
||||
*/
|
||||
export const plyForEach = register(['plyForEach', 'plyforeach'], function (factor, func, pat) {
|
||||
const result = pat
|
||||
.fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor))
|
||||
.squeezeJoin();
|
||||
if (__steps) {
|
||||
result._steps = Fraction(factor).mulmaybe(pat._steps);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -2490,6 +2586,37 @@ export const { fastchunk, fastChunk } = register(
|
||||
true,
|
||||
);
|
||||
|
||||
/**
|
||||
* Like `chunk`, but the function is applied to a looped subcycle of the source pattern.
|
||||
* @name chunkInto
|
||||
* @synonyms chunkinto
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
|
||||
* .bank("tr909")
|
||||
*/
|
||||
export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], function (n, func, pat) {
|
||||
return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iterback(n), func);
|
||||
});
|
||||
|
||||
/**
|
||||
* Like `chunkInto`, but moves backwards through the chunks.
|
||||
* @name chunkBackInto
|
||||
* @synonyms chunkbackinto
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
|
||||
* .bank("tr909")
|
||||
*/
|
||||
export const { chunkbackinto, chunkBackInto } = register(['chunkbackinto', 'chunkBackInto'], function (n, func, pat) {
|
||||
return pat.into(
|
||||
fastcat(true, ...Array(n - 1).fill(false))
|
||||
._iter(n)
|
||||
._early(1),
|
||||
func,
|
||||
);
|
||||
});
|
||||
|
||||
// TODO - redefine elsewhere in terms of mask
|
||||
export const bypass = register(
|
||||
'bypass',
|
||||
@@ -2505,7 +2632,7 @@ export const bypass = register(
|
||||
* Loops the pattern inside an `offset` for `cycles`.
|
||||
* If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it.
|
||||
* @name ribbon
|
||||
* @synonym rib
|
||||
* @synonyms rib
|
||||
* @param {number} offset start point of loop in cycles
|
||||
* @param {number} cycles loop length in cycles
|
||||
* @example
|
||||
@@ -2719,7 +2846,7 @@ export function stepcat(...timepats) {
|
||||
if (timepats.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
const findsteps = (x) => (Array.isArray(x) ? x : [x._steps, x]);
|
||||
const findsteps = (x) => (Array.isArray(x) ? x : [x._steps ?? 1, x]);
|
||||
timepats = timepats.map(findsteps);
|
||||
if (timepats.find((x) => x[0] === undefined)) {
|
||||
const times = timepats.map((a) => a[0]).filter((x) => x !== undefined);
|
||||
@@ -2871,6 +2998,24 @@ export const extend = stepRegister('extend', function (factor, pat) {
|
||||
return pat.fast(factor).expand(factor);
|
||||
});
|
||||
|
||||
/**
|
||||
* *Experimental*
|
||||
*
|
||||
* `replicate` is similar to `fast` in that it increases its density, but it also increases the step count
|
||||
* accordingly. So `stepcat("a b".replicate(2), "c d")` would be the same as `"a b a b c d"`, whereas
|
||||
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
|
||||
*
|
||||
* TODO: find out how this function differs from extend
|
||||
* @example
|
||||
* stepcat(
|
||||
* sound("bd bd - cp").replicate(2),
|
||||
* sound("bd - sd -")
|
||||
* ).pace(8)
|
||||
*/
|
||||
export const replicate = stepRegister('replicate', function (factor, pat) {
|
||||
return pat.repeatCycles(factor).fast(factor).expand(factor);
|
||||
});
|
||||
|
||||
/**
|
||||
* *Experimental*
|
||||
*
|
||||
@@ -3200,10 +3345,10 @@ export const slice = register(
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("bd!8").onTriggerTime((hap) => {console.info(hap)})
|
||||
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
|
||||
*/
|
||||
Pattern.prototype.onTriggerTime = function (func) {
|
||||
return this.onTrigger((t_deprecate, hap, currentTime, cps = 1, targetTime) => {
|
||||
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
|
||||
const diff = targetTime - currentTime;
|
||||
window.setTimeout(() => {
|
||||
func(hap);
|
||||
@@ -3340,3 +3485,142 @@ export const { beat } = register(
|
||||
['beat'],
|
||||
__beat((x) => x.innerJoin()),
|
||||
);
|
||||
|
||||
export const _morph = (from, to, by) => {
|
||||
by = Fraction(by);
|
||||
const dur = Fraction(1).div(from.length);
|
||||
const positions = (list) => {
|
||||
const result = [];
|
||||
for (const [pos, value] of list.entries()) {
|
||||
if (value) {
|
||||
result.push([Fraction(pos).div(list.length), value]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const arcs = zipWith(
|
||||
([posa, valuea], [posb, valueb]) => {
|
||||
const b = by.mul(posb - posa).add(posa);
|
||||
const e = b.add(dur);
|
||||
return new TimeSpan(b, e);
|
||||
},
|
||||
positions(from),
|
||||
positions(to),
|
||||
);
|
||||
function query(state) {
|
||||
const cycle = state.span.begin.sam();
|
||||
const cycleArc = state.span.cycleArc();
|
||||
const result = [];
|
||||
for (const whole of arcs) {
|
||||
const part = whole.intersection(cycleArc);
|
||||
if (part !== undefined) {
|
||||
result.push(
|
||||
new Hap(
|
||||
whole.withTime((x) => x.add(cycle)),
|
||||
part.withTime((x) => x.add(cycle)),
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return new Pattern(query).splitQueries();
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes two binary rhythms represented as lists of 1s and 0s, and a number
|
||||
* between 0 and 1 that morphs between them. The two lists should contain the same
|
||||
* number of true values.
|
||||
* @example
|
||||
* sound("hh").struct(morph([1,0,1,0,1,0,1,0], // straight rhythm
|
||||
* [1,1,0,1,0,1,0], // wonky rhythm
|
||||
* 0.25 // creates a slightly wonky rhythm
|
||||
* )
|
||||
* )
|
||||
* @example
|
||||
* sound("hh").struct(morph("1:0:1:0:1:0:1:0", // straight rhythm
|
||||
* "1:1:0:1:0:1:0", // wonky rhythm
|
||||
* sine.slow(8) // slowly morph between the rhythms
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
export const morph = (frompat, topat, bypat) => {
|
||||
frompat = reify(frompat);
|
||||
topat = reify(topat);
|
||||
bypat = reify(bypat);
|
||||
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
||||
};
|
||||
|
||||
/**
|
||||
* Soft-clipping distortion
|
||||
*
|
||||
* @name soft
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Hard-clipping distortion
|
||||
*
|
||||
* @name hard
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Cubic polynomial distortion
|
||||
*
|
||||
* @name cubic
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Diode-emulating distortion
|
||||
*
|
||||
* @name diode
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Asymmetrical diode distortion
|
||||
*
|
||||
* @name asym
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Wavefolding distortion
|
||||
*
|
||||
* @name fold
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Wavefolding distortion composed with sinusoid
|
||||
*
|
||||
* @name sinefold
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Distortion via Chebyshev polynomials
|
||||
*
|
||||
* @name chebyshev
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev'];
|
||||
for (const name of distAlgoNames) {
|
||||
// Add aliases for distortion algorithms
|
||||
Pattern.prototype[name] = function (args) {
|
||||
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
|
||||
return this.distort(argsPat);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NeoCyclist } from './neocyclist.mjs';
|
||||
import { Cyclist } from './cyclist.mjs';
|
||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { errorLogger, logger } from './logger.mjs';
|
||||
import { setTime } from './time.mjs';
|
||||
import { evalScope } from './evaluate.mjs';
|
||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||
@@ -21,6 +21,7 @@ export function repl({
|
||||
setInterval,
|
||||
clearInterval,
|
||||
id,
|
||||
mondo = false,
|
||||
}) {
|
||||
const state = {
|
||||
schedulerError: undefined,
|
||||
@@ -73,6 +74,14 @@ export function repl({
|
||||
return silence;
|
||||
};
|
||||
|
||||
// helper to get a patternified pure value out
|
||||
function unpure(pat) {
|
||||
if (pat._Pattern) {
|
||||
return pat.__pure;
|
||||
}
|
||||
return pat;
|
||||
}
|
||||
|
||||
const setPattern = async (pattern, autostart = true) => {
|
||||
pattern = editPattern?.(pattern) || pattern;
|
||||
await scheduler.setPattern(pattern, autostart);
|
||||
@@ -84,8 +93,25 @@ export function repl({
|
||||
const start = () => scheduler.start();
|
||||
const pause = () => scheduler.pause();
|
||||
const toggle = () => scheduler.toggle();
|
||||
const setCps = (cps) => scheduler.setCps(cps);
|
||||
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
||||
const setCps = (cps) => {
|
||||
scheduler.setCps(unpure(cps));
|
||||
return silence;
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes the global tempo to the given cycles per minute
|
||||
*
|
||||
* @name setcpm
|
||||
* @alias setCpm
|
||||
* @param {number} cpm cycles per minute
|
||||
* @example
|
||||
* setcpm(140/4) // =140 bpm in 4/4
|
||||
* $: s("bd*4,[- sd]*2").bank('tr707')
|
||||
*/
|
||||
const setCpm = (cpm) => {
|
||||
scheduler.setCps(unpure(cpm) / 60);
|
||||
return silence;
|
||||
};
|
||||
|
||||
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
|
||||
|
||||
@@ -102,8 +128,9 @@ export function repl({
|
||||
* all(x => x.pianoroll())
|
||||
* ```
|
||||
*/
|
||||
let allTransforms = [];
|
||||
const all = function (transform) {
|
||||
allTransform = transform;
|
||||
allTransforms.push(transform);
|
||||
return silence;
|
||||
};
|
||||
/** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern.
|
||||
@@ -179,10 +206,18 @@ export function repl({
|
||||
await injectPatternMethods();
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
await beforeEval?.({ code });
|
||||
allTransforms = []; // reset all transforms
|
||||
shouldHush && hush();
|
||||
|
||||
if (mondo) {
|
||||
code = `mondolang\`${code}\``;
|
||||
}
|
||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||
if (Object.keys(pPatterns).length) {
|
||||
let patterns = Object.values(pPatterns);
|
||||
let patterns = [];
|
||||
for (const [key, value] of Object.entries(pPatterns)) {
|
||||
patterns.push(value.withState((state) => state.setControls({ id: key })));
|
||||
}
|
||||
if (eachTransform) {
|
||||
// Explicit lambda so only element (not index and array) are passed
|
||||
patterns = patterns.map((x) => eachTransform(x));
|
||||
@@ -191,9 +226,12 @@ export function repl({
|
||||
} else if (eachTransform) {
|
||||
pattern = eachTransform(pattern);
|
||||
}
|
||||
if (allTransform) {
|
||||
pattern = allTransform(pattern);
|
||||
if (allTransforms.length) {
|
||||
for (let i in allTransforms) {
|
||||
pattern = allTransforms[i](pattern);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPattern(pattern)) {
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||
@@ -225,6 +263,7 @@ export function repl({
|
||||
export const getTrigger =
|
||||
({ getTime, defaultOutput }) =>
|
||||
async (hap, deadline, duration, cps, t) => {
|
||||
// ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
|
||||
// TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
|
||||
try {
|
||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||
@@ -232,9 +271,9 @@ export const getTrigger =
|
||||
}
|
||||
if (hap.context.onTrigger) {
|
||||
// call signature of output / onTrigger is different...
|
||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t);
|
||||
await hap.context.onTrigger(hap, getTime(), cps, t);
|
||||
}
|
||||
} catch (err) {
|
||||
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||
errorLogger(err, 'getTrigger');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,7 +71,6 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||
|
||||
/**
|
||||
* A sine signal between 0 and 1.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* n(sine.segment(16).range(0,15))
|
||||
@@ -100,7 +99,6 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||
|
||||
/**
|
||||
* A square signal between 0 and 1.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* n(square.segment(4).range(0,7)).scale("C:minor")
|
||||
@@ -266,7 +264,7 @@ export const randrun = (n) => {
|
||||
const rands = timeToRands(t.floor().add(0.5), n);
|
||||
const nums = rands
|
||||
.map((n, i) => [n, i])
|
||||
.sort((a, b) => a[0] > b[0] - a[0] < b[0])
|
||||
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
||||
.map((x) => x[1]);
|
||||
const i = t.cyclePos().mul(n).floor() % n;
|
||||
return nums[i];
|
||||
@@ -285,7 +283,7 @@ const _rearrangeWith = (ipat, n, pat) => {
|
||||
* @example
|
||||
* note("c d e f").sound("piano").shuffle(4)
|
||||
* @example
|
||||
* note("c d e f".shuffle(4), "g").sound("piano")
|
||||
* seq("c d e f".shuffle(4), "g").note().sound("piano")
|
||||
*/
|
||||
export const shuffle = register('shuffle', (n, pat) => {
|
||||
return _rearrangeWith(randrun(n), n, pat);
|
||||
@@ -298,7 +296,7 @@ export const shuffle = register('shuffle', (n, pat) => {
|
||||
* @example
|
||||
* note("c d e f").sound("piano").scramble(4)
|
||||
* @example
|
||||
* note("c d e f".scramble(4), "g").sound("piano")
|
||||
* seq("c d e f".scramble(4), "g").note().sound("piano")
|
||||
*/
|
||||
export const scramble = register('scramble', (n, pat) => {
|
||||
return _rearrangeWith(_irand(n)._segment(n), n, pat);
|
||||
@@ -398,6 +396,10 @@ export const chooseInWith = (pat, xs) => {
|
||||
*/
|
||||
export const choose = (...xs) => chooseWith(rand, xs);
|
||||
|
||||
// todo: doc
|
||||
export const chooseIn = (...xs) => chooseInWith(rand, xs);
|
||||
export const chooseOut = choose;
|
||||
|
||||
/**
|
||||
* Chooses from the given list of values (or patterns of values), according
|
||||
* to the pattern that the method is called on. The pattern should be in
|
||||
|
||||
@@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) {
|
||||
}
|
||||
|
||||
export const speak = register('speak', function (lang, voice, pat) {
|
||||
return pat.onTrigger((_, hap) => {
|
||||
return pat.onTrigger((hap) => {
|
||||
triggerSpeech(hap.value, lang, voice);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,9 +19,9 @@ export class State {
|
||||
return this.setSpan(func(this.span));
|
||||
}
|
||||
|
||||
// Returns new State with different controls
|
||||
// Returns new State with added controls.
|
||||
setControls(controls) {
|
||||
return new State(this.span, controls);
|
||||
return new State(this.span, { ...this.controls, ...controls });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
controls.test.mjs - <short description TODO>
|
||||
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/controls.test.mjs>
|
||||
Copyright (C) 2023 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/controls.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
drawLine.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/drawLine.test.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/drawLine.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { bjork } from '../euclid.mjs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { fastcat } from '../pattern.mjs';
|
||||
|
||||
describe('bjork', () => {
|
||||
it('should apply bjorklund to ons and steps', () => {
|
||||
expect(bjork(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
|
||||
expect(bjork(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
|
||||
expect(bjork(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
|
||||
expect(bjork(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
expect(bjork(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('euclid', () => {
|
||||
it('Can create euclid', () => {
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclid(3, 8)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 1/8: a', '3/8 → 1/2: a', '3/4 → 7/8: a']);
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclid(5, 8)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '3/8 → 1/2: a', '5/8 → 3/4: a', '3/4 → 7/8: a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('euclidRot', () => {
|
||||
it('Can create euclidRot', () => {
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclidRot(3, 8, 2)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '5/8 → 3/4: a']);
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclidRot(5, 8, 2)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '1/2 → 5/8: a', '5/8 → 3/4: a', '7/8 → 1/1: a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('euclidLegato', () => {
|
||||
it('Can create euclidLegato', () => {
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclidLegato(3, 8)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 3/8: a', '3/8 → 3/4: a', '3/4 → 1/1: a']);
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclidLegato(5, 8)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 1/4: a', '1/4 → 3/8: a', '3/8 → 5/8: a', '5/8 → 3/4: a', '3/4 → 1/1: a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('euclidLegatoRot', () => {
|
||||
it('Can create euclidLegatoRot', () => {
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclidLegatoRot(3, 8, 2)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 1/4: a', '1/4 → 5/8: a', '5/8 → 1/1: a']);
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclidLegatoRot(5, 8, 2)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 1/4: a', '1/4 → 1/2: a', '1/2 → 5/8: a', '5/8 → 7/8: a', '7/8 → 1/1: a']);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
fraction.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/fraction.test.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/fraction.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
pattern.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/pattern.test.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/pattern.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Fraction from 'fraction.js';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
TimeSpan,
|
||||
@@ -52,8 +52,11 @@ import {
|
||||
stackCentre,
|
||||
stepcat,
|
||||
sometimes,
|
||||
expand,
|
||||
} from '../index.mjs';
|
||||
|
||||
import { log, logValues } from '../pattern.mjs';
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
|
||||
import { n, s } from '../controls.mjs';
|
||||
@@ -884,7 +887,7 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
it('Doesnt drop haps in the 9th cycle', () => {
|
||||
// fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
|
||||
// fixed with https://codeberg.org/uzu/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
|
||||
expect(sequence(1, 2, 3).ply(2).early(8).firstCycle().length).toBe(6);
|
||||
});
|
||||
});
|
||||
@@ -1001,7 +1004,7 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('hurry', () => {
|
||||
it('Can speed up patterns and sounds', () => {
|
||||
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
|
||||
sameFirst(s(sequence('a', 'b')).hurry(2), s(sequence('a', 'b')).fast(2).speed(2));
|
||||
});
|
||||
});
|
||||
/*describe('composable functions', () => {
|
||||
@@ -1179,6 +1182,9 @@ describe('Pattern', () => {
|
||||
it('calculates undefined steps as the average', () => {
|
||||
expect(sameFirst(stepcat(pure(1), pure(2), pure(3).setSteps(undefined)), fastcat(1, 2, 3)));
|
||||
});
|
||||
it('works with auto-reified values', () => {
|
||||
expect(sameFirst(stepcat(expand(3, 'bd'), 'rim'), stepcat(expand(3, 'bd'), pure('rim'))));
|
||||
});
|
||||
});
|
||||
describe('shrink', () => {
|
||||
it('can shrink', () => {
|
||||
@@ -1267,4 +1273,75 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('unjoin', () => {
|
||||
it('destructures a pattern into subcycles', () => {
|
||||
sameFirst(
|
||||
fastcat('a', 'b', 'c', 'd')
|
||||
.unjoin(fastcat(true, fastcat(true, true)))
|
||||
.fmap(fast(2))
|
||||
.join(),
|
||||
fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd'),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('into', () => {
|
||||
it('applies a function to subcycles of a pattern', () => {
|
||||
sameFirst(
|
||||
fastcat('a', 'b', 'c', 'd').into(fastcat(fastcat('true', 'true'), 'true'), fast(2)),
|
||||
fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd'),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('chunkinto', () => {
|
||||
it('chunks into subcycles', () => {
|
||||
sameFirst(
|
||||
fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3),
|
||||
fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('chunkbackinto', () => {
|
||||
it('chunks into subcycles backwards', () => {
|
||||
sameFirst(
|
||||
fastcat('a', 'b', 'c').chunkBackInto(3, fast(2)).fast(3),
|
||||
fastcat('a', 'b', fastcat('c', 'c'), 'a', fastcat('b', 'b'), 'c', fastcat('a', 'a'), 'b', 'c'),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('log', () => {
|
||||
it('logs to console', () => {
|
||||
const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const pattern = pure('a').log();
|
||||
const haps = pattern.queryArc(0, 1);
|
||||
|
||||
// Force a trigger
|
||||
haps.forEach((hap) => {
|
||||
hap.context?.onTrigger?.(hap);
|
||||
});
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||
'%c[hap] 0/1 → 1/1: a',
|
||||
'background-color: black;color:white;border-radius:15px',
|
||||
);
|
||||
mockConsoleLog.mockRestore();
|
||||
});
|
||||
});
|
||||
describe('logValues', () => {
|
||||
it('logs values to console', () => {
|
||||
const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const pattern = pure('a').note('c#').logValues();
|
||||
const haps = pattern.queryArc(0, 1);
|
||||
|
||||
// Force a trigger
|
||||
haps.forEach((hap) => {
|
||||
hap.context?.onTrigger?.(hap);
|
||||
});
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||
'%c[hap] value:a note:c#',
|
||||
'background-color: black;color:white;border-radius:15px',
|
||||
);
|
||||
mockConsoleLog.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
/*
|
||||
util.test.mjs - Tests for the core 'util' module
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/util.test.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/util.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { pure } from '../pattern.mjs';
|
||||
import {
|
||||
isNote,
|
||||
tokenizeNote,
|
||||
noteToMidi,
|
||||
midiToFreq,
|
||||
freqToMidi,
|
||||
_mod,
|
||||
compose,
|
||||
flatten,
|
||||
fractionalArgs,
|
||||
freqToMidi,
|
||||
getFrequency,
|
||||
getPlayableNoteValue,
|
||||
parseNumeral,
|
||||
parseFractional,
|
||||
isNote,
|
||||
midiToFreq,
|
||||
noteToMidi,
|
||||
numeralArgs,
|
||||
fractionalArgs,
|
||||
parseFractional,
|
||||
parseNumeral,
|
||||
rotate,
|
||||
splitAt,
|
||||
tokenizeNote,
|
||||
zipWith,
|
||||
} from '../util.mjs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('isNote', () => {
|
||||
it('should recognize notes without accidentals', () => {
|
||||
@@ -233,3 +237,46 @@ describe('fractionalArgs', () => {
|
||||
expect(add('q', 2)).toBe(2.25);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rotate', () => {
|
||||
it('should rotate array to the left', () => {
|
||||
expect(rotate([0, 1, 2, 3], 2)).toStrictEqual([2, 3, 0, 1]);
|
||||
expect(rotate([0, 1, 2, 3], 0)).toStrictEqual([0, 1, 2, 3]);
|
||||
expect(rotate([0, 1, 2, 3], -3)).toStrictEqual([1, 2, 3, 0]);
|
||||
expect(rotate([0, 1, 2, 3], 3)).toStrictEqual([3, 0, 1, 2]);
|
||||
expect(rotate([0], 3)).toStrictEqual([0]);
|
||||
expect(rotate([], 3)).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flatten', () => {
|
||||
it('should flatten array by one level', () => {
|
||||
expect(flatten([0, 1, 2, 3])).toStrictEqual([0, 1, 2, 3]);
|
||||
expect(flatten([0, 1, [2, 3]])).toStrictEqual([0, 1, 2, 3]);
|
||||
expect(flatten([0, [1, [2, 3]]])).toStrictEqual([0, 1, [2, 3]]);
|
||||
expect(flatten([0])).toStrictEqual([0]);
|
||||
expect(flatten([])).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitAt', () => {
|
||||
it('should split array into two', () => {
|
||||
expect(splitAt(2, [0, 1, 2, 3])).toStrictEqual([
|
||||
[0, 1],
|
||||
[2, 3],
|
||||
]);
|
||||
expect(splitAt(0, [0, 1, 2, 3])).toStrictEqual([[], [0, 1, 2, 3]]);
|
||||
expect(splitAt(-3, [0, 1, 2, 3])).toStrictEqual([[0], [1, 2, 3]]);
|
||||
expect(splitAt(3, [0, 1, 2, 3])).toStrictEqual([[0, 1, 2], [3]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('zipWith', () => {
|
||||
it('should use the function to combine the two arrays element-wise', () => {
|
||||
expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2, 3])).toStrictEqual([0, 2, 4, 6]);
|
||||
expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2])).toStrictEqual([0, 2, 4, NaN]);
|
||||
expect(zipWith((a, b) => a + b, [0, 1, 2], [0, 1, 2, 3])).toStrictEqual([0, 2, 4]);
|
||||
expect(zipWith((a) => a, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([0, 1, 2]);
|
||||
expect(zipWith((a, b) => b, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ export class TimeSpan {
|
||||
}
|
||||
|
||||
intersection(other) {
|
||||
// Intersection of two timespans, returns None if they don't intersect.
|
||||
// Intersection of two timespans, returns undefined if they don't intersect.
|
||||
const intersect_begin = this.begin.max(other.begin);
|
||||
const intersect_end = this.end.min(other.end);
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import { logger } from './logger.mjs';
|
||||
|
||||
// returns true if the given string is a note
|
||||
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
||||
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
|
||||
export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]?$/.test(name);
|
||||
export const tokenizeNote = (note) => {
|
||||
if (typeof note !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
|
||||
if (!pc) {
|
||||
return [];
|
||||
}
|
||||
@@ -162,6 +162,7 @@ export const compose = (...funcs) => pipe(...funcs.reverse());
|
||||
// Removes 'None' values from given list
|
||||
export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
|
||||
|
||||
// flattens by one level
|
||||
export const flatten = (arr) => [].concat(...arr);
|
||||
|
||||
export const id = (a) => a;
|
||||
@@ -237,6 +238,7 @@ export const splitAt = function (index, value) {
|
||||
return [value.slice(0, index), value.slice(index)];
|
||||
};
|
||||
|
||||
// Uses the function f to combine the arrays xs, ys element-wise
|
||||
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||
|
||||
export const pairs = function (xs) {
|
||||
@@ -485,3 +487,13 @@ export function getCurrentKeyboardState() {
|
||||
// }
|
||||
// return lcm((x * y) / gcd(x, y), ...z);
|
||||
// };
|
||||
|
||||
// Takes values -- typically derived from events, i.e. `hap`s -- and renders them
|
||||
// into a readable format
|
||||
export function stringifyValues(value, compact = false) {
|
||||
return typeof value === 'object'
|
||||
? compact
|
||||
? JSON.stringify(value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||
: JSON.stringify(value)
|
||||
: value;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
|
||||
instrument = instrument || 'triangle';
|
||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
||||
return pat.onTrigger((time_deprecate, hap, currentTime, _cps, targetTime) => {
|
||||
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
|
||||
if (!_csound) {
|
||||
logger('[csound] not loaded yet', 'warning');
|
||||
return;
|
||||
@@ -142,7 +142,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
||||
p1 = `"${instrument}"`;
|
||||
}
|
||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||
return pat.onTrigger((tidal_time, hap) => {
|
||||
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
|
||||
if (!_csound) {
|
||||
logger('[csound] not loaded yet', 'warning');
|
||||
return;
|
||||
@@ -151,7 +151,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
||||
throw new Error('csound only support objects as hap values');
|
||||
}
|
||||
// Time in seconds counting from now.
|
||||
const p2 = tidal_time - getAudioContext().currentTime;
|
||||
const p2 = targetTime - currentTime;
|
||||
const p3 = hap.duration.valueOf() + 0;
|
||||
const frequency = getFrequency(hap);
|
||||
let { gain = 1, velocity = 0.9 } = hap.value;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/csound",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.6",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -6,7 +6,7 @@ const OFF_MESSAGE = 0x80;
|
||||
const CC_MESSAGE = 0xb0;
|
||||
|
||||
Pattern.prototype.midi = function (output) {
|
||||
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
return this.onTrigger((hap, currentTime, cps, targetTime) => {
|
||||
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
|
||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
||||
const latencyMs = 34;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Invoke } from './utils.mjs';
|
||||
|
||||
const collator = new ClockCollator({});
|
||||
|
||||
export async function oscTriggerTauri(t_deprecate, hap, currentTime, cps = 1, targetTime) {
|
||||
export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
|
||||
const controls = parseControlsFromHap(hap, cps);
|
||||
const params = [];
|
||||
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
|
||||
|
||||
@@ -84,9 +84,18 @@ Pattern.prototype.onPaint = function (painter) {
|
||||
state.controls.painters = [];
|
||||
}
|
||||
state.controls.painters.push(painter);
|
||||
return state;
|
||||
});
|
||||
};
|
||||
|
||||
// TODO - Why isn't this pure deep copy not working?
|
||||
// Pattern.prototype.onPaint = function (painter) {
|
||||
// return this.withState((state) => {
|
||||
// const painters = state.controls.painters ? [...state.controls.painters, painter] : [painter];
|
||||
// return new State(state.span, { ...state.controls, painters });
|
||||
// });
|
||||
// };
|
||||
|
||||
Pattern.prototype.getPainters = function () {
|
||||
let painters = [];
|
||||
this.queryArc(0, 0, { painters });
|
||||
@@ -150,7 +159,7 @@ export class Drawer {
|
||||
this.lastFrame = phase;
|
||||
this.visibleHaps = (this.visibleHaps || [])
|
||||
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
|
||||
.filter((h) => h.endClipped >= phase - lookbehind - lookahead)
|
||||
.filter((h) => h.whole && h.endClipped >= phase - lookbehind - lookahead)
|
||||
// add new haps with onset (think right edge bars scrolling in)
|
||||
.concat(haps.filter((h) => h.hasOnset()));
|
||||
const time = phase - lookahead;
|
||||
@@ -175,7 +184,7 @@ export class Drawer {
|
||||
// +0.1 = workaround for weird holes in query..
|
||||
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
|
||||
// remove all future haps
|
||||
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
|
||||
this.visibleHaps = this.visibleHaps.filter((h) => h.whole?.begin < t);
|
||||
this.painters = []; // will get populated by .onPaint calls attached to the pattern
|
||||
// query future haps
|
||||
const futureHaps = scheduler.pattern.queryArc(begin, end, { painters: this.painters });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/draw",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.5",
|
||||
"description": "Helpers for drawing with Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/embed",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
||||
"main": "embed.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -56,14 +56,15 @@ You can use button inputs to control different aspects of your music, such as ga
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`const gp = gamepad(0)
|
||||
// Use button values to control amplitude
|
||||
setcpm(120)
|
||||
// Use button values to control amplitude
|
||||
$: stack(
|
||||
s("[[hh hh] oh hh oh]/2").mask(gp.tglX).bank("RolandTR909"), // X btn for HH
|
||||
s("cr*1").mask(gp.Y).bank("RolandTR909"), // LB btn for CR
|
||||
s("bd").mask(gp.tglA).bank("RolandTR909"), // A btn for BD
|
||||
s("[ht - - mt - - lt - ]/2").mask(gp.tglB).bank("RolandTR909"), // B btn for Toms
|
||||
s("sd*4").mask(gp.RB).bank("RolandTR909"), // RB btn for SD
|
||||
).cpm(120)
|
||||
)
|
||||
`}
|
||||
/>
|
||||
|
||||
@@ -74,14 +75,13 @@ Analog sticks can be used for continuous control, such as pitch shifting or pann
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`const gp = gamepad(0)
|
||||
// Use analog stick for continuous control
|
||||
setcpm(120)
|
||||
// Use analog stick for continuous control
|
||||
$: note("c4 d3 a3 e3").sound("sawtooth")
|
||||
.lpf(gp.x1.range(100,4000))
|
||||
.lpq(gp.y1.range(5,30))
|
||||
.decay(gp.y2.range(0.1,2))
|
||||
.lpenv(gp.x2.range(-5,5))
|
||||
.cpm(120)
|
||||
`}
|
||||
.lpenv(gp.x2.range(-5,5))`}
|
||||
/>
|
||||
|
||||
### Button Sequences
|
||||
@@ -89,6 +89,7 @@ $: note("c4 d3 a3 e3").sound("sawtooth")
|
||||
You can define button sequences to trigger specific actions, like playing a sound when a sequence is detected.
|
||||
|
||||
<MiniRepl client:idle tune={`const gp = gamepad(0)
|
||||
setcpm(120)
|
||||
// Define button sequences
|
||||
const HADOUKEN = [
|
||||
'd', // Down
|
||||
@@ -99,7 +100,7 @@ const KONAMI = 'uuddlrlrba' //Konami Code ↑↑↓↓←→←→BA
|
||||
|
||||
// Check butto-n sequence (returns 1 while detected, 0 when not within last 1 second)
|
||||
$: s("free_hadouken -").slow(2)
|
||||
.mask(gp.btnSequence(HADOUKEN)).room(1).cpm(120)
|
||||
.mask(gp.btnSequence(HADOUKEN)).room(1)
|
||||
|
||||
// hadouken.wav by Syna-Max
|
||||
//https://freesound.org/people/Syna-Max/sounds/67674/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/gamepad",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.5",
|
||||
"description": "Gamepad Inputs for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/hydra",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.5",
|
||||
"description": "Hydra integration for strudel",
|
||||
"main": "hydra.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -333,7 +333,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
||||
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
||||
});
|
||||
|
||||
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
return this.onTrigger((hap, currentTime, cps, targetTime) => {
|
||||
if (!WebMidi.enabled) {
|
||||
logger('Midi not enabled');
|
||||
return;
|
||||
@@ -493,6 +493,9 @@ export async function midin(input) {
|
||||
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
// ensure refs for this input are initialized
|
||||
if (!refs[input]) {
|
||||
refs[input] = {};
|
||||
}
|
||||
const cc = (cc) => ref(() => refs[input][cc] || 0);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/midi",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.6",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, bench } from 'vitest';
|
||||
|
||||
import { calculateTactus } from '../../core/index.mjs';
|
||||
import { calculateSteps } from '../../core/index.mjs';
|
||||
import { mini } from '../index.mjs';
|
||||
|
||||
describe('mini', () => {
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
bench(
|
||||
'+tactus',
|
||||
() => {
|
||||
@@ -13,7 +13,7 @@ describe('mini', () => {
|
||||
{ time: 1000 },
|
||||
);
|
||||
|
||||
calculateTactus(false);
|
||||
calculateSteps(false);
|
||||
bench(
|
||||
'-tactus',
|
||||
() => {
|
||||
@@ -21,5 +21,5 @@ describe('mini', () => {
|
||||
},
|
||||
{ time: 1000 },
|
||||
);
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mini",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.5",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
mini.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/test/mini.test.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/mini/test/mini.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# mondo
|
||||
|
||||
a lisp-based language intended to be used as a custom dsl for patterns that can stand on its own feet.
|
||||
see the `test` folder for usage examples
|
||||
|
||||
more info:
|
||||
|
||||
- [uzulang I](https://garten.salat.dev/uzu/uzulang1.html)
|
||||
- [uzulang II](https://garten.salat.dev/uzu/uzulang2.html)
|
||||
|
||||
## Example Usage
|
||||
|
||||
```js
|
||||
import { MondoRunner } from 'mondolang'
|
||||
// define our library of functions and variables
|
||||
let lib = {
|
||||
add: (a, b) => a + b,
|
||||
mul: (a, b) => a * b,
|
||||
PI: Math.PI,
|
||||
};
|
||||
// this function will evaluate nodes in the syntax tree
|
||||
function evaluator(node) {
|
||||
// check if node is a leaf node (!= list)
|
||||
if (node.type !== 'list') {
|
||||
// check lib if we find a match in the lib, otherwise return value
|
||||
return lib[node.value] ?? node.value;
|
||||
}
|
||||
// now it can only be a list..
|
||||
const [fn, ...args] = node.children;
|
||||
// children in a list will already be evaluated
|
||||
// the first child is expected to be a function
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`"${fn}" is not a function`);
|
||||
}
|
||||
return fn(...args);
|
||||
}
|
||||
const runner = new MondoRunner({ evaluator });
|
||||
const pat = runner.run('add 1 (mul 2 PI)') // 7.283185307179586
|
||||
```
|
||||
@@ -0,0 +1,484 @@
|
||||
/*
|
||||
mondo.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/test/mini.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// evolved from https://garten.salat.dev/lisp/parser.html
|
||||
export class MondoParser {
|
||||
// these are the tokens we expect
|
||||
token_types = {
|
||||
comment: /^\/\/(.*?)(?=\n|$)/,
|
||||
quotes_double: /^"(.*?)"/,
|
||||
quotes_single: /^'(.*?)'/,
|
||||
open_list: /^\(/,
|
||||
close_list: /^\)/,
|
||||
open_angle: /^</,
|
||||
close_angle: /^>/,
|
||||
open_square: /^\[/,
|
||||
close_square: /^\]/,
|
||||
open_curly: /^\{/,
|
||||
close_curly: /^\}/,
|
||||
number: /^-?[0-9]*\.?[0-9]+/, // before pipe!
|
||||
// TODO: better error handling when "-" is used as rest, e.g "s [- bd]"
|
||||
op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? ..
|
||||
// dollar: /^\$/,
|
||||
pipe: /^#/,
|
||||
stack: /^[,$]/,
|
||||
or: /^[|]/,
|
||||
plain: /^[a-zA-Z0-9-~_^#]+/,
|
||||
};
|
||||
// matches next token
|
||||
next_token(code, offset = 0) {
|
||||
for (let type in this.token_types) {
|
||||
const match = code.match(this.token_types[type]);
|
||||
if (match) {
|
||||
let token = { type, value: match[0] };
|
||||
if (offset !== -1) {
|
||||
// add location
|
||||
token.loc = [offset, offset + match[0].length];
|
||||
}
|
||||
return token;
|
||||
}
|
||||
}
|
||||
throw new Error(`mondo: could not match '${code}'`);
|
||||
}
|
||||
// takes code string, returns list of matched tokens (if valid)
|
||||
tokenize(code, offset = 0) {
|
||||
let tokens = [];
|
||||
let locEnabled = offset !== -1;
|
||||
let trim = () => {
|
||||
// trim whitespace at start, update offset
|
||||
offset += code.length - code.trimStart().length;
|
||||
// trim start and end to not confuse parser
|
||||
return code.trim();
|
||||
};
|
||||
code = trim();
|
||||
while (code.length > 0) {
|
||||
code = trim();
|
||||
const token = this.next_token(code, locEnabled ? offset : -1);
|
||||
code = code.slice(token.value.length);
|
||||
offset += token.value.length;
|
||||
tokens.push(token);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
// take code, return abstract syntax tree
|
||||
parse(code, offset) {
|
||||
this.code = code;
|
||||
this.offset = offset;
|
||||
this.tokens = this.tokenize(code, offset);
|
||||
const expressions = [];
|
||||
while (this.tokens.length) {
|
||||
expressions.push(this.parse_expr());
|
||||
}
|
||||
if (expressions.length === 0) {
|
||||
// empty case
|
||||
return { type: 'list', children: [] };
|
||||
}
|
||||
// do we have multiple top level expressions or a single non list?
|
||||
if (expressions.length > 1 || expressions[0].type !== 'list') {
|
||||
return {
|
||||
type: 'list',
|
||||
children: this.desugar(expressions),
|
||||
};
|
||||
}
|
||||
// we have a single list
|
||||
return expressions[0];
|
||||
}
|
||||
// parses any valid expression
|
||||
parse_expr() {
|
||||
if (!this.tokens[0]) {
|
||||
throw new Error(`unexpected end of file`);
|
||||
// TODO: could we allow that? like (((((((( s bd
|
||||
// return { type: 'list', children: [] };
|
||||
}
|
||||
let next = this.tokens[0]?.type;
|
||||
if (next === 'open_list') {
|
||||
return this.parse_list();
|
||||
}
|
||||
if (next === 'open_angle') {
|
||||
return this.parse_angle();
|
||||
}
|
||||
if (next === 'open_square') {
|
||||
return this.parse_square();
|
||||
}
|
||||
if (next === 'open_curly') {
|
||||
return this.parse_curly();
|
||||
}
|
||||
return this.consume(next);
|
||||
}
|
||||
// Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']]
|
||||
split_children(children, split_type) {
|
||||
const chunks = [];
|
||||
while (true) {
|
||||
let splitIndex = children.findIndex((child) => child.type === split_type);
|
||||
if (splitIndex === -1) break;
|
||||
const chunk = children.slice(0, splitIndex);
|
||||
chunks.push(chunk);
|
||||
children = children.slice(splitIndex + 1);
|
||||
}
|
||||
chunks.push(children);
|
||||
return chunks;
|
||||
}
|
||||
desugar_split(children, split_type, next) {
|
||||
const chunks = this.split_children(children, split_type);
|
||||
if (chunks.length === 1) {
|
||||
return next(children);
|
||||
}
|
||||
// collect args of stack function
|
||||
const args = chunks
|
||||
.map((chunk) => {
|
||||
if (!chunk.length) {
|
||||
return; // useful for things like "$ s bd $ s hh*8" (first chunk is empty)
|
||||
}
|
||||
if (chunk.length === 1) {
|
||||
// chunks of one element can be added to the stack as is
|
||||
return chunk[0];
|
||||
}
|
||||
// chunks of multiple args
|
||||
chunk = next(chunk);
|
||||
return { type: 'list', children: chunk };
|
||||
})
|
||||
.filter(Boolean); // ignore empty chunks
|
||||
return [{ type: 'plain', value: split_type }, ...args];
|
||||
}
|
||||
// prevents to get a list, e.g. ((x y)) => (x y)
|
||||
unwrap_children(children) {
|
||||
if (children.length === 1) {
|
||||
return children[0].children;
|
||||
}
|
||||
return children;
|
||||
}
|
||||
desugar_ops(children) {
|
||||
while (true) {
|
||||
let opIndex = children.findIndex((child) => child.type === 'op');
|
||||
if (opIndex === -1) break;
|
||||
const op = { type: 'plain', value: children[opIndex].value };
|
||||
if (opIndex === children.length - 1) {
|
||||
//throw new Error(`cannot use operator as last child.`);
|
||||
children[opIndex] = op; // ignore operator if last child.. e.g. "note [c -]"
|
||||
continue;
|
||||
}
|
||||
if (opIndex === 0) {
|
||||
// regular function call (assuming each operator exists as function)
|
||||
children[opIndex] = op;
|
||||
continue;
|
||||
}
|
||||
// convert infix to prefix notation
|
||||
const left = children[opIndex - 1];
|
||||
const right = children[opIndex + 1];
|
||||
if (left.type === 'pipe') {
|
||||
// "x !* 2" => (* 2 x)
|
||||
children[opIndex] = op;
|
||||
continue;
|
||||
}
|
||||
// some careful error handling
|
||||
if (left.type === 'op') {
|
||||
throw new Error(`got 2 ops in a row: "${left.value}${op.value}"`);
|
||||
}
|
||||
if (right.type === 'op') {
|
||||
let err = `got 2 ops in a row: "${op.value}${right.value}"`;
|
||||
if (op.value === '-') {
|
||||
// yes i know this file is not supposed to know about rests x.X
|
||||
err += '. you probably want a rest, which is "_" in mondo!';
|
||||
}
|
||||
throw new Error(err);
|
||||
}
|
||||
const call = { type: 'list', children: [op, right, left] };
|
||||
// insert call while keeping other siblings
|
||||
children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)];
|
||||
children = this.unwrap_children(children);
|
||||
}
|
||||
return children;
|
||||
}
|
||||
get_lambda(args, children) {
|
||||
// (.fast 2) = (fn (_) (fast _ 2))
|
||||
children = this.desugar(children);
|
||||
const body = children.length === 1 ? children[0] : { type: 'list', children };
|
||||
return [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, body];
|
||||
}
|
||||
// returns location range of given ast (even if desugared)
|
||||
get_range(ast, range = [Infinity, 0]) {
|
||||
let union = (a, b) => [Math.min(a[0], b[0]), Math.max(a[1], b[1])];
|
||||
if (ast.loc) {
|
||||
return union(range, ast.loc);
|
||||
}
|
||||
if (ast.type !== 'list') {
|
||||
return range;
|
||||
}
|
||||
return ast.children.reduce((range, child) => {
|
||||
const childrange = this.get_range(child, range);
|
||||
return union(range, childrange);
|
||||
}, range);
|
||||
}
|
||||
errorhead(ast) {
|
||||
return `[mondo ${this.get_range(ast)?.join(':') || '?'}]`;
|
||||
}
|
||||
// returns original user code where the given ast originates (even if desugared)
|
||||
get_code_snippet(ast) {
|
||||
const [min, max] = this.get_range(ast);
|
||||
return this.code.slice(min - this.offset, max - this.offset);
|
||||
}
|
||||
desugar_pipes(children) {
|
||||
let chunks = this.split_children(children, 'pipe');
|
||||
while (chunks.length > 1) {
|
||||
let [left, right, ...rest] = chunks;
|
||||
|
||||
if (!left.length) {
|
||||
const arg = { type: 'plain', value: '_' };
|
||||
return this.get_lambda([arg], [arg, ...children]);
|
||||
}
|
||||
// s jazz hh.fast 2 => (fast 2 (s jazz hh))
|
||||
const call = left.length > 1 ? { type: 'list', children: left } : left[0];
|
||||
chunks = [[...right, call], ...rest];
|
||||
}
|
||||
// return next(chunks[0]);
|
||||
return chunks[0];
|
||||
}
|
||||
parse_pair(open_type, close_type) {
|
||||
const begin = this.tokens[0].loc?.[0];
|
||||
this.consume(open_type);
|
||||
const children = [];
|
||||
while (this.tokens[0]?.type !== close_type) {
|
||||
children.push(this.parse_expr());
|
||||
}
|
||||
const end = this.tokens[0].loc?.[1];
|
||||
this.consume(close_type);
|
||||
const node = { type: 'list', children };
|
||||
if (begin !== undefined) {
|
||||
node.loc = [begin, end];
|
||||
node.raw = this.code.slice(begin, end);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
desugar(children, type) {
|
||||
// if type is given, the first element is expected to contain it as plain value
|
||||
// e.g. with (square a b, c), we want to split (a b, c) and ignore "square"
|
||||
children = type ? children.slice(1) : children;
|
||||
children = this.desugar_split(children, 'stack', (children) =>
|
||||
this.desugar_split(children, 'or', (children) => {
|
||||
// chunks of multiple args
|
||||
if (type) {
|
||||
// the type we've removed before splitting needs to be added back
|
||||
children = [{ type: 'plain', value: type }, ...children];
|
||||
}
|
||||
children = this.desugar_ops(children);
|
||||
// children = this.desugar_pipes(children, (children) => this.desugar_dollars(children));
|
||||
children = this.desugar_pipes(children);
|
||||
return children;
|
||||
}),
|
||||
);
|
||||
return children;
|
||||
}
|
||||
parse_list() {
|
||||
let node = this.parse_pair('open_list', 'close_list');
|
||||
node.children = this.desugar(node.children);
|
||||
return node;
|
||||
}
|
||||
parse_angle() {
|
||||
let node = this.parse_pair('open_angle', 'close_angle');
|
||||
node.children.unshift({ type: 'plain', value: 'angle' });
|
||||
node.children = this.desugar(node.children, 'angle');
|
||||
return node;
|
||||
}
|
||||
parse_square() {
|
||||
let node = this.parse_pair('open_square', 'close_square');
|
||||
node.children.unshift({ type: 'plain', value: 'square' });
|
||||
node.children = this.desugar(node.children, 'square');
|
||||
return node;
|
||||
}
|
||||
parse_curly() {
|
||||
let node = this.parse_pair('open_curly', 'close_curly');
|
||||
node.children.unshift({ type: 'plain', value: 'curly' });
|
||||
node.children = this.desugar(node.children, 'curly');
|
||||
return node;
|
||||
}
|
||||
consume(type) {
|
||||
// shift removes first element and returns it
|
||||
const token = this.tokens.shift();
|
||||
if (token.type !== type) {
|
||||
throw new Error(`expected token type ${type}, got ${token.type}`);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
get_locations(code, offset = 0) {
|
||||
let walk = (ast, locations = []) => {
|
||||
if (ast.type === 'list') {
|
||||
return ast.children.forEach((child) => walk(child, locations));
|
||||
}
|
||||
if (ast.loc) {
|
||||
locations.push(ast.loc);
|
||||
}
|
||||
};
|
||||
const ast = this.parse(code, offset);
|
||||
let locations = [];
|
||||
walk(ast, locations);
|
||||
return locations;
|
||||
}
|
||||
}
|
||||
|
||||
export function printAst(ast, compact = false, lvl = 0) {
|
||||
const br = compact ? '' : '\n';
|
||||
const spaces = compact ? '' : Array(lvl).fill(' ').join('');
|
||||
if (ast.type === 'list') {
|
||||
return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${
|
||||
ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')'
|
||||
}`;
|
||||
}
|
||||
return `${ast.value}`;
|
||||
}
|
||||
|
||||
// lisp runner
|
||||
export class MondoRunner {
|
||||
constructor({ evaluator } = {}) {
|
||||
this.parser = new MondoParser();
|
||||
this.evaluator = evaluator;
|
||||
this.assert(typeof evaluator === 'function', `expected an evaluator function to be passed to new MondoRunner`);
|
||||
}
|
||||
// a helper to check conditions and throw if they are not met
|
||||
assert(condition, error) {
|
||||
if (!condition) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
run(code, scope, offset = 0) {
|
||||
const ast = this.parser.parse(code, offset);
|
||||
//console.log(printAst(ast));
|
||||
return this.evaluate(ast, scope);
|
||||
}
|
||||
evaluate_let(ast, scope) {
|
||||
// (let ((x 3) (y 4)) ...body)
|
||||
// = ((fn (x y) ...body) 3 4)
|
||||
const defs = ast.children[1].children;
|
||||
const args = defs.map((pair) => pair.children[0]);
|
||||
const vals = defs.map((pair) => pair.children[1]);
|
||||
const body = ast.children.slice(2);
|
||||
const lambda = {
|
||||
type: 'list',
|
||||
children: [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, ...body],
|
||||
};
|
||||
return this.evaluate({ type: 'list', children: [lambda, ...vals] }, scope);
|
||||
}
|
||||
evaluate_def(ast, scope) {
|
||||
// function definition special form?
|
||||
if (ast.children[1].type === 'list') {
|
||||
// (def (add a b) (+ a b))
|
||||
// => (def add (fn (a b) (+ a b)) )
|
||||
const args = ast.children[1].children.slice(1);
|
||||
const lambda = {
|
||||
// lambda
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'fn' },
|
||||
{ type: 'list', children: args },
|
||||
...ast.children.slice(2), // body
|
||||
],
|
||||
};
|
||||
// we mutate to make sure the old ast wont make a mess later
|
||||
ast.children[1] = ast.children[1].children[0];
|
||||
ast.children[2] = lambda;
|
||||
ast.children = ast.children.slice(0, 3); // throw away rest
|
||||
}
|
||||
// (def name body)
|
||||
if (ast.children.length !== 3) {
|
||||
throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`);
|
||||
}
|
||||
const name = ast.children[1].value;
|
||||
const body = this.evaluate(ast.children[2], scope);
|
||||
scope[name] = body;
|
||||
// def with fall through
|
||||
}
|
||||
evaluate_match(ast, scope) {
|
||||
// (match (p1 e1) (p2 e2) ... (pn en))
|
||||
// = cond in lisp
|
||||
if (ast.children.length < 2) {
|
||||
return;
|
||||
}
|
||||
const [_, ...body] = ast.children;
|
||||
for (let i = 0; i < body.length; ++i) {
|
||||
const [predicate, exp] = body[i].children;
|
||||
if (predicate.value === 'else') {
|
||||
return this.evaluate(exp, scope);
|
||||
}
|
||||
const outcome = this.evaluate(predicate, scope);
|
||||
if (outcome) {
|
||||
return this.evaluate(exp, scope);
|
||||
}
|
||||
}
|
||||
return undefined; // nothing was matched
|
||||
}
|
||||
evaluate_if(ast, scope) {
|
||||
// if is a special case of match
|
||||
if (ast.children.length !== 4) {
|
||||
return;
|
||||
}
|
||||
// (if predicate consequent alternative)
|
||||
const [_, predicate, consequent, alternative] = ast.children;
|
||||
// (match (predicate consequent) (else alternative))
|
||||
const matcher = {
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'match' },
|
||||
{ type: 'list', children: [predicate, consequent] },
|
||||
{ type: 'list', children: [{ type: 'plain', value: 'else' }, alternative] },
|
||||
],
|
||||
};
|
||||
return this.evaluate_match(matcher, scope);
|
||||
}
|
||||
evaluate_lambda(ast, scope) {
|
||||
// (fn (_) (ply 2 _)
|
||||
// ^args ^ body
|
||||
const [_, formalArgs, ...body] = ast.children;
|
||||
return (...args) => {
|
||||
const params = Object.fromEntries(formalArgs.children.map((arg, i) => [arg.value, args[i]]));
|
||||
const closure = {
|
||||
...scope,
|
||||
...params,
|
||||
};
|
||||
// body can have multiple expressions
|
||||
const res = body.map((exp) => this.evaluate(exp, closure));
|
||||
// last expression is the return value
|
||||
return res[res.length - 1];
|
||||
};
|
||||
}
|
||||
evaluate_list(ast, scope) {
|
||||
// evaluate all children before evaluating list (dont mutate!!!)
|
||||
const args = ast.children
|
||||
.filter((child) => child.type !== 'comment') // ignore comments
|
||||
.map((arg) => this.evaluate(arg, scope));
|
||||
const node = { type: 'list', children: args };
|
||||
return this.evaluator(node, scope);
|
||||
}
|
||||
evaluate_leaf(ast, scope) {
|
||||
if (ast.type === 'number') {
|
||||
ast.value = Number(ast.value);
|
||||
} else if (['quotes_double', 'quotes_single'].includes(ast.type)) {
|
||||
ast.value = ast.value.slice(1, -1);
|
||||
ast.type = 'string';
|
||||
}
|
||||
return this.evaluator(ast, scope);
|
||||
}
|
||||
evaluate(ast, scope = {}) {
|
||||
if (ast.type !== 'list') {
|
||||
return this.evaluate_leaf(ast, scope);
|
||||
}
|
||||
const name = ast.children[0]?.value;
|
||||
if (name === 'fn') {
|
||||
return this.evaluate_lambda(ast, scope);
|
||||
}
|
||||
if (name === 'match') {
|
||||
return this.evaluate_match(ast, scope);
|
||||
}
|
||||
if (name === 'if') {
|
||||
return this.evaluate_if(ast, scope);
|
||||
}
|
||||
if (name === 'let') {
|
||||
return this.evaluate_let(ast, scope);
|
||||
}
|
||||
if (name === 'def') {
|
||||
this.evaluate_def(ast, scope);
|
||||
}
|
||||
return this.evaluate_list(ast, scope);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "mondolang",
|
||||
"version": "1.1.1",
|
||||
"description": "a language for functional composition that translates to js",
|
||||
"main": "mondo.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/mondo.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"bench": "vitest bench",
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel/blob/main/packages/mondo/README.md",
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,978 @@
|
||||
/*
|
||||
mondo.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/test/mini.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { MondoParser, printAst, MondoRunner } from '../mondo.mjs';
|
||||
|
||||
const parser = new MondoParser();
|
||||
const p = (code) => parser.parse(code, -1);
|
||||
|
||||
describe('mondo tokenizer', () => {
|
||||
const parser = new MondoParser();
|
||||
it('should tokenize with locations', () =>
|
||||
expect(
|
||||
parser
|
||||
.tokenize('(one two three)')
|
||||
.map((t) => t.value + '=' + t.loc.join('-'))
|
||||
.join(' '),
|
||||
).toEqual('(=0-1 one=1-4 two=5-8 three=9-14 )=14-15'));
|
||||
// it('should parse with loangleions', () => expect(parser.parse('(one two three)')).toEqual());
|
||||
it('should get loangleions', () =>
|
||||
expect(parser.get_locations('s bd rim')).toEqual([
|
||||
[0, 1],
|
||||
[2, 4],
|
||||
[5, 8],
|
||||
]));
|
||||
});
|
||||
describe('mondo s-expressions parser', () => {
|
||||
it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] }));
|
||||
it('should parse a single item', () =>
|
||||
expect(p('a')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] }));
|
||||
it('should parse an empty list', () => expect(p('()')).toEqual({ type: 'list', children: [] }));
|
||||
it('should parse a list with 1 item', () =>
|
||||
expect(p('(a)')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] }));
|
||||
it('should parse a list with 2 items', () =>
|
||||
expect(p('(a b)')).toEqual({
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'a' },
|
||||
{ type: 'plain', value: 'b' },
|
||||
],
|
||||
}));
|
||||
it('should parse a list with 2 items', () =>
|
||||
expect(p('(a (b c))')).toEqual({
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'a' },
|
||||
{
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'b' },
|
||||
{ type: 'plain', value: 'c' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
it('should parse numbers', () =>
|
||||
expect(p('(1 .2 1.2 10 22.3)')).toEqual({
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'number', value: '1' },
|
||||
{ type: 'number', value: '.2' },
|
||||
{ type: 'number', value: '1.2' },
|
||||
{ type: 'number', value: '10' },
|
||||
{ type: 'number', value: '22.3' },
|
||||
],
|
||||
}));
|
||||
it('should parse comments', () =>
|
||||
expect(p('a // hello')).toEqual({
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'a' },
|
||||
{ type: 'comment', value: '// hello' },
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
let desguar = (a) => {
|
||||
return printAst(parser.parse(a), true);
|
||||
};
|
||||
|
||||
describe('mondo sugar', () => {
|
||||
it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(square a b c)'));
|
||||
it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(square a (square b c) d)'));
|
||||
it('should desugar <>', () => expect(desguar('<a b c>')).toEqual('(angle a b c)'));
|
||||
it('should desugar <> nested', () => expect(desguar('<a <b c> d>')).toEqual('(angle a (angle b c) d)'));
|
||||
it('should desugar mixed [] <>', () => expect(desguar('[a <b c>]')).toEqual('(square a (angle b c))'));
|
||||
it('should desugar mixed <> []', () => expect(desguar('<a [b c]>')).toEqual('(angle a (square b c))'));
|
||||
|
||||
it('should desugar #', () => expect(desguar('s jazz # fast 2')).toEqual('(fast 2 (s jazz))'));
|
||||
it('should desugar # square', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))'));
|
||||
it('should desugar # twice', () => expect(desguar('s jazz # fast 2 # slow 2')).toEqual('(slow 2 (fast 2 (s jazz)))'));
|
||||
it('should desugar # nested', () => expect(desguar('(s cp # fast 2)')).toEqual('(fast 2 (s cp))'));
|
||||
it('should desugar # within []', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))'));
|
||||
it('should desugar # within , within []', () =>
|
||||
expect(desguar('[bd cp # fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)'));
|
||||
|
||||
// it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))'));
|
||||
|
||||
it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))'));
|
||||
it('should desugar , | of []', () =>
|
||||
expect(desguar('[bd, hh | [oh rim]]')).toEqual('(stack bd (or hh (square oh rim)))'));
|
||||
it('should desugar , square', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)'));
|
||||
it('should desugar , square 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (square hh oh))'));
|
||||
it('should desugar , square 3', () =>
|
||||
expect(desguar('[bd cp, hh oh]')).toEqual('(stack (square bd cp) (square hh oh))'));
|
||||
it('should desugar , angle', () => expect(desguar('<bd, hh>')).toEqual('(stack bd hh)'));
|
||||
it('should desugar , angle 2', () => expect(desguar('<bd, hh oh>')).toEqual('(stack bd (angle hh oh))'));
|
||||
it('should desugar , angle 3', () =>
|
||||
expect(desguar('<bd cp, hh oh>')).toEqual('(stack (angle bd cp) (angle hh oh))'));
|
||||
it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))'));
|
||||
it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* 2 b) c (/ 3 d) e)'));
|
||||
it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* 3 (square b c)))'));
|
||||
it('should desugar []*<x y>', () => expect(desguar('[a b*<2 3> c]')).toEqual('(square a (* (angle 2 3) b) c)'));
|
||||
it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)'));
|
||||
it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))'));
|
||||
it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))'));
|
||||
it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)'));
|
||||
/* it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)'));
|
||||
it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))'));
|
||||
it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */
|
||||
|
||||
it('should desugar README example', () =>
|
||||
expect(desguar('s [bd hh*2 (cp # crush 4) <mt ht lt>] # speed .8')).toEqual(
|
||||
'(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))',
|
||||
));
|
||||
|
||||
it('should desugar (#)', () => expect(desguar('(#)')).toEqual('(fn (_) _)'));
|
||||
it('should desugar lambda', () => expect(desguar('(# fast 2)')).toEqual('(fn (_) (fast 2 _))'));
|
||||
it('should desugar lambda call', () => expect(desguar('((# mul 2) 2)')).toEqual('((fn (_) (mul 2 _)) 2)'));
|
||||
it('should desugar lambda with pipe', () =>
|
||||
expect(desguar('(# fast 2 # room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))'));
|
||||
/* const lambda = parser.parse('(lambda (_) (fast 2 _))');
|
||||
const target = { type: 'plain', value: 'xyz' };
|
||||
it('should desugar_lambda', () =>
|
||||
expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); */
|
||||
});
|
||||
|
||||
describe('mondo arithmetic', () => {
|
||||
let multi =
|
||||
(op) =>
|
||||
(init, ...rest) =>
|
||||
rest.reduce((acc, arg) => op(acc, arg), init);
|
||||
|
||||
let lib = {
|
||||
'+': multi((a, b) => a + b),
|
||||
add: multi((a, b) => a + b),
|
||||
'-': multi((a, b) => a - b),
|
||||
sub: multi((a, b) => a - b),
|
||||
'*': multi((a, b) => a * b),
|
||||
'/': multi((a, b) => a / b),
|
||||
mod: multi((a, b) => a % b),
|
||||
eq: (a, b) => a === b,
|
||||
lt: (a, b) => a < b,
|
||||
gt: (a, b) => a > b,
|
||||
and: (a, b) => a && b,
|
||||
or: (a, b) => a || b,
|
||||
not: (a) => !a,
|
||||
run: (...args) => args[args.length - 1],
|
||||
def: () => 0,
|
||||
sin: Math.sin,
|
||||
cos: Math.cos,
|
||||
PI: Math.PI,
|
||||
cons: (a, b) => [a, ...(Array.isArray(b) ? b : [b])],
|
||||
car: (pair) => pair[0],
|
||||
cdr: (pair) => pair.slice(1),
|
||||
list: (...items) => items,
|
||||
nil: [],
|
||||
isnull: (items) => items.length === 0,
|
||||
concat: (...msgs) => msgs.join(''),
|
||||
error: (...msgs) => {
|
||||
throw new Error(msgs.join(' '));
|
||||
},
|
||||
};
|
||||
function evaluator(node, scope) {
|
||||
if (node.type !== 'list') {
|
||||
// is leaf
|
||||
return scope[node.value] ?? lib[node.value] ?? node.value;
|
||||
}
|
||||
// is list
|
||||
const [fn, ...args] = node.children;
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`"${fn}": expected function, got ${typeof fn} "${fn}"`);
|
||||
}
|
||||
return fn(...args);
|
||||
}
|
||||
const runner = new MondoRunner({ evaluator });
|
||||
let evaluate = (exp, scope) => runner.run(`run ${exp}`, scope);
|
||||
let pretty = (exp) => printAst(runner.parser.parse(exp), false);
|
||||
//it('should eval nested expression', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28'));
|
||||
|
||||
it('eval number', () => expect(evaluate('2')).toEqual(2));
|
||||
it('eval string', () => expect(evaluate('abc')).toEqual('abc'));
|
||||
it('eval list', () => expect(evaluate('(+ 1 2)')).toEqual(3));
|
||||
it('eval nested list', () => expect(evaluate('(+ 1 (+ 2 3))')).toEqual(6));
|
||||
it('def number', () => expect(evaluate('(def a 2) a')).toEqual(2));
|
||||
it('def + ref number', () => expect(evaluate('(def a 2) (* a a)')).toEqual(4));
|
||||
it('def + call lambda', () => expect(evaluate('(def sqr (fn (x) (* x x))) (sqr 3)')).toEqual(9));
|
||||
|
||||
// sicp
|
||||
it('sicp 8.1', () => expect(evaluate('(+ 137 349)')).toEqual(486));
|
||||
it('sicp 8.2', () => expect(evaluate('(- 1000 334)')).toEqual(666));
|
||||
it('sicp 8.3', () => expect(evaluate('(* 5 99)')).toEqual(495));
|
||||
it('sicp 8.4', () => expect(evaluate('(/ 10 5)')).toEqual(2));
|
||||
it('sicp 8.5', () => expect(evaluate('(+ 2.7 10)')).toEqual(12.7));
|
||||
it('sicp 9.1', () => expect(evaluate('(+ 21 35 12 7)')).toEqual(75));
|
||||
it('sicp 9.2', () => expect(evaluate('(* 25 4 12)')).toEqual(1200));
|
||||
it('sicp 9.3', () => expect(evaluate('(+ (* 3 5) (- 10 6))')).toEqual(19));
|
||||
it('sicp 9.4', () =>
|
||||
expect(pretty('(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))')).toEqual(`(+
|
||||
(* 3
|
||||
(+
|
||||
(* 2 4)
|
||||
(+ 3 5)
|
||||
)
|
||||
)
|
||||
(+
|
||||
(- 10 7) 6
|
||||
)
|
||||
)`)); // this is not exactly pretty printing by convention..
|
||||
|
||||
let scope = {};
|
||||
it('sicp 11.1', () => expect(evaluate('(def size 2) (* 5 size)', scope)).toEqual(10));
|
||||
it('sicp 11.2', () =>
|
||||
expect(evaluate('(def pi 3.14159) (def radius 10) (* pi (* radius radius))', scope)).toEqual(314.159));
|
||||
it('sicp 11.3', () => expect(evaluate('(def circumference (* 2 pi radius))', scope)).toEqual(0));
|
||||
it('sicp 11.4', () => expect(evaluate('circumference', scope)).toEqual(62.8318));
|
||||
it('sicp 13.1', () => expect(evaluate('(* (+ 2 (* 4 6)) (+ 3 5 7))')).toEqual(390));
|
||||
it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0));
|
||||
// it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0));
|
||||
it('sicp 17.1', () => expect(evaluate('(square 21)', scope)).toEqual(441));
|
||||
it('sicp 17.2', () => expect(evaluate('(square (+ 2 5))', scope)).toEqual(49));
|
||||
it('sicp 17.3', () => expect(evaluate('(square (square 3))', scope)).toEqual(81));
|
||||
it('sicp 17.4', () => expect(evaluate(`(def (sumofsquares x y) (+ (square x) (square y)))`, scope)).toEqual(0));
|
||||
it('sicp 17.5', () => expect(evaluate(`(sumofsquares 3 4)`, scope)).toEqual(25));
|
||||
it('sicp 17.6', () => expect(evaluate(`(def (f a) (sumofsquares (+ a 1) (* a 2))) (f 5)`, scope)).toEqual(136));
|
||||
it('sicp 21.1', () => expect(evaluate(`(sumofsquares (+ 5 1) (* 5 2))`, scope)).toEqual(136));
|
||||
|
||||
it('sicp 22.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`(def (abs x)
|
||||
(match
|
||||
((gt x 0) x)
|
||||
((eq x 0) 0)
|
||||
((lt x 0) (- 0 x))
|
||||
))`, // sicp was doing (- x), which doesnt work with our -
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
it('sicp gt1', () => expect(evaluate(`(gt -12 0)`, scope)).toEqual(false));
|
||||
it('sicp gt2', () => expect(evaluate(`(gt 0 -12)`, scope)).toEqual(true));
|
||||
it('sicp lt1', () => expect(evaluate(`(lt -12 0)`, scope)).toEqual(true));
|
||||
it('sicp lt2', () => expect(evaluate(`(lt 0 -12)`, scope)).toEqual(false));
|
||||
|
||||
it('sicp 24.1', () => expect(evaluate(`(abs (- 3))`, scope)).toEqual(3));
|
||||
it('sicp 24.2', () => expect(evaluate(`(abs (+ 3))`, scope)).toEqual(3));
|
||||
it('sicp 24.3', () => expect(evaluate(`(abs -12)`, scope)).toEqual(12));
|
||||
|
||||
it('sicp 24.4', () => expect(evaluate(`(def (abs x) (if (lt x 0) (- 0 x) x))`, scope)).toEqual(0));
|
||||
it('sicp 24.5', () => expect(evaluate(`(abs -13)`, scope)).toEqual(13));
|
||||
it('sicp 25.1', () => expect(evaluate(`(and (gt 6 5) (lt 6 10))`, scope)).toEqual(true));
|
||||
it('sicp 25.2', () => expect(evaluate(`(and (gt 4 5) (lt 6 10))`, scope)).toEqual(false));
|
||||
|
||||
it('sicp ex1.1.1', () => expect(evaluate(`(def a 3)`, scope)).toEqual(0));
|
||||
it('sicp ex1.1.2', () => expect(evaluate(`(def b (+ a 1))`, scope)).toEqual(0));
|
||||
it('sicp ex1.1.3', () => expect(evaluate(`(+ a b (* a b))`, scope)).toEqual(19));
|
||||
it('sicp ex1.1.4', () => expect(evaluate(`(if (and (gt b a) (lt b (* a b))) b a)`, scope)).toEqual(4));
|
||||
it('sicp ex1.1.5', () => expect(evaluate(`(match ((eq a 4) 6) ((eq b 4) (+ 6 7 a)) (else 25))`, scope)).toEqual(16));
|
||||
it('sicp ex1.1.6', () => expect(evaluate(`(+ 2 (if (gt b a) b a))`, scope)).toEqual(6));
|
||||
it('sicp ex1.1.7', () =>
|
||||
expect(evaluate(`(* (match ((gt a b) a) ((lt a b) b) (else -1)) (+ a 1))`, scope)).toEqual(16));
|
||||
|
||||
// .. cant use "+" and "-" as standalone expressions, because they are parsed as operators...
|
||||
it('sicp ex1.4.1', () => expect(evaluate(`(def (foo a b) ((if (gt b 0) add sub) a b))`, scope)).toEqual(0));
|
||||
it('sicp ex1.4.1', () => expect(evaluate(`(foo 3 1)`, scope)).toEqual(4));
|
||||
it('sicp ex1.4.2', () => expect(evaluate(`(foo 3 -1)`, scope)).toEqual(4));
|
||||
|
||||
// 1.1.7 Example: Square Roots by Newton’s Method
|
||||
it('sicp 30.1', () =>
|
||||
expect(evaluate(`(def (goodenuf guess x) (lt (abs (- (square guess) x)) 0.001))`, scope)).toEqual(0));
|
||||
it('sicp 30.2', () => expect(evaluate(`(goodenuf 1 1.001)`, scope)).toEqual(true));
|
||||
it('sicp 30.3', () => expect(evaluate(`(goodenuf 1 1.002)`, scope)).toEqual(false));
|
||||
it('sicp 30.4', () => expect(evaluate(`(def (average x y) (/ (+ x y) 2))`, scope)).toEqual(0));
|
||||
it('sicp 30.5', () => expect(evaluate(`(average 18 20)`, scope)).toEqual(19));
|
||||
it('sicp 30.6', () => expect(evaluate(`(def (improve guess x) (average guess (/ x guess)))`, scope)).toEqual(0));
|
||||
it('sicp 31.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`(def (sqrtiter guess x) (if (goodenuf guess x)
|
||||
guess
|
||||
(sqrtiter (improve guess x) x)))`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
it('sicp 31.2', () => expect(evaluate(`(def (sqrt x) (sqrtiter 1.0 x))`, scope)).toEqual(0));
|
||||
it('sicp 31.3', () => expect(evaluate(`(sqrt 9)`, scope)).toEqual(3.00009155413138));
|
||||
it('sicp 31.4', () => expect(evaluate(`(sqrt (+ 100 37))`, scope)).toEqual(11.704699917758145));
|
||||
// eslint-disable-next-line no-loss-of-precision
|
||||
it('sicp 31.5', () => expect(evaluate(`(sqrt (+ (sqrt 2) (sqrt 3)))`, scope)).toEqual(1.77392790232078925));
|
||||
it('sicp 31.6', () => expect(evaluate(`(square (sqrt 1000))`, scope)).toEqual(1000.000369924366));
|
||||
|
||||
// lexical scoping
|
||||
it('sicp 39.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (sqrt x)
|
||||
(def (goodenough guess)
|
||||
(lt (abs (- (square guess) x)) 0.001))
|
||||
(def (improve guess)
|
||||
(average guess (/ x guess)))
|
||||
(def (sqrt-iter guess)
|
||||
(if (goodenough guess) guess (sqrt-iter (improve guess))))
|
||||
(sqrtiter 1.0))
|
||||
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
// recursive fac
|
||||
it('sicp 41.1', () => expect(evaluate(`(def (fac n) (if (eq n 1) 1 (* n (fac (- n 1)))))`, scope)).toEqual(0));
|
||||
it('sicp 41.2', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24));
|
||||
|
||||
// iterative fac
|
||||
it('sicp 41.3', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (factorial n) (factiter 1 1 n))
|
||||
(def (factiter product counter maxcount)
|
||||
(if (gt counter maxcount)
|
||||
product
|
||||
(factiter (* counter product)
|
||||
(+ counter 1)
|
||||
maxcount)))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24));
|
||||
|
||||
// 46.1
|
||||
/* (def (+ a b)
|
||||
(if (= a 0) b (inc (+ (dec a) b))))
|
||||
(def (+ a b)
|
||||
(if (= a 0) b (+ (dec a) (inc b)))) */
|
||||
|
||||
// Exercise 1.10
|
||||
// Ackermann’s function
|
||||
it('sicp 47.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (A x y) (match ((eq y 0) 0)
|
||||
((eq x 0) (* 2 y))
|
||||
((eq y 1) 2)
|
||||
(else (A (- x 1) (A x (- y 1))))))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
it('sicp 47.2', () => expect(evaluate(`(A 1 10)`, scope)).toEqual(1024));
|
||||
it('sicp 47.3', () => expect(evaluate(`(A 2 4)`, scope)).toEqual(65536));
|
||||
it('sicp 47.4', () => expect(evaluate(`(A 3 3)`, scope)).toEqual(65536));
|
||||
it('sicp 47.5', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (f n) (A 0 n))
|
||||
(def (g n) (A 1 n)))
|
||||
(def (h n) (A 2 n))
|
||||
(def (k n) (* 5 n n))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
// Tree Recursion
|
||||
// recursive process
|
||||
it('sicp 48.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (fib n) (match ((eq n 0) 0) ((eq n 1) 1)
|
||||
(else (+ (fib (- n 1)) (fib (- n 2))))))
|
||||
(fib 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(13));
|
||||
|
||||
// iterative process
|
||||
it('sicp 48.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (fib n) (fibiter 1 0 n))
|
||||
(def (fibiter a b count) (if (eq count 0)
|
||||
b
|
||||
(fibiter (+ a b) a (- count 1))))
|
||||
(fib 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(13));
|
||||
|
||||
// example: counting change
|
||||
it('sicp 52.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (countchange amount) (cc amount 5))
|
||||
(def (cc amount kindsofcoins)
|
||||
(match
|
||||
((eq amount 0) 1)
|
||||
((or (lt amount 0) (eq kindsofcoins 0)) 0)
|
||||
(else (+
|
||||
(cc amount (- kindsofcoins 1))
|
||||
(cc (- amount (firstdenomination kindsofcoins)) kindsofcoins)))))
|
||||
|
||||
(def (firstdenomination kindsofcoins)
|
||||
(match
|
||||
((eq kindsofcoins 1) 1)
|
||||
((eq kindsofcoins 2) 5)
|
||||
((eq kindsofcoins 3) 10)
|
||||
((eq kindsofcoins 4) 25)
|
||||
((eq kindsofcoins 5) 50)))
|
||||
|
||||
(countchange 100)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(292));
|
||||
|
||||
// todo: pascals triangle
|
||||
it('sicp 57.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (cube x) (* x x x))
|
||||
(def (p x) (sub (* 3 x) (* 4 (cube x))))
|
||||
(def (sine angle)
|
||||
(if (not (gt (abs angle) 0.1)) angle
|
||||
(p (sine (/ angle 3.0)))))
|
||||
|
||||
(sine 12.15)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(-0.39980345741334));
|
||||
|
||||
// exponentiation recursive
|
||||
it('sicp 57.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (expt b n) (if (eq n 0) 1 (* b (expt b (- n 1)))))
|
||||
(expt 2 4)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(16));
|
||||
|
||||
// exponentiation iterative
|
||||
it('sicp 58.1b', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (expt b n) (exptiter b n 1))
|
||||
(def (exptiter b counter product) (if (eq counter 0)
|
||||
product
|
||||
(exptiter b (- counter 1) (* b product))))
|
||||
(expt 2 5)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(32));
|
||||
|
||||
// exponentiation fast
|
||||
it('sicp 58.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (fastexpt b n) (match ((eq n 0) 1)
|
||||
((iseven n) (square (fastexpt b (/ n 2)))) (else (* b (fastexpt b (- n 1))))))
|
||||
(def (iseven n)
|
||||
(eq (mod n 2) 0))
|
||||
(fastexpt 2 5)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(32));
|
||||
|
||||
// * = repeated addition
|
||||
it('sicp 60.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`(def (mult a b) (if (eq b 0)
|
||||
0
|
||||
(+ a (* a (- b 1)))))
|
||||
(mult 3 15)
|
||||
`,
|
||||
),
|
||||
).toEqual(45));
|
||||
|
||||
// gcd / euclid
|
||||
it('sicp 63.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`(def (gcd a b) (if (eq b 0)
|
||||
a
|
||||
(gcd b (mod a b))))
|
||||
(gcd 20 6)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(2));
|
||||
|
||||
// 65 smallest divisor
|
||||
// 67 fermat test
|
||||
// ....
|
||||
|
||||
// higher order procedures
|
||||
|
||||
it('sicp 77.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (sum term a next b)
|
||||
(if (gt a b) 0 (+ (term a)
|
||||
(sum term (next a) next b))))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
it('sicp 78.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (inc n) (+ n 1))
|
||||
(def (cube a) (* a a a))
|
||||
(def (sumcubes a b)
|
||||
(sum cube a inc b))
|
||||
(sumcubes 1 10)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(3025));
|
||||
|
||||
it('sicp 78.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (identity x) x)
|
||||
(def (sumintegers a b)
|
||||
(sum identity a inc b))
|
||||
(sumintegers 1 10)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(55));
|
||||
|
||||
// pisum
|
||||
it('sicp 79.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (pisum a b)
|
||||
(def (piterm x)
|
||||
(/ 1.0 (* x (+ x 2))))
|
||||
(def (pinext x) (+ x 4))
|
||||
(sum piterm a pinext b))
|
||||
(* 8 (pisum 1 1000))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(3.139592655589783));
|
||||
|
||||
// integral
|
||||
it('sicp 79.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (integral f a b dx)
|
||||
(def (adddx x) (+ x dx))
|
||||
(* (sum f (+ a (/ dx 2.0)) adddx b) dx))
|
||||
(integral cube 0 1 0.01)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0.24998750000000042));
|
||||
// maximum callstack...
|
||||
//it('sicp 79.3', () => expect(evaluate(`(integral cube 0 1 0.001)`, scope)).toEqual(0.249999875000001));
|
||||
|
||||
//lambdas
|
||||
it('sicp 83.1', () => expect(evaluate(`((fn (x) (+ x 4)) 5)`)).toEqual(9));
|
||||
|
||||
it('sicp 83.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (pisum a b)
|
||||
(sum (fn (x) (/ 1.0 (* x (+ x 2))))
|
||||
a
|
||||
(fn (x) (+ x 4))
|
||||
b))
|
||||
(* 8 (pisum 1 1000))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(3.139592655589783));
|
||||
it('sicp 83.3', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (integral f a b dx)
|
||||
(* (sum f
|
||||
(+ a (/ dx 2.0))
|
||||
(fn (x) (+ x dx))
|
||||
b)
|
||||
dx))
|
||||
(integral cube 0 1 0.01)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0.24998750000000042));
|
||||
it('sicp 84.1', () => expect(evaluate(`((fn (x y z) (+ x y (square z))) 1 2 3)`, scope)).toEqual(12));
|
||||
|
||||
// let expressions
|
||||
it('sicp 87.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(+ (let ((x 3))
|
||||
(+ x (* x 10))) x)
|
||||
`,
|
||||
{ x: 5 },
|
||||
),
|
||||
).toEqual(38));
|
||||
it('sicp 87.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(let ((x 3)
|
||||
(y (+ x 2)))
|
||||
(* x y))
|
||||
`,
|
||||
{ x: 2 },
|
||||
),
|
||||
).toEqual(12));
|
||||
it('sicp 88.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (f g) (g 2))
|
||||
(f square)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(4));
|
||||
it('sicp 88.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (f g) (g 2))
|
||||
(f (fn (z) (* z (+ z 1))))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(6));
|
||||
|
||||
// Finding roots of equations by the half-interval method
|
||||
it('sicp 89.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (search f negpoint pospoint)
|
||||
(let ((midpoint (average negpoint pospoint)))
|
||||
(if (closeenough negpoint pospoint)
|
||||
midpoint
|
||||
(let ((testvalue (f midpoint)))
|
||||
(match ((positive testvalue)
|
||||
(search f negpoint midpoint))
|
||||
((negative testvalue)
|
||||
(search f midpoint pospoint))
|
||||
(else midpoint))))))
|
||||
|
||||
(def (closeenough x y) (lt (abs (- x y)) 0.001))
|
||||
|
||||
(def (negative x) (lt x 0))
|
||||
(def (positive x) (gt x 0))
|
||||
|
||||
(def (halfintervalmethod f a b) (let ((avalue (f a))
|
||||
(bvalue (f b)))
|
||||
(match ((and (negative avalue) (positive bvalue))
|
||||
(search f a b))
|
||||
((and (negative bvalue) (positive avalue))
|
||||
(search f b a)) (else
|
||||
(error "Values are not of opposite sign" a b)))))
|
||||
|
||||
(halfintervalmethod sin 2.0 4.0)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(3.14111328125));
|
||||
|
||||
it('sicp 89.1', () =>
|
||||
expect(evaluate(`(halfintervalmethod (fn (x) (- (* x x x) (* 2 x) 3)) 1.0 2.0)`, scope)).toEqual(1.89306640625));
|
||||
|
||||
// Finding fixed points of functions
|
||||
it('sicp 92.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def tolerance 0.00001)
|
||||
(def (fixedpoint f first-guess)
|
||||
(def (closeenough v1 v2) (lt (abs (- v1 v2)) tolerance))
|
||||
(def (try guess)
|
||||
(let ((next (f guess)))
|
||||
(if (closeenough guess next) next (try next))))
|
||||
(try first-guess))
|
||||
|
||||
(fixedpoint cos 1.0)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0.7390822985224023));
|
||||
it('sicp 93.1', () =>
|
||||
expect(evaluate(`(fixedpoint (fn (y) (+ (sin y) (cos y))) 1.0)`, scope)).toEqual(1.2587315962971173));
|
||||
// Maximum call stack size exceeded (expected)
|
||||
/* it('sicp 93.2', () =>
|
||||
expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (/ x y)) 1.0)) (sqrt 4)`, scope)).toEqual(0)); */
|
||||
it('sicp 93.3', () =>
|
||||
expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (average y (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual(
|
||||
2.6457513110645907,
|
||||
));
|
||||
// Procedures as Returned Values
|
||||
it('sicp 97.1', () =>
|
||||
expect(evaluate(`(def (averagedamp f) (fn (x) (average x (f x)))) ((averagedamp square) 10)`, scope)).toEqual(55));
|
||||
it('sicp 98.1', () =>
|
||||
expect(evaluate(`(def (sqrt x) (fixedpoint (averagedamp (fn (y) (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual(
|
||||
2.6457513110645907,
|
||||
));
|
||||
it('sicp 98.2', () =>
|
||||
expect(
|
||||
evaluate(`(def (cuberoot x) (fixedpoint (averagedamp (fn (y) (/ x (square y)))) 1.0)) (cuberoot 7)`, scope),
|
||||
).toEqual(1.912934258514886));
|
||||
it('sicp 99.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (deriv g) (fn (x) (/ (- (g (+ x dx)) (g x)) dx)))
|
||||
(def dx 0.00001)
|
||||
(def (cube x) (* x x x))
|
||||
((deriv cube) 5)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(75.00014999664018));
|
||||
// With the aid of deriv, we can express Newton’s method as a fixed-point process:
|
||||
it('sicp 100.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (newtontransform g)
|
||||
(fn (x) (- x (/ (g x) ((deriv g) x)))))
|
||||
(def (newtonsmethod g guess) (fixedpoint (newtontransform g) guess))
|
||||
(def (sqrt x) (newtonsmethod (fn (y) (- (square y) x)) 1.0))
|
||||
(sqrt 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(2.6457513110645907));
|
||||
// whatever this is
|
||||
it('sicp 101.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (fixedpointoftransform g transform guess) (fixedpoint (transform g) guess))
|
||||
(def (sqrt x) (fixedpointoftransform
|
||||
(fn (y) (/ x y)) averagedamp 1.0))
|
||||
(sqrt 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(2.6457513110645907));
|
||||
it('sicp 101.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (sqrt x) (fixedpointoftransform
|
||||
(fn (y) (- (square y) x)) newtontransform 1.0))
|
||||
(sqrt 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(2.6457513110645907));
|
||||
|
||||
// data abstraction
|
||||
|
||||
// rational arithmetic
|
||||
it('sicp 114.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (addrat x y)
|
||||
(makerat (+ (*
|
||||
(numer x) (denom y))
|
||||
(* (numer y) (denom x)))
|
||||
(* (denom x) (denom y))))
|
||||
(def (subrat x y)
|
||||
(makerat (- (* (numer x) (denom y))
|
||||
(* (numer y) (denom x)))
|
||||
(* (denom x) (denom y))))
|
||||
(def (mulrat x y)
|
||||
(makerat (* (numer x) (numer y))
|
||||
(* (denom x) (denom y))))
|
||||
(def (divrat x y)
|
||||
(makerat (* (numer x) (denom y))
|
||||
(* (denom x) (numer y))))
|
||||
(def (equalrat x y)
|
||||
(eq (* (numer x) (denom y))
|
||||
(* (numer y) (denom x))))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
// markerat number denom
|
||||
it('sicp 117.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (makerat n d) (cons n d))
|
||||
(def (numer x) (car x))
|
||||
(def (denom x) (cdr x))
|
||||
(def (printrat x) (concat (numer x) ':' (denom x)))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
it('sicp 117.1', () => expect(evaluate(`(def onehalf (makerat 1 2)) (printrat onehalf)`, scope)).toEqual('1:2'));
|
||||
it('sicp 117.2', () =>
|
||||
expect(evaluate(`(def onethird (makerat 1 3)) (printrat (addrat onehalf onethird))`, scope)).toEqual('5:6'));
|
||||
it('sicp 117.3', () => expect(evaluate(`(printrat (mulrat onehalf onethird))`, scope)).toEqual('1:6'));
|
||||
it('sicp 117.4', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('6:9'));
|
||||
it('sicp 118.1', () =>
|
||||
expect(evaluate(`(def (makerat n d) (let ((g (gcd n d))) (cons (/ n g) (/ d g))))`, scope)).toEqual(0));
|
||||
it('sicp 118.1', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('2:3'));
|
||||
|
||||
let lscope = {};
|
||||
// pairs with lambda
|
||||
it('sicp 124.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (cons x y)
|
||||
(def (dispatch m)
|
||||
(match
|
||||
((eq m 0) x)
|
||||
((eq m 1) y)
|
||||
(else (error "argument not 0 or 1: CONS" m)))
|
||||
) dispatch)
|
||||
(def (car z) (z 0))
|
||||
(def (cdr z) (z 1))
|
||||
`,
|
||||
lscope,
|
||||
),
|
||||
).toEqual(0));
|
||||
it('sicp 124.1', () => expect(evaluate(`(car (cons first second))`, lscope)).toEqual('first'));
|
||||
it('sicp 124.2', () => expect(evaluate(`(cdr (cons first second))`, lscope)).toEqual('second'));
|
||||
// lists
|
||||
it('sicp 135.1', () => expect(evaluate(`(list 1 2 3 4)`)).toEqual([1, 2, 3, 4]));
|
||||
it('sicp 137.1', () => expect(evaluate(`(car (list 1 2 3 4))`)).toEqual(1));
|
||||
it('sicp 137.2', () => expect(evaluate(`(cdr (list 1 2 3 4))`)).toEqual([2, 3, 4]));
|
||||
it('sicp 137.3', () => expect(evaluate(`(car (cdr (list 1 2 3 4)))`)).toEqual(2));
|
||||
it('sicp 137.4', () => expect(evaluate(`(cons 10 (list 1 2 3 4))`)).toEqual([10, 1, 2, 3, 4]));
|
||||
// listref
|
||||
it('sicp 138.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (listref items n) (if (eq n 0) (car items)
|
||||
(listref (cdr items) (- n 1))))
|
||||
(def squares (list 1 4 9 16 25))
|
||||
(listref squares 3)`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(16));
|
||||
// length recursive
|
||||
it('sicp 138.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (length items)
|
||||
(if (isnull items) 0
|
||||
(+ 1 (length (cdr items)))))
|
||||
(def odds (list 1 3 5 7))
|
||||
(length odds)`,
|
||||
),
|
||||
).toEqual(4));
|
||||
// length iterative
|
||||
it('sicp 139.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (length items)
|
||||
(def (lengthiter a count)
|
||||
(if (isnull a) count
|
||||
(lengthiter (cdr a) (+ 1 count))))
|
||||
(lengthiter items 0))
|
||||
(def odds (list 1 3 5 7))
|
||||
(length odds)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(4));
|
||||
// append
|
||||
it('sicp 139.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (append list1 list2)
|
||||
(if (isnull list1)
|
||||
list2
|
||||
(cons (car list1) (append (cdr list1) list2))))
|
||||
(append squares odds)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual([1, 4, 9, 16, 25, 1, 3, 5, 7]));
|
||||
// (define (f x y . z) ⟨body⟩) <- tbd: variable argument count
|
||||
// Mapping over lists
|
||||
|
||||
it('sicp 143.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (scalelist items factor) (if (isnull items) nil
|
||||
(cons (* (car items) factor)
|
||||
(scalelist (cdr items) factor))))
|
||||
(scalelist (list 1 2 3 4 5) 10)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual([10, 20, 30, 40, 50]));
|
||||
|
||||
it('sicp 143.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (map proc items) (if (isnull items) nil
|
||||
(cons (proc (car items))
|
||||
(map proc (cdr items)))))
|
||||
(map abs (list -10 2.5 -11.6 17))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual([10, 2.5, 11.6, 17]));
|
||||
it('sicp 143.1', () => expect(evaluate(`(map (fn (x) (* x x)) (list 1 2 3 4))`, scope)).toEqual([1, 4, 9, 16]));
|
||||
it('sicp 143.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (scalelist items factor) (map (fn (x) (* x factor)) items))
|
||||
(scalelist (list 1 2 3 4 5) 10)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual([10, 20, 30, 40, 50]));
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'vite';
|
||||
//import { dependencies } from './package.json';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'mondo.mjs'),
|
||||
formats: ['es'],
|
||||
fileName: (ext) => ({ es: 'mondo.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
// external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
# @strudel/mondough
|
||||
|
||||
connects mondo to strudel.
|
||||
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
strudelScope,
|
||||
reify,
|
||||
fast,
|
||||
slow,
|
||||
seq,
|
||||
stepcat,
|
||||
replicate,
|
||||
expand,
|
||||
pace,
|
||||
chooseIn,
|
||||
degradeBy,
|
||||
silence,
|
||||
} from '@strudel/core';
|
||||
import { registerLanguage } from '@strudel/transpiler';
|
||||
import { MondoRunner } from 'mondolang';
|
||||
|
||||
const tail = (friend, pat) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend);
|
||||
|
||||
const arrayRange = (start, stop, step = 1) =>
|
||||
Array.from({ length: Math.abs(stop - start) / step + 1 }, (_, index) =>
|
||||
start < stop ? start + index * step : start - index * step,
|
||||
);
|
||||
const range = (max, min) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b))));
|
||||
|
||||
let nope = (...args) => args[args.length - 1];
|
||||
|
||||
let lib = {};
|
||||
lib['nope'] = nope;
|
||||
lib['-'] = (a, b) => b.early(a);
|
||||
lib['+'] = (a, b) => b.late(a);
|
||||
lib['_'] = silence;
|
||||
lib['~'] = silence;
|
||||
lib.curly = stepcat;
|
||||
lib.square = (...args) => stepcat(...args).setSteps(1);
|
||||
lib.angle = (...args) => stepcat(...args).pace(1);
|
||||
lib['*'] = fast;
|
||||
lib['/'] = slow;
|
||||
lib['!'] = replicate;
|
||||
lib['@'] = expand;
|
||||
lib['%'] = pace;
|
||||
lib['?'] = degradeBy; // todo: default 0.5 not working..
|
||||
lib[':'] = tail;
|
||||
lib['..'] = range;
|
||||
lib['def'] = () => silence;
|
||||
lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]"
|
||||
//lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct
|
||||
|
||||
function evaluator(node, scope) {
|
||||
const { type } = node;
|
||||
// node is list
|
||||
if (type === 'list') {
|
||||
const { children } = node;
|
||||
const [name, ...args] = children;
|
||||
// some functions wont be reified to make sure they work (e.g. see extend below)
|
||||
if (typeof name === 'function') {
|
||||
return name(...args);
|
||||
}
|
||||
if (name.value === 'def') {
|
||||
return silence;
|
||||
}
|
||||
// name is expected to be a pattern of functions!
|
||||
const first = name.firstCycle(true)[0];
|
||||
const type = typeof first?.value;
|
||||
if (type !== 'function') {
|
||||
throw new Error(`[mondough] expected function, got "${first?.value}"`);
|
||||
}
|
||||
return name
|
||||
.fmap((fn) => {
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`[mondough] "${fn}" is not a function b`);
|
||||
}
|
||||
return fn(...args);
|
||||
})
|
||||
.innerJoin();
|
||||
}
|
||||
// node is leaf
|
||||
let { value } = node;
|
||||
if (type === 'plain' && scope[value]) {
|
||||
return reify(scope[value]); // -> local scope has no location
|
||||
}
|
||||
const variable = lib[value] ?? strudelScope[value];
|
||||
// problem: collisions when we want a string that happens to also be a variable name
|
||||
// example: "s sine" -> sine is also a variable
|
||||
let pat;
|
||||
if (type === 'plain' && typeof variable !== 'undefined') {
|
||||
// some function names are not patternable, so we skip reification here
|
||||
if (['!', 'extend', '@', 'expand', 'square', 'angle', 'all', 'setcpm', 'setcps'].includes(value)) {
|
||||
return variable;
|
||||
}
|
||||
pat = reify(variable);
|
||||
} else {
|
||||
pat = reify(value);
|
||||
}
|
||||
if (node.loc) {
|
||||
pat = pat.withLoc(node.loc[0], node.loc[1]);
|
||||
}
|
||||
return pat;
|
||||
}
|
||||
|
||||
let runner = new MondoRunner({ evaluator });
|
||||
|
||||
export function mondo(code, offset = 0) {
|
||||
if (Array.isArray(code)) {
|
||||
code = code.join('');
|
||||
}
|
||||
const pat = runner.run(code, undefined, offset);
|
||||
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
|
||||
}
|
||||
|
||||
export let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
|
||||
|
||||
export const mondi = (str, offset) => {
|
||||
const code = `[${str}]`;
|
||||
return mondo(code, offset);
|
||||
};
|
||||
|
||||
// tell transpiler how to get locations for mondo`` calls
|
||||
registerLanguage('mondo', {
|
||||
getLocations,
|
||||
});
|
||||
|
||||
// this is like mondo, but with a zero offset
|
||||
export const mondolang = (code) => mondo(code, 0);
|
||||
registerLanguage('mondolang', {
|
||||
getLocations: (code) => getLocations(code, 0),
|
||||
});
|
||||
// uncomment the following to use mondo as mini notation language
|
||||
/* registerLanguage('minilang', {
|
||||
name: 'mondi',
|
||||
getLocations,
|
||||
}); */
|
||||