mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-17 08:06:50 -04:00
Compare commits
1469 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90a58858ca | |||
| 8149b85a55 | |||
| e79b6c514a | |||
| aaa894d74b | |||
| b55721f16f | |||
| b39d948d71 | |||
| 0fee279e3d | |||
| c7d8a9cb82 | |||
| b50075f102 | |||
| fb3ca9f960 | |||
| 058b971870 | |||
| 610ea414c1 | |||
| cc6997baec | |||
| d126c28b85 | |||
| c1649b9dc0 | |||
| b0a8d12112 | |||
| b5160d38cb | |||
| 360e9b5936 | |||
| 85a6cd828e | |||
| e698f19a64 | |||
| 83c1b580dd | |||
| 9273e1f8ed | |||
| 5287825279 | |||
| 5567959afd | |||
| eaab36b051 | |||
| 9a4f3af580 | |||
| b83664dfce | |||
| 349f7ae965 | |||
| 6f41716ab5 | |||
| abeb62dea8 | |||
| 7cfed80443 | |||
| 36aa7d9b25 | |||
| 2a449ad3c0 | |||
| a71c5957cc | |||
| eef6c7d55e | |||
| 3872322da1 | |||
| f1e850a62a | |||
| 8d9278419d | |||
| 0862827a51 | |||
| f37a220a05 | |||
| aa51c2b6dd | |||
| 92da70a7ae | |||
| a55862fc25 | |||
| 09b14a5702 | |||
| 260b831815 | |||
| 665cf6ac65 | |||
| 5e90021698 | |||
| 16aeb7e12c | |||
| b4cbec173b | |||
| 8c9e06c329 | |||
| e4a35fdd72 | |||
| 370d82e66c | |||
| 876e851517 | |||
| 190fe37a9f | |||
| 623661f8d2 | |||
| f31bc3f8c5 | |||
| 12fc48544c | |||
| 3b4e44a9fd | |||
| 7850fb727e | |||
| 9b3122837e | |||
| d4c59cc73b | |||
| fe36bf54b2 | |||
| 4d8308a763 | |||
| 11f26c1e2c | |||
| e10104d401 | |||
| 5e09c3c318 | |||
| 576b88221d | |||
| 7dc2a0091b | |||
| 89a4a85589 | |||
| a3baf07a5c | |||
| 8e6909478c | |||
| 2d2d0cabb3 | |||
| 6f04a1b8fa | |||
| a9ad3a56ad | |||
| 3069b14e9c | |||
| ae868c6372 | |||
| c287ff554c | |||
| 3ccbd35fad | |||
| 51cc69208f | |||
| 1fec2f7a46 | |||
| ffffdd2d7d | |||
| f8c3e3d5aa | |||
| eccce6fa84 | |||
| 44be6cee2c | |||
| 799afd511a | |||
| 37c4541468 | |||
| 119a66acd0 | |||
| 105193a452 | |||
| 02d793e9e0 | |||
| 90647a8ba8 | |||
| 06950a6bd8 | |||
| 0c2147d9c5 | |||
| 51e8174689 | |||
| 185318a70c | |||
| a88759ba94 | |||
| 96ac054392 | |||
| bd7f4dd73f | |||
| 2e22152089 | |||
| 98a89aea11 | |||
| 372e33c066 | |||
| 52641db590 | |||
| f9ad5f0d01 | |||
| 3db1c7a521 | |||
| b6d1f96930 | |||
| 61a9b01062 | |||
| f0faf5b42e | |||
| 11733d5e05 | |||
| c4a38d9008 | |||
| 0edd7e14b7 | |||
| 8b7bb7b6ae | |||
| 39d5955e58 | |||
| 536327f403 | |||
| 8679dc63be | |||
| 1e9979ae18 | |||
| d2dffe3186 | |||
| 9971867e2f | |||
| 8c93e578a0 | |||
| 1ba5d2e1ca | |||
| ed792fc0d4 | |||
| 4e575c44b3 | |||
| fc06181217 | |||
| 0d6fcf78d8 | |||
| 82225f0b81 | |||
| 2177e13b49 | |||
| b63f4eb503 | |||
| cd7bc09f9c | |||
| ccf775e976 | |||
| 811346e83d | |||
| d3240a75d0 | |||
| d93e396ce5 | |||
| d74cc234c4 | |||
| 0fda83d8de | |||
| a1ec8545fb | |||
| 087f574355 | |||
| 1de784b817 | |||
| db79c05d9a | |||
| 1fdb1f0da6 | |||
| e2bfaa6233 | |||
| 6089849d35 | |||
| 6e02bf59e9 | |||
| 12228c56d9 | |||
| 3b631cb6af | |||
| b27a58df7e | |||
| 308f139193 | |||
| 692fc4e11d | |||
| b52f65c4d7 | |||
| 299275e368 | |||
| bb71c519db | |||
| 9ec7109dc8 | |||
| 5b67fccb1b | |||
| 9807c4b7a1 | |||
| 9039600f6d | |||
| b5b0156e08 | |||
| 352e647d37 | |||
| e6b66f2525 | |||
| f5075906e2 | |||
| a6f57bced8 | |||
| d35bf9591c | |||
| d96f3f9dd4 | |||
| 1f4c2f8c5a | |||
| 95719654f3 | |||
| 4b921c47f5 | |||
| f069f53fae | |||
| 1494cc38fc | |||
| a035412956 | |||
| 08662ea831 | |||
| d97103268b | |||
| a959e2e34d | |||
| 602bfbdd79 | |||
| 0785bdd37a | |||
| 8b62b6e1b1 | |||
| d1c8ebd15b | |||
| 13088e4001 | |||
| 5c6fa1c82b | |||
| fd81867e7b | |||
| 78293e64dd | |||
| 930fc10aec | |||
| 0b2a4fda44 | |||
| efdce6d4cd | |||
| c0b0ea4493 | |||
| 0fcec0a0ca | |||
| c0701d8a35 | |||
| ae51092044 | |||
| b7fcccb8db | |||
| b21a52f5f3 | |||
| 2bdcfe13e6 | |||
| 12a97ec429 | |||
| 99298ce9f2 | |||
| a0ed25d373 | |||
| dc6c29c668 | |||
| 3a85f28b5c | |||
| 95c44b1ce2 | |||
| b88e0ecb78 | |||
| c26f6b72d9 | |||
| 6cd3cc96a3 | |||
| c138cf0b76 | |||
| 8edb086178 | |||
| 0f74b5a4b1 | |||
| 6de2feff05 | |||
| a9811ee955 | |||
| 98b6c11a82 | |||
| 3df2929d12 | |||
| 30d1f423cb | |||
| e0c5566f35 | |||
| c5a2ae47d7 | |||
| 17654b885b | |||
| acaf0ca0af | |||
| 6a201be1fd | |||
| 8617b9164e | |||
| 991e5f1b3c | |||
| 55c533c947 | |||
| d17543d5d9 | |||
| 73169563c4 | |||
| d351cb9f7f | |||
| 251be60630 | |||
| 18d7222804 | |||
| ff1c37d361 | |||
| 3abe6db3de | |||
| b9ca9d3b06 | |||
| 9bbc04843d | |||
| ddf61a6438 | |||
| ba35a81e9b | |||
| 13133583ca | |||
| e91b981740 | |||
| 1458ee64ec | |||
| 4e2a5e1663 | |||
| c98b0e9687 | |||
| 4e25990de9 | |||
| 4b51f1a3fc | |||
| 5490b01004 | |||
| 43176e1f1d | |||
| fffd3c07ca | |||
| 952bd38e70 | |||
| 8589f5cc89 | |||
| ec594d52f8 | |||
| 848cbf9c60 | |||
| e02be7dd9a | |||
| 33d35e47a6 | |||
| 18d62b32c2 | |||
| 58ed51b49c | |||
| 9f9fe529e4 | |||
| aad721890a | |||
| e63a5d10c8 | |||
| 15681262de | |||
| b5d3aa81f9 | |||
| a688061616 | |||
| f788d347ec | |||
| a33ce67dd4 | |||
| 8a22c5627a | |||
| 7b529a30f4 | |||
| 4910b34f26 | |||
| 74ad521fc9 | |||
| 6a73b43e3a | |||
| b59b26460d | |||
| aa324ae25d | |||
| ff412f6396 | |||
| ac148b2f32 | |||
| 6f5d096e6d | |||
| ff1e6714c6 | |||
| 41d82eaadf | |||
| be6b011204 | |||
| 65e48c05f0 | |||
| cee08ea67d | |||
| 35ef26c013 | |||
| b08a0b8102 | |||
| 220342154b | |||
| ef7182cd76 | |||
| 6059c69995 | |||
| 892d8ba152 | |||
| da11069f75 | |||
| bf72908dc9 | |||
| 81ab5c78e2 | |||
| de503c7d63 | |||
| 487ccb90a1 | |||
| 4bfeaa47bf | |||
| 9a4d8036a6 | |||
| a50bd6375d | |||
| 14508e91a9 | |||
| 2d5064bd5b | |||
| fdc4819e39 | |||
| efddbb616b | |||
| db4d0f26a8 | |||
| 06cffbc31b | |||
| 3cfb0f4c76 | |||
| 436ba067bc | |||
| f8724bf47e | |||
| 1f7e293204 | |||
| 86d2652258 | |||
| f5c0cc2e2d | |||
| 4463d4a417 | |||
| df35a5c9b1 | |||
| dda93b3c13 | |||
| 5de40b6bf5 | |||
| dcbe799d25 | |||
| a5641aef03 | |||
| 668f97a4c8 | |||
| e9d577496b | |||
| 66fba947ac | |||
| 18c7e02609 | |||
| 7e1351507a | |||
| 032acbd27c | |||
| 8b51f0fbd2 | |||
| ead5942ef0 | |||
| cfe369d9fe | |||
| 2750a3c130 | |||
| a7f6d6ecac | |||
| 3680282450 | |||
| 3b3435c999 | |||
| e2ae6278c2 | |||
| 37d6723e3c | |||
| 961bbf6f55 | |||
| fc71d349ed | |||
| 97c8ab0f71 | |||
| 28534ea547 | |||
| 1a75b79a33 | |||
| 75dfb0af35 | |||
| 073fa09648 | |||
| 3015e7b203 | |||
| 7ac0cdc0f9 | |||
| 49234183a3 | |||
| ed7763df92 | |||
| b818a02f76 | |||
| 886f8449fd | |||
| 5de6643604 | |||
| 6283273d81 | |||
| 5506d18bc5 | |||
| c8f8f02a46 | |||
| 4c5d6f1d6c | |||
| f349e36345 | |||
| 1574c4ab13 | |||
| 0dc9bc3ab6 | |||
| 0006d57a4a | |||
| 70f7e73b9a | |||
| b67b049802 | |||
| ff99dbcd22 | |||
| 0198697737 | |||
| a742a71f67 | |||
| 8f6c1531fa | |||
| d9f56f11cb | |||
| 7ff877464f | |||
| 7c367eb1e8 | |||
| 20848aac09 | |||
| 7716574076 | |||
| 89cd0c769b | |||
| a21b3d788f | |||
| 7994ba8b38 | |||
| 81b4cb2f16 | |||
| 14af1df6a3 | |||
| ac3bd7d7db | |||
| 632e8e9634 | |||
| 1e3f09f69b | |||
| f3f18ffca7 | |||
| ceb3aa0627 | |||
| 014555fe5d | |||
| fdb76867a7 | |||
| 57157fde9c | |||
| 4a3540cf2b | |||
| 540bd938f2 | |||
| c6fbebd996 | |||
| 18d3a7e23a | |||
| cbae355896 | |||
| 924e8a764a | |||
| 40e629cb0e | |||
| 99ec261f79 | |||
| 9daea5bbc2 | |||
| 4704c03d48 | |||
| 9dbf52bedb | |||
| cfd04ab7fa | |||
| 33dbf75e3b | |||
| 8ffaeeb171 | |||
| 451828e5d4 | |||
| 176877f2b8 | |||
| 61b0d08b5f | |||
| f50a6ce1ae | |||
| 8549d6ea8b | |||
| 90a90a87a5 | |||
| 760bdea521 | |||
| 0fd783e83f | |||
| 42caa9058b | |||
| 9ab315ac2c | |||
| a08ad07537 | |||
| c6ce763f7c | |||
| 70d1eb17a7 | |||
| 0393205eda | |||
| 5c05a5a63e | |||
| b01d9f849b | |||
| 0ab904275c | |||
| 5f82a86801 | |||
| 13a5d8de86 | |||
| f024b71808 | |||
| cd35a17904 | |||
| be3f68ed06 | |||
| 64b8fd9584 | |||
| b001d1d0f1 | |||
| 64ad75c79e | |||
| 12eb361d69 | |||
| dd51bd0a2f | |||
| 0fb478a7c8 | |||
| 9694ecf55b | |||
| fe80478b8a | |||
| a253c26bee | |||
| a500f983f5 | |||
| 3579b6f8f3 | |||
| 14cb954213 | |||
| 6170eef441 | |||
| ddb9af4d57 | |||
| 6db2c0bf93 | |||
| 46f70e6a5e | |||
| 0b994a1b0f | |||
| 0326244956 | |||
| ea1b2252fc | |||
| 0da25ea7cc | |||
| 3209afa4ff | |||
| 33e2631e5d | |||
| f02a41b675 | |||
| f497d7b212 | |||
| 0f18f5eb81 | |||
| 8da81104aa | |||
| 97cbdc6b0f | |||
| 7f53f4e36b | |||
| c350f262e6 | |||
| 3e727893bf | |||
| 2a2c7437e4 | |||
| 4f0885d3af | |||
| 5656cd2fc6 | |||
| b6e42876ad | |||
| 3671344867 | |||
| 30b149053c | |||
| cdd40df9d3 | |||
| 9d661808ba | |||
| 6a5d849b3c | |||
| 25bbd73a90 | |||
| 3d27a5f8c7 | |||
| 44ef372399 | |||
| fd09dff381 | |||
| 10580a0bec | |||
| f3f87b0a46 | |||
| ac606bcf71 | |||
| bb50df07e7 | |||
| f7ac033e1c | |||
| a1043352c1 | |||
| e15fecf6cf | |||
| 3e99f345ef | |||
| 0a77770ef3 | |||
| 379b8392dd | |||
| 0fdae70461 | |||
| 087423c754 | |||
| ce64c98fb7 | |||
| e11a8a9dbb | |||
| ef4a68a24d | |||
| 0ec65c85d3 | |||
| 74326f226d | |||
| 554e0d7a39 | |||
| 1b99ac92dd | |||
| ff0b77908a | |||
| 83d19638ba | |||
| a7d484b1a8 | |||
| d87beb37b8 | |||
| dcdb383960 | |||
| 478a7e95ea | |||
| f7b3b582cc | |||
| d1719dc277 | |||
| 9d8c812aa3 | |||
| 2e390acce9 | |||
| e04797d891 | |||
| fb6195b081 | |||
| c6491a3ccd | |||
| 014ae2107e | |||
| 1457eb9d19 | |||
| 6f5f8cc7d8 | |||
| 9ec3fee455 | |||
| 2d52168c26 | |||
| aa5dd7920f | |||
| d60956e82a | |||
| 91fd3fb5ca | |||
| 46f3b662a7 | |||
| 54a61dcdae | |||
| b58f5232c7 | |||
| 9a5bda862c | |||
| 7f67adb639 | |||
| 48fd943622 | |||
| 08350d00be | |||
| 84bf4a2686 | |||
| 2823456e96 | |||
| a373629fc2 | |||
| 0aa2f72a3a | |||
| c032f6dde1 | |||
| f448d8f334 | |||
| a5d4a43753 | |||
| 52e01321ec | |||
| 19692e0122 | |||
| cd1da3cb81 | |||
| ffaa8f4ab3 | |||
| f3da9d680d | |||
| 6118bd24f2 | |||
| fddbb2e2ac | |||
| cb7156b091 | |||
| b5914a3f12 | |||
| 348d576ff3 | |||
| 7507654749 | |||
| c25aad0c4a | |||
| 4aa32415cd | |||
| cde23bbf98 | |||
| 555fe0b629 | |||
| be44c28fd3 | |||
| 657091307e | |||
| 458dc6e07d | |||
| cfaecf9fcb | |||
| 902f722d4c | |||
| ea2d8b2773 | |||
| b319754dc3 | |||
| 35f2040717 | |||
| 4121fc926e | |||
| fd9ab987e6 | |||
| 6c3a235b08 | |||
| 07cb861385 | |||
| 217e348fc3 | |||
| ad917afba4 | |||
| 9539c6d317 | |||
| e946e1e01e | |||
| fea1e71a99 | |||
| 9731eb8c5b | |||
| e058ae54ef | |||
| ba6714f384 | |||
| 0a0a761e23 | |||
| 88e8368022 | |||
| 468ccc6288 | |||
| 77e0237148 | |||
| ffdfb07f72 | |||
| 203ed88024 | |||
| 9923bcd16f | |||
| 0179ec051f | |||
| 05b6353355 | |||
| 47c6ed8829 | |||
| ff9ebd64e3 | |||
| cd2b098b81 | |||
| 7000a4cead | |||
| 5aa983b45b | |||
| 800989419b | |||
| 362679954a | |||
| 2732d64beb | |||
| 88684d6d9e | |||
| 9afddf0850 | |||
| a2d7d6e81f | |||
| 49bcb5e19f | |||
| 3744ea6218 | |||
| b2c6d87633 | |||
| e4f538b674 | |||
| ea0e0b4396 | |||
| 1ac784dc7a | |||
| d980c9ca4c | |||
| 6a2a60df0d | |||
| ee9b89b6d5 | |||
| 68cc109933 | |||
| 1015bb0fdc | |||
| 7da94dd109 | |||
| 171808cf2c | |||
| 78d14cdfea | |||
| 19982a2ffb | |||
| 79904560fc | |||
| 28aee1c58d | |||
| d4512f360b | |||
| 48d0ffe868 | |||
| 62af12f3b9 | |||
| efa4bfef2f | |||
| fc3ead4c01 | |||
| ca9c813b40 | |||
| e3b0f47c40 | |||
| bf13e42092 | |||
| 4345fda9bc | |||
| 4059e9faa9 | |||
| 95fcce1bcf | |||
| 86cde9716a | |||
| 6e315a50ed | |||
| 500c0b6534 | |||
| db75dca118 | |||
| d5f37e2cbe | |||
| a120a9895b | |||
| e9c1db020f | |||
| 08cd319a0b | |||
| 896ec69329 | |||
| 32e64bbb6e | |||
| 0974540e5f | |||
| e06963e616 | |||
| a675815529 | |||
| 3d48135b32 | |||
| 8f09e7f097 | |||
| e8b09f1124 | |||
| 57ba353594 | |||
| b695e090cd | |||
| 69ecb7b54f | |||
| f57fe182ed | |||
| 10ee11c886 | |||
| 45842b1f05 | |||
| 9ca5f9ad48 | |||
| bb00036a30 | |||
| 4a2a540500 | |||
| 09217216dc | |||
| 22a64c4931 | |||
| 93f520b941 | |||
| c84714dc3a | |||
| a7aa09a13e | |||
| efa1cdfcb2 | |||
| 70695daae8 | |||
| 62fdba0600 | |||
| 5733e00908 | |||
| e56abaefd7 | |||
| c71e893e8e | |||
| 3992356517 | |||
| dbb84ef129 | |||
| 8781fa2e33 | |||
| 605a1365d3 | |||
| ebb1bbc966 | |||
| d4518f7a6e | |||
| 68c9008019 | |||
| cafde1e910 | |||
| 41f5f3fe6e | |||
| cf12ecfa34 | |||
| 4fa668927f | |||
| 24d2b5b4bd | |||
| 4da2e003e1 | |||
| 205debde9c | |||
| ba6c7b0901 | |||
| 8e5d02fd25 | |||
| 11d3251c67 | |||
| 9ec5787b4d | |||
| 027c669ad7 | |||
| 405c12ba3f | |||
| 64fd53a8e5 | |||
| 069d63e3cf | |||
| 66049e0e25 | |||
| db7ef01dbb | |||
| 1a6b649fd6 | |||
| 548619775e | |||
| 4b79a75f39 | |||
| 28912fff97 | |||
| 98a83ddcc0 | |||
| e9d965ce7a | |||
| b7624813cc | |||
| 734e451bd9 | |||
| 7a96436085 | |||
| 670af8fb1b | |||
| 0abd062e5c | |||
| 1286468d5a | |||
| 0f3da5be4a | |||
| 9170269e06 | |||
| 8bb460701f | |||
| c0a7173ca4 | |||
| 949e15d8c6 | |||
| 0caedeb481 | |||
| 83ca8d95be | |||
| 8483868783 | |||
| 6961388545 | |||
| 8ef1ae5202 | |||
| 26b8f93cea | |||
| a576523976 | |||
| cf21c4085b | |||
| 204f655d6d | |||
| fb14865785 | |||
| 5cd052cc59 | |||
| af49c4117c | |||
| dd736130a0 | |||
| cee8bda676 | |||
| 6420a72ceb | |||
| 380e7c7899 | |||
| 16b4058a8b | |||
| e3fba0257f | |||
| cb8b65f6f3 | |||
| 3944a8ed54 | |||
| 2ceafe6379 | |||
| 9f058bd88a | |||
| 1dd0ff13d8 | |||
| 6061f10230 | |||
| ea59fafb52 | |||
| 2ff71126de | |||
| 6d522e1743 | |||
| b37f3c819d | |||
| 1b41994e5f | |||
| 4fd4b0d713 | |||
| 7b43ed702e | |||
| 56cd77942b | |||
| 37dc723b4b | |||
| d7ea37c917 | |||
| c1d2bf9b9f | |||
| 36f837730a | |||
| b8f0a1dd82 | |||
| 2d1b62a978 | |||
| 0792d0d59d | |||
| 8c6da1a658 | |||
| 26be10f937 | |||
| ef4d9a13ed | |||
| 320d987ede | |||
| 51a4c4d13b | |||
| aaf3ae5f9c | |||
| 858f4f74b3 | |||
| 3feed90039 | |||
| b46330fc7a | |||
| 5ca7ce1f75 | |||
| d1d9b37ec7 | |||
| 823115c4ec | |||
| 8fe3fda27f | |||
| 63fffd7493 | |||
| 243a350e2b | |||
| 8c09e7ba36 | |||
| d89502f91f | |||
| 847602f933 | |||
| 8fcf8fe39c | |||
| d59eec9296 | |||
| b9122ebc34 | |||
| 38e066ee49 | |||
| 232a04a1a2 | |||
| dcb145635c | |||
| c9b73c68a6 | |||
| 08f7925ea9 | |||
| ed79ec7940 | |||
| 869a44dd04 | |||
| 6a9e9ebc4c | |||
| f18a2a11fa | |||
| 9aaaa755c7 | |||
| 818cd9044b | |||
| 006ca12b6d | |||
| b5f9ab7c20 | |||
| 0981f95f16 | |||
| f64ef32eaa | |||
| 7a19659c9e | |||
| b1202fcd55 | |||
| b8f991eddb | |||
| d0013f55a2 | |||
| 7efaff4f6f | |||
| bc7a84e462 | |||
| 61657bd1b1 | |||
| fc63f4f89c | |||
| 2c44c252d7 | |||
| 613fc6e3ef | |||
| 0a9604d10e | |||
| 1cabedc65e | |||
| 4e5a0d9e02 | |||
| bf373d894d | |||
| e6bbad84c1 | |||
| 4de477d46c | |||
| 264cab3c9b | |||
| 7f9ef59bea | |||
| 8ebbef4cbb | |||
| 0c98a84242 | |||
| 2bbdd21b2f | |||
| 794cfe949b | |||
| 6a5010a2d6 | |||
| 61559fee9d | |||
| 2118a20408 | |||
| b0bc20cb83 | |||
| 2cd77b075b | |||
| 03450c6085 | |||
| 5d3c522da2 | |||
| bf1ba6efc0 | |||
| 67ad82d52e | |||
| f437d976f8 | |||
| c94443faaa | |||
| c6d57f6a56 | |||
| 09b06da403 | |||
| df73ce8a60 | |||
| 8b22c2e04f | |||
| b4f4d6cea2 | |||
| cdf7b2acf1 | |||
| 94651731a5 | |||
| 56424d29a3 | |||
| c5083b2aef | |||
| 1b06795ffe | |||
| 6679cd60ce | |||
| 843a3c16ba | |||
| df40204a57 | |||
| 210910eb6e | |||
| ef427758da | |||
| def6bbf683 | |||
| 5f37a4a21c | |||
| dc869987f9 | |||
| b98d2201e5 | |||
| 8dd6440bb8 | |||
| a2d83c48eb | |||
| a0ae8f4a07 | |||
| 719dcbcd4d | |||
| caa207e868 | |||
| 306004ecd2 | |||
| bcac3c5b84 | |||
| ccc90108df | |||
| b9781915a4 | |||
| 1890e65b75 | |||
| 2f4da3ed52 | |||
| 27a6ac0c0b | |||
| b81a66f07e | |||
| bc43fc9585 | |||
| 4f7c013c78 | |||
| 09b15a07c6 | |||
| 4340f024d3 | |||
| 98e9780819 | |||
| a45d63595f | |||
| 5fbd1453b4 | |||
| f827201ee0 | |||
| 485a96962a | |||
| 98a104f731 | |||
| ff987f14d7 | |||
| 4dd08268dc | |||
| c18a13d4fc | |||
| 7a79995727 | |||
| 01eca562c5 | |||
| f491d0c0c4 | |||
| edf2287e13 | |||
| fcaab633c1 | |||
| 33b6579b5d | |||
| 72a8e4bc22 | |||
| c67b02b7b0 | |||
| 486cd00384 | |||
| caea7bb39d | |||
| e8fb7ee683 | |||
| e4ee4211c8 | |||
| 3fa3ebee14 | |||
| 8e531f400e | |||
| 5727c0dc8a | |||
| f599088cdf | |||
| d78da55f54 | |||
| 9052ff71e6 | |||
| 713da887ca | |||
| ad5950697e | |||
| 85d0d06cab | |||
| be6ec558e3 | |||
| dd2f60810a | |||
| 6c0b86d8f4 | |||
| 517ada6dbf | |||
| 0a098e3008 | |||
| c40078ffc9 | |||
| 729e0afdbe | |||
| eab86840d3 | |||
| 43aa2247f1 | |||
| afe17a507e | |||
| e621020474 | |||
| ac2aa7f288 | |||
| 805bd7e930 | |||
| f9bf2122b7 | |||
| 4e3e533e13 | |||
| 611c429912 | |||
| e2898ee5bf | |||
| e1a532500e | |||
| 4cf412b93d | |||
| 54c9c434e0 | |||
| de19f3e5fe | |||
| 8304993481 | |||
| 4c838aeaca | |||
| 4ec1145dce | |||
| 547d925065 | |||
| 874633051b | |||
| 2293f1ba15 | |||
| 483c3f61b5 | |||
| 83263968f2 | |||
| f279c61792 | |||
| 9717c696d8 | |||
| 94a594c777 | |||
| f1ae8a17cf | |||
| add9c43827 | |||
| d3f6a5c296 | |||
| 4aea78b7c0 | |||
| 4313fe670d | |||
| 2e8a1b67ea | |||
| e73044c533 | |||
| 418f09e364 | |||
| 08f207a396 | |||
| 4f57060708 | |||
| 1507724ecd | |||
| 4bc2f64b24 | |||
| 839ba31dfb | |||
| 2341452d91 | |||
| 7ab263b514 | |||
| 45b5b5960f | |||
| 1b8ec1f01c | |||
| c92cb1c096 | |||
| 8f31fbe275 | |||
| aa76419eda | |||
| 21503c0c38 | |||
| d957b19f55 | |||
| 122c4242ce | |||
| e769ed2fd8 | |||
| c722904831 | |||
| c93faa9564 | |||
| ce6495a46d | |||
| 04f2b905af | |||
| b668a2c0d2 | |||
| 5fc8f10602 | |||
| a27f399a9e | |||
| ff5d14fb63 | |||
| 27fb2d2d5b | |||
| 0b9d59bf0e | |||
| 23e059a065 | |||
| 45c7b29a96 | |||
| 3e6c3fda60 | |||
| 1a1363ef77 | |||
| b5c51e231b | |||
| 22711dd7be | |||
| 1ba4d3d5d1 | |||
| 823ab73585 | |||
| 66c444efac | |||
| b688b8921a | |||
| 00550ddab5 | |||
| 42fedec088 | |||
| ff3bc0e8c2 | |||
| dde7dff870 | |||
| 25ffc9cf96 | |||
| eca92cf5b5 | |||
| 372bdf8be7 | |||
| c7c90b0647 | |||
| 14c2da4fa2 | |||
| 64275cfd22 | |||
| f0b099d9f7 | |||
| c99d957bc8 | |||
| 51b14d55ec | |||
| f92d9994df | |||
| 431e1b87bd | |||
| 3ec2fc6030 | |||
| 99f416bf6b | |||
| 89acc4870b | |||
| 314c739c3a | |||
| 30b18fc997 | |||
| 168ac60853 | |||
| dff0c0c3b0 | |||
| ea3e9de7ab | |||
| c93fd900bc | |||
| a761aa9a37 | |||
| f37eb92d93 | |||
| 2c7cc5d325 | |||
| 16659c82d2 | |||
| 4ed3ca4128 | |||
| 9b2899c8fb | |||
| fada7fc9cc | |||
| 2be5bca42b | |||
| b1ebf6367c | |||
| 57e373f578 | |||
| 5c01874a32 | |||
| dd89e11633 | |||
| 4e51f19780 | |||
| 33d37d8dee | |||
| 60ce34af43 | |||
| 2602ff6e34 | |||
| ef5cd36a37 | |||
| 7d47f5aba3 | |||
| 33cb506c71 | |||
| 5fb36acb04 | |||
| 5f985f359b | |||
| 09045d5387 | |||
| 371af755df | |||
| 0485632e22 | |||
| c3d6de2412 | |||
| 36d49b11d9 | |||
| 866277510e | |||
| da8e00eb4e | |||
| cd30b2af3f | |||
| 38b3337d35 | |||
| 84d34aa12b | |||
| 96b7b68900 | |||
| 2a6ffbf5a4 | |||
| 1ac99e260b | |||
| 519062c8d0 | |||
| 78d7cdf662 | |||
| a191b1e811 | |||
| b372161874 | |||
| 5bbec7b17c | |||
| 54b847a8ef | |||
| a40a0f730f | |||
| a677b94297 | |||
| d102528d0a | |||
| 854692f975 | |||
| bdc321df4d | |||
| 230e8e9d4d | |||
| 96c49689ec | |||
| 698f560d56 | |||
| 67ee493e56 | |||
| 4fc7ef698a | |||
| 89434fc3bd | |||
| 6129f9afe8 | |||
| e5f38dbbc6 | |||
| 5534db95bc | |||
| 37c45424fd | |||
| ed44f4ed0c | |||
| f829f35cfa | |||
| 3033acadbb | |||
| 5c2a8a68c0 | |||
| e9544f8f92 | |||
| 1a6743dba9 | |||
| 06d6621c14 | |||
| d7b82edae3 | |||
| 5eae156134 | |||
| d8843e6057 | |||
| cf7c734778 | |||
| 240a8930ef | |||
| 4f3046593a | |||
| a0bd5bdd35 | |||
| e3bc63e4fa | |||
| fbf7c6293f | |||
| e128b02da1 | |||
| 2168dbc6fc | |||
| 97db895354 | |||
| 48fc68bfdb | |||
| 3741ffbb59 | |||
| caf58492f2 | |||
| 0c5bdd0205 | |||
| ae6da0d6ef | |||
| bdbb4cb495 | |||
| 566c358e8d | |||
| 324cffe6df | |||
| dadb8119e8 | |||
| 335d54b5f4 | |||
| 0fd851999f | |||
| 50f88798d8 | |||
| 8033cfc49d | |||
| 882f450ee8 | |||
| 4d60a582fe | |||
| 7ef3d33d0e | |||
| 24328ee8ae | |||
| d45c380389 | |||
| fbb3d144a1 | |||
| 7e7e299bce | |||
| d18116fcbe | |||
| e38513f077 | |||
| 08c4c641ec | |||
| 5d7e46b246 | |||
| dac906a79d | |||
| ecfeac4d63 | |||
| db1fc399a1 | |||
| 60616a5228 | |||
| 699c8d5b60 | |||
| 4264f013af | |||
| ecc4e1803c | |||
| 5c230c2d65 | |||
| 6af681354b | |||
| 5f381cf153 | |||
| 2ed290a214 | |||
| cf11472348 | |||
| 2c14514c56 | |||
| 053e6d8325 | |||
| 603c55fda7 | |||
| 113adcc5cc | |||
| a5bf8f6732 | |||
| c4b482634f | |||
| 4b083d6f81 | |||
| 04973d153b | |||
| 69ce7c78a4 | |||
| b64cf197f2 | |||
| a8b0279853 | |||
| e715e2ba2f | |||
| 985de36647 | |||
| b7b576e38b | |||
| 5018669ded | |||
| c5342d3eb0 | |||
| 0e7a0b5754 | |||
| 933ef02df0 | |||
| 513e0d748d | |||
| 852f01fddd | |||
| 9ebada63e9 | |||
| dbda70eee9 | |||
| 18bd10dfed | |||
| f76d03a0dc | |||
| 86ba5c9c1a | |||
| 5b56fbed97 | |||
| dfdd918ea0 | |||
| df2c7ec1a1 | |||
| 0d179f2042 | |||
| 63c6a9735c | |||
| f7e3f7d187 | |||
| cc54da5980 | |||
| ca82796081 | |||
| d092068bd2 | |||
| 0c1529237c | |||
| 6dbb2cb98a | |||
| ead763b909 | |||
| 415139851e | |||
| 5c99b72e2f | |||
| 82cb6c93cf | |||
| d21734ccf2 | |||
| 91934006d5 | |||
| 014c258c57 | |||
| 5e76bc3f2f | |||
| 7f6940f3a5 | |||
| 8d5682deb0 | |||
| 819abbfa7a | |||
| 227ad332b8 | |||
| 17c83e1a85 | |||
| 5d699496ca | |||
| e4f4fe81ae | |||
| abe28367fb | |||
| cc394d9f74 | |||
| 476b51ee31 | |||
| ec823ade14 | |||
| 0c3777f7d7 | |||
| a679e47c0d | |||
| 1e82735241 | |||
| a09ec6b0b5 | |||
| 0134a50ffa | |||
| 1da005e811 | |||
| 8ab912429b | |||
| 88d12223a5 | |||
| 6c363d9c4b | |||
| 6d3d9659e2 | |||
| 8977c5cef4 | |||
| d3645f6c63 | |||
| dbb057621d | |||
| 3a65bc0650 | |||
| 1eb6ff8c17 | |||
| 505cc01a18 | |||
| fdc4bb455f | |||
| c1a27aa6e4 | |||
| a14c7233d4 | |||
| c786d77b07 | |||
| e4913dfb73 | |||
| dfd33bab8e | |||
| 19b8f073b1 | |||
| b38b16f70e | |||
| dd69e4caff | |||
| 762a17d949 | |||
| d90cb23f22 | |||
| 1ccc391c8e | |||
| cafcead62e | |||
| 71ca46b21b | |||
| acf339ad13 | |||
| de4726ec8d | |||
| e58aff41f9 | |||
| 3c8acf9e0e | |||
| 782e8522c5 | |||
| dd4464cb08 | |||
| 3fdfd12448 | |||
| a11d23ba62 | |||
| 6bab362171 | |||
| 5d28c5c825 | |||
| b9173ebe50 | |||
| 0a1577ff43 | |||
| a616d6fd3e | |||
| 149b2ab9f9 | |||
| 4406dd88d1 | |||
| 48a950faf8 | |||
| 0ffbb5cb2c | |||
| f4c31bcf2c | |||
| f0e412a443 | |||
| ce91d36b7a | |||
| a022e5bb93 | |||
| 8e61673bd6 | |||
| 88e39d22f1 | |||
| 1441374f6e | |||
| d1cedaa3e8 | |||
| 5b721f6453 | |||
| a5f05b4715 | |||
| 96f49f117a | |||
| 88e1211762 | |||
| 59e1664223 | |||
| 42922a0d75 | |||
| 4f8a2a0900 | |||
| 9a2a26e904 | |||
| 067c3433b6 | |||
| d0b07c4890 | |||
| 24f8f48a2f | |||
| acafcc9888 | |||
| ec4cfe6f86 | |||
| 7b0c80c8be | |||
| 29029714ee | |||
| cd1203d1e2 | |||
| 4dfc34515c | |||
| 11a9f3ced3 | |||
| 5be1a7000c | |||
| 9059c1b850 | |||
| ec60fe42be | |||
| 275931969e | |||
| e37e0f189a | |||
| 8103943396 | |||
| 9497c99e9e | |||
| 2172d1a767 | |||
| e72c47869a | |||
| 5ea2124e91 | |||
| 0c08919bfa | |||
| a7562c3803 | |||
| 5f4d8f6862 | |||
| 9c7d0951b7 | |||
| 75e57c75cf | |||
| 04502b05d4 | |||
| e3e3a8404a | |||
| 48bf17614f | |||
| 1941b1ff65 | |||
| 46aa3d275a | |||
| d667e790b9 | |||
| bc345b569b | |||
| ee84b4e013 | |||
| e77129fa51 | |||
| 591855919e | |||
| 7af6188a8a | |||
| 1cd31cfce9 | |||
| 227dda921c | |||
| bdf2047ea1 | |||
| 0522996b7f | |||
| e9e25ef8d8 | |||
| a08325c975 | |||
| 4d8adde9d5 | |||
| f2b2f4f226 | |||
| 40bbc57269 | |||
| 48526824d6 | |||
| 745c37f2d1 | |||
| 0e185b7335 | |||
| 596ae696f9 | |||
| b5e2bdf0f9 | |||
| 4e09cc14b6 | |||
| 58dc46b445 | |||
| f34d0a29e8 | |||
| 365a90e72e | |||
| 72f4d3efa8 | |||
| 98f4d4de8b | |||
| 57f84748ec | |||
| 4fb57d2818 | |||
| aff819069f | |||
| af585588af | |||
| eaf6617a43 | |||
| 4b294c4aff | |||
| c2354bffa2 | |||
| ea1ffca4ae | |||
| 86a68d78c1 | |||
| 2e4a3516c8 | |||
| e71fec0e95 | |||
| 9478915cf4 | |||
| f955d77d71 | |||
| c1255bfa88 | |||
| 9aaeee2026 | |||
| 27b3a4083b | |||
| 2f1d963126 | |||
| 235e82a348 | |||
| 17d0b7d291 | |||
| 22c60fb894 | |||
| dc3a873abc | |||
| 543d5f0619 | |||
| 8b088ab6c2 | |||
| 29009e14f4 | |||
| bf3e90baf5 | |||
| 3986c407ff | |||
| 8bd56d29d6 | |||
| 467b3db824 | |||
| 6025dd6f79 | |||
| 984f3d2781 | |||
| 0a558b963d | |||
| 1227d98904 | |||
| f533e4a5d0 | |||
| 0b00ad98e6 | |||
| 8988408d18 | |||
| f858d89f7b | |||
| 964ee68974 | |||
| 8244ba7754 | |||
| 21b2184750 | |||
| 8d1c98f03e | |||
| 684bab99a9 | |||
| 7a4a8a4581 | |||
| b8843a0eb6 | |||
| 563dfbea43 | |||
| 488ebf8f1a | |||
| 03158bd689 | |||
| 765f2df18e | |||
| a15d8553e0 | |||
| 3a790b03ac | |||
| 90e5e03531 | |||
| a4158c16f0 | |||
| ffc0b364ca | |||
| 1efb8f6681 | |||
| 55b2d7f68f | |||
| 7f0e98ea93 | |||
| 67c21c18cf | |||
| 0cd82c7e57 | |||
| 1942c36443 | |||
| 93251582ab | |||
| decd2d6625 | |||
| 6824dcb89b | |||
| eeac075a29 | |||
| b864b1dec3 | |||
| 130fb9c217 | |||
| 688a3d29fd | |||
| 800f2f466b | |||
| 655a9ff1a4 | |||
| 12f8902c5f | |||
| 9cdb1a53f9 | |||
| f5efa18e96 | |||
| fcebde5abd | |||
| 7fbd4527e5 | |||
| d79716c3f8 | |||
| 884811fd99 | |||
| 712a2ae018 | |||
| 4f460eeb32 | |||
| 913416a9d5 | |||
| fd61001b4d | |||
| ce4d2c17a7 | |||
| a42ebd7aaf | |||
| f38718004f | |||
| 1ddf968750 | |||
| 71dc108b2d | |||
| 82728be44b | |||
| bebfa23cc9 | |||
| 1dad987073 | |||
| 6728abf595 | |||
| 669fce36e9 | |||
| b11003c472 | |||
| 187f80bfc9 | |||
| 611d216bb0 | |||
| 8a1cd32d50 | |||
| 2a18b27114 | |||
| 55f91143d9 | |||
| cf564a7d06 | |||
| b55067ad60 | |||
| a437c698eb | |||
| f8ada70fe7 | |||
| 196d9db3fa | |||
| e29eaf3785 | |||
| 7048ea473f | |||
| 720b1eeb27 | |||
| 4e282dac5b | |||
| 42343254b1 | |||
| e98829c96b | |||
| 5791f12a28 | |||
| fe9a6e8fdd | |||
| dd3f4e4751 | |||
| a89fa766ea | |||
| 39c2cf6c72 | |||
| c1ce72469c | |||
| eca76dd7c4 | |||
| 621b2d8fc9 | |||
| 4039b962b4 | |||
| 70cc0b93f0 | |||
| 81e90cd4e4 | |||
| 80bd3be0a1 | |||
| 829a794df7 | |||
| 00997408b8 | |||
| 4d3bdfc52f | |||
| b14d0926ed | |||
| 59b46db8db | |||
| a240284727 | |||
| 3d314324e0 | |||
| b9829bb696 | |||
| 85f03fb1d0 | |||
| 69810303e5 | |||
| 5b0e31acf3 | |||
| f0a934e014 | |||
| 328e4db1a2 | |||
| a708265e14 | |||
| a0618f7f47 | |||
| f2bd9fae01 | |||
| 64f717f81b | |||
| fd17377c18 | |||
| 6698fdb6e6 | |||
| e2eab1ee5d | |||
| aee9bf7cc2 | |||
| 63d9d554b9 | |||
| f6c04f9e74 | |||
| e167300194 | |||
| 319400ab13 | |||
| 38b1717fac | |||
| d5938a016c | |||
| 3ac592b18a | |||
| 82d6103907 | |||
| b353db67d4 | |||
| 5334abebaa | |||
| ad053361dc | |||
| ad62ccd8c7 | |||
| ecd93c9a92 | |||
| 774c02afab | |||
| 18f1f7d83d | |||
| 9eb3266891 | |||
| c3a3a68054 | |||
| 925fb30e05 | |||
| 363895761e | |||
| c191accc64 | |||
| 71b6b83ab3 | |||
| d532f8007e | |||
| 44574af3eb | |||
| faa4019b27 | |||
| c1e934d171 | |||
| 5fd88f9213 | |||
| 2bb095d47d | |||
| a4a30e8b60 | |||
| 4c05876066 | |||
| 23be3e5ba3 | |||
| f69d776cf6 | |||
| 25795eaecc | |||
| 3e1e92a6f1 | |||
| f2f3511a2f | |||
| 0ce70f4d0d | |||
| cac5d96c16 | |||
| 25d6ff1c90 | |||
| 87de8c0800 | |||
| 34ba762b42 | |||
| badc4bcc2f | |||
| 729b4e18ab | |||
| c3f03f50b3 | |||
| 8fd4cfe0a3 | |||
| 8dee07082d | |||
| 11e33f5c70 | |||
| 3d68b0c566 | |||
| fb8dd537e0 | |||
| affce4a4a1 | |||
| a479a5740b | |||
| 8e68b4e001 | |||
| d10b6f04cf | |||
| 052f2db0ea | |||
| 4b9a6ffee5 | |||
| 6d6784d429 | |||
| b3d2d3028c | |||
| 68814dd97c | |||
| ac39a609d7 | |||
| ba5cf5ba83 | |||
| 394e77c152 | |||
| c7fdf5245a | |||
| 6d91b2d984 | |||
| 36a401c6a7 | |||
| cbb59eafc8 | |||
| 5cd80b5817 | |||
| 6306cc9c34 | |||
| e335b07efe | |||
| e86144255c | |||
| e2c4a3b591 | |||
| 81c20e6792 | |||
| 204941c1e4 | |||
| 307afa5c52 | |||
| 92dc50e348 | |||
| 5c6c5c9608 | |||
| e27b8aeb66 | |||
| 7274a455a8 | |||
| 91dfef52ea | |||
| 9147b4e39f | |||
| 052159ce6f | |||
| ea6d728b89 | |||
| 093049a1f8 | |||
| 17fa8d904e | |||
| a996a7043d | |||
| 822659f7fd | |||
| e1fb0acff7 | |||
| d132a9bec5 | |||
| f9f1b55185 | |||
| 15b32abadf | |||
| 93e565f8df | |||
| 69703072df | |||
| ad3a324719 | |||
| 8c9e178206 | |||
| 8a812dd853 | |||
| 7250824e6b | |||
| cc1a61d607 | |||
| e7fe1ab567 | |||
| cad24714d4 | |||
| 4acdabe439 | |||
| b5b0fbbd38 | |||
| 2680eed681 | |||
| 1b8a12dd2a | |||
| b0c6236c24 | |||
| 8646410e6f | |||
| b2743108f1 | |||
| 4be798d15a | |||
| 4c4d0b426e | |||
| 58104b5a3a | |||
| 68a3842c8c | |||
| 987cc63764 | |||
| a8672ec432 | |||
| a9f5bd679d | |||
| f2b5dcf466 | |||
| 6b26f999e4 | |||
| f2a70396d1 | |||
| b50daed0d0 | |||
| e664834fd9 | |||
| 4c49e30763 | |||
| 2d2c671e6f | |||
| d2f47262de | |||
| cb13eb7f3b | |||
| 608283ba65 | |||
| f5e5ef8662 | |||
| d5a1832f31 | |||
| e7def57c5b | |||
| 0d325a3ec4 |
@@ -0,0 +1,20 @@
|
|||||||
|
krill-parser.js
|
||||||
|
krill.pegjs
|
||||||
|
.eslintrc.json
|
||||||
|
server.js
|
||||||
|
tidal-sniffer.js
|
||||||
|
*.jsx
|
||||||
|
tunejs.js
|
||||||
|
out/**
|
||||||
|
postcss.config.js
|
||||||
|
postcss.config.cjs
|
||||||
|
tailwind.config.js
|
||||||
|
tailwind.config.cjs
|
||||||
|
vite.config.js
|
||||||
|
/**/dist/**/*
|
||||||
|
!**/*.mjs
|
||||||
|
**/*.tsx
|
||||||
|
**/*.ts
|
||||||
|
**/*.json
|
||||||
|
**/dev-dist
|
||||||
|
**/dist
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": ["eslint:recommended"],
|
||||||
|
"overrides": [],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": ["import"],
|
||||||
|
"rules": {
|
||||||
|
"no-unused-vars": ["warn", { "destructuredArrayIgnorePattern": ".", "ignoreRestSiblings": false }],
|
||||||
|
"import/no-extraneous-dependencies": ["error", {"devDependencies": true}]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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@v2
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 7
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
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@v1
|
||||||
|
with:
|
||||||
|
# Upload entire repository
|
||||||
|
path: "./website/dist"
|
||||||
|
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v1
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
name: Tauri Builder
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
rust_target: x86_64-unknown-linux-gnu
|
||||||
|
- os: macos-latest
|
||||||
|
rust_target: x86_64-apple-darwin
|
||||||
|
- os: macos-latest
|
||||||
|
rust_target: aarch64-apple-darwin
|
||||||
|
- os: windows-latest
|
||||||
|
rust_target: x86_64-pc-windows-msvc
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8.6.2
|
||||||
|
|
||||||
|
- name: Node.js setup
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: latest
|
||||||
|
# node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
- name: Install Rust (Stable)
|
||||||
|
run:
|
||||||
|
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||||
|
|
||||||
|
- name: Install dependencies (ubuntu only)
|
||||||
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Install app dependencies from lockfile and build web
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build the app
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
# tauri-action replaces \_\_VERSION\_\_ with the app version
|
||||||
|
tagName: ${{ github.ref_name }}
|
||||||
|
releaseName: "Strudel v__VERSION__"
|
||||||
|
releaseBody: |
|
||||||
|
See the assets to download this version and install.
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: false
|
||||||
@@ -7,13 +7,18 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [16, 17]
|
node-version: [18]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v3
|
- uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
version: 7
|
||||||
cache: 'npm'
|
- uses: actions/setup-node@v3
|
||||||
- run: npm install
|
with:
|
||||||
- run: npm test
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'pnpm'
|
||||||
|
- run: pnpm install
|
||||||
|
- run: pnpm run format-check
|
||||||
|
- run: pnpm run lint
|
||||||
|
- run: pnpm test
|
||||||
|
|||||||
+14
-1
@@ -30,4 +30,17 @@ mytunes.ts
|
|||||||
doc
|
doc
|
||||||
out
|
out
|
||||||
.parcel-cache
|
.parcel-cache
|
||||||
repl_old
|
repl_old
|
||||||
|
tutorial.rendered.mdx
|
||||||
|
doc.json
|
||||||
|
talk/public/EmuSP12
|
||||||
|
talk/public/samples
|
||||||
|
server/samples/old
|
||||||
|
repl/stats.html
|
||||||
|
coverage
|
||||||
|
public/icons/apple-splash-*
|
||||||
|
dev-dist
|
||||||
|
Dirt-Samples
|
||||||
|
tidal-drum-machines
|
||||||
|
webaudiofontdata
|
||||||
|
src-tauri/target
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
*.md
|
||||||
|
*.json
|
||||||
|
*.yml
|
||||||
|
*.
|
||||||
|
**/out
|
||||||
|
**/dist
|
||||||
|
packages/mini/krill-parser.js
|
||||||
|
packages/xen/tunejs.js
|
||||||
|
paper
|
||||||
|
pnpm-lock.yaml
|
||||||
|
pnpm-workspace.yaml
|
||||||
|
**/dev-dist
|
||||||
+14
-15
@@ -1,16 +1,15 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"jsxSingleQuote": false,
|
"jsxSingleQuote": false,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"jsxBracketSameLine": false,
|
"bracketSameLine": false,
|
||||||
"arrowParens": "always",
|
"arrowParens": "always",
|
||||||
"proseWrap": "preserve",
|
"proseWrap": "preserve",
|
||||||
"htmlWhitespaceSensitivity": "css",
|
"htmlWhitespaceSensitivity": "css",
|
||||||
"endOfLine": "lf"
|
"endOfLine": "lf"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+5
-1
@@ -2,5 +2,9 @@
|
|||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"subspan",
|
"subspan",
|
||||||
"vals"
|
"vals"
|
||||||
]
|
],
|
||||||
|
"yaml.schemas": {
|
||||||
|
"https://json.schemastore.org/github-workflow.json": "file:///home/felix/projects/strudel/.github/workflows/deploy.yml"
|
||||||
|
},
|
||||||
|
"testing.automaticallyOpenPeekView": "never"
|
||||||
}
|
}
|
||||||
+61
-23
@@ -12,8 +12,8 @@ To get in touch with the contributors, either
|
|||||||
|
|
||||||
## Ask a Question
|
## Ask a Question
|
||||||
|
|
||||||
If you have any questions about strudel, make sure you've read the
|
If you have any questions about strudel, make sure you've glanced through the
|
||||||
[tutorial](https://strudel.tidalcycles.org/tutorial/) to find out if it answers your question.
|
[docs](https://strudel.tidalcycles.org/learn/) to find out if it answers your question.
|
||||||
If not, use one of the Communication Channels above!
|
If not, use one of the Communication Channels above!
|
||||||
|
|
||||||
Don't be afraid to ask! Your question might be of great value for other people too.
|
Don't be afraid to ask! Your question might be of great value for other people too.
|
||||||
@@ -29,11 +29,10 @@ If you made some music with strudel, you can give back some love and share what
|
|||||||
Your creation could also be part of the random selection in the REPL if you want.
|
Your creation could also be part of the random selection in the REPL if you want.
|
||||||
Use one of the Communication Channels listed above.
|
Use one of the Communication Channels listed above.
|
||||||
|
|
||||||
## Improve the Tutorial
|
## Improve the Docs
|
||||||
|
|
||||||
If you find some weak spots in the [tutorial](https://strudel.tidalcycles.org/),
|
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/workshop/getting-started/),
|
||||||
you are welcome to improve them by editing [this file](https://github.com/tidalcycles/strudel/blob/main/repl/src/tutorial/tutorial.mdx).
|
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
|
||||||
This will even work without setting up a development environment, only a github account is required.
|
|
||||||
|
|
||||||
## Propose a Feature
|
## Propose a Feature
|
||||||
|
|
||||||
@@ -50,40 +49,43 @@ Please check that it has not been reported before.
|
|||||||
To fix a bug that has been reported,
|
To fix a bug that has been reported,
|
||||||
|
|
||||||
1. check that nobody else is already fixing it and respond to the issue to let people know you're on it
|
1. check that nobody else is already fixing it and respond to the issue to let people know you're on it
|
||||||
3. fork the repository
|
2. fork the repository
|
||||||
4. make sure you've setup the project (see below)
|
3. make sure you've setup the project (see below)
|
||||||
5. hopefully fix the bug
|
4. hopefully fix the bug
|
||||||
6. make sure the tests pass
|
5. make sure the tests pass
|
||||||
7. send a pull request
|
6. send a pull request
|
||||||
|
|
||||||
## Write Tests
|
## Write Tests
|
||||||
|
|
||||||
There are still many tests that have not been written yet! Reading and writing tests is a great opportunity to get familiar with the codebase.
|
There are still many tests that have not been written yet! Reading and writing tests is a great opportunity to get familiar with the codebase.
|
||||||
You can find the tests in each package in the `test` folder. To run all tests, run `npm test` from the root folder.
|
You can find the tests in each package in the `test` folder. To run all tests, run `pnpm test` from the root folder.
|
||||||
|
|
||||||
## Project Setup
|
## Project Setup
|
||||||
|
|
||||||
To get the project up and running for development, make sure you have installed:
|
To get the project up and running for development, make sure you have installed:
|
||||||
|
|
||||||
- git
|
- [git](https://git-scm.com/)
|
||||||
- node, preferably v16
|
- [node](https://nodejs.org/en/) >= 18
|
||||||
|
- [pnpm](https://pnpm.io/) (`npm i pnpm -g`)
|
||||||
|
|
||||||
then, do the following:
|
then, do the following:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/tidalcycles/strudel.git && cd strudel
|
git clone https://github.com/tidalcycles/strudel.git && cd strudel
|
||||||
npm i # install at root to symlink packages
|
pnpm i # install at root to symlink packages
|
||||||
npx lerna bootstrap # install all dependencies in packages
|
pnpm start # start repl
|
||||||
cd repl && npm i # install repl dependencies
|
|
||||||
npm run start # start repl
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Those commands might look slightly different for your OS.
|
Those commands might look slightly different for your OS.
|
||||||
Please report any problems you've had with the setup instructions!
|
Please report any problems you've had with the setup instructions!
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
To make sure the code changes only where it should, we are using prettier to unify the code style.
|
To make sure the code changes only where it should, we are using prettier to unify the code style.
|
||||||
|
|
||||||
|
- You can format all files at once by running `pnpm prettier` from the project root
|
||||||
|
- Run `pnpm format-check` from the project root to check if all files are well formatted
|
||||||
|
|
||||||
If you use VSCode, you can
|
If you use VSCode, you can
|
||||||
|
|
||||||
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||||
@@ -91,19 +93,55 @@ If you use VSCode, you can
|
|||||||
3. Choose "Configure Default Formatter..."
|
3. Choose "Configure Default Formatter..."
|
||||||
4. Select prettier
|
4. Select prettier
|
||||||
|
|
||||||
|
## ESLint
|
||||||
|
|
||||||
|
To prevent unwanted runtime errors, this project uses [eslint](https://eslint.org/).
|
||||||
|
|
||||||
|
- You can check for lint errors by running `pnpm lint`
|
||||||
|
|
||||||
|
There are also eslint extensions / plugins for most editors.
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
- Run all tests with `pnpm test`
|
||||||
|
- Run all tests with UI using `pnpm test-ui`
|
||||||
|
|
||||||
|
## Running all CI Checks
|
||||||
|
|
||||||
|
When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests.
|
||||||
|
You can run the same check with `pnpm check`
|
||||||
|
|
||||||
## Package Workflow
|
## Package Workflow
|
||||||
|
|
||||||
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
|
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
|
||||||
When you run `npm i` on the root folder, [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces) will symlink all packages
|
When you run `pnpm i` on the root folder, [pnpm workspaces](https://pnpm.io/workspaces) will install all dependencies of all subpackages. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
|
||||||
in the `node_modules` folder. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
|
allowing to develop multiple packages at the same time.
|
||||||
which allows developing multiple packages at the same time
|
|
||||||
|
|
||||||
## Package Publishing
|
## Package Publishing
|
||||||
|
|
||||||
To publish all packages that have been changed since the last release, run:
|
To publish all packages that have been changed since the last release, run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npx lerna publish
|
npm login
|
||||||
|
|
||||||
|
# this will increment all the versions in package.json files of non private packages to selected versions
|
||||||
|
npx lerna version --no-private
|
||||||
|
|
||||||
|
# publish all packages inside /packages using pnpm! don't use lerna to publish!!
|
||||||
|
pnpm --filter "./packages/**" publish --dry-run
|
||||||
|
|
||||||
|
# the last command was only a dry-run, make sure everything looks ok, if yes, run the same command without flag
|
||||||
|
```
|
||||||
|
|
||||||
|
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
|
||||||
|
Important: Always publish with `pnpm`, as `npm` does not support overriding main files in `publishConfig`, which is done in all the packages.
|
||||||
|
|
||||||
|
### New Packages
|
||||||
|
|
||||||
|
To add a new package, you have to publish it manually the first time, using:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd packages/<package-name> && pnpm publish --access public
|
||||||
```
|
```
|
||||||
|
|
||||||
## Have Fun
|
## Have Fun
|
||||||
|
|||||||
@@ -2,19 +2,20 @@
|
|||||||
|
|
||||||
[](https://github.com/tidalcycles/strudel/actions)
|
[](https://github.com/tidalcycles/strudel/actions)
|
||||||
|
|
||||||
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This is unstable software, please tread carefully.
|
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is slowly stabilising, but please continue to tread carefully.
|
||||||
|
|
||||||
- Try it here: <https://strudel.tidalcycles.org/>
|
- Try it here: <https://strudel.tidalcycles.org/>
|
||||||
- Tutorial: <https://strudel.tidalcycles.org/tutorial/>
|
- Docs: <https://strudel.tidalcycles.org/learn/>
|
||||||
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
||||||
|
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
||||||
|
|
||||||
## Running Locally
|
## Running Locally
|
||||||
|
|
||||||
After cloning the project, you can run the REPL locally:
|
After cloning the project, you can run the REPL locally:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run setup
|
pnpm run setup
|
||||||
npm run repl
|
pnpm run repl
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using Strudel In Your Project
|
## Using Strudel In Your Project
|
||||||
@@ -38,6 +39,12 @@ Click on the package names to find out more about each one.
|
|||||||
|
|
||||||
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
|
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
|
||||||
|
|
||||||
|
<a href="https://github.com/tidalcycles/strudel/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=tidalcycles/strudel" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
Made with [contrib.rocks](https://contrib.rocks).
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
There is a #strudel channel on the TidalCycles discord: <https://discord.com/invite/HGEdXmRkzT>
|
There is a #strudel channel on the TidalCycles discord: <https://discord.com/invite/HGEdXmRkzT>
|
||||||
|
|||||||
+1806
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 150 KiB |
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"files": {
|
|
||||||
"main.css": "/static/css/main.35f2ca4c.css",
|
|
||||||
"main.js": "/static/js/main.f848d51e.js",
|
|
||||||
"static/js/787.1c52cb78.chunk.js": "/static/js/787.1c52cb78.chunk.js",
|
|
||||||
"static/media/logo.svg": "/static/media/logo.ac95051720b3dccfe511e0e02d8e1029.svg",
|
|
||||||
"index.html": "/index.html",
|
|
||||||
"main.35f2ca4c.css.map": "/static/css/main.35f2ca4c.css.map",
|
|
||||||
"main.f848d51e.js.map": "/static/js/main.f848d51e.js.map",
|
|
||||||
"787.1c52cb78.chunk.js.map": "/static/js/787.1c52cb78.chunk.js.map"
|
|
||||||
},
|
|
||||||
"entrypoints": [
|
|
||||||
"static/css/main.35f2ca4c.css",
|
|
||||||
"static/js/main.f848d51e.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Strudel REPL"/><title>Strudel REPL</title><script defer="defer" src="/static/js/main.f848d51e.js"></script><link href="/static/css/main.35f2ca4c.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
|
||||||
Vendored
-6
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
"use strict";(self.webpackChunk_strudel_cycles_repl=self.webpackChunk_strudel_cycles_repl||[]).push([[787],{787:function(e,t,n){n.r(t),n.d(t,{getCLS:function(){return y},getFCP:function(){return g},getFID:function(){return C},getLCP:function(){return P},getTTFB:function(){return D}});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,p=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=p(),d(),s((function(){setTimeout((function(){v=p(),d()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime<i.firstHiddenTime&&(r.value=e.startTime,r.entries.push(e),n(!0)))},o=window.performance&&performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint")[0],f=o?null:c("paint",a);(o||f)&&(n=m(e,r,t),o&&a(o),s((function(i){r=u("FCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,n(!0)}))}))})))},h=!1,T=-1,y=function(e,t){h||(g((function(e){T=e.value})),h=!0);var n,i=function(t){T>-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},p=c("layout-shift",v);p&&(n=m(i,r,t),f((function(){p.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r<a-w){var e={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+r};o.forEach((function(t){t(e)})),o=[]}},b=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),p=function(e){e.startTime<a.firstHiddenTime&&(v.value=e.processingStart-e.startTime,v.entries.push(e),n(!0))},d=c("first-input",p);n=m(e,v,t),d&&f((function(){d.takeRecords().map(p),d.disconnect()}),!0),d&&s((function(){var a;v=u("FID"),n=m(e,v,t),o=[],r=-1,i=null,F(addEventListener),a=p,o.push(a),S()}))},k={},P=function(e,t){var n,i=l(),r=u("LCP"),a=function(e){var t=e.startTime;t<i.firstHiddenTime&&(r.value=t,r.entries.push(e),n())},o=c("largest-contentful-paint",a);if(o){n=m(e,r,t);var v=function(){k[r.id]||(o.takeRecords().map(a),o.disconnect(),k[r.id]=!0,n(!0))};["keydown","click"].forEach((function(e){addEventListener(e,v,{once:!0,capture:!0})})),f(v,!0),s((function(i){r=u("LCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,k[r.id]=!0,n(!0)}))}))}))}},D=function(e){var t,n=u("TTFB");t=function(){try{var t=performance.getEntriesByType("navigation")[0]||function(){var e=performance.timing,t={entryType:"navigation",startTime:0};for(var n in e)"navigationStart"!==n&&"toJSON"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]);
|
|
||||||
//# sourceMappingURL=787.1c52cb78.chunk.js.map
|
|
||||||
-1
File diff suppressed because one or more lines are too long
Vendored
-3
File diff suppressed because one or more lines are too long
-96
@@ -1,96 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
Strudel - javascript-based environment for live coding algorithmic (musical) patterns
|
|
||||||
https://strudel.tidalcycles.org / https://github.com/tidalcycles/strudel/
|
|
||||||
|
|
||||||
Copyright (C) Strudel contributors
|
|
||||||
https://github.com/tidalcycles/strudel/graphs/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/>.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
object-assign
|
|
||||||
(c) Sindre Sorhus
|
|
||||||
@license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*! *****************************************************************************
|
|
||||||
Copyright (c) Microsoft Corporation.
|
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
purpose with or without fee is hereby granted.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
***************************************************************************** */
|
|
||||||
|
|
||||||
/*! @license ReactCodeMirror - MIT License - Tom Golden (8162045+tbjgolden@users.noreply.github.com) */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @license Fraction.js v4.2.0 05/03/2022
|
|
||||||
* https://www.xarg.org/2014/03/rational-numbers-in-javascript/
|
|
||||||
*
|
|
||||||
* Copyright (c) 2021, Robert Eisele (robert@xarg.org)
|
|
||||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
||||||
**/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tone.js
|
|
||||||
* @author Yotam Mann
|
|
||||||
* @license http://opensource.org/licenses/MIT MIT License
|
|
||||||
* @copyright 2014-2019 Yotam Mann
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @license React v0.20.2
|
|
||||||
* scheduler.production.min.js
|
|
||||||
*
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @license React v17.0.2
|
|
||||||
* react-dom.production.min.js
|
|
||||||
*
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @license React v17.0.2
|
|
||||||
* react-jsx-runtime.production.min.js
|
|
||||||
*
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @license React v17.0.2
|
|
||||||
* react.production.min.js
|
|
||||||
*
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
Vendored
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><link rel="icon" href="/tutorial/favicon.e3ab9dd9.ico"><link rel="stylesheet" type="text/css" href="/tutorial/index.a3acddda.css"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content="Strudel REPL"><title>Strudel Tutorial</title></head><body> <div id="root"></div> <noscript>You need to enable JavaScript to run this app.</noscript> <script src="/tutorial/index.55306425.js" defer></script> </body></html>
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// this barrel export is currently only used to find undocumented exports
|
||||||
|
export * from './packages/core/index.mjs';
|
||||||
|
export * from './packages/csound/index.mjs';
|
||||||
|
export * from './packages/embed/index.mjs';
|
||||||
|
export * from './packages/eval/index.mjs';
|
||||||
|
export * from './packages/midi/index.mjs';
|
||||||
|
export * from './packages/mini/index.mjs';
|
||||||
|
export * from './packages/osc/index.mjs';
|
||||||
|
export * from './packages/react/index.mjs';
|
||||||
|
export * from './packages/serial/index.mjs';
|
||||||
|
export * from './packages/soundfonts/index.mjs';
|
||||||
|
export * from './packages/tonal/index.mjs';
|
||||||
|
export * from './packages/tone/index.mjs';
|
||||||
|
export * from './packages/transpiler/index.mjs';
|
||||||
|
export * from './packages/webaudio/index.mjs';
|
||||||
|
export * from './packages/webdirt/index.mjs';
|
||||||
|
export * from './packages/xen/index.mjs';
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"source": {
|
"source": {
|
||||||
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
|
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
|
||||||
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser"
|
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
|
||||||
},
|
},
|
||||||
"plugins": ["plugins/markdown"],
|
"plugins": ["plugins/markdown"],
|
||||||
"opts": {
|
"opts": {
|
||||||
|
|||||||
+3
-1
@@ -2,5 +2,7 @@
|
|||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"version": "independent"
|
"version": "independent",
|
||||||
|
"npmClient": "pnpm",
|
||||||
|
"useWorkspaces": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# my-patterns
|
||||||
|
|
||||||
|
This directory can be used to save your own patterns, which then get
|
||||||
|
made into a pattern swatch.
|
||||||
|
|
||||||
|
Example: <https://felixroos.github.io/strudel/swatch/>
|
||||||
|
|
||||||
|
## deploy
|
||||||
|
|
||||||
|
### 1. fork the [strudel repo on github](https://github.com/tidalcycles/strudel.git)
|
||||||
|
|
||||||
|
### 2. clone your fork to your machine `git clone https://github.com/<your-username>/strudel.git strudel && cd strudel`
|
||||||
|
|
||||||
|
### 3. create a separate branch like `git branch patternuary && git checkout patternuary`
|
||||||
|
|
||||||
|
### 4. save one or more .txt files in the my-patterns folder
|
||||||
|
|
||||||
|
### 5. edit `website/public/CNAME` to contain `<your-username>.github.io/strudel`
|
||||||
|
|
||||||
|
### 6. edit `website/astro.config.mjs` to use site: `https://<your-username>.github.io` and base `/strudel`, like this
|
||||||
|
|
||||||
|
```js
|
||||||
|
const site = 'https://<your-username>.github.io';
|
||||||
|
const base = '/strudel';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. commit & push the changes
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git add . && git commit -m "site config" && git push --set-upstream origin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. deploy to github pages
|
||||||
|
|
||||||
|
- go to settings -> pages and select "Github Actions" as source
|
||||||
|
- go to settings -> environments -> github-pages and press the edit button next to `main` and type in `patternuary` (under "Deployment branches")
|
||||||
|
- go to Actions -> `Build and Deploy` and click `Run workflow` with branch `patternuary`
|
||||||
|
|
||||||
|
### 9. view your patterns at `<your-username>.github.io/strudel/swatch/`
|
||||||
|
|
||||||
|
Alternatively, github pages allows you to use a custom domain, like https://mycooldomain.org/swatch/. [See their documentation for details](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site).
|
||||||
|
|
||||||
|
### 10. optional: automatic deployment
|
||||||
|
|
||||||
|
If you want to automatically deploy your site on push, go to `deploy.yml` and change `workflow_dispatch` to `push`.
|
||||||
|
|
||||||
|
## running locally
|
||||||
|
|
||||||
|
- install dependencies with `npm run setup`
|
||||||
|
- run dev server with `npm run repl` and open `http://localhost:3000/strudel/swatch/`
|
||||||
|
|
||||||
|
## tests fail?
|
||||||
|
|
||||||
|
Your tests might fail if the code does not follow prettiers format.
|
||||||
|
In that case, run `npm run codeformat`. To disable that, remove `npm run format-check` from `test.yml`
|
||||||
|
|
||||||
|
## updating your fork
|
||||||
|
|
||||||
|
To update your fork, you can pull the main branch and merge it into your `patternuary` branch.
|
||||||
Generated
-20337
File diff suppressed because it is too large
Load Diff
+42
-17
@@ -1,23 +1,32 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/monorepo",
|
"name": "@strudel.cycles/monorepo",
|
||||||
"version": "0.0.1",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Port of tidalcycles to javascript",
|
"description": "Port of tidalcycles to javascript",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run test --workspaces --if-present && cd repl && npm run test",
|
"setup": "pnpm i",
|
||||||
"bootstrap": "lerna bootstrap",
|
"pretest": "npm run jsdoc-json",
|
||||||
"setup": "npm i && npm run bootstrap && cd repl && npm i",
|
"prebuild": "npm run jsdoc-json",
|
||||||
"repl": "cd repl && npm run dev",
|
"prestart": "npm run jsdoc-json",
|
||||||
|
"test": "npm run pretest && vitest run --version",
|
||||||
|
"test-ui": "npm run pretest && vitest --ui",
|
||||||
|
"test-coverage": "npm run pretest && vitest --coverage",
|
||||||
|
"snapshot": "npm run pretest && vitest run -u --silent",
|
||||||
|
"repl": "npm run prestart && cd website && npm run dev",
|
||||||
|
"start": "npm run prestart && cd website && npm run dev",
|
||||||
|
"dev": "npm run prestart && cd website && npm run dev",
|
||||||
|
"build": "npm run prebuild && cd website && npm run build",
|
||||||
|
"preview": "cd website && npm run preview",
|
||||||
"osc": "cd packages/osc && npm run server",
|
"osc": "cd packages/osc && npm run server",
|
||||||
"build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build",
|
|
||||||
"preview": "npx serve ./out",
|
|
||||||
"deploy": "gh-pages -d out",
|
|
||||||
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
||||||
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json"
|
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
|
||||||
|
"lint": "eslint . --ext mjs,js --quiet",
|
||||||
|
"codeformat": "prettier --write .",
|
||||||
|
"format-check": "prettier --check .",
|
||||||
|
"report-undocumented": "npm run jsdoc-json && node undocumented.mjs > undocumented.json",
|
||||||
|
"check": "npm run format-check && npm run lint && npm run test",
|
||||||
|
"iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
@@ -35,13 +44,29 @@
|
|||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"homepage": "https://strudel.tidalcycles.org",
|
||||||
|
"dependencies": {
|
||||||
|
"@strudel.cycles/core": "workspace:*",
|
||||||
|
"@strudel.cycles/mini": "workspace:*",
|
||||||
|
"@strudel.cycles/tonal": "workspace:*",
|
||||||
|
"@strudel.cycles/transpiler": "workspace:*",
|
||||||
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
|
"@strudel.cycles/xen": "workspace:*",
|
||||||
|
"acorn": "^8.8.1",
|
||||||
|
"dependency-tree": "^9.0.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^1.4.0",
|
||||||
|
"@vitest/ui": "^0.28.0",
|
||||||
|
"canvas": "^2.11.2",
|
||||||
|
"eslint": "^8.39.0",
|
||||||
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"gh-pages": "^4.0.0",
|
"jsdoc": "^4.0.2",
|
||||||
"jsdoc": "^3.6.10",
|
|
||||||
"jsdoc-json": "^2.0.2",
|
"jsdoc-json": "^2.0.2",
|
||||||
"jsdoc-to-markdown": "^7.1.1",
|
"jsdoc-to-markdown": "^8.0.0",
|
||||||
"lerna": "^4.0.0",
|
"lerna": "^6.6.1",
|
||||||
"mocha": "^9.1.4"
|
"prettier": "^2.8.8",
|
||||||
|
"rollup-plugin-visualizer": "^5.8.1",
|
||||||
|
"vitest": "^0.28.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Packages
|
||||||
|
|
||||||
|
Each folder represents one of the @strudel.cycles/* packages [published to npm](https://www.npmjs.com/org/strudel.cycles).
|
||||||
|
|
||||||
|
To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs.
|
||||||
|
|
||||||
|
This is a graphical view of all the packages: [full screen](https://raw.githubusercontent.com/tidalcycles/strudel/main/dependencies.svg)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Generated with
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx depcruise --include-only "^packages" -X "node_modules" --output-type dot packages | dot -T svg > dependencygraph.svg
|
||||||
|
```
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# @strudel/codemirror
|
||||||
|
|
||||||
|
This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it.
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
|
import { EditorView, keymap, Decoration, lineNumbers, highlightActiveLineGutter } from '@codemirror/view';
|
||||||
|
import { defaultKeymap } from '@codemirror/commands';
|
||||||
|
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { StateField, StateEffect } from '@codemirror/state';
|
||||||
|
import { oneDark } from './themes/one-dark';
|
||||||
|
import { repl, Drawer } from '@strudel.cycles/core';
|
||||||
|
|
||||||
|
// https://codemirror.net/docs/guide/
|
||||||
|
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
|
||||||
|
let state = EditorState.create({
|
||||||
|
doc: initialCode,
|
||||||
|
extensions: [
|
||||||
|
theme,
|
||||||
|
javascript(),
|
||||||
|
lineNumbers(),
|
||||||
|
highlightField,
|
||||||
|
highlightActiveLineGutter(),
|
||||||
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
|
keymap.of(defaultKeymap),
|
||||||
|
flashField,
|
||||||
|
EditorView.updateListener.of((v) => onChange(v)),
|
||||||
|
keymap.of([
|
||||||
|
{
|
||||||
|
key: 'Ctrl-Enter',
|
||||||
|
run: () => onEvaluate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Ctrl-.',
|
||||||
|
run: () => onStop(),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return new EditorView({
|
||||||
|
state,
|
||||||
|
parent: root,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// highlighting
|
||||||
|
//
|
||||||
|
|
||||||
|
export const setHighlights = StateEffect.define();
|
||||||
|
export const highlightField = StateField.define({
|
||||||
|
create() {
|
||||||
|
return Decoration.none;
|
||||||
|
},
|
||||||
|
update(highlights, tr) {
|
||||||
|
try {
|
||||||
|
for (let e of tr.effects) {
|
||||||
|
if (e.is(setHighlights)) {
|
||||||
|
const { haps } = e.value;
|
||||||
|
const marks =
|
||||||
|
haps
|
||||||
|
.map((hap) =>
|
||||||
|
(hap.context.locations || []).map(({ start, end }) => {
|
||||||
|
// const color = hap.context.color || e.value.color || '#FFCA28';
|
||||||
|
let from = tr.newDoc.line(start.line).from + start.column;
|
||||||
|
let to = tr.newDoc.line(end.line).from + end.column;
|
||||||
|
const l = tr.newDoc.length;
|
||||||
|
if (from > l || to > l) {
|
||||||
|
return; // dont mark outside of range, as it will throw an error
|
||||||
|
}
|
||||||
|
const mark = Decoration.mark({
|
||||||
|
attributes: { style: `outline: 2px solid #FFCA28;` },
|
||||||
|
});
|
||||||
|
return mark.range(from, to);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.flat()
|
||||||
|
.filter(Boolean) || [];
|
||||||
|
highlights = Decoration.set(marks, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return highlights;
|
||||||
|
} catch (err) {
|
||||||
|
// console.warn('highlighting error', err);
|
||||||
|
return Decoration.set([]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
|
});
|
||||||
|
|
||||||
|
// helper to simply trigger highlighting for given haps
|
||||||
|
export function highlightHaps(view, haps) {
|
||||||
|
view.dispatch({ effects: setHighlights.of({ haps }) });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// flash
|
||||||
|
//
|
||||||
|
|
||||||
|
export const setFlash = StateEffect.define();
|
||||||
|
const flashField = StateField.define({
|
||||||
|
create() {
|
||||||
|
return Decoration.none;
|
||||||
|
},
|
||||||
|
update(flash, tr) {
|
||||||
|
try {
|
||||||
|
for (let e of tr.effects) {
|
||||||
|
if (e.is(setFlash)) {
|
||||||
|
if (e.value) {
|
||||||
|
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||||
|
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||||
|
} else {
|
||||||
|
flash = Decoration.set([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flash;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('flash error', err);
|
||||||
|
return flash;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const flash = (view, ms = 200) => {
|
||||||
|
view.dispatch({ effects: setFlash.of(true) });
|
||||||
|
setTimeout(() => {
|
||||||
|
view.dispatch({ effects: setFlash.of(false) });
|
||||||
|
}, ms);
|
||||||
|
};
|
||||||
|
|
||||||
|
export class StrudelMirror {
|
||||||
|
constructor(options) {
|
||||||
|
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
|
||||||
|
this.code = initialCode;
|
||||||
|
|
||||||
|
this.drawer = new Drawer((haps, time) => {
|
||||||
|
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
|
||||||
|
this.highlight(currentFrame);
|
||||||
|
onDraw?.(haps, time, currentFrame);
|
||||||
|
}, drawTime);
|
||||||
|
|
||||||
|
const prebaked = prebake();
|
||||||
|
prebaked.then(async () => {
|
||||||
|
if (!onDraw) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { scheduler, evaluate } = await this.repl;
|
||||||
|
// draw first frame instantly
|
||||||
|
prebaked.then(async () => {
|
||||||
|
await evaluate(this.code, false);
|
||||||
|
this.drawer.invalidate(scheduler);
|
||||||
|
onDraw?.(this.drawer.visibleHaps, 0, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.repl = repl({
|
||||||
|
...replOptions,
|
||||||
|
onToggle: async (started) => {
|
||||||
|
replOptions?.onToggle?.(started);
|
||||||
|
const { scheduler } = await this.repl;
|
||||||
|
if (started) {
|
||||||
|
this.drawer.start(scheduler);
|
||||||
|
} else {
|
||||||
|
this.drawer.stop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeEval: async () => {
|
||||||
|
await prebaked;
|
||||||
|
},
|
||||||
|
afterEval: (options) => {
|
||||||
|
replOptions?.afterEval?.(options);
|
||||||
|
this.drawer.invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.editor = initEditor({
|
||||||
|
root,
|
||||||
|
initialCode,
|
||||||
|
onChange: (v) => {
|
||||||
|
this.code = v.state.doc.toString();
|
||||||
|
},
|
||||||
|
onEvaluate: () => this.evaluate(),
|
||||||
|
onStop: () => this.stop(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async evaluate() {
|
||||||
|
const { evaluate } = await this.repl;
|
||||||
|
this.flash();
|
||||||
|
await evaluate(this.code);
|
||||||
|
}
|
||||||
|
async stop() {
|
||||||
|
const { scheduler } = await this.repl;
|
||||||
|
scheduler.stop();
|
||||||
|
}
|
||||||
|
flash(ms) {
|
||||||
|
flash(this.editor, ms);
|
||||||
|
}
|
||||||
|
highlight(haps) {
|
||||||
|
highlightHaps(this.editor, haps);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "@strudel/codemirror",
|
||||||
|
"version": "0.8.4",
|
||||||
|
"description": "Codemirror Extensions for Strudel",
|
||||||
|
"main": "codemirror.mjs",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"tidalcycles",
|
||||||
|
"strudel",
|
||||||
|
"pattern",
|
||||||
|
"livecoding",
|
||||||
|
"algorave"
|
||||||
|
],
|
||||||
|
"author": "Felix Roos <flix91@gmail.com>",
|
||||||
|
"contributors": [
|
||||||
|
"Alex McLean <alex@slab.org>"
|
||||||
|
],
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/commands": "^6.2.4",
|
||||||
|
"@codemirror/lang-javascript": "^6.1.7",
|
||||||
|
"@codemirror/language": "^6.6.0",
|
||||||
|
"@codemirror/state": "^6.2.0",
|
||||||
|
"@codemirror/view": "^6.10.0",
|
||||||
|
"@lezer/highlight": "^1.1.4",
|
||||||
|
"@strudel.cycles/core": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^4.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
|
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
|
||||||
|
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
|
||||||
|
|
||||||
|
const chalky = '#e5c07b',
|
||||||
|
coral = '#e06c75',
|
||||||
|
cyan = '#56b6c2',
|
||||||
|
invalid = '#ffffff',
|
||||||
|
ivory = '#abb2bf',
|
||||||
|
stone = '#7d8799', // Brightened compared to original to increase contrast
|
||||||
|
malibu = '#61afef',
|
||||||
|
sage = '#98c379',
|
||||||
|
whiskey = '#d19a66',
|
||||||
|
violet = '#c678dd',
|
||||||
|
darkBackground = '#21252b',
|
||||||
|
highlightBackground = '#2c313a',
|
||||||
|
background = '#282c34',
|
||||||
|
tooltipBackground = '#353a42',
|
||||||
|
selection = '#3E4451',
|
||||||
|
cursor = '#528bff';
|
||||||
|
|
||||||
|
/// The colors used in the theme, as CSS color strings.
|
||||||
|
export const color = {
|
||||||
|
chalky,
|
||||||
|
coral,
|
||||||
|
cyan,
|
||||||
|
invalid,
|
||||||
|
ivory,
|
||||||
|
stone,
|
||||||
|
malibu,
|
||||||
|
sage,
|
||||||
|
whiskey,
|
||||||
|
violet,
|
||||||
|
darkBackground,
|
||||||
|
highlightBackground,
|
||||||
|
background,
|
||||||
|
tooltipBackground,
|
||||||
|
selection,
|
||||||
|
cursor,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The editor theme styles for One Dark.
|
||||||
|
export const oneDarkTheme = EditorView.theme(
|
||||||
|
{
|
||||||
|
'&': {
|
||||||
|
color: ivory,
|
||||||
|
backgroundColor: background,
|
||||||
|
},
|
||||||
|
|
||||||
|
'.cm-content': {
|
||||||
|
caretColor: cursor,
|
||||||
|
},
|
||||||
|
|
||||||
|
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
|
||||||
|
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
|
||||||
|
{ backgroundColor: selection },
|
||||||
|
|
||||||
|
'.cm-panels': { backgroundColor: darkBackground, color: ivory },
|
||||||
|
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
||||||
|
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
||||||
|
|
||||||
|
'.cm-searchMatch': {
|
||||||
|
backgroundColor: '#72a1ff59',
|
||||||
|
outline: '1px solid #457dff',
|
||||||
|
},
|
||||||
|
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||||
|
backgroundColor: '#6199ff2f',
|
||||||
|
},
|
||||||
|
|
||||||
|
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
|
||||||
|
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
||||||
|
|
||||||
|
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||||
|
backgroundColor: '#bad0f847',
|
||||||
|
},
|
||||||
|
|
||||||
|
'.cm-gutters': {
|
||||||
|
backgroundColor: background,
|
||||||
|
color: stone,
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
|
||||||
|
'.cm-activeLineGutter': {
|
||||||
|
backgroundColor: highlightBackground,
|
||||||
|
},
|
||||||
|
|
||||||
|
'.cm-foldPlaceholder': {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: '#ddd',
|
||||||
|
},
|
||||||
|
|
||||||
|
'.cm-tooltip': {
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: tooltipBackground,
|
||||||
|
},
|
||||||
|
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||||
|
borderTopColor: 'transparent',
|
||||||
|
borderBottomColor: 'transparent',
|
||||||
|
},
|
||||||
|
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||||
|
borderTopColor: tooltipBackground,
|
||||||
|
borderBottomColor: tooltipBackground,
|
||||||
|
},
|
||||||
|
'.cm-tooltip-autocomplete': {
|
||||||
|
'& > ul > li[aria-selected]': {
|
||||||
|
backgroundColor: highlightBackground,
|
||||||
|
color: ivory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ dark: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The highlighting style for code in the One Dark theme.
|
||||||
|
export const oneDarkHighlightStyle = HighlightStyle.define([
|
||||||
|
{ tag: t.keyword, color: violet },
|
||||||
|
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
|
||||||
|
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
|
||||||
|
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
|
||||||
|
{ tag: [t.definition(t.name), t.separator], color: ivory },
|
||||||
|
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
|
||||||
|
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
|
||||||
|
{ tag: [t.meta, t.comment], color: stone },
|
||||||
|
{ tag: t.strong, fontWeight: 'bold' },
|
||||||
|
{ tag: t.emphasis, fontStyle: 'italic' },
|
||||||
|
{ tag: t.strikethrough, textDecoration: 'line-through' },
|
||||||
|
{ tag: t.link, color: stone, textDecoration: 'underline' },
|
||||||
|
{ tag: t.heading, fontWeight: 'bold', color: coral },
|
||||||
|
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
|
||||||
|
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
|
||||||
|
{ tag: t.invalid, color: invalid },
|
||||||
|
]);
|
||||||
|
|
||||||
|
/// Extension to enable the One Dark theme (both the editor theme and
|
||||||
|
/// the highlight style).
|
||||||
|
export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { dependencies } from './package.json';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'codemirror.mjs'),
|
||||||
|
formats: ['es', 'cjs'],
|
||||||
|
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [...Object.keys(dependencies)],
|
||||||
|
},
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
examples
|
||||||
@@ -33,6 +33,7 @@ b: 3/2 - 7/4
|
|||||||
c: 7/4 - 2
|
c: 7/4 - 2
|
||||||
```
|
```
|
||||||
|
|
||||||
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-qmz6qr?file=/src/index.js).
|
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-forked-9ywhv7?file=/src/index.js).
|
||||||
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/package-examples/packages/core/examples/canvas.html)
|
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/canvas.html)
|
||||||
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/package-examples/packages/core/examples/metro.html)
|
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/vanilla.html)
|
||||||
|
- [open minimal vite example](./examples/vite-vanilla-repl/)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
||||||
|
import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
|
||||||
|
const { createParams } = controls;
|
||||||
|
|
||||||
|
let clearColor = '#22222210';
|
||||||
|
|
||||||
|
Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } = {}) {
|
||||||
|
window.frame && cancelAnimationFrame(window.frame);
|
||||||
|
const ctx = getDrawContext();
|
||||||
|
const { clientWidth: ww, clientHeight: wh } = ctx.canvas;
|
||||||
|
let smearPart = smear === 0 ? '99' : Number((1 - smear) * 100).toFixed(0);
|
||||||
|
smearPart = smearPart.length === 1 ? `0${smearPart}` : smearPart;
|
||||||
|
clearColor = `#200010${smearPart}`;
|
||||||
|
const render = (t) => {
|
||||||
|
let frame;
|
||||||
|
/* if (sync) {
|
||||||
|
t = scheduler.now();
|
||||||
|
frame = this.queryArc(t, t);
|
||||||
|
} else { */
|
||||||
|
t = Math.round(t);
|
||||||
|
frame = this.slow(1000).queryArc(t, t);
|
||||||
|
// }
|
||||||
|
ctx.fillStyle = clearColor;
|
||||||
|
ctx.fillRect(0, 0, ww, wh);
|
||||||
|
frame.forEach((f) => {
|
||||||
|
let { x, y, w, h, s, r, angle = 0, fill = 'darkseagreen' } = f.value;
|
||||||
|
w *= ww;
|
||||||
|
h *= wh;
|
||||||
|
if (r !== undefined && angle !== undefined) {
|
||||||
|
const radians = angle * 2 * Math.PI;
|
||||||
|
const [cx, cy] = [(ww - w) / 2, (wh - h) / 2];
|
||||||
|
x = cx + Math.cos(radians) * r * cx;
|
||||||
|
y = cy + Math.sin(radians) * r * cy;
|
||||||
|
} else {
|
||||||
|
x *= ww - w;
|
||||||
|
y *= wh - h;
|
||||||
|
}
|
||||||
|
const val = { ...f.value, x, y, w, h };
|
||||||
|
ctx.fillStyle = fill;
|
||||||
|
if (s === 'rect') {
|
||||||
|
ctx.fillRect(x, y, w, h);
|
||||||
|
} else if (s === 'ellipse') {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(x + w / 2, y + h / 2, w / 2, h / 2, 0, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
callback && callback(ctx, val, f);
|
||||||
|
});
|
||||||
|
window.frame = requestAnimationFrame(render);
|
||||||
|
};
|
||||||
|
window.frame = requestAnimationFrame(render);
|
||||||
|
return silence;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const { x, y, w, h, angle, r, fill, smear } = createParams('x', 'y', 'w', 'h', 'angle', 'r', 'fill', 'smear');
|
||||||
|
|
||||||
|
export const rescale = register('rescale', function (f, pat) {
|
||||||
|
return pat.mul(x(f).w(f).y(f).h(f));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const moveXY = register('moveXY', function (dx, dy, pat) {
|
||||||
|
return pat.add(x(dx).y(dy));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zoomIn = register('zoomIn', function (f, pat) {
|
||||||
|
const d = pure(1).sub(f).div(2);
|
||||||
|
return pat.rescale(f).move(d, d);
|
||||||
|
});
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
export const colorMap = {
|
||||||
|
aliceblue: '#f0f8ff',
|
||||||
|
antiquewhite: '#faebd7',
|
||||||
|
aqua: '#00ffff',
|
||||||
|
aquamarine: '#7fffd4',
|
||||||
|
azure: '#f0ffff',
|
||||||
|
beige: '#f5f5dc',
|
||||||
|
bisque: '#ffe4c4',
|
||||||
|
black: '#000000',
|
||||||
|
blanchedalmond: '#ffebcd',
|
||||||
|
blue: '#0000ff',
|
||||||
|
blueviolet: '#8a2be2',
|
||||||
|
brown: '#a52a2a',
|
||||||
|
burlywood: '#deb887',
|
||||||
|
cadetblue: '#5f9ea0',
|
||||||
|
chartreuse: '#7fff00',
|
||||||
|
chocolate: '#d2691e',
|
||||||
|
coral: '#ff7f50',
|
||||||
|
cornflowerblue: '#6495ed',
|
||||||
|
cornsilk: '#fff8dc',
|
||||||
|
crimson: '#dc143c',
|
||||||
|
cyan: '#00ffff',
|
||||||
|
darkblue: '#00008b',
|
||||||
|
darkcyan: '#008b8b',
|
||||||
|
darkgoldenrod: '#b8860b',
|
||||||
|
darkgray: '#a9a9a9',
|
||||||
|
darkgreen: '#006400',
|
||||||
|
darkgrey: '#a9a9a9',
|
||||||
|
darkkhaki: '#bdb76b',
|
||||||
|
darkmagenta: '#8b008b',
|
||||||
|
darkolivegreen: '#556b2f',
|
||||||
|
darkorange: '#ff8c00',
|
||||||
|
darkorchid: '#9932cc',
|
||||||
|
darkred: '#8b0000',
|
||||||
|
darksalmon: '#e9967a',
|
||||||
|
darkseagreen: '#8fbc8f',
|
||||||
|
darkslateblue: '#483d8b',
|
||||||
|
darkslategray: '#2f4f4f',
|
||||||
|
darkslategrey: '#2f4f4f',
|
||||||
|
darkturquoise: '#00ced1',
|
||||||
|
darkviolet: '#9400d3',
|
||||||
|
deeppink: '#ff1493',
|
||||||
|
deepskyblue: '#00bfff',
|
||||||
|
dimgray: '#696969',
|
||||||
|
dimgrey: '#696969',
|
||||||
|
dodgerblue: '#1e90ff',
|
||||||
|
firebrick: '#b22222',
|
||||||
|
floralwhite: '#fffaf0',
|
||||||
|
forestgreen: '#228b22',
|
||||||
|
fuchsia: '#ff00ff',
|
||||||
|
gainsboro: '#dcdcdc',
|
||||||
|
ghostwhite: '#f8f8ff',
|
||||||
|
gold: '#ffd700',
|
||||||
|
goldenrod: '#daa520',
|
||||||
|
gray: '#808080',
|
||||||
|
green: '#008000',
|
||||||
|
greenyellow: '#adff2f',
|
||||||
|
grey: '#808080',
|
||||||
|
honeydew: '#f0fff0',
|
||||||
|
hotpink: '#ff69b4',
|
||||||
|
indianred: '#cd5c5c',
|
||||||
|
indigo: '#4b0082',
|
||||||
|
ivory: '#fffff0',
|
||||||
|
khaki: '#f0e68c',
|
||||||
|
lavender: '#e6e6fa',
|
||||||
|
lavenderblush: '#fff0f5',
|
||||||
|
lawngreen: '#7cfc00',
|
||||||
|
lemonchiffon: '#fffacd',
|
||||||
|
lightblue: '#add8e6',
|
||||||
|
lightcoral: '#f08080',
|
||||||
|
lightcyan: '#e0ffff',
|
||||||
|
lightgoldenrodyellow: '#fafad2',
|
||||||
|
lightgray: '#d3d3d3',
|
||||||
|
lightgreen: '#90ee90',
|
||||||
|
lightgrey: '#d3d3d3',
|
||||||
|
lightpink: '#ffb6c1',
|
||||||
|
lightsalmon: '#ffa07a',
|
||||||
|
lightseagreen: '#20b2aa',
|
||||||
|
lightskyblue: '#87cefa',
|
||||||
|
lightslategray: '#778899',
|
||||||
|
lightslategrey: '#778899',
|
||||||
|
lightsteelblue: '#b0c4de',
|
||||||
|
lightyellow: '#ffffe0',
|
||||||
|
lime: '#00ff00',
|
||||||
|
limegreen: '#32cd32',
|
||||||
|
linen: '#faf0e6',
|
||||||
|
magenta: '#ff00ff',
|
||||||
|
maroon: '#800000',
|
||||||
|
mediumaquamarine: '#66cdaa',
|
||||||
|
mediumblue: '#0000cd',
|
||||||
|
mediumorchid: '#ba55d3',
|
||||||
|
mediumpurple: '#9370db',
|
||||||
|
mediumseagreen: '#3cb371',
|
||||||
|
mediumslateblue: '#7b68ee',
|
||||||
|
mediumspringgreen: '#00fa9a',
|
||||||
|
mediumturquoise: '#48d1cc',
|
||||||
|
mediumvioletred: '#c71585',
|
||||||
|
midnightblue: '#191970',
|
||||||
|
mintcream: '#f5fffa',
|
||||||
|
mistyrose: '#ffe4e1',
|
||||||
|
moccasin: '#ffe4b5',
|
||||||
|
navajowhite: '#ffdead',
|
||||||
|
navy: '#000080',
|
||||||
|
oldlace: '#fdf5e6',
|
||||||
|
olive: '#808000',
|
||||||
|
olivedrab: '#6b8e23',
|
||||||
|
orange: '#ffa500',
|
||||||
|
orangered: '#ff4500',
|
||||||
|
orchid: '#da70d6',
|
||||||
|
palegoldenrod: '#eee8aa',
|
||||||
|
palegreen: '#98fb98',
|
||||||
|
paleturquoise: '#afeeee',
|
||||||
|
palevioletred: '#db7093',
|
||||||
|
papayawhip: '#ffefd5',
|
||||||
|
peachpuff: '#ffdab9',
|
||||||
|
peru: '#cd853f',
|
||||||
|
pink: '#ffc0cb',
|
||||||
|
plum: '#dda0dd',
|
||||||
|
powderblue: '#b0e0e6',
|
||||||
|
purple: '#800080',
|
||||||
|
red: '#ff0000',
|
||||||
|
rosybrown: '#bc8f8f',
|
||||||
|
royalblue: '#4169e1',
|
||||||
|
saddlebrown: '#8b4513',
|
||||||
|
salmon: '#fa8072',
|
||||||
|
sandybrown: '#f4a460',
|
||||||
|
seagreen: '#2e8b57',
|
||||||
|
seashell: '#fff5ee',
|
||||||
|
sienna: '#a0522d',
|
||||||
|
silver: '#c0c0c0',
|
||||||
|
skyblue: '#87ceeb',
|
||||||
|
slateblue: '#6a5acd',
|
||||||
|
slategray: '#708090',
|
||||||
|
slategrey: '#708090',
|
||||||
|
snow: '#fffafa',
|
||||||
|
springgreen: '#00ff7f',
|
||||||
|
steelblue: '#4682b4',
|
||||||
|
tan: '#d2b48c',
|
||||||
|
teal: '#008080',
|
||||||
|
thistle: '#d8bfd8',
|
||||||
|
tomato: '#ff6347',
|
||||||
|
turquoise: '#40e0d0',
|
||||||
|
violet: '#ee82ee',
|
||||||
|
wheat: '#f5deb3',
|
||||||
|
white: '#ffffff',
|
||||||
|
whitesmoke: '#f5f5f5',
|
||||||
|
yellow: '#ffff00',
|
||||||
|
yellowgreen: '#9acd32',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function convertColorToNumber(color) {
|
||||||
|
// Convert color to lowercase for easier matching
|
||||||
|
color = color.toLowerCase();
|
||||||
|
|
||||||
|
// If the color is a hex code, convert it to a number
|
||||||
|
if (color[0] === '#') {
|
||||||
|
return convertHexToNumber(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the color is a named color, return the corresponding number
|
||||||
|
if (colorMap[color] !== undefined) {
|
||||||
|
return convertHexToNumber(colorMap[color]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the color is not recognized, return null
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertHexToNumber(hex) {
|
||||||
|
// Remove the leading '#' from the hex code
|
||||||
|
hex = hex.slice(1);
|
||||||
|
|
||||||
|
// Convert the hex code to a number
|
||||||
|
return parseInt(hex, 16);
|
||||||
|
}
|
||||||
+808
-276
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
cyclist.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.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 createClock from './zyklus.mjs';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
|
export class Cyclist {
|
||||||
|
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||||
|
this.started = false;
|
||||||
|
this.cps = 1;
|
||||||
|
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||||
|
this.lastBegin = 0; // query begin of last tick
|
||||||
|
this.lastEnd = 0; // query end of last tick
|
||||||
|
this.getTime = getTime; // get absolute time
|
||||||
|
this.onToggle = onToggle;
|
||||||
|
this.latency = latency; // fixed trigger time offset
|
||||||
|
const round = (x) => Math.round(x * 1000) / 1000;
|
||||||
|
this.clock = createClock(
|
||||||
|
getTime,
|
||||||
|
// called slightly before each cycle
|
||||||
|
(phase, duration, tick) => {
|
||||||
|
if (tick === 0) {
|
||||||
|
this.origin = phase;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const time = getTime();
|
||||||
|
const begin = this.lastEnd;
|
||||||
|
this.lastBegin = begin;
|
||||||
|
const end = round(begin + duration * this.cps);
|
||||||
|
this.lastEnd = end;
|
||||||
|
const haps = this.pattern.queryArc(begin, end);
|
||||||
|
const tickdeadline = phase - time; // time left till phase begins
|
||||||
|
this.lastTick = time + tickdeadline;
|
||||||
|
|
||||||
|
haps.forEach((hap) => {
|
||||||
|
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||||
|
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
|
||||||
|
const duration = hap.duration / this.cps;
|
||||||
|
onTrigger?.(hap, deadline, duration, this.cps);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger(`[cyclist] error: ${e.message}`);
|
||||||
|
onError?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interval, // duration of each cycle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
now() {
|
||||||
|
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
|
||||||
|
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
|
||||||
|
}
|
||||||
|
setStarted(v) {
|
||||||
|
this.started = v;
|
||||||
|
this.onToggle?.(v);
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
if (!this.pattern) {
|
||||||
|
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||||
|
}
|
||||||
|
logger('[cyclist] start');
|
||||||
|
this.clock.start();
|
||||||
|
this.setStarted(true);
|
||||||
|
}
|
||||||
|
pause() {
|
||||||
|
logger('[cyclist] pause');
|
||||||
|
this.clock.pause();
|
||||||
|
this.setStarted(false);
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
logger('[cyclist] stop');
|
||||||
|
this.clock.stop();
|
||||||
|
this.lastEnd = 0;
|
||||||
|
this.setStarted(false);
|
||||||
|
}
|
||||||
|
setPattern(pat, autostart = false) {
|
||||||
|
this.pattern = pat;
|
||||||
|
if (autostart && !this.started) {
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCps(cps = 1) {
|
||||||
|
this.cps = cps;
|
||||||
|
}
|
||||||
|
log(begin, end, haps) {
|
||||||
|
const onsets = haps.filter((h) => h.hasOnset());
|
||||||
|
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
draw.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Pattern, getTime, State, TimeSpan } from './index.mjs';
|
||||||
|
|
||||||
|
export const getDrawContext = (id = 'test-canvas') => {
|
||||||
|
let canvas = document.querySelector('#' + id);
|
||||||
|
if (!canvas) {
|
||||||
|
canvas = document.createElement('canvas');
|
||||||
|
canvas.id = id;
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
|
||||||
|
document.body.prepend(canvas);
|
||||||
|
}
|
||||||
|
return canvas.getContext('2d');
|
||||||
|
};
|
||||||
|
|
||||||
|
Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
||||||
|
if (window.strudelAnimation) {
|
||||||
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
|
}
|
||||||
|
const ctx = getDrawContext();
|
||||||
|
let cycle,
|
||||||
|
events = [];
|
||||||
|
const animate = (time) => {
|
||||||
|
const t = getTime();
|
||||||
|
if (from !== undefined && to !== undefined) {
|
||||||
|
const currentCycle = Math.floor(t);
|
||||||
|
if (cycle !== currentCycle) {
|
||||||
|
cycle = currentCycle;
|
||||||
|
const begin = currentCycle + from;
|
||||||
|
const end = currentCycle + to;
|
||||||
|
setTimeout(() => {
|
||||||
|
events = this.query(new State(new TimeSpan(begin, end)))
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((event) => event.part.begin.equals(event.whole.begin));
|
||||||
|
onQuery?.(events);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback(ctx, events, t, time);
|
||||||
|
window.strudelAnimation = requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanupDraw = (clearScreen = true) => {
|
||||||
|
const ctx = getDrawContext();
|
||||||
|
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||||
|
if (window.strudelAnimation) {
|
||||||
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
|
}
|
||||||
|
if (window.strudelScheduler) {
|
||||||
|
clearInterval(window.strudelScheduler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Pattern.prototype.onPaint = function (onPaint) {
|
||||||
|
// this is evil! TODO: add pattern.context
|
||||||
|
this.context = { onPaint };
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
// const round = (x) => Math.round(x * 1000) / 1000;
|
||||||
|
|
||||||
|
// encapsulates starting and stopping animation frames
|
||||||
|
export class Framer {
|
||||||
|
constructor(onFrame, onError) {
|
||||||
|
this.onFrame = onFrame;
|
||||||
|
this.onError = onError;
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
const self = this;
|
||||||
|
let frame = requestAnimationFrame(function updateHighlights(time) {
|
||||||
|
try {
|
||||||
|
self.onFrame(time);
|
||||||
|
} catch (err) {
|
||||||
|
self.onError(err);
|
||||||
|
}
|
||||||
|
frame = requestAnimationFrame(updateHighlights);
|
||||||
|
});
|
||||||
|
self.cancel = () => {
|
||||||
|
cancelAnimationFrame(frame);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
if (this.cancel) {
|
||||||
|
this.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncs animation frames to a cyclist scheduler
|
||||||
|
// see vite-vanilla-repl-cm6 for an example
|
||||||
|
export class Drawer {
|
||||||
|
constructor(onDraw, drawTime) {
|
||||||
|
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
|
||||||
|
lookbehind = Math.abs(lookbehind);
|
||||||
|
this.visibleHaps = [];
|
||||||
|
this.lastFrame = null;
|
||||||
|
this.drawTime = drawTime;
|
||||||
|
this.framer = new Framer(
|
||||||
|
() => {
|
||||||
|
if (!this.scheduler) {
|
||||||
|
console.warn('Drawer: no scheduler');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// calculate current frame time (think right side of screen for pianoroll)
|
||||||
|
const phase = this.scheduler.now() + lookahead;
|
||||||
|
// first frame just captures the phase
|
||||||
|
if (this.lastFrame === null) {
|
||||||
|
this.lastFrame = phase;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// query haps from last frame till now. take last 100ms max
|
||||||
|
const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase);
|
||||||
|
this.lastFrame = phase;
|
||||||
|
this.visibleHaps = (this.visibleHaps || [])
|
||||||
|
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
|
||||||
|
.filter((h) => h.whole.end >= phase - lookbehind - lookahead)
|
||||||
|
// add new haps with onset (think right edge bars scrolling in)
|
||||||
|
.concat(haps.filter((h) => h.hasOnset()));
|
||||||
|
const time = phase - lookahead;
|
||||||
|
onDraw(this.visibleHaps, time, this);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.warn('draw error', err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
invalidate(scheduler = this.scheduler) {
|
||||||
|
if (!scheduler) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.scheduler = scheduler;
|
||||||
|
const t = scheduler.now();
|
||||||
|
let [_, lookahead] = this.drawTime;
|
||||||
|
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
|
||||||
|
// remove all future haps
|
||||||
|
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
|
||||||
|
// query future haps
|
||||||
|
const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query..
|
||||||
|
// append future haps
|
||||||
|
this.visibleHaps = this.visibleHaps.concat(futureHaps);
|
||||||
|
}
|
||||||
|
start(scheduler) {
|
||||||
|
this.scheduler = scheduler;
|
||||||
|
this.invalidate();
|
||||||
|
this.framer.start();
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
if (this.framer) {
|
||||||
|
this.framer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,23 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import Fraction, { gcd } from './fraction.mjs';
|
import Fraction, { gcd } from './fraction.mjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intended for a debugging, drawLine renders the pattern as a string, where each character represents the same time span.
|
||||||
|
* Should only be used with single characters as values, otherwise the character slots will be messed up.
|
||||||
|
* Character legend:
|
||||||
|
*
|
||||||
|
* - "|" cycle separator
|
||||||
|
* - "-" hold previous value
|
||||||
|
* - "." silence
|
||||||
|
*
|
||||||
|
* @param {Pattern} pattern the pattern to use
|
||||||
|
* @param {number} chars max number of characters (approximately)
|
||||||
|
* @returns string
|
||||||
|
* @example
|
||||||
|
* const line = drawLine("0 [1 2 3]", 10); // |0--123|0--123
|
||||||
|
* console.log(line);
|
||||||
|
* silence;
|
||||||
|
*/
|
||||||
function drawLine(pat, chars = 60) {
|
function drawLine(pat, chars = 60) {
|
||||||
let cycle = 0;
|
let cycle = 0;
|
||||||
let pos = Fraction(0);
|
let pos = Fraction(0);
|
||||||
|
|||||||
+152
-16
@@ -1,35 +1,171 @@
|
|||||||
/*
|
/*
|
||||||
euclid.mjs - <short description TODO>
|
euclid.mjs - Bjorklund/Euclidean/Diaspora rhythms
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs>
|
Copyright (C) 2023 Rohan Drape and strudel contributors
|
||||||
|
|
||||||
|
See <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs> for authors of this file.
|
||||||
|
|
||||||
|
The Bjorklund algorithm implementation is ported from the Haskell Music Theory Haskell module by Rohan Drape -
|
||||||
|
https://rohandrape.net/?t=hmt
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, timeCat } from './pattern.mjs';
|
import { Pattern, timeCat, register, silence } from './pattern.mjs';
|
||||||
import bjork from 'bjork';
|
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
|
||||||
import { rotate } from './util.mjs';
|
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
|
|
||||||
const euclid = (pulses, steps, rotation = 0) => {
|
const left = function (n, x) {
|
||||||
const b = bjork(steps, pulses);
|
const [ons, offs] = n;
|
||||||
|
const [xs, ys] = x;
|
||||||
|
const [_xs, __xs] = splitAt(offs, xs);
|
||||||
|
return [
|
||||||
|
[offs, ons - offs],
|
||||||
|
[zipWith((a, b) => a.concat(b), _xs, ys), __xs],
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const right = function (n, x) {
|
||||||
|
const [ons, offs] = n;
|
||||||
|
const [xs, ys] = x;
|
||||||
|
const [_ys, __ys] = splitAt(ons, ys);
|
||||||
|
const result = [
|
||||||
|
[ons, offs - ons],
|
||||||
|
[zipWith((a, b) => a.concat(b), xs, _ys), __ys],
|
||||||
|
];
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _bjork = function (n, x) {
|
||||||
|
const [ons, offs] = n;
|
||||||
|
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bjork = function (ons, steps) {
|
||||||
|
const offs = steps - ons;
|
||||||
|
const x = Array(ons).fill([1]);
|
||||||
|
const y = Array(offs).fill([0]);
|
||||||
|
const result = _bjork([ons, offs], [x, y]);
|
||||||
|
return flatten(result[1][0]).concat(flatten(result[1][1]));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the structure of the pattern to form an euclidean rhythm.
|
||||||
|
* Euclidian rhythms are rhythms obtained using the greatest common
|
||||||
|
* divisor of two numbers. They were described in 2004 by Godfried
|
||||||
|
* Toussaint, a canadian computer scientist. Euclidian rhythms are
|
||||||
|
* really useful for computer/algorithmic music because they can
|
||||||
|
* describe a large number of rhythms with a couple of numbers.
|
||||||
|
*
|
||||||
|
* @memberof Pattern
|
||||||
|
* @name euclid
|
||||||
|
* @param {number} pulses the number of onsets / beats
|
||||||
|
* @param {number} steps the number of steps to fill
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* // The Cuban tresillo pattern.
|
||||||
|
* note("c3").euclid(3,8)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence.
|
||||||
|
* @memberof Pattern
|
||||||
|
* @name euclidRot
|
||||||
|
* @param {number} pulses the number of onsets / beats
|
||||||
|
* @param {number} steps the number of steps to fill
|
||||||
|
* @param {number} rotation offset in steps
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* // A Samba rhythm necklace from Brazil
|
||||||
|
* note("c3").euclidRot(3,16,14)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
|
||||||
|
* note("c3").euclid(2,5)
|
||||||
|
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
|
||||||
|
* note("c3").euclid(3,4)
|
||||||
|
* @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm.
|
||||||
|
* note("c3").euclidRot(3,5,2)
|
||||||
|
* @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)
|
||||||
|
* @example // Another Ruchenitza Bulgarian folk-dance rhythm.
|
||||||
|
* note("c3").euclid(4,7)
|
||||||
|
* @example // The Aksak rhythm of Turkey.
|
||||||
|
* note("c3").euclid(4,9)
|
||||||
|
* @example // The metric pattern used by Frank Zappa in his piece titled Outside Now.
|
||||||
|
* note("c3").euclid(4,11)
|
||||||
|
* @example // Yields the York-Samai pattern, a popular Arab rhythm.
|
||||||
|
* note("c3").euclid(5,6)
|
||||||
|
* @example // The Nawakhat pattern, another popular Arab rhythm.
|
||||||
|
* note("c3").euclid(5,7)
|
||||||
|
* @example // The Cuban cinquillo pattern.
|
||||||
|
* note("c3").euclid(5,8)
|
||||||
|
* @example // A popular Arab rhythm called Agsag-Samai.
|
||||||
|
* note("c3").euclid(5,9)
|
||||||
|
* @example // The metric pattern used by Moussorgsky in Pictures at an Exhibition.
|
||||||
|
* note("c3").euclid(5,11)
|
||||||
|
* @example // The Venda clapping pattern of a South African children’s song.
|
||||||
|
* note("c3").euclid(5,12)
|
||||||
|
* @example // The Bossa-Nova rhythm necklace of Brazil.
|
||||||
|
* note("c3").euclid(5,16)
|
||||||
|
* @example // A typical rhythm played on the Bendir (frame drum).
|
||||||
|
* note("c3").euclid(7,8)
|
||||||
|
* @example // A common West African bell pattern.
|
||||||
|
* note("c3").euclid(7,12)
|
||||||
|
* @example // A Samba rhythm necklace from Brazil.
|
||||||
|
* note("c3").euclidRot(7,16,14)
|
||||||
|
* @example // A rhythm necklace used in the Central African Republic.
|
||||||
|
* note("c3").euclid(9,16)
|
||||||
|
* @example // A rhythm necklace of the Aka Pygmies of Central Africa.
|
||||||
|
* note("c3").euclidRot(11,24,14)
|
||||||
|
* @example // Another rhythm necklace of the Aka Pygmies of the upper Sangha.
|
||||||
|
* note("c3").euclidRot(13,24,5)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const _euclidRot = function (pulses, steps, rotation) {
|
||||||
|
const b = bjork(pulses, steps);
|
||||||
if (rotation) {
|
if (rotation) {
|
||||||
return rotate(b, -rotation);
|
return rotate(b, -rotation);
|
||||||
}
|
}
|
||||||
return b;
|
return b;
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.euclid = function (pulses, steps, rotation = 0) {
|
export const euclid = register('euclid', function (pulses, steps, pat) {
|
||||||
return this.struct(euclid(pulses, steps, rotation));
|
return pat.struct(_euclidRot(pulses, steps, 0));
|
||||||
};
|
});
|
||||||
|
|
||||||
Pattern.prototype.euclidLegato = function (pulses, steps, rotation = 0) {
|
export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) {
|
||||||
const bin_pat = euclid(pulses, steps, rotation);
|
return pat.struct(_euclidRot(pulses, steps, rotation));
|
||||||
const firstOne = bin_pat.indexOf(1);
|
});
|
||||||
const gapless = rotate(bin_pat, firstOne)
|
|
||||||
|
/**
|
||||||
|
* Similar to `euclid`, but each pulse is held until the next pulse,
|
||||||
|
* so there will be no gaps.
|
||||||
|
* @name euclidLegato
|
||||||
|
* @memberof Pattern
|
||||||
|
* @example
|
||||||
|
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const _euclidLegato = function (pulses, steps, rotation, pat) {
|
||||||
|
if (pulses < 1) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
|
const bin_pat = _euclidRot(pulses, steps, rotation);
|
||||||
|
const gapless = bin_pat
|
||||||
.join('')
|
.join('')
|
||||||
.split('1')
|
.split('1')
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.map((s) => [s.length + 1, true]);
|
.map((s) => [s.length + 1, true]);
|
||||||
return this.struct(timeCat(...gapless)).late(Fraction(firstOne).div(steps));
|
return pat.struct(timeCat(...gapless));
|
||||||
};
|
};
|
||||||
|
|
||||||
export default euclid;
|
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {
|
||||||
|
return _euclidLegato(pulses, steps, 0, pat);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
||||||
|
return _euclidLegato(pulses, steps, rotation, pat);
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
evaluate.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.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 { isPattern } from './index.mjs';
|
||||||
|
|
||||||
|
export const evalScope = async (...args) => {
|
||||||
|
const results = await Promise.allSettled(args);
|
||||||
|
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||||
|
results.forEach((result, i) => {
|
||||||
|
if (result.status === 'rejected') {
|
||||||
|
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Object.assign(globalThis, ...modules);
|
||||||
|
// below is a fix for above commented out line
|
||||||
|
// same error as https://github.com/vitest-dev/vitest/issues/1807 when running this on astro server
|
||||||
|
modules.forEach((module) => {
|
||||||
|
Object.entries(module).forEach(([name, value]) => {
|
||||||
|
globalThis[name] = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function safeEval(str, options = {}) {
|
||||||
|
const { wrapExpression = true, wrapAsync = true } = options;
|
||||||
|
if (wrapExpression) {
|
||||||
|
str = `{${str}}`;
|
||||||
|
}
|
||||||
|
if (wrapAsync) {
|
||||||
|
str = `(async ()=>${str})()`;
|
||||||
|
}
|
||||||
|
const body = `"use strict";return (${str})`;
|
||||||
|
return Function(body)();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const evaluate = async (code, transpiler) => {
|
||||||
|
if (transpiler) {
|
||||||
|
code = transpiler(code); // transform syntactically correct js code to semantically usable code
|
||||||
|
}
|
||||||
|
// if no transpiler is given, we expect a single instruction (!wrapExpression)
|
||||||
|
const options = { wrapExpression: !!transpiler };
|
||||||
|
let evaluated = await safeEval(code, options);
|
||||||
|
if (!isPattern(evaluated)) {
|
||||||
|
console.log('evaluated', evaluated);
|
||||||
|
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||||
|
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||||
|
}
|
||||||
|
return { mode: 'javascript', pattern: evaluated };
|
||||||
|
};
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="text"
|
id="text"
|
||||||
value="cat('a', 'b')"
|
value="seq('a', ['b', 'c'])"
|
||||||
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
|
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
/>
|
/>
|
||||||
<div id="output"></div>
|
<div id="output"></div>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.0.2');
|
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8');
|
||||||
Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
|
Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
|
||||||
const input = document.getElementById('text');
|
const input = document.getElementById('text');
|
||||||
const getEvents = () => {
|
const getEvents = () => {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="text"
|
id="text"
|
||||||
value="cat('orange', 'indigo')"
|
value="seq('tomato', 'indigo', ['white', 'steelblue']).fast(4)"
|
||||||
style="width: 100%; font-size: 2em; background: black; color: white; outline: none; position: absolute; top: 0"
|
style="width: 100%; font-size: 2em; background: black; color: white; outline: none; position: absolute; top: 0"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
/>
|
/>
|
||||||
<canvas id="canvas"></canvas>
|
<canvas id="canvas"></canvas>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.0.2');
|
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8');
|
||||||
// this adds all strudel functions to the global scope, to be used by eval
|
// this adds all strudel functions to the global scope, to be used by eval
|
||||||
Object.assign(window, strudel);
|
Object.assign(window, strudel);
|
||||||
// setup elements
|
// setup elements
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Buildless Vanilla Strudel REPL</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; background: #222">
|
||||||
|
<div style="display: grid; height: 100vh; grid-template-rows: 32px auto">
|
||||||
|
<button id="start" style="width: 100vw; height: 32px">evaluate</button>
|
||||||
|
<textarea
|
||||||
|
id="text"
|
||||||
|
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
|
||||||
|
spellcheck="false"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div id="output"></div>
|
||||||
|
<script type="module">
|
||||||
|
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.6.8';
|
||||||
|
import { mini } from 'https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0';
|
||||||
|
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.6.0';
|
||||||
|
import {
|
||||||
|
getAudioContext,
|
||||||
|
webaudioOutput,
|
||||||
|
initAudioOnFirstClick,
|
||||||
|
} from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0';
|
||||||
|
|
||||||
|
initAudioOnFirstClick();
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
const input = document.getElementById('text');
|
||||||
|
input.innerHTML = getTune();
|
||||||
|
|
||||||
|
evalScope(
|
||||||
|
controls,
|
||||||
|
import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8'),
|
||||||
|
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0'),
|
||||||
|
import('https://cdn.skypack.dev/@strudel.cycles/tonal@0.6.0'),
|
||||||
|
import('https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { evaluate } = repl({
|
||||||
|
defaultOutput: webaudioOutput,
|
||||||
|
getTime: () => ctx.currentTime,
|
||||||
|
transpiler,
|
||||||
|
});
|
||||||
|
document.getElementById('start').addEventListener('click', () => evaluate(input.value));
|
||||||
|
|
||||||
|
function getTune() {
|
||||||
|
return `await samples('github:tidalcycles/Dirt-Samples/master')
|
||||||
|
|
||||||
|
stack(
|
||||||
|
// amen
|
||||||
|
n("0 1 2 3 4 5 6 7")
|
||||||
|
.sometimes(x=>x.ply(2))
|
||||||
|
.rarely(x=>x.speed("2 | -2"))
|
||||||
|
.sometimesBy(.4, x=>x.delay(".5"))
|
||||||
|
.s("amencutup")
|
||||||
|
.slow(2)
|
||||||
|
.room(.5)
|
||||||
|
,
|
||||||
|
// bass
|
||||||
|
sine.add(saw.slow(4)).range(0,7).segment(8)
|
||||||
|
.superimpose(x=>x.add(.1))
|
||||||
|
.scale('G0 minor').note()
|
||||||
|
.s("sawtooth").decay(.1).sustain(0)
|
||||||
|
.gain(.4).cutoff(perlin.range(300,3000).slow(8)).resonance(10)
|
||||||
|
.degradeBy("0 0.1 .5 .1")
|
||||||
|
.rarely(add(note("12")))
|
||||||
|
,
|
||||||
|
// chord
|
||||||
|
note("Bb3,D4".superimpose(x=>x.add(.2)))
|
||||||
|
.s('sawtooth').cutoff(1000).struct("<~@3 [~ x]>")
|
||||||
|
.decay(.05).sustain(.0).delay(.8).delaytime(.125).room(.8)
|
||||||
|
,
|
||||||
|
// alien
|
||||||
|
s("breath").room(1).shape(.6).chop(16).rev().mask("<x ~@7>")
|
||||||
|
,
|
||||||
|
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
|
||||||
|
).reset("<x@7 x(5,8)>")`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# vite-vanilla-repl-cm6
|
||||||
|
|
||||||
|
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite Vanilla Strudel REPL</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<nav>
|
||||||
|
<button id="play">eval</button>
|
||||||
|
<button id="stop">stop</button>
|
||||||
|
</nav>
|
||||||
|
<div class="container">
|
||||||
|
<div id="editor"></div>
|
||||||
|
<div id="output"></div>
|
||||||
|
</div>
|
||||||
|
<canvas id="roll"></canvas>
|
||||||
|
</main>
|
||||||
|
<script type="module" src="./main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { StrudelMirror } from '@strudel/codemirror';
|
||||||
|
import { funk42 } from './tunes';
|
||||||
|
import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
|
||||||
|
import './style.css';
|
||||||
|
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
||||||
|
import { transpiler } from '@strudel.cycles/transpiler';
|
||||||
|
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
|
||||||
|
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
|
||||||
|
|
||||||
|
// init canvas
|
||||||
|
const canvas = document.getElementById('roll');
|
||||||
|
canvas.width = canvas.width * 2;
|
||||||
|
canvas.height = canvas.height * 2;
|
||||||
|
const drawContext = canvas.getContext('2d');
|
||||||
|
const drawTime = [-2, 2]; // time window of drawn haps
|
||||||
|
|
||||||
|
const editor = new StrudelMirror({
|
||||||
|
defaultOutput: webaudioOutput,
|
||||||
|
getTime: () => getAudioContext().currentTime,
|
||||||
|
transpiler,
|
||||||
|
root: document.getElementById('editor'),
|
||||||
|
initialCode: funk42,
|
||||||
|
drawTime,
|
||||||
|
onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }),
|
||||||
|
prebake: async () => {
|
||||||
|
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
|
||||||
|
const loadModules = evalScope(
|
||||||
|
controls,
|
||||||
|
import('@strudel.cycles/core'),
|
||||||
|
import('@strudel.cycles/mini'),
|
||||||
|
import('@strudel.cycles/tonal'),
|
||||||
|
import('@strudel.cycles/webaudio'),
|
||||||
|
);
|
||||||
|
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('play').addEventListener('click', () => editor.evaluate());
|
||||||
|
document.getElementById('stop').addEventListener('click', () => editor.stop());
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-vanilla-repl-cm6",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^4.3.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@strudel/codemirror": "workspace:*",
|
||||||
|
"@strudel.cycles/core": "workspace:*",
|
||||||
|
"@strudel.cycles/mini": "workspace:*",
|
||||||
|
"@strudel.cycles/soundfonts": "workspace:*",
|
||||||
|
"@strudel.cycles/tonal": "workspace:*",
|
||||||
|
"@strudel.cycles/transpiler": "workspace:*",
|
||||||
|
"@strudel.cycles/webaudio": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
body,
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: #282c34;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
flex-grow: 1;
|
||||||
|
max-height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#roll {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
|
||||||
|
await samples('github:felixroos/samples/main')
|
||||||
|
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||||
|
|
||||||
|
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
|
||||||
|
// make it 22 edo
|
||||||
|
.fmap(v => Math.pow(2,v/22))
|
||||||
|
// mess with the base frequency
|
||||||
|
.mul("<300 [300@3 200]>/8").freq()
|
||||||
|
.layer(
|
||||||
|
// chords
|
||||||
|
x=>x.div(freq(2)).s("flute").euclidLegato("<3 2>",8)
|
||||||
|
.shape(.4).lpf(sine.range(800,4000).slow(8)),
|
||||||
|
// adlibs
|
||||||
|
x=>x.arp("{0 3 2 [1 3]}%1.5")
|
||||||
|
.s('xylo').mul(freq(2))
|
||||||
|
.delay(.5).delayfeedback(.4).juxBy(.5, rev)
|
||||||
|
.hpf(sine.range(200,3000).slow(8)),
|
||||||
|
// bass
|
||||||
|
x=>x.arp("[0 [2 1?]](5,8)").s('sawtooth').div(freq(4))
|
||||||
|
.lpf(sine.range(400,2000).slow(8)).lpq(8).shape(.4)
|
||||||
|
.off(1/8, x=>x.mul(freq(2)).degradeBy(.5)).gain(.3)
|
||||||
|
).clip(1).release(.2)
|
||||||
|
.stack(
|
||||||
|
// drums
|
||||||
|
s("bd sd:<2 1>, [~ hh]*2, [~ rim]").bank('RolandTR909')
|
||||||
|
.off(1/8, x=>x.speed(2).gain(.4)).sometimes(ply(2)).gain(.8)
|
||||||
|
.mask("<0@4 1@12>/4")
|
||||||
|
.reset("<x@15 [x(3,8) x*[4 8]]>")
|
||||||
|
// wait for it...
|
||||||
|
).fast(2/3)
|
||||||
|
//.crush(6) // remove "//" if you dare`;
|
||||||
|
|
||||||
|
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
|
||||||
|
|
||||||
|
await samples('github:felixroos/samples/main')
|
||||||
|
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||||
|
|
||||||
|
addVoicings('hip', {
|
||||||
|
m11: ['2M 3m 4P 7m'],
|
||||||
|
'^7#11': ['3M 4A 5P 7M'],
|
||||||
|
}, ['C4', 'C6'])
|
||||||
|
|
||||||
|
stack(
|
||||||
|
stack(
|
||||||
|
"<Bbm11 A^7#11>/2".voicings('hip').note()
|
||||||
|
.s("gm_epiano1:2")
|
||||||
|
.arp("[<[0 1 2 3] [3 2 1 0]> ~@5]/2")
|
||||||
|
.release(2).late(.25).lpf(2000),
|
||||||
|
"<Bb1 A1>/2".note().s('gm_acoustic_bass'),
|
||||||
|
n("<0 2 3>(3,8)".off(1/8, add(4)))
|
||||||
|
.scale("<Bb4:minor A4:lydian>/2")
|
||||||
|
.s('gm_electric_guitar_jazz')
|
||||||
|
.decay(sine.range(.05, .2).slow(32)).sustain(0)
|
||||||
|
.delay(.5).lpf(sine.range(100,5000).slow(64))
|
||||||
|
.gain(.7).room(.5).pan(sine.range(0,1).slow(11))
|
||||||
|
).add(perlin.range(0,.25).note()),
|
||||||
|
stack(
|
||||||
|
s("bd:1(3,8) rim").bank('RolandTR707').slow(2).room("<0 <.1 .6>>")
|
||||||
|
.when("<0@7 1>",x=>x.echoWith(3, .0625, (x,i) => x.speed(1+i*.24))),
|
||||||
|
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.late("[0 .05]*2").late(12)
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const funk42 = `// froos - how to funk in 42 lines of code
|
||||||
|
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
|
||||||
|
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
|
||||||
|
|
||||||
|
await samples('github:felixroos/samples/main')
|
||||||
|
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||||
|
|
||||||
|
setcps(.5)
|
||||||
|
|
||||||
|
let drums = stack(
|
||||||
|
s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"),
|
||||||
|
s("hh*4").begin(.2).release(.02).end(.25).release(.02)
|
||||||
|
.gain(.3).bank('RolandTR707').late(.02).room(.5),
|
||||||
|
//s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02)
|
||||||
|
).fast(2)
|
||||||
|
|
||||||
|
let wurli = note(\`<
|
||||||
|
[[a2,g3,[b3 c4],e4] ~ [g3,c4,e4](3,8)@4 ~@2]!3
|
||||||
|
[[e2,e3,a3,b3,e4]@3 [e2,e3,ab3,b3,e4]@5]>\`)
|
||||||
|
.s("gm_epiano1:5").decay(.2).sustain("<[1 0@7]!3 1>")
|
||||||
|
.gain("<[.8@2 .4@14]!3 .7>").room(.3)
|
||||||
|
|
||||||
|
let organ = note("<[~@3 [a3,d4,f#4]@2 [[a3,c4,e4]@2 ~] ~@2]!3 ~>".add(12))
|
||||||
|
.s("gm_percussive_organ:2").gain(.6).lpf(1800).pan(.2).room(.3);
|
||||||
|
|
||||||
|
let clav = note(\`<
|
||||||
|
[~@3 a2 [g3,[b3 c4],e4]@2 ~ a2 [g3,b3,e4] ~@2 [g3,c4,e4] ~@4]!3
|
||||||
|
[~@3 e3 [[a3 b3],c3,e3]@2 ~ e2 [e3,a3]@3 [b3,e3] ~@2 [b3,e3]@2]>\`)
|
||||||
|
.s("gm_clavinet:1").decay("<.25!3 [.25 .4]>").sustain(0)
|
||||||
|
.gain(.7).pan(.8).room(.2);
|
||||||
|
|
||||||
|
let bass = note(\`<
|
||||||
|
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
|
||||||
|
[a1 [~ [g2 a2]] [e3 d3] [c3 [g3 a3]]]
|
||||||
|
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
|
||||||
|
[e2@6 e1@5 e1 [[d2 e3] g1]@4]
|
||||||
|
>\`).s("gm_electric_bass_pick:1").release(.1)
|
||||||
|
|
||||||
|
stack(
|
||||||
|
drums
|
||||||
|
,wurli
|
||||||
|
,organ
|
||||||
|
,clav
|
||||||
|
,bass
|
||||||
|
)`;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
!dist
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# vite-vanilla-repl
|
||||||
|
|
||||||
|
This folder demonstrates how to set up a strudel repl using vite and vanilla JS. Run it using:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{b as s,h as i,m,a as n,c as p,p as t}from"./index.4cbc0a10.js";export{s as SyntaxError,i as h,m as mini,n as minify,p as parse,t as patternifyAST};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{g as s,f as d,i as t,n as r,l as u,j as o,d as f,k as p,r as g,s as i,w as l,e as m}from"./index.4cbc0a10.js";export{s as getAudioContext,d as getCachedBuffer,t as getLoadedBuffer,r as getLoadedSamples,u as loadBuffer,o as loadGithubSamples,f as panic,p as resetLoadedSamples,g as reverseBuffer,i as samples,l as webaudioOutput,m as webaudioOutputTrigger};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite Vanilla Strudel REPL</title>
|
||||||
|
<script type="module" crossorigin src="/tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/assets/index.4cbc0a10.js"></script>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; background: #222">
|
||||||
|
<div style="display: grid; height: 100vh">
|
||||||
|
<textarea
|
||||||
|
id="text"
|
||||||
|
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
|
||||||
|
spellcheck="false"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="start"
|
||||||
|
style="
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 10px;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid white;
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
evaluate
|
||||||
|
</button>
|
||||||
|
<div id="output"></div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite Vanilla Strudel REPL</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; background: #222">
|
||||||
|
<div style="display: grid; height: 100vh">
|
||||||
|
<textarea
|
||||||
|
id="text"
|
||||||
|
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
|
||||||
|
spellcheck="false"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="start"
|
||||||
|
style="
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 10px;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid white;
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
evaluate
|
||||||
|
</button>
|
||||||
|
<div id="output"></div>
|
||||||
|
<script type="module" src="./main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { controls, repl, evalScope } from '@strudel.cycles/core';
|
||||||
|
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
||||||
|
import { transpiler } from '@strudel.cycles/transpiler';
|
||||||
|
import tune from './tune.mjs';
|
||||||
|
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
const input = document.getElementById('text');
|
||||||
|
input.innerHTML = tune;
|
||||||
|
initAudioOnFirstClick();
|
||||||
|
|
||||||
|
evalScope(
|
||||||
|
controls,
|
||||||
|
import('@strudel.cycles/core'),
|
||||||
|
import('@strudel.cycles/mini'),
|
||||||
|
import('@strudel.cycles/webaudio'),
|
||||||
|
import('@strudel.cycles/tonal'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { evaluate } = repl({
|
||||||
|
defaultOutput: webaudioOutput,
|
||||||
|
getTime: () => ctx.currentTime,
|
||||||
|
transpiler,
|
||||||
|
});
|
||||||
|
document.getElementById('start').addEventListener('click', () => {
|
||||||
|
ctx.resume();
|
||||||
|
console.log('eval', input.value);
|
||||||
|
evaluate(input.value);
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-vanilla-repl",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build-githack": "vite build --base /tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^4.3.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@strudel.cycles/core": "workspace:*",
|
||||||
|
"@strudel.cycles/mini": "workspace:*",
|
||||||
|
"@strudel.cycles/transpiler": "workspace:*",
|
||||||
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
|
"@strudel.cycles/tonal": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export default `await samples('github:tidalcycles/Dirt-Samples/master')
|
||||||
|
|
||||||
|
stack(
|
||||||
|
// amen
|
||||||
|
n("0 1 2 3 4 5 6 7")
|
||||||
|
.sometimes(x=>x.ply(2))
|
||||||
|
.rarely(x=>x.speed("2 | -2"))
|
||||||
|
.sometimesBy(.4, x=>x.delay(".5"))
|
||||||
|
.s("amencutup")
|
||||||
|
.slow(2)
|
||||||
|
.room(.5)
|
||||||
|
,
|
||||||
|
// bass
|
||||||
|
sine.add(saw.slow(4)).range(0,7).segment(8)
|
||||||
|
.superimpose(x=>x.add(.1))
|
||||||
|
.scale('G0 minor').note()
|
||||||
|
.s("sawtooth").decay(.1).sustain(0)
|
||||||
|
.gain(.4).cutoff(perlin.range(300,3000).slow(8)).resonance(10)
|
||||||
|
.degradeBy("0 0.1 .5 .1")
|
||||||
|
.rarely(add(note("12")))
|
||||||
|
,
|
||||||
|
// chord
|
||||||
|
note("Bb3,D4".superimpose(x=>x.add(.2)))
|
||||||
|
.s('sawtooth').cutoff(1000).struct("<~@3 [~ x]>")
|
||||||
|
.decay(.05).sustain(.0).delay(.8).delaytime(.125).room(.8)
|
||||||
|
,
|
||||||
|
// alien
|
||||||
|
s("breath").room(1).shape(.6).chop(16).rev().mask("<x ~@7>")
|
||||||
|
,
|
||||||
|
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
|
||||||
|
).reset("<x@7 x(5,8)>")`;
|
||||||
@@ -55,7 +55,8 @@ Fraction.prototype.min = function (other) {
|
|||||||
return this.lt(other) ? this : other;
|
return this.lt(other) ? this : other;
|
||||||
};
|
};
|
||||||
|
|
||||||
Fraction.prototype.show = function () {
|
Fraction.prototype.show = function (/* excludeWhole = false */) {
|
||||||
|
// return this.toFraction(excludeWhole);
|
||||||
return this.s * this.n + '/' + this.d;
|
return this.s * this.n + '/' + this.d;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ const fraction = (n) => {
|
|||||||
-> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
|
-> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
|
||||||
-> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
|
-> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
|
||||||
*/
|
*/
|
||||||
n = String(n);
|
// n = String(n); // this is actually faster but imprecise...
|
||||||
}
|
}
|
||||||
return Fraction(n);
|
return Fraction(n);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
// this is a shortcut to eval code from a gist
|
// this is a shortcut to eval code from a gist
|
||||||
// why? to be able to shorten strudel code + e.g. be able to change instruments after links have been generated
|
// why? to be able to shorten strudel code + e.g. be able to change instruments after links have been generated
|
||||||
export default (route) =>
|
export default (route, cache = true) =>
|
||||||
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${Date.now()}`)
|
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${cache ? '' : Date.now()}`)
|
||||||
.then((res) => res.text())
|
.then((res) => res.text())
|
||||||
.then((code) => eval(code));
|
.then((code) => eval(code));
|
||||||
|
|||||||
+53
-11
@@ -32,7 +32,11 @@ export class Hap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get duration() {
|
get duration() {
|
||||||
return this.whole.end.sub(this.whole.begin);
|
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
get endClipped() {
|
||||||
|
return this.whole.begin.add(this.duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
wholeOrPart() {
|
wholeOrPart() {
|
||||||
@@ -79,18 +83,43 @@ export class Hap {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show(compact = false) {
|
||||||
return (
|
const value =
|
||||||
'(' + (this.whole == undefined ? '~' : this.whole.show()) + ', ' + this.part.show() + ', ' + this.value + ')'
|
typeof this.value === 'object'
|
||||||
);
|
? compact
|
||||||
|
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||||
|
: JSON.stringify(this.value)
|
||||||
|
: this.value;
|
||||||
|
var spans = '';
|
||||||
|
if (this.whole == undefined) {
|
||||||
|
spans = '~' + this.part.show;
|
||||||
|
} else {
|
||||||
|
var is_whole = this.whole.begin.equals(this.part.begin) && this.whole.end.equals(this.part.end);
|
||||||
|
if (!this.whole.begin.equals(this.part.begin)) {
|
||||||
|
spans = this.whole.begin.show() + ' ⇜ ';
|
||||||
|
}
|
||||||
|
if (!is_whole) {
|
||||||
|
spans += '(';
|
||||||
|
}
|
||||||
|
spans += this.part.show();
|
||||||
|
if (!is_whole) {
|
||||||
|
spans += ')';
|
||||||
|
}
|
||||||
|
if (!this.whole.end.equals(this.part.end)) {
|
||||||
|
spans += ' ⇝ ' + this.whole.end.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '[ ' + spans + ' | ' + value + ' ]';
|
||||||
}
|
}
|
||||||
|
|
||||||
showWhole() {
|
showWhole(compact = false) {
|
||||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${this.value}`;
|
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
|
||||||
}
|
typeof this.value === 'object'
|
||||||
|
? compact
|
||||||
showWhole() {
|
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${this.value}`;
|
: JSON.stringify(this.value)
|
||||||
|
: this.value
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
combineContext(b) {
|
combineContext(b) {
|
||||||
@@ -101,6 +130,19 @@ export class Hap {
|
|||||||
setContext(context) {
|
setContext(context) {
|
||||||
return new Hap(this.whole, this.part, this.value, context);
|
return new Hap(this.whole, this.part, this.value, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureObjectValue() {
|
||||||
|
/* if (isNote(hap.value)) {
|
||||||
|
// supports primitive hap values that look like notes
|
||||||
|
hap.value = { note: hap.value };
|
||||||
|
} */
|
||||||
|
if (typeof this.value !== 'object') {
|
||||||
|
throw new Error(
|
||||||
|
`expected hap.value to be an object, but got "${this.value}". Hint: append .note() or .s() to the end`,
|
||||||
|
'error',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Hap;
|
export default Hap;
|
||||||
|
|||||||
+27
-4
@@ -4,10 +4,11 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
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 * from './controls.mjs';
|
import controls from './controls.mjs';
|
||||||
export * from './euclid.mjs';
|
export * from './euclid.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
export { Fraction };
|
import { logger } from './logger.mjs';
|
||||||
|
export { Fraction, controls };
|
||||||
export * from './hap.mjs';
|
export * from './hap.mjs';
|
||||||
export * from './pattern.mjs';
|
export * from './pattern.mjs';
|
||||||
export * from './signal.mjs';
|
export * from './signal.mjs';
|
||||||
@@ -15,5 +16,27 @@ export * from './state.mjs';
|
|||||||
export * from './timespan.mjs';
|
export * from './timespan.mjs';
|
||||||
export * from './util.mjs';
|
export * from './util.mjs';
|
||||||
export * from './speak.mjs';
|
export * from './speak.mjs';
|
||||||
export * as gist from './gist.js';
|
export * from './evaluate.mjs';
|
||||||
// export * from './value.mjs';
|
export * from './repl.mjs';
|
||||||
|
export * from './cyclist.mjs';
|
||||||
|
export * from './logger.mjs';
|
||||||
|
export * from './time.mjs';
|
||||||
|
export * from './draw.mjs';
|
||||||
|
export * from './animate.mjs';
|
||||||
|
export * from './pianoroll.mjs';
|
||||||
|
export * from './spiral.mjs';
|
||||||
|
export * from './ui.mjs';
|
||||||
|
export { default as drawLine } from './drawLine.mjs';
|
||||||
|
export { default as gist } from './gist.js';
|
||||||
|
// below won't work with runtime.mjs (json import fails)
|
||||||
|
/* import * as p from './package.json';
|
||||||
|
export const version = p.version; */
|
||||||
|
logger('🌀 @strudel.cycles/core loaded 🌀');
|
||||||
|
if (globalThis._strudelLoaded) {
|
||||||
|
console.warn(
|
||||||
|
`@strudel.cycles/core was loaded more than once...
|
||||||
|
This might happen when you have multiple versions of strudel installed.
|
||||||
|
Please check with "npm ls @strudel.cycles/core".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
globalThis._strudelLoaded = true;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export const logKey = 'strudel.log';
|
||||||
|
|
||||||
|
export function logger(message, type, data = {}) {
|
||||||
|
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
||||||
|
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(logKey, {
|
||||||
|
detail: {
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.key = logKey;
|
||||||
Generated
-1639
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/core",
|
"name": "@strudel.cycles/core",
|
||||||
"version": "0.1.0",
|
"version": "0.8.2",
|
||||||
"description": "Port of Tidal Cycles to JavaScript",
|
"description": "Port of Tidal Cycles to JavaScript",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha --colors"
|
"test": "vitest run",
|
||||||
|
"build": "vite build",
|
||||||
|
"prepublishOnly": "pnpm build"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -25,11 +31,11 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"homepage": "https://strudel.tidalcycles.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bjork": "^0.0.1",
|
|
||||||
"fraction.js": "^4.2.0"
|
"fraction.js": "^4.2.0"
|
||||||
},
|
},
|
||||||
|
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mocha": "^9.2.2"
|
"vite": "^4.3.3",
|
||||||
},
|
"vitest": "^0.28.0"
|
||||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2"
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1804
-932
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,328 @@
|
|||||||
|
/*
|
||||||
|
pianoroll.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/pianoroll.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 { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
||||||
|
|
||||||
|
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||||
|
const getValue = (e) => {
|
||||||
|
let { value } = e;
|
||||||
|
if (typeof e.value !== 'object') {
|
||||||
|
value = { value };
|
||||||
|
}
|
||||||
|
let { note, n, freq, s } = value;
|
||||||
|
if (freq) {
|
||||||
|
return freqToMidi(freq);
|
||||||
|
}
|
||||||
|
note = note ?? n;
|
||||||
|
if (typeof note === 'string') {
|
||||||
|
return noteToMidi(note);
|
||||||
|
}
|
||||||
|
if (typeof note === 'number') {
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
if (s) {
|
||||||
|
return '_' + s;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
Pattern.prototype.pianoroll = function ({
|
||||||
|
cycles = 4,
|
||||||
|
playhead = 0.5,
|
||||||
|
overscan = 1,
|
||||||
|
flipTime = 0,
|
||||||
|
flipValues = 0,
|
||||||
|
hideNegative = false,
|
||||||
|
// inactive = '#C9E597',
|
||||||
|
// inactive = '#FFCA28',
|
||||||
|
inactive = '#7491D2',
|
||||||
|
active = '#FFCA28',
|
||||||
|
// background = '#2A3236',
|
||||||
|
background = 'transparent',
|
||||||
|
smear = 0,
|
||||||
|
playheadColor = 'white',
|
||||||
|
minMidi = 10,
|
||||||
|
maxMidi = 90,
|
||||||
|
autorange = 0,
|
||||||
|
timeframe: timeframeProp,
|
||||||
|
fold = 0,
|
||||||
|
vertical = 0,
|
||||||
|
labels = 0,
|
||||||
|
} = {}) {
|
||||||
|
const ctx = getDrawContext();
|
||||||
|
const w = ctx.canvas.width;
|
||||||
|
const h = ctx.canvas.height;
|
||||||
|
let from = -cycles * playhead;
|
||||||
|
let to = cycles * (1 - playhead);
|
||||||
|
|
||||||
|
if (timeframeProp) {
|
||||||
|
console.warn('timeframe is deprecated! use from/to instead');
|
||||||
|
from = 0;
|
||||||
|
to = timeframeProp;
|
||||||
|
}
|
||||||
|
const timeAxis = vertical ? h : w;
|
||||||
|
const valueAxis = vertical ? w : h;
|
||||||
|
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
|
||||||
|
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
|
||||||
|
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
|
||||||
|
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
|
||||||
|
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
|
||||||
|
let foldValues = [];
|
||||||
|
flipTime && timeRange.reverse();
|
||||||
|
flipValues && valueRange.reverse();
|
||||||
|
|
||||||
|
this.draw(
|
||||||
|
(ctx, events, t) => {
|
||||||
|
ctx.fillStyle = background;
|
||||||
|
ctx.globalAlpha = 1; // reset!
|
||||||
|
if (!smear) {
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
}
|
||||||
|
const inFrame = (event) =>
|
||||||
|
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
|
||||||
|
events.filter(inFrame).forEach((event) => {
|
||||||
|
const isActive = event.whole.begin <= t && event.endClipped > t;
|
||||||
|
ctx.fillStyle = event.context?.color || inactive;
|
||||||
|
ctx.strokeStyle = event.context?.color || active;
|
||||||
|
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
|
||||||
|
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
||||||
|
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||||
|
const value = getValue(event);
|
||||||
|
const valuePx = scale(
|
||||||
|
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
|
||||||
|
...valueRange,
|
||||||
|
);
|
||||||
|
let margin = 0;
|
||||||
|
const offset = scale(t / timeExtent, ...timeRange);
|
||||||
|
let coords;
|
||||||
|
if (vertical) {
|
||||||
|
coords = [
|
||||||
|
valuePx + 1 - (flipValues ? barThickness : 0), // x
|
||||||
|
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
|
||||||
|
barThickness - 2, // width
|
||||||
|
durationPx - 2, // height
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
coords = [
|
||||||
|
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
|
||||||
|
valuePx + 1 - (flipValues ? 0 : barThickness), // y
|
||||||
|
durationPx - 2, // widith
|
||||||
|
barThickness - 2, // height
|
||||||
|
];
|
||||||
|
}
|
||||||
|
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
||||||
|
if (labels) {
|
||||||
|
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
|
||||||
|
ctx.font = `${barThickness * 0.75}px monospace`;
|
||||||
|
ctx.strokeStyle = 'black';
|
||||||
|
ctx.fillStyle = isActive ? 'white' : 'black';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
ctx.fillText(label, ...coords);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ctx.globalAlpha = 1; // reset!
|
||||||
|
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||||
|
// draw playhead
|
||||||
|
ctx.strokeStyle = playheadColor;
|
||||||
|
ctx.beginPath();
|
||||||
|
if (vertical) {
|
||||||
|
ctx.moveTo(0, playheadPosition);
|
||||||
|
ctx.lineTo(valueAxis, playheadPosition);
|
||||||
|
} else {
|
||||||
|
ctx.moveTo(playheadPosition, 0);
|
||||||
|
ctx.lineTo(playheadPosition, valueAxis);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: from - overscan,
|
||||||
|
to: to + overscan,
|
||||||
|
onQuery: (events) => {
|
||||||
|
const { min, max, values } = events.reduce(
|
||||||
|
({ min, max, values }, e) => {
|
||||||
|
const v = getValue(e);
|
||||||
|
return {
|
||||||
|
min: v < min ? v : min,
|
||||||
|
max: v > max ? v : max,
|
||||||
|
values: values.includes(v) ? values : [...values, v],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ min: Infinity, max: -Infinity, values: [] },
|
||||||
|
);
|
||||||
|
if (autorange) {
|
||||||
|
minMidi = min;
|
||||||
|
maxMidi = max;
|
||||||
|
valueExtent = maxMidi - minMidi + 1;
|
||||||
|
}
|
||||||
|
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
|
||||||
|
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
// this function allows drawing a pianoroll without ties to Pattern.prototype
|
||||||
|
// it will probably replace the above in the future
|
||||||
|
export function pianoroll({
|
||||||
|
time,
|
||||||
|
haps,
|
||||||
|
cycles = 4,
|
||||||
|
playhead = 0.5,
|
||||||
|
flipTime = 0,
|
||||||
|
flipValues = 0,
|
||||||
|
hideNegative = false,
|
||||||
|
// inactive = '#C9E597',
|
||||||
|
// inactive = '#FFCA28',
|
||||||
|
inactive = '#7491D2',
|
||||||
|
active = '#FFCA28',
|
||||||
|
// background = '#2A3236',
|
||||||
|
background = 'transparent',
|
||||||
|
smear = 0,
|
||||||
|
playheadColor = 'white',
|
||||||
|
minMidi = 10,
|
||||||
|
maxMidi = 90,
|
||||||
|
autorange = 0,
|
||||||
|
timeframe: timeframeProp,
|
||||||
|
fold = 0,
|
||||||
|
vertical = 0,
|
||||||
|
labels = false,
|
||||||
|
ctx,
|
||||||
|
} = {}) {
|
||||||
|
const w = ctx.canvas.width;
|
||||||
|
const h = ctx.canvas.height;
|
||||||
|
let from = -cycles * playhead;
|
||||||
|
let to = cycles * (1 - playhead);
|
||||||
|
|
||||||
|
if (timeframeProp) {
|
||||||
|
console.warn('timeframe is deprecated! use from/to instead');
|
||||||
|
from = 0;
|
||||||
|
to = timeframeProp;
|
||||||
|
}
|
||||||
|
const timeAxis = vertical ? h : w;
|
||||||
|
const valueAxis = vertical ? w : h;
|
||||||
|
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
|
||||||
|
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
|
||||||
|
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
|
||||||
|
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
|
||||||
|
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
|
||||||
|
let foldValues = [];
|
||||||
|
flipTime && timeRange.reverse();
|
||||||
|
flipValues && valueRange.reverse();
|
||||||
|
|
||||||
|
// onQuery
|
||||||
|
const { min, max, values } = haps.reduce(
|
||||||
|
({ min, max, values }, e) => {
|
||||||
|
const v = getValue(e);
|
||||||
|
return {
|
||||||
|
min: v < min ? v : min,
|
||||||
|
max: v > max ? v : max,
|
||||||
|
values: values.includes(v) ? values : [...values, v],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ min: Infinity, max: -Infinity, values: [] },
|
||||||
|
);
|
||||||
|
if (autorange) {
|
||||||
|
minMidi = min;
|
||||||
|
maxMidi = max;
|
||||||
|
valueExtent = maxMidi - minMidi + 1;
|
||||||
|
}
|
||||||
|
// foldValues = values.sort((a, b) => a - b);
|
||||||
|
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
|
||||||
|
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
||||||
|
|
||||||
|
ctx.fillStyle = background;
|
||||||
|
ctx.globalAlpha = 1; // reset!
|
||||||
|
if (!smear) {
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
}
|
||||||
|
/* const inFrame = (event) =>
|
||||||
|
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
|
||||||
|
haps
|
||||||
|
// .filter(inFrame)
|
||||||
|
.forEach((event) => {
|
||||||
|
const isActive = event.whole.begin <= time && event.whole.end > time;
|
||||||
|
const color = event.value?.color || event.context?.color;
|
||||||
|
ctx.fillStyle = color || inactive;
|
||||||
|
ctx.strokeStyle = color || active;
|
||||||
|
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
|
||||||
|
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
||||||
|
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||||
|
const value = getValue(event);
|
||||||
|
const valuePx = scale(
|
||||||
|
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
|
||||||
|
...valueRange,
|
||||||
|
);
|
||||||
|
let margin = 0;
|
||||||
|
const offset = scale(time / timeExtent, ...timeRange);
|
||||||
|
let coords;
|
||||||
|
if (vertical) {
|
||||||
|
coords = [
|
||||||
|
valuePx + 1 - (flipValues ? barThickness : 0), // x
|
||||||
|
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
|
||||||
|
barThickness - 2, // width
|
||||||
|
durationPx - 2, // height
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
coords = [
|
||||||
|
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
|
||||||
|
valuePx + 1 - (flipValues ? 0 : barThickness), // y
|
||||||
|
durationPx - 2, // widith
|
||||||
|
barThickness - 2, // height
|
||||||
|
];
|
||||||
|
}
|
||||||
|
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
||||||
|
if (labels) {
|
||||||
|
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
|
||||||
|
ctx.font = `${barThickness * 0.75}px monospace`;
|
||||||
|
ctx.strokeStyle = 'black';
|
||||||
|
ctx.fillStyle = isActive ? 'white' : 'black';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
ctx.fillText(label, ...coords);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ctx.globalAlpha = 1; // reset!
|
||||||
|
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||||
|
// draw playhead
|
||||||
|
ctx.strokeStyle = playheadColor;
|
||||||
|
ctx.beginPath();
|
||||||
|
if (vertical) {
|
||||||
|
ctx.moveTo(0, playheadPosition);
|
||||||
|
ctx.lineTo(valueAxis, playheadPosition);
|
||||||
|
} else {
|
||||||
|
ctx.moveTo(playheadPosition, 0);
|
||||||
|
ctx.lineTo(playheadPosition, valueAxis);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDrawOptions(drawTime, options = {}) {
|
||||||
|
let [lookbehind, lookahead] = drawTime;
|
||||||
|
lookbehind = Math.abs(lookbehind);
|
||||||
|
const cycles = lookahead + lookbehind;
|
||||||
|
const playhead = lookbehind / cycles;
|
||||||
|
return { fold: 1, ...options, cycles, playhead };
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern.prototype.punchcard = function (options) {
|
||||||
|
return this.onPaint((ctx, time, haps, drawTime) =>
|
||||||
|
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Pattern.prototype.pianoroll = function (options) {
|
||||||
|
return this.onPaint((ctx, time, haps, drawTime) =>
|
||||||
|
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),
|
||||||
|
);
|
||||||
|
}; */
|
||||||
|
|
||||||
|
export function drawPianoroll(options) {
|
||||||
|
const { drawTime, ...rest } = options;
|
||||||
|
pianoroll({ ...getDrawOptions(drawTime), ...rest });
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { Cyclist } from './cyclist.mjs';
|
||||||
|
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
|
import { setTime } from './time.mjs';
|
||||||
|
import { evalScope } from './evaluate.mjs';
|
||||||
|
import { register } from './pattern.mjs';
|
||||||
|
|
||||||
|
export function repl({
|
||||||
|
interval,
|
||||||
|
defaultOutput,
|
||||||
|
onSchedulerError,
|
||||||
|
onEvalError,
|
||||||
|
beforeEval,
|
||||||
|
afterEval,
|
||||||
|
getTime,
|
||||||
|
transpiler,
|
||||||
|
onToggle,
|
||||||
|
editPattern,
|
||||||
|
}) {
|
||||||
|
const scheduler = new Cyclist({
|
||||||
|
interval,
|
||||||
|
onTrigger: getTrigger({ defaultOutput, getTime }),
|
||||||
|
onError: onSchedulerError,
|
||||||
|
getTime,
|
||||||
|
onToggle,
|
||||||
|
});
|
||||||
|
const setPattern = (pattern, autostart = true) => {
|
||||||
|
pattern = editPattern?.(pattern) || pattern;
|
||||||
|
scheduler.setPattern(pattern, autostart);
|
||||||
|
};
|
||||||
|
setTime(() => scheduler.now()); // TODO: refactor?
|
||||||
|
const evaluate = async (code, autostart = true) => {
|
||||||
|
if (!code) {
|
||||||
|
throw new Error('no code to evaluate');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await beforeEval?.({ code });
|
||||||
|
let { pattern } = await _evaluate(code, transpiler);
|
||||||
|
|
||||||
|
logger(`[eval] code updated`);
|
||||||
|
setPattern(pattern, autostart);
|
||||||
|
afterEval?.({ code, pattern });
|
||||||
|
return pattern;
|
||||||
|
} catch (err) {
|
||||||
|
// console.warn(`[repl] eval error: ${err.message}`);
|
||||||
|
logger(`[eval] error: ${err.message}`, 'error');
|
||||||
|
onEvalError?.(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const stop = () => scheduler.stop();
|
||||||
|
const start = () => scheduler.start();
|
||||||
|
const pause = () => scheduler.pause();
|
||||||
|
const setCps = (cps) => scheduler.setCps(cps);
|
||||||
|
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
||||||
|
|
||||||
|
// the following functions use the cps value, which is why they are defined here..
|
||||||
|
const loopAt = register('loopAt', (cycles, pat) => {
|
||||||
|
return pat.loopAtCps(cycles, scheduler.cps);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fit = register('fit', (pat) =>
|
||||||
|
pat.withHap((hap) =>
|
||||||
|
hap.withValue((v) => ({
|
||||||
|
...v,
|
||||||
|
speed: scheduler.cps / hap.whole.duration, // overwrite speed completely?
|
||||||
|
unit: 'c',
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
evalScope({
|
||||||
|
loopAt,
|
||||||
|
fit,
|
||||||
|
setCps,
|
||||||
|
setcps: setCps,
|
||||||
|
setCpm,
|
||||||
|
setcpm: setCpm,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTrigger =
|
||||||
|
({ getTime, defaultOutput }) =>
|
||||||
|
async (hap, deadline, duration, cps) => {
|
||||||
|
try {
|
||||||
|
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||||
|
await defaultOutput(hap, deadline, duration, cps);
|
||||||
|
}
|
||||||
|
if (hap.context.onTrigger) {
|
||||||
|
// call signature of output / onTrigger is different...
|
||||||
|
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
+350
-85
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Hap } from './hap.mjs';
|
import { Hap } from './hap.mjs';
|
||||||
import { Pattern, fastcat, reify, silence, stack } from './pattern.mjs';
|
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
import { id } from './util.mjs';
|
import { id } from './util.mjs';
|
||||||
|
|
||||||
@@ -20,19 +20,63 @@ export const signal = (func) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isaw = signal((t) => 1 - (t % 1));
|
export const isaw = signal((t) => 1 - (t % 1));
|
||||||
export const isaw2 = isaw._toBipolar();
|
export const isaw2 = isaw.toBipolar();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sawtooth signal between 0 and 1.
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
* @example
|
||||||
|
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
|
||||||
|
* @example
|
||||||
|
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
||||||
|
*
|
||||||
|
*/
|
||||||
export const saw = signal((t) => t % 1);
|
export const saw = signal((t) => t % 1);
|
||||||
export const saw2 = saw._toBipolar();
|
export const saw2 = saw.toBipolar();
|
||||||
|
|
||||||
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||||
export const sine = sine2._fromBipolar();
|
|
||||||
|
/**
|
||||||
|
* A sine signal between 0 and 1.
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
* @example
|
||||||
|
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const sine = sine2.fromBipolar();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cosine signal between 0 and 1.
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
* @example
|
||||||
|
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor').note()
|
||||||
|
*
|
||||||
|
*/
|
||||||
export const cosine = sine._early(Fraction(1).div(4));
|
export const cosine = sine._early(Fraction(1).div(4));
|
||||||
export const cosine2 = sine2._early(Fraction(1).div(4));
|
export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A square signal between 0 and 1.
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
* @example
|
||||||
|
* square.segment(2).range(0,7).scale('C minor').note()
|
||||||
|
*
|
||||||
|
*/
|
||||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
export const square = signal((t) => Math.floor((t * 2) % 2));
|
||||||
export const square2 = square._toBipolar();
|
export const square2 = square.toBipolar();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A triangle signal between 0 and 1.
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
* @example
|
||||||
|
* tri.segment(8).range(0,7).scale('C minor').note()
|
||||||
|
*
|
||||||
|
*/
|
||||||
export const tri = fastcat(isaw, saw);
|
export const tri = fastcat(isaw, saw);
|
||||||
export const tri2 = fastcat(isaw2, saw2);
|
export const tri2 = fastcat(isaw2, saw2);
|
||||||
|
|
||||||
@@ -66,28 +110,120 @@ const timeToRandsPrime = (seed, n) => {
|
|||||||
|
|
||||||
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A discrete pattern of numbers from 0 to n-1
|
||||||
|
* @example
|
||||||
|
* run(4).scale('C4 major').note()
|
||||||
|
* // "0 1 2 3".scale('C4 major').note()
|
||||||
|
*/
|
||||||
|
export const run = (n) => saw.range(0, n).floor().segment(n);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A continuous pattern of random numbers, between 0 and 1.
|
||||||
|
*
|
||||||
|
* @name rand
|
||||||
|
* @example
|
||||||
|
* // randomly change the cutoff
|
||||||
|
* s("bd sd,hh*4").cutoff(rand.range(500,2000))
|
||||||
|
*
|
||||||
|
*/
|
||||||
export const rand = signal(timeToRand);
|
export const rand = signal(timeToRand);
|
||||||
|
/**
|
||||||
|
* A continuous pattern of random numbers, between -1 and 1
|
||||||
|
*/
|
||||||
|
export const rand2 = rand.toBipolar();
|
||||||
|
|
||||||
export const _brandBy = (p) => rand.fmap((x) => x < p);
|
export const _brandBy = (p) => rand.fmap((x) => x < p);
|
||||||
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
||||||
export const brand = _brandBy(0.5);
|
export const brand = _brandBy(0.5);
|
||||||
|
|
||||||
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A continuous pattern of random integers, between 0 and n-1.
|
||||||
|
*
|
||||||
|
* @name irand
|
||||||
|
* @param {number} n max value (exclusive)
|
||||||
|
* @example
|
||||||
|
* // randomly select scale notes from 0 - 7 (= C to C)
|
||||||
|
* irand(8).struct("x(3,8)").scale('C minor').note()
|
||||||
|
*
|
||||||
|
*/
|
||||||
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
||||||
|
|
||||||
export const chooseWith = (pat, xs) => {
|
export const __chooseWith = (pat, xs) => {
|
||||||
xs = xs.map(reify);
|
xs = xs.map(reify);
|
||||||
if (xs.length == 0) {
|
if (xs.length == 0) {
|
||||||
return silence;
|
return silence;
|
||||||
}
|
}
|
||||||
return pat
|
return pat.range(0, xs.length).fmap((i) => xs[Math.floor(i)]);
|
||||||
.range(0, xs.length)
|
};
|
||||||
.fmap((i) => xs[Math.floor(i)])
|
/**
|
||||||
.outerJoin();
|
* Choose from the list of values (or patterns of values) using the given
|
||||||
|
* pattern of numbers, which should be in the range of 0..1
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {*} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
*/
|
||||||
|
export const chooseWith = (pat, xs) => {
|
||||||
|
return __chooseWith(pat, xs).outerJoin();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As with {chooseWith}, but the structure comes from the chosen values, rather
|
||||||
|
* than the pattern you're using to choose with.
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {*} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
*/
|
||||||
|
export const chooseInWith = (pat, xs) => {
|
||||||
|
return __chooseWith(pat, xs).innerJoin();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses randomly from the given list of elements.
|
||||||
|
* @param {...any} xs values / patterns to choose from.
|
||||||
|
* @returns {Pattern} - a continuous pattern.
|
||||||
|
*/
|
||||||
export const choose = (...xs) => chooseWith(rand, xs);
|
export const choose = (...xs) => chooseWith(rand, xs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses from the given list of values (or patterns of values), according
|
||||||
|
* to the pattern that the method is called on. The pattern should be in
|
||||||
|
* the range 0 .. 1.
|
||||||
|
* @param {...any} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
*/
|
||||||
|
Pattern.prototype.choose = function (...xs) {
|
||||||
|
return chooseWith(this, xs);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As with choose, but the pattern that this method is called on should be
|
||||||
|
* in the range -1 .. 1
|
||||||
|
* @param {...any} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
*/
|
||||||
|
Pattern.prototype.choose2 = function (...xs) {
|
||||||
|
return chooseWith(this.fromBipolar(), xs);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks one of the elements at random each cycle.
|
||||||
|
* @returns {Pattern}
|
||||||
|
* @example
|
||||||
|
* chooseCycles("bd", "hh", "sd").s().fast(4)
|
||||||
|
* @example
|
||||||
|
* "bd | hh | sd".s().fast(4)
|
||||||
|
*/
|
||||||
|
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
|
||||||
|
|
||||||
|
export const randcat = chooseCycles;
|
||||||
|
|
||||||
const _wchooseWith = function (pat, ...pairs) {
|
const _wchooseWith = function (pat, ...pairs) {
|
||||||
const values = pairs.map((pair) => reify(pair[0]));
|
const values = pairs.map((pair) => reify(pair[0]));
|
||||||
const weights = [];
|
const weights = [];
|
||||||
@@ -110,6 +246,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
|||||||
|
|
||||||
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
|
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
|
||||||
|
|
||||||
|
// this function expects pat to be a pattern of floats...
|
||||||
export const perlinWith = (pat) => {
|
export const perlinWith = (pat) => {
|
||||||
const pata = pat.fmap(Math.floor);
|
const pata = pat.fmap(Math.floor);
|
||||||
const patb = pat.fmap((t) => Math.floor(t) + 1);
|
const patb = pat.fmap((t) => Math.floor(t) + 1);
|
||||||
@@ -118,101 +255,229 @@ export const perlinWith = (pat) => {
|
|||||||
return pat.sub(pata).fmap(interp).appBoth(pata.fmap(timeToRand)).appBoth(patb.fmap(timeToRand));
|
return pat.sub(pata).fmap(interp).appBoth(pata.fmap(timeToRand)).appBoth(patb.fmap(timeToRand));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const perlin = perlinWith(time);
|
/**
|
||||||
|
* Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1.
|
||||||
|
*
|
||||||
|
* @name perlin
|
||||||
|
* @example
|
||||||
|
* // randomly change the cutoff
|
||||||
|
* s("bd sd,hh*4").cutoff(perlin.range(500,2000))
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
||||||
|
|
||||||
Pattern.prototype._degradeByWith = function (withPat, x) {
|
export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
|
||||||
return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x));
|
pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
|
||||||
};
|
);
|
||||||
|
|
||||||
Pattern.prototype._degradeBy = function (x) {
|
/**
|
||||||
return this._degradeByWith(rand, x);
|
* Randomly removes events from the pattern by a given amount.
|
||||||
};
|
* 0 = 0% chance of removal
|
||||||
|
* 1 = 100% chance of removal
|
||||||
|
*
|
||||||
|
* @name degradeBy
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {number} amount - a number between 0 and 1
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").degradeBy(0.2)
|
||||||
|
* @example
|
||||||
|
* s("[hh?0.2]*8")
|
||||||
|
*/
|
||||||
|
export const degradeBy = register('degradeBy', function (x, pat) {
|
||||||
|
return pat._degradeByWith(rand, x);
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype.degrade = function () {
|
/**
|
||||||
return this._degradeBy(0.5);
|
*
|
||||||
};
|
* Randomly removes 50% of events from the pattern. Shorthand for `.degradeBy(0.5)`
|
||||||
|
*
|
||||||
|
* @name degrade
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").degrade()
|
||||||
|
* @example
|
||||||
|
* s("[hh?]*8")
|
||||||
|
*/
|
||||||
|
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
||||||
|
|
||||||
Pattern.prototype._undegradeBy = function (x) {
|
/**
|
||||||
return this._degradeByWith(
|
* Inverse of {@link Pattern#degradeBy}: Randomly removes events from the pattern by a given amount.
|
||||||
|
* 0 = 100% chance of removal
|
||||||
|
* 1 = 0% chance of removal
|
||||||
|
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
|
||||||
|
*
|
||||||
|
* @name undegradeBy
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {number} amount - a number between 0 and 1
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").undegradeBy(0.2)
|
||||||
|
*/
|
||||||
|
export const undegradeBy = register('undegradeBy', function (x, pat) {
|
||||||
|
return pat._degradeByWith(
|
||||||
rand.fmap((r) => 1 - r),
|
rand.fmap((r) => 1 - r),
|
||||||
x,
|
x,
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
Pattern.prototype.undegrade = function () {
|
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
||||||
return this._undegradeBy(0.5);
|
|
||||||
};
|
|
||||||
|
|
||||||
Pattern.prototype._sometimesBy = function (x, func) {
|
/**
|
||||||
return stack(this._degradeBy(x), func(this._undegradeBy(1 - x)));
|
*
|
||||||
};
|
* Randomly applies the given function by the given probability.
|
||||||
|
* Similar to {@link Pattern#someCyclesBy}
|
||||||
|
*
|
||||||
|
* @name sometimesBy
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {number | Pattern} probability - a number between 0 and 1
|
||||||
|
* @param {function} function - the transformation to apply
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
|
||||||
Pattern.prototype.sometimesBy = function (patx, func) {
|
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
||||||
const pat = this;
|
|
||||||
return reify(patx)
|
return reify(patx)
|
||||||
.fmap((x) => pat._sometimesBy(x, func))
|
.fmap((x) => stack(pat._degradeBy(x), func(pat._undegradeBy(1 - x))))
|
||||||
.innerJoin();
|
.innerJoin();
|
||||||
};
|
});
|
||||||
|
|
||||||
Pattern.prototype._sometimesByPre = function (x, func) {
|
/**
|
||||||
return stack(this._degradeBy(x), func(this).undegradeBy(1 - x));
|
*
|
||||||
};
|
* Applies the given function with a 50% chance
|
||||||
|
*
|
||||||
|
* @name sometimes
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {function} function - the transformation to apply
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*4").sometimes(x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
export const sometimes = register('sometimes', function (func, pat) {
|
||||||
|
return pat._sometimesBy(0.5, func);
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype.sometimesByPre = function (patx, func) {
|
/**
|
||||||
const pat = this;
|
*
|
||||||
|
* Randomly applies the given function by the given probability on a cycle by cycle basis.
|
||||||
|
* Similar to {@link Pattern#sometimesBy}
|
||||||
|
*
|
||||||
|
* @name someCyclesBy
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {number | Pattern} probability - a number between 0 and 1
|
||||||
|
* @param {function} function - the transformation to apply
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
|
||||||
return reify(patx)
|
return reify(patx)
|
||||||
.fmap((x) => pat._sometimesByPre(x, func))
|
.fmap((x) =>
|
||||||
|
stack(
|
||||||
|
pat._degradeByWith(rand._segment(1), x),
|
||||||
|
func(pat._degradeByWith(rand.fmap((r) => 1 - r)._segment(1), 1 - x)),
|
||||||
|
),
|
||||||
|
)
|
||||||
.innerJoin();
|
.innerJoin();
|
||||||
};
|
});
|
||||||
|
|
||||||
Pattern.prototype.sometimes = function (func) {
|
/**
|
||||||
return this._sometimesBy(0.5, func);
|
*
|
||||||
};
|
* Shorthand for `.someCyclesBy(0.5, fn)`
|
||||||
|
*
|
||||||
|
* @name someCycles
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
export const someCycles = register('someCycles', function (func, pat) {
|
||||||
|
return pat._someCyclesBy(0.5, func);
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype.sometimesPre = function (func) {
|
/**
|
||||||
return this._sometimesByPre(0.5, func);
|
*
|
||||||
};
|
* Shorthand for `.sometimesBy(0.75, fn)`
|
||||||
|
*
|
||||||
|
* @name often
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").often(x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
export const often = register('often', function (func, pat) {
|
||||||
|
return pat.sometimesBy(0.75, func);
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype._someCyclesBy = function (x, func) {
|
/**
|
||||||
return stack(
|
*
|
||||||
this._degradeByWith(rand._segment(1), x),
|
* Shorthand for `.sometimesBy(0.25, fn)`
|
||||||
func(this._degradeByWith(rand.fmap((r) => 1 - r)._segment(1), 1 - x)),
|
*
|
||||||
);
|
* @name rarely
|
||||||
};
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").rarely(x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
export const rarely = register('rarely', function (func, pat) {
|
||||||
|
return pat.sometimesBy(0.25, func);
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype.someCyclesBy = function (patx, func) {
|
/**
|
||||||
const pat = this;
|
*
|
||||||
return reify(patx)
|
* Shorthand for `.sometimesBy(0.1, fn)`
|
||||||
.fmap((x) => pat._someCyclesBy(x, func))
|
*
|
||||||
.innerJoin();
|
* @name almostNever
|
||||||
};
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").almostNever(x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
export const almostNever = register('almostNever', function (func, pat) {
|
||||||
|
return pat.sometimesBy(0.1, func);
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype.someCycles = function (func) {
|
/**
|
||||||
return this._someCyclesBy(0.5, func);
|
*
|
||||||
};
|
* Shorthand for `.sometimesBy(0.9, fn)`
|
||||||
|
*
|
||||||
|
* @name almostAlways
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").almostAlways(x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
export const almostAlways = register('almostAlways', function (func, pat) {
|
||||||
|
return pat.sometimesBy(0.9, func);
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype.often = function (func) {
|
/**
|
||||||
return this.sometimesBy(0.75, func);
|
*
|
||||||
};
|
* Shorthand for `.sometimesBy(0, fn)` (never calls fn)
|
||||||
|
*
|
||||||
|
* @name never
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").never(x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
export const never = register('never', function (_, pat) {
|
||||||
|
return pat;
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype.rarely = function (func) {
|
/**
|
||||||
return this.sometimesBy(0.25, func);
|
*
|
||||||
};
|
* Shorthand for `.sometimesBy(1, fn)` (always calls fn)
|
||||||
|
*
|
||||||
Pattern.prototype.almostNever = function (func) {
|
* @name always
|
||||||
return this.sometimesBy(0.1, func);
|
* @memberof Pattern
|
||||||
};
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
Pattern.prototype.almostAlways = function (func) {
|
* s("hh*8").always(x=>x.speed("0.5"))
|
||||||
return this.sometimesBy(0.9, func);
|
*/
|
||||||
};
|
export const always = register('always', function (func, pat) {
|
||||||
|
return func(pat);
|
||||||
Pattern.prototype.never = function (func) {
|
});
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
Pattern.prototype.always = function (func) {
|
|
||||||
return func(this);
|
|
||||||
};
|
|
||||||
|
|
||||||
Pattern.prototype.patternified.push('degradeBy', 'undegradeBy');
|
|
||||||
|
|||||||
+6
-13
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
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, patternify2 } from './index.mjs';
|
import { register } from './index.mjs';
|
||||||
|
|
||||||
let synth;
|
let synth;
|
||||||
try {
|
try {
|
||||||
@@ -16,7 +16,7 @@ try {
|
|||||||
let allVoices = synth?.getVoices();
|
let allVoices = synth?.getVoices();
|
||||||
// console.log('voices', allVoices);
|
// console.log('voices', allVoices);
|
||||||
|
|
||||||
function speak(words, lang, voice) {
|
function triggerSpeech(words, lang, voice) {
|
||||||
synth.cancel();
|
synth.cancel();
|
||||||
const utterance = new SpeechSynthesisUtterance(words);
|
const utterance = new SpeechSynthesisUtterance(words);
|
||||||
utterance.lang = lang;
|
utterance.lang = lang;
|
||||||
@@ -31,15 +31,8 @@ function speak(words, lang, voice) {
|
|||||||
speechSynthesis.speak(utterance);
|
speechSynthesis.speak(utterance);
|
||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype._speak = function (lang, voice) {
|
export const speak = register('speak', function (lang, voice, pat) {
|
||||||
return this._withHap((hap) => {
|
return pat.onTrigger((_, hap) => {
|
||||||
const onTrigger = (time, hap) => {
|
triggerSpeech(hap.value, lang, voice);
|
||||||
speak(hap.value, lang, voice);
|
|
||||||
};
|
|
||||||
return hap.setContext({ ...hap.context, onTrigger });
|
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
Pattern.prototype.speak = function (lang, voice) {
|
|
||||||
return patternify2(Pattern.prototype._speak)(reify(lang), reify(voice), this);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { Pattern } from './index.mjs';
|
||||||
|
|
||||||
|
// polar coords -> xy
|
||||||
|
function fromPolar(angle, radius, cx, cy) {
|
||||||
|
const radians = ((angle - 90) * Math.PI) / 180;
|
||||||
|
return [cx + Math.cos(radians) * radius, cy + Math.sin(radians) * radius];
|
||||||
|
}
|
||||||
|
|
||||||
|
const xyOnSpiral = (angle, margin, cx, cy, rotate = 0) => fromPolar((angle + rotate) * 360, margin * angle, cx, cy); // TODO: logSpiral
|
||||||
|
|
||||||
|
// draw spiral / segment of spiral
|
||||||
|
function spiralSegment(options) {
|
||||||
|
let {
|
||||||
|
ctx,
|
||||||
|
from = 0,
|
||||||
|
to = 3,
|
||||||
|
margin = 50,
|
||||||
|
cx = 100,
|
||||||
|
cy = 100,
|
||||||
|
rotate = 0,
|
||||||
|
thickness = margin / 2,
|
||||||
|
color = '#0000ff30',
|
||||||
|
cap = 'round',
|
||||||
|
stretch = 1,
|
||||||
|
fromOpacity = 1,
|
||||||
|
toOpacity = 1,
|
||||||
|
} = options;
|
||||||
|
from *= stretch;
|
||||||
|
to *= stretch;
|
||||||
|
rotate *= stretch;
|
||||||
|
ctx.lineWidth = thickness;
|
||||||
|
ctx.lineCap = cap;
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.globalAlpha = fromOpacity;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
let [sx, sy] = xyOnSpiral(from, margin, cx, cy, rotate);
|
||||||
|
ctx.moveTo(sx, sy);
|
||||||
|
|
||||||
|
const increment = 1 / 60;
|
||||||
|
let angle = from;
|
||||||
|
while (angle <= to) {
|
||||||
|
const [x, y] = xyOnSpiral(angle, margin, cx, cy, rotate);
|
||||||
|
//ctx.lineWidth = angle*thickness;
|
||||||
|
ctx.globalAlpha = ((angle - from) / (to - from)) * toOpacity;
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
angle += increment;
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern.prototype.spiral = function (options = {}) {
|
||||||
|
const {
|
||||||
|
stretch = 1,
|
||||||
|
size = 80,
|
||||||
|
thickness = size / 2,
|
||||||
|
cap = 'butt', // round butt squar,
|
||||||
|
inset = 3, // start angl,
|
||||||
|
playheadColor = '#ffffff90',
|
||||||
|
playheadLength = 0.02,
|
||||||
|
playheadThickness = thickness,
|
||||||
|
padding = 0,
|
||||||
|
steady = 1,
|
||||||
|
inactiveColor = '#ffffff20',
|
||||||
|
colorizeInactive = 0,
|
||||||
|
fade = true,
|
||||||
|
// logSpiral = true,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
function spiral({ ctx, time, haps, drawTime }) {
|
||||||
|
ctx.clearRect(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight);
|
||||||
|
const [cx, cy] = [ctx.canvas.width / 2, ctx.canvas.height / 2];
|
||||||
|
const settings = {
|
||||||
|
margin: size / stretch,
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
stretch,
|
||||||
|
cap,
|
||||||
|
thickness,
|
||||||
|
};
|
||||||
|
|
||||||
|
const playhead = {
|
||||||
|
...settings,
|
||||||
|
thickness: playheadThickness,
|
||||||
|
from: inset - playheadLength,
|
||||||
|
to: inset,
|
||||||
|
color: playheadColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [min] = drawTime;
|
||||||
|
const rotate = steady * time;
|
||||||
|
haps.forEach((hap) => {
|
||||||
|
const isActive = hap.whole.begin <= time && hap.endClipped > time;
|
||||||
|
const from = hap.whole.begin - time + inset;
|
||||||
|
const to = hap.endClipped - time + inset - padding;
|
||||||
|
const { color } = hap.context;
|
||||||
|
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
|
||||||
|
spiralSegment({
|
||||||
|
ctx,
|
||||||
|
...settings,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
rotate,
|
||||||
|
color: colorizeInactive || isActive ? color : inactiveColor,
|
||||||
|
fromOpacity: opacity,
|
||||||
|
toOpacity: opacity,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
spiralSegment({
|
||||||
|
ctx,
|
||||||
|
...playhead,
|
||||||
|
rotate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
controls.test.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/controls.test.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import controls from '../controls.mjs';
|
||||||
|
import { mini } from '../../mini/mini.mjs';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('controls', () => {
|
||||||
|
it('should support controls', () => {
|
||||||
|
expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
|
||||||
|
});
|
||||||
|
it('should support compound controls', () => {
|
||||||
|
expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
|
||||||
|
expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
|
||||||
|
{ s: 'bd', n: 3 },
|
||||||
|
{ s: 'sd', n: 4, gain: 1.4 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('should support ignore extra elements in compound controls', () => {
|
||||||
|
expect(controls.s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
|
||||||
|
{ s: 'bd', n: 3, gain: 0.4 },
|
||||||
|
{ s: 'sd', n: 4, gain: 0.5 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,52 +5,42 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { fastcat, stack, slowcat, silence, pure } from '../pattern.mjs';
|
import { fastcat, stack, slowcat, silence, pure } from '../pattern.mjs';
|
||||||
import { strict as assert } from 'assert';
|
import { describe, it, expect } from 'vitest';
|
||||||
import drawLine from '../drawLine.mjs';
|
import drawLine from '../drawLine.mjs';
|
||||||
|
|
||||||
describe('drawLine', () => {
|
describe('drawLine', () => {
|
||||||
it('supports equal lengths', () => {
|
it('supports equal lengths', () => {
|
||||||
assert.equal(drawLine(fastcat(0), 4), '|0|0');
|
expect(drawLine(fastcat(0), 4)).toEqual('|0|0');
|
||||||
assert.equal(drawLine(fastcat(0, 1), 4), '|01|01');
|
expect(drawLine(fastcat(0, 1), 4)).toEqual('|01|01');
|
||||||
assert.equal(drawLine(fastcat(0, 1, 2), 6), '|012|012');
|
expect(drawLine(fastcat(0, 1, 2), 6)).toEqual('|012|012');
|
||||||
});
|
});
|
||||||
it('supports unequal lengths', () => {
|
it('supports unequal lengths', () => {
|
||||||
assert.equal(drawLine(fastcat(0, [1, 2]), 10), '|0-12|0-12');
|
expect(drawLine(fastcat(0, [1, 2]), 10)).toEqual('|0-12|0-12');
|
||||||
assert.equal(drawLine(fastcat(0, [1, 2, 3]), 10), '|0--123|0--123');
|
expect(drawLine(fastcat(0, [1, 2, 3]), 10)).toEqual('|0--123|0--123');
|
||||||
assert.equal(drawLine(fastcat(0, 1, [2, 3]), 10), '|0-1-23|0-1-23');
|
expect(drawLine(fastcat(0, 1, [2, 3]), 10)).toEqual('|0-1-23|0-1-23');
|
||||||
});
|
});
|
||||||
it('supports unequal silence', () => {
|
it('supports unequal silence', () => {
|
||||||
assert.equal(drawLine(fastcat(0, silence, [1, 2]), 10), '|0-..12|0-..12');
|
expect(drawLine(fastcat(0, silence, [1, 2]), 10)).toEqual('|0-..12|0-..12');
|
||||||
});
|
});
|
||||||
it('supports polyrhythms', () => {
|
it('supports polyrhythms', () => {
|
||||||
'0*2 1*3';
|
// assert.equal(drawLine(fastcat(pure(0).fast(2), pure(1).fast(3)), 10), '|0--0--1-1-1-');
|
||||||
assert.equal(drawLine(fastcat(pure(0).fast(2), pure(1).fast(3)), 10), '|0--0--1-1-1-');
|
expect(drawLine(fastcat(pure(0).fast(2), pure(1).fast(3)), 10)).toEqual('|0--0--1-1-1-');
|
||||||
});
|
});
|
||||||
it('supports multiple lines', () => {
|
it('supports multiple lines', () => {
|
||||||
assert.equal(
|
expect(drawLine(fastcat(0, stack(1, 2)), 10)).toEqual(`|01|01|01|01
|
||||||
drawLine(fastcat(0, stack(1, 2)), 10),
|
|.2|.2|.2|.2`);
|
||||||
`|01|01|01|01
|
|
||||||
|.2|.2|.2|.2`,
|
expect(drawLine(fastcat(0, 1, stack(2, 3)), 10)).toEqual(`|012|012|012
|
||||||
);
|
|..3|..3|..3`);
|
||||||
assert.equal(
|
|
||||||
drawLine(fastcat(0, 1, stack(2, 3)), 10),
|
expect(drawLine(fastcat(0, stack(1, 2, 3)), 10)).toEqual(`|01|01|01|01
|
||||||
`|012|012|012
|
|
||||||
|..3|..3|..3`,
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
drawLine(fastcat(0, stack(1, 2, 3)), 10),
|
|
||||||
`|01|01|01|01
|
|
||||||
|.2|.2|.2|.2
|
|.2|.2|.2|.2
|
||||||
|.3|.3|.3|.3`,
|
|.3|.3|.3|.3`);
|
||||||
);
|
expect(drawLine(fastcat(0, 1, stack(2, 3, 4)), 10)).toEqual(`|012|012|012
|
||||||
assert.equal(
|
|
||||||
drawLine(fastcat(0, 1, stack(2, 3, 4)), 10),
|
|
||||||
`|012|012|012
|
|
||||||
|..3|..3|..3
|
|..3|..3|..3
|
||||||
|..4|..4|..4`,
|
|..4|..4|..4`);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it('supports unequal cycle lengths', () => {
|
it('supports unequal cycle lengths', () => {
|
||||||
assert.equal(drawLine(slowcat(0, [1, 2]), 10), `|0|12|0|12`);
|
expect(drawLine(slowcat(0, [1, 2]), 10)).toEqual(`|0|12|0|12`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Fraction, { gcd } from '../fraction.mjs';
|
import Fraction, { gcd } from '../fraction.mjs';
|
||||||
import { strict as assert } from 'assert';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('gcd', () => {
|
describe('gcd', () => {
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
const F = Fraction._original;
|
const F = Fraction._original;
|
||||||
assert.equal(gcd(F(1 / 6), F(1 / 4)).toFraction(), '1/12');
|
expect(gcd(F(1 / 6), F(1 / 4)).toFraction()).toEqual('1/12');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+420
-315
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
|||||||
|
/*test for issue 302 support alternative solmization types */
|
||||||
|
import { sol2note } from '../util.mjs';
|
||||||
|
import { test } from 'vitest';
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
|
test('solmization - letters', () => {
|
||||||
|
const result = sol2note(60, 'letters');
|
||||||
|
const expected = 'C4';
|
||||||
|
assert.equal(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('solmization - solfeggio', () => {
|
||||||
|
const result = sol2note(60, 'solfeggio');
|
||||||
|
const expected = 'Do4';
|
||||||
|
assert.equal(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('solmization - indian', () => {
|
||||||
|
const result = sol2note(60, 'indian');
|
||||||
|
const expected = 'Sa4';
|
||||||
|
assert.equal(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('solmization - german', () => {
|
||||||
|
const result = sol2note(60, 'german');
|
||||||
|
const expected = 'C4';
|
||||||
|
assert.equal(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('solmization - byzantine', () => {
|
||||||
|
const result = sol2note(60, 'byzantine');
|
||||||
|
const expected = 'Ni4';
|
||||||
|
assert.equal(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('solmization - japanese', () => {
|
||||||
|
const result = sol2note(60, 'japanese');
|
||||||
|
const expected = 'I4';
|
||||||
|
assert.equal(result, expected);
|
||||||
|
});
|
||||||
@@ -1,120 +1,235 @@
|
|||||||
/*
|
/*
|
||||||
util.test.mjs - <short description TODO>
|
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://github.com/tidalcycles/strudel/blob/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/>.
|
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 { strict as assert } from 'assert';
|
|
||||||
import { pure } from '../pattern.mjs';
|
import { pure } from '../pattern.mjs';
|
||||||
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency } from '../util.mjs';
|
import {
|
||||||
|
isNote,
|
||||||
|
tokenizeNote,
|
||||||
|
noteToMidi,
|
||||||
|
midiToFreq,
|
||||||
|
freqToMidi,
|
||||||
|
_mod,
|
||||||
|
compose,
|
||||||
|
getFrequency,
|
||||||
|
getPlayableNoteValue,
|
||||||
|
parseNumeral,
|
||||||
|
parseFractional,
|
||||||
|
numeralArgs,
|
||||||
|
fractionalArgs,
|
||||||
|
} from '../util.mjs';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('isNote', () => {
|
describe('isNote', () => {
|
||||||
it('should recognize notes without accidentals', () => {
|
it('should recognize notes without accidentals', () => {
|
||||||
'C3 D3 E3 F3 G3 A3 B3 C4 D5 c5 d5 e5'.split(' ').forEach((note) => {
|
'C3 D3 E3 F3 G3 A3 B3 C4 D5 c5 d5 e5'.split(' ').forEach((note) => {
|
||||||
assert.equal(isNote(note), true);
|
expect(isNote(note)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should recognize notes with accidentals', () => {
|
it('should recognize notes with accidentals', () => {
|
||||||
'C#3 D##3 Eb3 Fbb3 Bb5'.split(' ').forEach((note) => {
|
'C#3 D##3 Eb3 Fbb3 Bb5'.split(' ').forEach((note) => {
|
||||||
assert.equal(isNote(note), true);
|
expect(isNote(note)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('should recognize notes without octave', () => {
|
||||||
|
expect(isNote('C')).toBe(true);
|
||||||
|
expect(isNote('Bb')).toBe(true);
|
||||||
|
});
|
||||||
it('should not recognize invalid notes', () => {
|
it('should not recognize invalid notes', () => {
|
||||||
assert.equal(isNote('H5'), false);
|
expect(isNote('H5')).toBe(false);
|
||||||
assert.equal(isNote('C'), false);
|
expect(isNote('X')).toBe(false);
|
||||||
assert.equal(isNote('X'), false);
|
expect(isNote(1)).toBe(false);
|
||||||
assert.equal(isNote(1), false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isNote', () => {
|
describe('isNote', () => {
|
||||||
it('should tokenize notes without accidentals', () => {
|
it('should tokenize notes without accidentals', () => {
|
||||||
assert.deepStrictEqual(tokenizeNote('C3'), ['C', '', 3]);
|
expect(tokenizeNote('C3')).toStrictEqual(['C', '', 3]);
|
||||||
assert.deepStrictEqual(tokenizeNote('D3'), ['D', '', 3]);
|
expect(tokenizeNote('D3')).toStrictEqual(['D', '', 3]);
|
||||||
assert.deepStrictEqual(tokenizeNote('E3'), ['E', '', 3]);
|
expect(tokenizeNote('E3')).toStrictEqual(['E', '', 3]);
|
||||||
assert.deepStrictEqual(tokenizeNote('F3'), ['F', '', 3]);
|
expect(tokenizeNote('F3')).toStrictEqual(['F', '', 3]);
|
||||||
assert.deepStrictEqual(tokenizeNote('G3'), ['G', '', 3]);
|
expect(tokenizeNote('G3')).toStrictEqual(['G', '', 3]);
|
||||||
assert.deepStrictEqual(tokenizeNote('A3'), ['A', '', 3]);
|
expect(tokenizeNote('A3')).toStrictEqual(['A', '', 3]);
|
||||||
assert.deepStrictEqual(tokenizeNote('B3'), ['B', '', 3]);
|
expect(tokenizeNote('B3')).toStrictEqual(['B', '', 3]);
|
||||||
assert.deepStrictEqual(tokenizeNote('C4'), ['C', '', 4]);
|
expect(tokenizeNote('C4')).toStrictEqual(['C', '', 4]);
|
||||||
assert.deepStrictEqual(tokenizeNote('D5'), ['D', '', 5]);
|
expect(tokenizeNote('D5')).toStrictEqual(['D', '', 5]);
|
||||||
});
|
});
|
||||||
it('should tokenize notes with accidentals', () => {
|
it('should tokenize notes with accidentals', () => {
|
||||||
assert.deepStrictEqual(tokenizeNote('C#3'), ['C', '#', 3]);
|
expect(tokenizeNote('C#3')).toStrictEqual(['C', '#', 3]);
|
||||||
assert.deepStrictEqual(tokenizeNote('D##3'), ['D', '##', 3]);
|
expect(tokenizeNote('D##3')).toStrictEqual(['D', '##', 3]);
|
||||||
assert.deepStrictEqual(tokenizeNote('Eb3'), ['E', 'b', 3]);
|
expect(tokenizeNote('Eb3')).toStrictEqual(['E', 'b', 3]);
|
||||||
assert.deepStrictEqual(tokenizeNote('Fbb3'), ['F', 'bb', 3]);
|
expect(tokenizeNote('Fbb3')).toStrictEqual(['F', 'bb', 3]);
|
||||||
assert.deepStrictEqual(tokenizeNote('Bb5'), ['B', 'b', 5]);
|
expect(tokenizeNote('Bb5')).toStrictEqual(['B', 'b', 5]);
|
||||||
});
|
});
|
||||||
it('should tokenize notes without octave', () => {
|
it('should tokenize notes without octave', () => {
|
||||||
assert.deepStrictEqual(tokenizeNote('C'), ['C', '', undefined]);
|
expect(tokenizeNote('C')).toStrictEqual(['C', '', undefined]);
|
||||||
assert.deepStrictEqual(tokenizeNote('C#'), ['C', '#', undefined]);
|
expect(tokenizeNote('C#')).toStrictEqual(['C', '#', undefined]);
|
||||||
assert.deepStrictEqual(tokenizeNote('Bb'), ['B', 'b', undefined]);
|
expect(tokenizeNote('Bb')).toStrictEqual(['B', 'b', undefined]);
|
||||||
assert.deepStrictEqual(tokenizeNote('Bbb'), ['B', 'bb', undefined]);
|
expect(tokenizeNote('Bbb')).toStrictEqual(['B', 'bb', undefined]);
|
||||||
});
|
});
|
||||||
it('should not tokenize invalid notes', () => {
|
it('should not tokenize invalid notes', () => {
|
||||||
assert.deepStrictEqual(tokenizeNote('X'), []);
|
expect(tokenizeNote('X')).toStrictEqual([]);
|
||||||
assert.deepStrictEqual(tokenizeNote('asfasf'), []);
|
expect(tokenizeNote('asfasf')).toStrictEqual([]);
|
||||||
assert.deepStrictEqual(tokenizeNote(123), []);
|
expect(tokenizeNote(123)).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('toMidi', () => {
|
describe('noteToMidi', () => {
|
||||||
it('should turn notes into midi', () => {
|
it('should turn notes into midi', () => {
|
||||||
assert.equal(toMidi('A4'), 69);
|
expect(noteToMidi('A4')).toEqual(69);
|
||||||
assert.equal(toMidi('C4'), 60);
|
expect(noteToMidi('C4')).toEqual(60);
|
||||||
assert.equal(toMidi('Db4'), 61);
|
expect(noteToMidi('Db4')).toEqual(61);
|
||||||
assert.equal(toMidi('C3'), 48);
|
expect(noteToMidi('C3')).toEqual(48);
|
||||||
assert.equal(toMidi('Cb3'), 47);
|
expect(noteToMidi('Cb3')).toEqual(47);
|
||||||
assert.equal(toMidi('Cbb3'), 46);
|
expect(noteToMidi('Cbb3')).toEqual(46);
|
||||||
assert.equal(toMidi('C#3'), 49);
|
expect(noteToMidi('C#3')).toEqual(49);
|
||||||
assert.equal(toMidi('C#3'), 49);
|
expect(noteToMidi('C#3')).toEqual(49);
|
||||||
assert.equal(toMidi('C##3'), 50);
|
expect(noteToMidi('C##3')).toEqual(50);
|
||||||
|
});
|
||||||
|
it('should throw an error when given a non-note', () => {
|
||||||
|
expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||||
|
expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('fromMidi', () => {
|
describe('midiToFreq', () => {
|
||||||
it('should turn midi into frequency', () => {
|
it('should turn midi into frequency', () => {
|
||||||
assert.equal(fromMidi(69), 440);
|
expect(midiToFreq(69)).toEqual(440);
|
||||||
assert.equal(fromMidi(57), 220);
|
expect(midiToFreq(57)).toEqual(220);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('freqToMidi', () => {
|
||||||
|
it('should turn frequency into midi', () => {
|
||||||
|
expect(freqToMidi(440)).toEqual(69);
|
||||||
|
expect(freqToMidi(220)).toEqual(57);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('getFrequency', () => {
|
describe('getFrequency', () => {
|
||||||
|
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||||
|
it('should turn note into frequency', () => {
|
||||||
|
expect(getFrequency(happify('a4'))).toEqual(440);
|
||||||
|
expect(getFrequency(happify('a3'))).toEqual(220);
|
||||||
|
});
|
||||||
it('should turn midi into frequency', () => {
|
it('should turn midi into frequency', () => {
|
||||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
expect(getFrequency(happify(69, { type: 'midi' }))).toEqual(440);
|
||||||
assert.equal(getFrequency(happify('a4')), 440);
|
expect(getFrequency(happify(57, { type: 'midi' }))).toEqual(220);
|
||||||
assert.equal(getFrequency(happify('a3')), 220);
|
});
|
||||||
assert.equal(getFrequency(happify(440, { type: 'frequency' })), 440); // TODO: migrate when values are objects..
|
it('should return frequencies unchanged', () => {
|
||||||
assert.equal(getFrequency(happify(432, { type: 'frequency' })), 432);
|
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440);
|
||||||
|
expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432);
|
||||||
|
});
|
||||||
|
it('should turn object with a "freq" property into frequency', () => {
|
||||||
|
expect(getFrequency(happify({ freq: 220 }))).toEqual(220);
|
||||||
|
expect(getFrequency(happify({ freq: 440 }))).toEqual(440);
|
||||||
|
});
|
||||||
|
it('should throw an error when given a non-note', () => {
|
||||||
|
expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`);
|
||||||
|
expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mod', () => {
|
describe('_mod', () => {
|
||||||
it('should work like regular modulo with positive numbers', () => {
|
it('should work like regular modulo with positive numbers', () => {
|
||||||
assert.equal(mod(0, 3), 0);
|
expect(_mod(0, 3)).toEqual(0);
|
||||||
assert.equal(mod(1, 3), 1);
|
expect(_mod(1, 3)).toEqual(1);
|
||||||
assert.equal(mod(2, 3), 2);
|
expect(_mod(2, 3)).toEqual(2);
|
||||||
assert.equal(mod(3, 3), 0);
|
expect(_mod(3, 3)).toEqual(0);
|
||||||
assert.equal(mod(4, 3), 1);
|
expect(_mod(4, 3)).toEqual(1);
|
||||||
assert.equal(mod(4, 2), 0);
|
expect(_mod(4, 2)).toEqual(0);
|
||||||
});
|
});
|
||||||
it('should work with negative numbers', () => {
|
it('should work with negative numbers', () => {
|
||||||
assert.equal(mod(-1, 3), 2);
|
expect(_mod(-1, 3)).toEqual(2);
|
||||||
assert.equal(mod(-2, 3), 1);
|
expect(_mod(-2, 3)).toEqual(1);
|
||||||
assert.equal(mod(-3, 3), 0);
|
expect(_mod(-3, 3)).toEqual(0);
|
||||||
assert.equal(mod(-4, 3), 2);
|
expect(_mod(-4, 3)).toEqual(2);
|
||||||
assert.equal(mod(-5, 3), 1);
|
expect(_mod(-5, 3)).toEqual(1);
|
||||||
assert.equal(mod(-3, 2), 1);
|
expect(_mod(-3, 2)).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('compose', () => {
|
describe('compose', () => {
|
||||||
const add1 = (a) => a + 1;
|
const add1 = (a) => a + 1;
|
||||||
it('should compose', () => {
|
it('should compose', () => {
|
||||||
assert.equal(compose(add1, add1)(0), 2);
|
expect(compose(add1, add1)(0)).toEqual(2);
|
||||||
assert.equal(compose(add1)(0), 1);
|
expect(compose(add1)(0)).toEqual(1);
|
||||||
});
|
});
|
||||||
const addS = (s) => (a) => a + s;
|
const addS = (s) => (a) => a + s;
|
||||||
it('should compose left to right', () => {
|
it('should compose left to right', () => {
|
||||||
assert.equal(compose(addS('a'), addS('b'))(''), 'ab');
|
expect(compose(addS('a'), addS('b'))('')).toEqual('ab');
|
||||||
assert.equal(compose(addS('a'), addS('b'))('x'), 'xab');
|
expect(compose(addS('a'), addS('b'))('x')).toEqual('xab');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getPlayableNoteValue', () => {
|
||||||
|
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||||
|
it('should return object "note" property', () => {
|
||||||
|
expect(getPlayableNoteValue(happify({ note: 'a4' }))).toEqual('a4');
|
||||||
|
});
|
||||||
|
it('should return object "n" property', () => {
|
||||||
|
expect(getPlayableNoteValue(happify({ n: 'a4' }))).toEqual('a4');
|
||||||
|
});
|
||||||
|
it('should return object "value" property', () => {
|
||||||
|
expect(getPlayableNoteValue(happify({ value: 'a4' }))).toEqual('a4');
|
||||||
|
});
|
||||||
|
it('should turn midi into frequency', () => {
|
||||||
|
expect(getPlayableNoteValue(happify(57, { type: 'midi' }))).toEqual(220);
|
||||||
|
});
|
||||||
|
it('should return frequency value', () => {
|
||||||
|
expect(getPlayableNoteValue(happify(220, { type: 'frequency' }))).toEqual(220);
|
||||||
|
});
|
||||||
|
it('should throw an error if value is not an object, number, or string', () => {
|
||||||
|
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`);
|
||||||
|
expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseNumeral', () => {
|
||||||
|
it('should parse numbers as is', () => {
|
||||||
|
expect(parseNumeral(4)).toBe(4);
|
||||||
|
expect(parseNumeral(0)).toBe(0);
|
||||||
|
expect(parseNumeral(20)).toBe(20);
|
||||||
|
expect(parseNumeral('20')).toBe(20);
|
||||||
|
expect(parseNumeral(1.5)).toBe(1.5);
|
||||||
|
});
|
||||||
|
it('should parse notes', () => {
|
||||||
|
expect(parseNumeral('c')).toBe(48);
|
||||||
|
expect(parseNumeral('c4')).toBe(60);
|
||||||
|
expect(parseNumeral('c#4')).toBe(61);
|
||||||
|
expect(parseNumeral('db4')).toBe(61);
|
||||||
|
});
|
||||||
|
it('should throw an error for unknown strings', () => {
|
||||||
|
expect(() => parseNumeral('xyz')).toThrowError('cannot parse as numeral: "xyz"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseFractional', () => {
|
||||||
|
it('should parse numbers as is', () => {
|
||||||
|
expect(parseFractional(4)).toBe(4);
|
||||||
|
expect(parseFractional(0)).toBe(0);
|
||||||
|
expect(parseFractional(20)).toBe(20);
|
||||||
|
expect(parseFractional('20')).toBe(20);
|
||||||
|
expect(parseFractional(1.5)).toBe(1.5);
|
||||||
|
});
|
||||||
|
it('should parse fractional shorthands values', () => {
|
||||||
|
expect(parseFractional('w')).toBe(1);
|
||||||
|
expect(parseFractional('h')).toBe(0.5);
|
||||||
|
expect(parseFractional('q')).toBe(0.25);
|
||||||
|
expect(parseFractional('e')).toBe(0.125);
|
||||||
|
});
|
||||||
|
it('should throw an error for unknown strings', () => {
|
||||||
|
expect(() => parseFractional('xyz')).toThrowError('cannot parse as fractional: "xyz"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('numeralArgs', () => {
|
||||||
|
it('should convert function arguments to numbers', () => {
|
||||||
|
const add = numeralArgs((a, b) => a + b);
|
||||||
|
expect(add('c4', 2)).toBe(62);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('fractionalArgs', () => {
|
||||||
|
it('should convert function arguments to numbers', () => {
|
||||||
|
const add = fractionalArgs((a, b) => a + b);
|
||||||
|
expect(add('q', 2)).toBe(2.25);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,21 +4,27 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
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 { strict as assert } from 'assert';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { map, valued, mul } from '../value.mjs';
|
import { map, valued, mul } from '../value.mjs';
|
||||||
|
import controls from '../controls.mjs';
|
||||||
|
const { n } = controls;
|
||||||
|
|
||||||
describe('Value', () => {
|
describe('Value', () => {
|
||||||
it('unionWith', () => {
|
it('unionWith', () => {
|
||||||
const { value } = valued({ freq: 2000, distortion: 1.2 }).unionWith({ distortion: 2 }, mul);
|
const { value } = valued({ freq: 2000, distortion: 1.2 }).unionWith({ distortion: 2 }, mul);
|
||||||
assert.deepStrictEqual(value, { freq: 2000, distortion: 2.4 });
|
expect(value).toStrictEqual({ freq: 2000, distortion: 2.4 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('experiments', () => {
|
it('experiments', () => {
|
||||||
assert.equal(map(mul(5), valued(3)).value, 15);
|
expect(map(mul(5), valued(3)).value).toEqual(15);
|
||||||
assert.equal(map(mul(null), valued(3)).value, 0);
|
expect(map(mul(null), valued(3)).value).toEqual(0);
|
||||||
assert.equal(map(mul(3), valued(null)).value, null);
|
expect(map(mul(3), valued(null)).value).toEqual(null);
|
||||||
assert.equal(valued(3).map(mul).ap(3).value, 9);
|
expect(valued(3).map(mul).ap(3).value).toEqual(9);
|
||||||
assert.equal(valued(mul).ap(3).ap(3).value, 9);
|
expect(valued(mul).ap(3).ap(3).value).toEqual(9);
|
||||||
assert.equal(valued(3).mul(3).value, 9);
|
expect(valued(3).mul(3).value).toEqual(9);
|
||||||
|
});
|
||||||
|
it('union bare numbers for numeral props', () => {
|
||||||
|
expect(n(3).cutoff(500).add(10).firstCycleValues).toEqual([{ n: 13, cutoff: 510 }]);
|
||||||
|
expect(n(3).cutoff(500).mul(2).firstCycleValues).toEqual([{ n: 6, cutoff: 1000 }]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
let time;
|
||||||
|
export function getTime() {
|
||||||
|
if (!time) {
|
||||||
|
throw new Error('no time set! use setTime to define a time source');
|
||||||
|
}
|
||||||
|
return time();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTime(func) {
|
||||||
|
time = func;
|
||||||
|
}
|
||||||
@@ -18,6 +18,11 @@ export class TimeSpan {
|
|||||||
const end = this.end;
|
const end = this.end;
|
||||||
const end_sam = end.sam();
|
const end_sam = end.sam();
|
||||||
|
|
||||||
|
// Support zero-width timespans
|
||||||
|
if (begin.equals(end)) {
|
||||||
|
return [new TimeSpan(begin, end)];
|
||||||
|
}
|
||||||
|
|
||||||
while (end.gt(begin)) {
|
while (end.gt(begin)) {
|
||||||
// If begin and end are in the same cycle, we're done.
|
// If begin and end are in the same cycle, we're done.
|
||||||
if (begin.sam().equals(end_sam)) {
|
if (begin.sam().equals(end_sam)) {
|
||||||
@@ -105,7 +110,7 @@ export class TimeSpan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
return this.begin.show() + ' -> ' + this.end.show();
|
return this.begin.show() + ' → ' + this.end.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,14 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Tone } from './tone.mjs';
|
import { getTime } from './time.mjs';
|
||||||
|
|
||||||
export const hideHeader = () => {
|
|
||||||
document.getElementById('header').style = 'display:none';
|
|
||||||
};
|
|
||||||
|
|
||||||
function frame(callback) {
|
function frame(callback) {
|
||||||
if (window.strudelAnimation) {
|
if (window.strudelAnimation) {
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
}
|
}
|
||||||
const animate = (animationTime) => {
|
const animate = (animationTime) => {
|
||||||
const toneTime = Tone.getTransport().seconds;
|
callback(animationTime, getTime());
|
||||||
callback(animationTime, toneTime);
|
|
||||||
window.strudelAnimation = requestAnimationFrame(animate);
|
window.strudelAnimation = requestAnimationFrame(animate);
|
||||||
};
|
};
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
@@ -31,7 +26,7 @@ export const backgroundImage = function (src, animateOptions = {}) {
|
|||||||
({
|
({
|
||||||
style: () => (container.style = bg + ';' + value),
|
style: () => (container.style = bg + ';' + value),
|
||||||
className: () => (container.className = value + ' ' + initialClassName),
|
className: () => (container.className = value + ' ' + initialClassName),
|
||||||
}[option]());
|
})[option]();
|
||||||
};
|
};
|
||||||
const funcOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'function');
|
const funcOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'function');
|
||||||
const stringOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'string');
|
const stringOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'string');
|
||||||
@@ -51,6 +46,7 @@ export const cleanupUi = () => {
|
|||||||
const container = document.getElementById('code');
|
const container = document.getElementById('code');
|
||||||
if (container) {
|
if (container) {
|
||||||
container.style = '';
|
container.style = '';
|
||||||
container.className = 'grow relative'; // has to match App.tsx
|
// TODO: find a way to remove that duplication..
|
||||||
|
container.className = 'grow flex text-gray-100 relative overflow-auto cursor-text pb-0'; // has to match App.tsx
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
+185
-20
@@ -5,12 +5,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// returns true if the given string is a note
|
// returns true if the given string is a note
|
||||||
export const isNote = (name) => /^[a-gA-G][#b]*[0-9]$/.test(name);
|
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 tokenizeNote = (note) => {
|
export const tokenizeNote = (note) => {
|
||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#b]*)([0-9])?$/)?.slice(1) || [];
|
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9])?$/)?.slice(1) || [];
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -18,30 +19,84 @@ export const tokenizeNote = (note) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// turns the given note into its midi number representation
|
// turns the given note into its midi number representation
|
||||||
export const toMidi = (note) => {
|
export const noteToMidi = (note) => {
|
||||||
const [pc, acc, oct] = tokenizeNote(note);
|
const [pc, acc, oct = 3] = tokenizeNote(note);
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
throw new Error('not a note: "' + note + '"');
|
throw new Error('not a note: "' + note + '"');
|
||||||
}
|
}
|
||||||
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
|
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
|
||||||
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1 }[char], 0) || 0;
|
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1, f: -1 }[char], 0) || 0;
|
||||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
export const fromMidi = (n) => {
|
export const midiToFreq = (n) => {
|
||||||
return Math.pow(2, (n - 69) / 12) * 440;
|
return Math.pow(2, (n - 69) / 12) * 440;
|
||||||
};
|
};
|
||||||
|
|
||||||
// modulo that works with negative numbers e.g. mod(-1, 3) = 2
|
export const freqToMidi = (freq) => {
|
||||||
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
|
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
||||||
export const mod = (n, m) => ((n % m) + m) % m;
|
};
|
||||||
|
|
||||||
|
export const valueToMidi = (value, fallbackValue) => {
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
throw new Error('valueToMidi: expected object value');
|
||||||
|
}
|
||||||
|
let { freq, note } = value;
|
||||||
|
if (typeof freq === 'number') {
|
||||||
|
return freqToMidi(freq);
|
||||||
|
}
|
||||||
|
if (typeof note === 'string') {
|
||||||
|
return noteToMidi(note);
|
||||||
|
}
|
||||||
|
if (typeof note === 'number') {
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
if (!fallbackValue) {
|
||||||
|
throw new Error('valueToMidi: expected freq or note to be set');
|
||||||
|
}
|
||||||
|
return fallbackValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||||
|
* @noAutocomplete
|
||||||
|
*/
|
||||||
|
export const getFreq = (noteOrMidi) => {
|
||||||
|
if (typeof noteOrMidi === 'number') {
|
||||||
|
return midiToFreq(noteOrMidi);
|
||||||
|
}
|
||||||
|
return midiToFreq(noteToMidi(noteOrMidi));
|
||||||
|
};
|
||||||
|
|
||||||
|
const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
||||||
|
/**
|
||||||
|
* @deprecated only used in workshop (first-notes)
|
||||||
|
* @noAutocomplete
|
||||||
|
*/
|
||||||
|
export const midi2note = (n) => {
|
||||||
|
const oct = Math.floor(n / 12) - 1;
|
||||||
|
const pc = pcs[n % 12];
|
||||||
|
return pc + oct;
|
||||||
|
};
|
||||||
|
|
||||||
|
// modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does)
|
||||||
|
export const _mod = (n, m) => ((n % m) + m) % m;
|
||||||
|
|
||||||
export const getPlayableNoteValue = (hap) => {
|
export const getPlayableNoteValue = (hap) => {
|
||||||
let { value: note, context } = hap;
|
let { value, context } = hap;
|
||||||
|
let note = value;
|
||||||
|
if (typeof note === 'object' && !Array.isArray(note)) {
|
||||||
|
note = note.note || note.n || note.value;
|
||||||
|
if (note === undefined) {
|
||||||
|
throw new Error(`cannot find a playable note for ${JSON.stringify(value)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||||
note = fromMidi(hap.value);
|
note = midiToFreq(hap.value);
|
||||||
} else if (typeof note === 'string' && !isNote(note)) {
|
} else if (typeof note === 'number' && context.type === 'frequency') {
|
||||||
throw new Error('not a note: ' + note);
|
note = hap.value; // legacy workaround.. will be removed in the future
|
||||||
|
} else if (typeof note !== 'string' || !isNote(note)) {
|
||||||
|
throw new Error('not a note: ' + JSON.stringify(note));
|
||||||
}
|
}
|
||||||
return note;
|
return note;
|
||||||
};
|
};
|
||||||
@@ -49,15 +104,18 @@ export const getPlayableNoteValue = (hap) => {
|
|||||||
export const getFrequency = (hap) => {
|
export const getFrequency = (hap) => {
|
||||||
let { value, context } = hap;
|
let { value, context } = hap;
|
||||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||||
if (typeof value === 'object' && value.freq) {
|
if (typeof value === 'object') {
|
||||||
return value.freq;
|
if (value.freq) {
|
||||||
|
return value.freq;
|
||||||
|
}
|
||||||
|
return getFreq(value.note || value.n || value.value);
|
||||||
}
|
}
|
||||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||||
value = fromMidi(hap.value);
|
value = midiToFreq(hap.value);
|
||||||
} else if (typeof value === 'string' && isNote(value)) {
|
} else if (typeof value === 'string' && isNote(value)) {
|
||||||
value = fromMidi(toMidi(hap.value));
|
value = midiToFreq(noteToMidi(hap.value));
|
||||||
} else if (typeof value !== 'number') {
|
} else if (typeof value !== 'number') {
|
||||||
throw new Error('not a note or frequency:' + value);
|
throw new Error('not a note or frequency: ' + value);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
@@ -86,9 +144,9 @@ export const constant = (a, b) => a;
|
|||||||
|
|
||||||
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
||||||
|
|
||||||
export function curry(func, overload) {
|
export function curry(func, overload, arity = func.length) {
|
||||||
const fn = function curried(...args) {
|
const fn = function curried(...args) {
|
||||||
if (args.length >= func.length) {
|
if (args.length >= arity) {
|
||||||
return func.apply(this, args);
|
return func.apply(this, args);
|
||||||
} else {
|
} else {
|
||||||
const partial = function (...args2) {
|
const partial = function (...args2) {
|
||||||
@@ -106,3 +164,110 @@ export function curry(func, overload) {
|
|||||||
}
|
}
|
||||||
return fn;
|
return fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseNumeral(numOrString) {
|
||||||
|
const asNumber = Number(numOrString);
|
||||||
|
if (!isNaN(asNumber)) {
|
||||||
|
return asNumber;
|
||||||
|
}
|
||||||
|
if (isNote(numOrString)) {
|
||||||
|
return noteToMidi(numOrString);
|
||||||
|
}
|
||||||
|
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapArgs(fn, mapFn) {
|
||||||
|
return (...args) => fn(...args.map(mapFn));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function numeralArgs(fn) {
|
||||||
|
return mapArgs(fn, parseNumeral);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseFractional(numOrString) {
|
||||||
|
const asNumber = Number(numOrString);
|
||||||
|
if (!isNaN(asNumber)) {
|
||||||
|
return asNumber;
|
||||||
|
}
|
||||||
|
const specialValue = {
|
||||||
|
pi: Math.PI,
|
||||||
|
w: 1,
|
||||||
|
h: 0.5,
|
||||||
|
q: 0.25,
|
||||||
|
e: 0.125,
|
||||||
|
s: 0.0625,
|
||||||
|
t: 1 / 3,
|
||||||
|
f: 0.2,
|
||||||
|
x: 1 / 6,
|
||||||
|
}[numOrString];
|
||||||
|
if (typeof specialValue !== 'undefined') {
|
||||||
|
return specialValue;
|
||||||
|
}
|
||||||
|
throw new Error(`cannot parse as fractional: "${numOrString}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
|
||||||
|
|
||||||
|
export const splitAt = function (index, value) {
|
||||||
|
return [value.slice(0, index), value.slice(index)];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||||
|
|
||||||
|
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||||
|
|
||||||
|
/* solmization, not used yet */
|
||||||
|
const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/
|
||||||
|
const indian = [
|
||||||
|
'Sa',
|
||||||
|
'Re',
|
||||||
|
'Ga',
|
||||||
|
'Ma',
|
||||||
|
'Pa',
|
||||||
|
'Dha',
|
||||||
|
'Ni',
|
||||||
|
]; /*indian musical notes, seems like they do not use flats or sharps*/
|
||||||
|
const german = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Hb', 'H']; /*german & dutch musical notes*/
|
||||||
|
const byzantine = [
|
||||||
|
'Ni',
|
||||||
|
'Pab',
|
||||||
|
'Pa',
|
||||||
|
'Voub',
|
||||||
|
'Vou',
|
||||||
|
'Ga',
|
||||||
|
'Dib',
|
||||||
|
'Di',
|
||||||
|
'Keb',
|
||||||
|
'Ke',
|
||||||
|
'Zob',
|
||||||
|
'Zo',
|
||||||
|
]; /*byzantine musical notes*/
|
||||||
|
const japanese = [
|
||||||
|
'I',
|
||||||
|
'Ro',
|
||||||
|
'Ha',
|
||||||
|
'Ni',
|
||||||
|
'Ho',
|
||||||
|
'He',
|
||||||
|
'To',
|
||||||
|
]; /*traditional japanese musical notes, seems like they do not use falts or sharps*/
|
||||||
|
|
||||||
|
const english = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
||||||
|
|
||||||
|
export const sol2note = (n, notation = 'letters') => {
|
||||||
|
const pc =
|
||||||
|
notation === 'solfeggio'
|
||||||
|
? solfeggio /*check if its is any of the following*/
|
||||||
|
: notation === 'indian'
|
||||||
|
? indian
|
||||||
|
: notation === 'german'
|
||||||
|
? german
|
||||||
|
: notation === 'byzantine'
|
||||||
|
? byzantine
|
||||||
|
: notation === 'japanese'
|
||||||
|
? japanese
|
||||||
|
: english; /*if not use standard version*/
|
||||||
|
const note = pc[n % 12]; /*calculating the midi value to the note*/
|
||||||
|
const oct = Math.floor(n / 12) - 1;
|
||||||
|
return note + oct;
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import { curry } from './util.mjs';
|
import { curry } from './util.mjs';
|
||||||
|
|
||||||
export function unionWithObj(a, b, func) {
|
export function unionWithObj(a, b, func) {
|
||||||
|
if (typeof b?.value === 'number') {
|
||||||
|
// https://github.com/tidalcycles/strudel/issues/262
|
||||||
|
const numKeys = Object.keys(a).filter((k) => typeof a[k] === 'number');
|
||||||
|
const numerals = Object.fromEntries(numKeys.map((k) => [k, b.value]));
|
||||||
|
b = Object.assign(b, numerals);
|
||||||
|
delete b.value;
|
||||||
|
}
|
||||||
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
|
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
|
||||||
return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])])));
|
return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])])));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { dependencies } from './package.json';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'index.mjs'),
|
||||||
|
formats: ['es', 'cjs'],
|
||||||
|
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [...Object.keys(dependencies)],
|
||||||
|
},
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// will move to https://github.com/felixroos/zyklus
|
||||||
|
// TODO: started flag
|
||||||
|
|
||||||
|
function createClock(
|
||||||
|
getTime,
|
||||||
|
callback, // called slightly before each cycle
|
||||||
|
duration = 0.05, // duration of each cycle
|
||||||
|
interval = 0.1, // interval between callbacks
|
||||||
|
overlap = 0.1, // overlap between callbacks
|
||||||
|
) {
|
||||||
|
let tick = 0; // counts callbacks
|
||||||
|
let phase = 0; // next callback time
|
||||||
|
let precision = 10 ** 4; // used to round phase
|
||||||
|
let minLatency = 0.01;
|
||||||
|
const setDuration = (setter) => (duration = setter(duration));
|
||||||
|
overlap = overlap || interval / 2;
|
||||||
|
const onTick = () => {
|
||||||
|
const t = getTime();
|
||||||
|
const lookahead = t + interval + overlap; // the time window for this tick
|
||||||
|
if (phase === 0) {
|
||||||
|
phase = t + minLatency;
|
||||||
|
}
|
||||||
|
// callback as long as we're inside the lookahead
|
||||||
|
while (phase < lookahead) {
|
||||||
|
phase = Math.round(phase * precision) / precision;
|
||||||
|
phase >= t && callback(phase, duration, tick);
|
||||||
|
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
|
||||||
|
phase += duration; // increment phase by duration
|
||||||
|
tick++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let intervalID;
|
||||||
|
const start = () => {
|
||||||
|
clear(); // just in case start was called more than once
|
||||||
|
onTick();
|
||||||
|
intervalID = setInterval(onTick, interval * 1000);
|
||||||
|
};
|
||||||
|
const clear = () => intervalID !== undefined && clearInterval(intervalID);
|
||||||
|
const pause = () => clear();
|
||||||
|
const stop = () => {
|
||||||
|
tick = 0;
|
||||||
|
phase = 0;
|
||||||
|
clear();
|
||||||
|
};
|
||||||
|
const getPhase = () => phase;
|
||||||
|
// setCallback
|
||||||
|
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
|
||||||
|
}
|
||||||
|
export default createClock;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user