mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-18 16:46:10 -04:00
Compare commits
1231 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35b9c82bdd | |||
| 0fcfc308fd | |||
| 931016143c | |||
| 8e2d883e86 | |||
| 8db443ba26 | |||
| 34e9d73908 | |||
| deed379dba | |||
| 5b4322c964 | |||
| 91bb0a1d36 | |||
| 47b65846e9 | |||
| 55eafaf4d8 | |||
| 2e3869c0e0 | |||
| 4015a11bd2 | |||
| 15f16006d0 | |||
| ce79a44848 | |||
| d253e35b6c | |||
| 19ef941c9d | |||
| ac7658d0a0 | |||
| 635a6c62b0 | |||
| acc100da5c | |||
| 69f8498d4b | |||
| 975eb684b9 | |||
| 5c4ad57309 | |||
| 0be5f77247 | |||
| bbf8577f85 | |||
| d8eb186ee4 | |||
| 54829bd28c | |||
| 5c01760b28 | |||
| a189626e8b | |||
| 6a1fa7e67f | |||
| 267f58b7be | |||
| 9348a8015a | |||
| 57ad278137 | |||
| 5d8eea7299 | |||
| 0a3694fb82 | |||
| a194180130 | |||
| 64cc8caeec | |||
| f427b56062 | |||
| 93d151b943 | |||
| e0c4997f1e | |||
| 07791001dd | |||
| 936d6fdafb | |||
| 81b33afcdb | |||
| 0190604a7f | |||
| 8494353ace | |||
| 971d497e81 | |||
| ca36a31f79 | |||
| 559f31792f | |||
| 83eedcb2f5 | |||
| a3bd04732c | |||
| e8ed57f588 | |||
| ebeefc0ac4 | |||
| 3d06528393 | |||
| 558e9e36ff | |||
| 0a6373f64c | |||
| 0f24e97855 | |||
| cbfa09a2da | |||
| 3805195109 | |||
| 183d3ea4a1 | |||
| 8e4549d6d3 | |||
| 506989b0d7 | |||
| 8f475515e9 | |||
| df23ea6cdf | |||
| bea25a28cb | |||
| 97d292adbb | |||
| e242e82b3f | |||
| 6479c60387 | |||
| c7f811e14d | |||
| 539160630d | |||
| 8a3e9171a4 | |||
| 010329dcfc | |||
| 16851cf8f8 | |||
| 8f06bc4b57 | |||
| 7839d6b4bf | |||
| eb3e6d60b9 | |||
| fd08b25388 | |||
| ff923a6b6e | |||
| 30a15cb4b7 | |||
| 7d8cb59a7a | |||
| 1aca5db447 | |||
| 2fbd9a3d54 | |||
| 934a3cdec4 | |||
| f987633498 | |||
| 1a209c8b82 | |||
| 3e66e1ee93 | |||
| e39cd2c9cd | |||
| 8a8f6a66a4 | |||
| b6171600c2 | |||
| 3a60f63fd2 | |||
| 422a9ad525 | |||
| 29fe4fd907 | |||
| 22774596ca | |||
| 8f355dcf96 | |||
| abe775c3fd | |||
| 95e0496d51 | |||
| bd9a80827e | |||
| 39204ad94d | |||
| 1e8a3c2918 | |||
| 1af55c0d19 | |||
| a1e07f3079 | |||
| 4e7f5e9cda | |||
| 12d8cdff92 | |||
| 9c66d9f1a7 | |||
| e2b4f6f5ac | |||
| 50ed466895 | |||
| 126556634a | |||
| aa25265cc2 | |||
| 73558eb49c | |||
| cc49e0f78e | |||
| 6c07feb312 | |||
| a8c2586da6 | |||
| c400deac74 | |||
| 6a2486d9f2 | |||
| b5720355f9 | |||
| ab015ff48a | |||
| eb8ac29136 | |||
| 14b1a1203f | |||
| 060d2cfe2b | |||
| 04a0952a22 | |||
| 1a0dff4081 | |||
| 2fd2bdba60 | |||
| 2e8ae38d33 | |||
| bbfdd2cc32 | |||
| 4086df95b7 | |||
| aba4e72d29 | |||
| 8b96e3a6d1 | |||
| 731d545a3f | |||
| e6eb6c19a8 | |||
| af35a1af09 | |||
| fc4c70c94f | |||
| 064bff6e40 | |||
| 69211b8701 | |||
| 33f4f5fccc | |||
| af740c3abb | |||
| 8e2407cfa9 | |||
| 86c945114d | |||
| bd16b403e3 | |||
| f00a08ef9e | |||
| 31067d5cb5 | |||
| bc917dcf8b | |||
| 619ca5385d | |||
| aa36d25b9a | |||
| fe1fc81f42 | |||
| 34373a9d73 | |||
| 16210e121f | |||
| 53af147ede | |||
| fc32981886 | |||
| aede6e4b26 | |||
| ac36423f85 | |||
| 4071c1af18 | |||
| 49e381f258 | |||
| 21f5d639a3 | |||
| 0eccbae048 | |||
| a629c5c931 | |||
| f83a156317 | |||
| 2857e816d2 | |||
| c2ef90d67c | |||
| 2cc92a2b93 | |||
| 13d56b6b28 | |||
| d036635915 | |||
| 5537652497 | |||
| f1d491d52e | |||
| d299e76c82 | |||
| 52d3ee0f85 | |||
| 4f0f388ca2 | |||
| df03e9a379 | |||
| 52a311aae6 | |||
| 6365e1626c | |||
| 8d96a071d6 | |||
| e8f6124a07 | |||
| 14a5e7dcb6 | |||
| 7889017434 | |||
| ba8f996103 | |||
| 8d8c843ccf | |||
| a46bd9b36e | |||
| 96a977990a | |||
| 41a40c74ef | |||
| 837dfcaba4 | |||
| ac441b4340 | |||
| 4362cb9317 | |||
| dc977b292d | |||
| 10bd668033 | |||
| 5d0abe4151 | |||
| 89ec87d32a | |||
| 1d48d5d5ef | |||
| 0565558e21 | |||
| 6320d3c08c | |||
| 4511732252 | |||
| f5c087fc2a | |||
| d2fc784060 | |||
| be2ec969c4 | |||
| 456250a176 | |||
| cbe7e69e13 | |||
| d3a17891f8 | |||
| e32c965a4b | |||
| 5982e3ebea | |||
| ea58238453 | |||
| 1e1dbd6085 | |||
| 6b98e414c6 | |||
| 29cb6195b3 | |||
| d99af7c4ad | |||
| 48e4051310 | |||
| 95446a76ef | |||
| 6d874af45f | |||
| dad18a24fb | |||
| 8b9a2e1d84 | |||
| dd16a1cbf4 | |||
| 2900401d3e | |||
| b5312c27dc | |||
| 4319c43ceb | |||
| be77882d70 | |||
| 78f816978c | |||
| 076b6f1c82 | |||
| 0978fbb15e | |||
| 2de18846b0 | |||
| a71a167133 | |||
| 6359bbe139 | |||
| d1c713fa29 | |||
| 16506c5ae2 | |||
| 398533877c | |||
| cdc200e1da | |||
| dd78dc9606 | |||
| ef284e9d13 | |||
| f4032dad22 | |||
| 5d55b9c9ef | |||
| d9d05e21c0 | |||
| 1043baf08a | |||
| dfd22b5e44 | |||
| 4a95bf67da | |||
| 8742d50bad | |||
| 6dce9d5deb | |||
| a8712fd8ce | |||
| 8f291b0f80 | |||
| 7f9cc7cd2d | |||
| d102e1dbbc | |||
| 83ea3bb138 | |||
| 1f39e1c04d | |||
| 48e0691eec | |||
| fd946107df | |||
| 094b30cd26 | |||
| 708675077f | |||
| 82c4926f19 | |||
| 29dab578e7 | |||
| 1e75f045b7 | |||
| cfdd4ac36c | |||
| f2e16f946c | |||
| 0a1da6f651 | |||
| 062e45ee6a | |||
| 83145784d7 | |||
| 486e6457a9 | |||
| 7efea5726b | |||
| 7eb2f7659e | |||
| 112ca1875a | |||
| bc641d8e4c | |||
| d0c68e91dc | |||
| c373a0473b | |||
| 708fea637c | |||
| c5a4184437 | |||
| 446cf8c694 | |||
| 3df776d062 | |||
| ae7807643b | |||
| 5ebaa3e1ce | |||
| 4a25eab01a | |||
| e885e05a7a | |||
| 012b19c79e | |||
| 0875e805b4 | |||
| f1700d5e47 | |||
| 30a5176090 | |||
| 43c40f7bb0 | |||
| 9b3509c062 | |||
| 85fee0cc01 | |||
| 0f07bfc9d7 | |||
| 75098adbdb | |||
| 8dba865ca7 | |||
| 2fcbffeaf9 | |||
| 619ffdd5e1 | |||
| 1af2da0063 | |||
| 9d1231ce0a | |||
| 4d1110b3f3 | |||
| 6a5bf521c2 | |||
| 3410c332d5 | |||
| 8e41332dfa | |||
| 9f56a335b7 | |||
| 8db8307c9e | |||
| 641d9f5ebd | |||
| 05887967e0 | |||
| 232b9c13d7 | |||
| c0b00339c5 | |||
| bfcb2070d1 | |||
| 1e3a23e017 | |||
| 524109056f | |||
| c2df8aa6b9 | |||
| 1666b50857 | |||
| e839c271cd | |||
| 823c208c71 | |||
| ee01b4a3a0 | |||
| d5ed065011 | |||
| 78a6ec2f98 | |||
| 7f60a9e52b | |||
| 7383dca9ee | |||
| 8a7d72bf24 | |||
| 4af7262f45 | |||
| daa62202fa | |||
| f911fceb9e | |||
| e9e5fac610 | |||
| 5017ac6f74 | |||
| 1d92ee76b9 | |||
| 7d01764a30 | |||
| 62d8955da9 | |||
| 4f94de86db | |||
| a6f7c57d19 | |||
| 1690030b1d | |||
| bf343eb499 | |||
| 4f55144232 | |||
| c9b18f6fa4 | |||
| 416a03aea1 | |||
| f62b268f1d | |||
| 6ce8a3feba | |||
| 94dd547a74 | |||
| f5c7b58b3a | |||
| fa7c16dbe7 | |||
| 6a43eb6755 | |||
| c524ebc7f3 | |||
| 0deb905cdf | |||
| 4cadbcbbdf | |||
| 96eef75f74 | |||
| 51d9e02e07 | |||
| c4c21c4f0c | |||
| c00ed14bc3 | |||
| 8f39a5eeac | |||
| 305715277c | |||
| d4eebf1a77 | |||
| fcb247b62a | |||
| 7556da7839 | |||
| eb93a6c149 | |||
| b870ab204a | |||
| fba2422a64 | |||
| da674d3e05 | |||
| 57e1c1bd89 | |||
| e3463b3c3d | |||
| a3898c7589 | |||
| 1221c6144c | |||
| c77b0098a7 | |||
| f779e81993 | |||
| 5c71bb95a0 | |||
| 1525992d73 | |||
| a227c69408 | |||
| 972bfa3319 | |||
| 360b920935 | |||
| 1416a0de53 | |||
| abe25be05e | |||
| 664a19a5c4 | |||
| 8efadd2547 | |||
| ed1f3e1e8c | |||
| 4aaf9eaed4 | |||
| 0416e4d212 | |||
| b9247db001 | |||
| 74ef157fa1 | |||
| d4724b0e12 | |||
| dbfdbcf2b7 | |||
| 0986b14508 | |||
| 325152c782 | |||
| c136d12194 | |||
| 7557952155 | |||
| b0b41e6718 | |||
| 22e3067b8c | |||
| e54b2fb207 | |||
| e9ab01ab06 | |||
| 058e554b0f | |||
| 6d9327aebc | |||
| c23d7850c7 | |||
| 2854f0cf34 | |||
| a89e87ca2e | |||
| 5419ca431b | |||
| e8b674badd | |||
| 8af40a9c61 | |||
| f54aa3d823 | |||
| d544bf4665 | |||
| e5570a6017 | |||
| 5e91287e3e | |||
| 4e3d4a7060 | |||
| 3d80da9816 | |||
| bf63b716af | |||
| 38d389b838 | |||
| ebdca32c80 | |||
| 751a825616 | |||
| c06a3710c1 | |||
| d5e67fe13f | |||
| 00c7da45c0 | |||
| 3347817350 | |||
| 88ece92a78 | |||
| 03a73fb95e | |||
| 23c55c9636 | |||
| 12c6f4e95f | |||
| a468ddb85b | |||
| 051bdaccc7 | |||
| 5dc04d9574 | |||
| 60129413d3 | |||
| 15df20d22d | |||
| b37c560ea5 | |||
| 5a3272fe29 | |||
| 0baa8383f2 | |||
| d2bf9370ea | |||
| fd11aa5eec | |||
| cea33783a2 | |||
| 0d3eaf7f9a | |||
| c761cd54b4 | |||
| a8a055d32c | |||
| cc323d0d61 | |||
| fc87a6776a | |||
| f5ac57f87b | |||
| 90624abf2e | |||
| e5ec62695c | |||
| fb7c2b61d3 | |||
| dd64bf0fc9 | |||
| 434eb3b256 | |||
| 6da98a5f31 | |||
| 8fef75385a | |||
| c66b9219a8 | |||
| 2a1f8823e8 | |||
| 3da65f90e3 | |||
| a36cc24087 | |||
| 1829d8cadf | |||
| 02efc090ef | |||
| b184348f55 | |||
| 06743cb072 | |||
| 6fa66fb1d0 | |||
| 23c0ab9648 | |||
| 6422047cff | |||
| 46c2c72637 | |||
| c088590244 | |||
| 192ba69682 | |||
| 2cc428e968 | |||
| 15c451a6d9 | |||
| b46e8c07be | |||
| 961e931c31 | |||
| b1d49552ae | |||
| d31d154841 | |||
| d03892bbcf | |||
| 0cf0c1c585 | |||
| 0695b4bee9 | |||
| 0cfec3a7bd | |||
| 6224cd40d4 | |||
| 18ae82174d | |||
| aa1b9d11dc | |||
| 0a82471112 | |||
| 8052e10f0c | |||
| 1146f6ba52 | |||
| 738cea0025 | |||
| 6c28398d20 | |||
| 4fe75143d0 | |||
| 3cd59cdea5 | |||
| 9dca7eb878 | |||
| b45fd5ce3a | |||
| 229b1470f7 | |||
| 0a349e1707 | |||
| d176e9ccf0 | |||
| 710c3acb5d | |||
| a7f5d0e0c1 | |||
| 9928526dda | |||
| 2f8111be0e | |||
| 68113937ff | |||
| c598fa026b | |||
| 491c99c348 | |||
| d49aa8242a | |||
| 9ffbc06cf9 | |||
| af52ac30ba | |||
| 7aea1b5fc8 | |||
| 500ddf3946 | |||
| 3431eaf309 | |||
| 9a13c58583 | |||
| 77a48e8351 | |||
| 6cb156d876 | |||
| d8677c6261 | |||
| 6ef9ca18e9 | |||
| 6667738dc9 | |||
| 4f7415b0ec | |||
| c6f3a8b7c7 | |||
| 4229d673c9 | |||
| 98b7859605 | |||
| a8db707440 | |||
| 873a412179 | |||
| 0778b4809e | |||
| f85fb35c66 | |||
| 5ada0daf40 | |||
| 2ed5f5aa65 | |||
| ae5e6e3ab8 | |||
| 9ba7b9b4d4 | |||
| a45f1fba45 | |||
| e6b47d2cf2 | |||
| b04a262433 | |||
| 96bafa7f0b | |||
| b10612da5c | |||
| 171f4206a8 | |||
| bd83d19197 | |||
| 95cf201c6d | |||
| 4107711dae | |||
| 67f0758cb5 | |||
| 1c99944a32 | |||
| d3ea4959b4 | |||
| 70b27fbff2 | |||
| 96c2144857 | |||
| 0655af4d4f | |||
| 7adfe6f2df | |||
| 3fc0fdbe62 | |||
| fd73571744 | |||
| 80d861c12c | |||
| 45df7bfea3 | |||
| 106772e480 | |||
| 3506de8d1a | |||
| 32456d6966 | |||
| bdc2af9733 | |||
| a7daf41934 | |||
| fe6ff2528a | |||
| 7ceb8708b0 | |||
| 4d22d957a1 | |||
| 79f1a863b6 | |||
| 98db3d37ef | |||
| c02976782b | |||
| ceb4376eb8 | |||
| 0566b7bcb3 | |||
| 6d0ecb9f5e | |||
| 0901948564 | |||
| e6d028fd6b | |||
| 945e7082cd | |||
| 0d6bdfb514 | |||
| 70712bdab6 | |||
| 53771d7f95 | |||
| 6d32d4024d | |||
| f387589342 | |||
| 47a5080352 | |||
| e7e60d3ed8 | |||
| 5314c83534 | |||
| 204c964050 | |||
| 1ede99f71e | |||
| 3c1844046a | |||
| eefa90a2bc | |||
| efa0737660 | |||
| 6232560273 | |||
| e051d383a0 | |||
| 4fb5c1828d | |||
| 6f3b2fa66b | |||
| ccb42c831c | |||
| f17459fdc3 | |||
| a053f40e59 | |||
| 9a0459fdd3 | |||
| 46a859736a | |||
| e4524a2be9 | |||
| f3ee1cc947 | |||
| 2386782280 | |||
| 4d92922263 | |||
| 86bc28dfb3 | |||
| 721f707c94 | |||
| 0c7b731fa6 | |||
| f1eaa83af5 | |||
| 1bc0934228 | |||
| f26fd18c7a | |||
| c68dba8c21 | |||
| f768e1eece | |||
| d329ccc4e3 | |||
| 19f2434694 | |||
| 0006f6483d | |||
| f99557cc05 | |||
| f785823e00 | |||
| 62cd2263c2 | |||
| 7157634db0 | |||
| 6eec4277c1 | |||
| 78f6fdf169 | |||
| e01c6dc18d | |||
| 51dd408733 | |||
| 1ff7548b52 | |||
| f7fe44063a | |||
| 0355c358b8 | |||
| 4e0a60db00 | |||
| 3604d3940c | |||
| 02b9bd82cf | |||
| 86442adb20 | |||
| 569137eb22 | |||
| 5e3a37c9e8 | |||
| 4651066472 | |||
| 8d325b96ea | |||
| b52cb198b5 | |||
| bcee6632af | |||
| 150a1b8eed | |||
| 4c49b84213 | |||
| b27f177138 | |||
| b2f63897f9 | |||
| 2ee392be9b | |||
| f37652edb3 | |||
| 27049eb706 | |||
| a2974099c1 | |||
| 041a809b07 | |||
| 2ecc6b30aa | |||
| d582bbbb60 | |||
| ca3eda8fc7 | |||
| 1bc86c359e | |||
| 1e2c26123b | |||
| 4d25e3b90b | |||
| 83c820a18c | |||
| 3eface706c | |||
| 8d143f4bf1 | |||
| 88fbd78492 | |||
| 9a235a7c15 | |||
| d99607ea14 | |||
| 5150ec06dc | |||
| b9f190562e | |||
| eb189d077d | |||
| 9756d63cf1 | |||
| a800b32529 | |||
| 8f56ef6507 | |||
| 92e0d09c28 | |||
| 64d80f98db | |||
| 01391d316b | |||
| 1d32da075d | |||
| 9b32900b96 | |||
| ecf837636b | |||
| 5a76bc93b8 | |||
| b14e5577b4 | |||
| 4121ac91ee | |||
| c44f202c44 | |||
| 9059b16c7c | |||
| 3760f51c3c | |||
| aa18293f1d | |||
| 63066937e9 | |||
| 34f0d631d9 | |||
| b3522fea6a | |||
| 2ae3ce53f3 | |||
| 2adacbd27b | |||
| 45eed1b7e7 | |||
| b36d334f78 | |||
| bd648185bc | |||
| 67f1dc32a5 | |||
| d47d9dadf4 | |||
| b52f8c2925 | |||
| 59ce624521 | |||
| 70b9fbfcf6 | |||
| 74b9279416 | |||
| c3ba8ee0fa | |||
| 4f5ca0767d | |||
| d0a1374d2f | |||
| a747c976fe | |||
| 94fcea1040 | |||
| 7db684e308 | |||
| 1981e1c34c | |||
| 5274508665 | |||
| 6f68bdcf5b | |||
| cf20f1059f | |||
| 307c69edb9 | |||
| 9a07b15952 | |||
| a178d310fd | |||
| 12451ba5fc | |||
| 69554c0762 | |||
| 3152bc123a | |||
| 4871abbd71 | |||
| bd2333377f | |||
| 76aaedba66 | |||
| b0f316eecd | |||
| 49903abed7 | |||
| d106875605 | |||
| c13feccdcd | |||
| 6aebf99ebc | |||
| 5d6ccf56d7 | |||
| 0323f75bae | |||
| f31fdb0988 | |||
| 92024f68cf | |||
| f9c1e46c01 | |||
| a5af6d5f4c | |||
| b1d06c4eb7 | |||
| 89bf3b2817 | |||
| ea8aabceed | |||
| 9fd2d3d640 | |||
| 967dea346f | |||
| 57bc0b7032 | |||
| 9974311344 | |||
| b0bdd09032 | |||
| f477273d8a | |||
| 014303b0d5 | |||
| 201fe726b6 | |||
| 2ed3a5c582 | |||
| 975a198ee9 | |||
| 837f8f318d | |||
| f96827d052 | |||
| a9dc0912d0 | |||
| b5039ecce8 | |||
| 624affe092 | |||
| 9b5842b20e | |||
| edfa0c65f9 | |||
| 39e81aa77c | |||
| 8bde87246b | |||
| e2a082f748 | |||
| d7c2cf562f | |||
| f44ceb46cd | |||
| f524dde5ae | |||
| 47aa4cf198 | |||
| 1b55a5e3e0 | |||
| cb57d8f497 | |||
| e12421cee8 | |||
| 53484db768 | |||
| 4c337a5114 | |||
| 63ae95186b | |||
| 8c0065f563 | |||
| 5c41d6789c | |||
| e19f799046 | |||
| 7dac1e275d | |||
| d74d50c40a | |||
| 8b4be4d100 | |||
| de4460a047 | |||
| 0ed615a312 | |||
| e8e8f888dd | |||
| 3776791239 | |||
| 77fee0b866 | |||
| 5671b40707 | |||
| dec039ead3 | |||
| fb81f6f268 | |||
| deb973afa5 | |||
| 2dea3911ba | |||
| e0a7fb8290 | |||
| 30e402b9f1 | |||
| f6d9ad51c6 | |||
| 34ba81a841 | |||
| d7fae2620e | |||
| 9979a08cb0 | |||
| 17dd48f2a4 | |||
| 355ac2ea29 | |||
| 000e1293db | |||
| 48fa04f164 | |||
| ed8f5bf24f | |||
| e17df79133 | |||
| 9519d286bd | |||
| b462f19ef1 | |||
| 1cdb5964c6 | |||
| 592e54a53f | |||
| fe60f34015 | |||
| b5dc65d52a | |||
| 93a4efcf6d | |||
| 40f3212efa | |||
| 53d6ef04b8 | |||
| c29d032027 | |||
| 7fcd9d8a83 | |||
| fc034830d0 | |||
| 48e06bd213 | |||
| 96e0cce2d2 | |||
| 88f677e910 | |||
| 9f50f6ef0e | |||
| 20e8a30583 | |||
| 89c06046b5 | |||
| 9c13f6bb53 | |||
| b5edcde638 | |||
| 93e8186388 | |||
| 8e35079fd1 | |||
| 9663c2ec85 | |||
| 7da7554493 | |||
| 372e93e8f8 | |||
| 9b58cf9890 | |||
| 9fac2c3c33 | |||
| 505ad17447 | |||
| 129327077a | |||
| 034df01e1f | |||
| be1e5a7d0e | |||
| 9872a89a29 | |||
| cf445633c2 | |||
| 2cec7bc1a6 | |||
| 46bfd74e8d | |||
| f6b1e13113 | |||
| f0c3d38ea7 | |||
| d4afbc63e2 | |||
| 466574a856 | |||
| a8d0d39055 | |||
| 1b04243af6 | |||
| 29082808cb | |||
| d23cac506c | |||
| 8959c630ae | |||
| 35a4f9808d | |||
| 49906e98b7 | |||
| 7b6f63db05 | |||
| 37f97e1c17 | |||
| 60e559a910 | |||
| f6f5ee0375 | |||
| 72d1f129cf | |||
| 9474ed7d85 | |||
| 99194814a9 | |||
| 87feac447b | |||
| 0a070a2084 | |||
| 1d3f87a431 | |||
| f38d1c272e | |||
| b0bbd58f66 | |||
| 73fd801907 | |||
| bc59f20af4 | |||
| daf9187572 | |||
| 783c856fd7 | |||
| f939e00a84 | |||
| b31faa9bbc | |||
| 853a31b638 | |||
| faaed61384 | |||
| b50f686a96 | |||
| f45a3933de | |||
| 98a1786c10 | |||
| 1f16ebc5b2 | |||
| 79e8e430f1 | |||
| 6440621d5d | |||
| 59a4cd9cf8 | |||
| 6238be7f2c | |||
| 4051276d74 | |||
| 145510266a | |||
| 4d361a2b3d | |||
| c4fe664215 | |||
| ace71693b7 | |||
| f19ac748e6 | |||
| a7713213f9 | |||
| 845571a716 | |||
| 8b1dfab077 | |||
| 6969800192 | |||
| 37a13d1a00 | |||
| 8c89938eaf | |||
| 5116bf74ad | |||
| a6f969614e | |||
| c1b7476f43 | |||
| 0ca4c1de0f | |||
| ec44103d20 | |||
| 89fe0f11ae | |||
| 63a01088a0 | |||
| c3e48f1b4d | |||
| 31cd626820 | |||
| 8dd0e9600d | |||
| 10d86ff835 | |||
| cec8553fb5 | |||
| b03bf9ec3d | |||
| a750490eea | |||
| 5281c61f99 | |||
| 52628920d9 | |||
| ac2e450e38 | |||
| f8d1e9e004 | |||
| e551dfeddb | |||
| 2f9244e90d | |||
| 056fae518b | |||
| a45d87667f | |||
| 7225b0b18d | |||
| e72c9cbebf | |||
| c787bb26c7 | |||
| 20f3ddd277 | |||
| 5cac182b26 | |||
| 78b2b5ccc3 | |||
| 60cee7fd2f | |||
| 2bd21df762 | |||
| a32ed02745 | |||
| 93c5eeb99e | |||
| ff45baba2c | |||
| cc4d38b4a5 | |||
| bf30587180 | |||
| 1b372a9a7b | |||
| d9f78e1283 | |||
| 2ddd2d1f39 | |||
| 6b31160bbd | |||
| 74aa85b38b | |||
| 8e9b607fa1 | |||
| 5332f99a5a | |||
| e1a8bd333d | |||
| 018a45e2d4 | |||
| 2c43fb2512 | |||
| 3e5664a877 | |||
| ac41426a20 | |||
| 8a0cf14fd7 | |||
| 362f0df117 | |||
| 1190f6e091 | |||
| ebf93c06dd | |||
| fe8debe6e8 | |||
| b8866cf575 | |||
| 2ce8576ab2 | |||
| 1bca05e349 | |||
| 8146725f89 | |||
| 34c44a29c6 | |||
| 7c948dd812 | |||
| 61919a1b3e | |||
| 9be10890e1 | |||
| f51109cb26 | |||
| 9aec18b3a0 | |||
| b374cdf47c | |||
| b16f235e1c | |||
| c26e5bbf0d | |||
| 020c326458 | |||
| dacd1e6146 | |||
| 25b2eb5783 | |||
| 7c3dbac083 | |||
| 54b5d69c8d | |||
| 407510e265 | |||
| f7a283f87d | |||
| 508a6c92b2 | |||
| 9045ce05da | |||
| 456be920d3 | |||
| 71b4e14dd3 | |||
| fb7a9e2083 | |||
| 69e99fa902 | |||
| 5e7e1ff0e8 | |||
| 18e9ceb501 | |||
| 458c97b927 | |||
| 4fd3e6e99c | |||
| c606f86e9d | |||
| e7376c4fc8 | |||
| 886b16d6fd | |||
| 64c2e7c1e8 | |||
| 88e933ee25 | |||
| 1f62d85d8b | |||
| 3fd5fdf1ee | |||
| e419733716 | |||
| ecc8e0a108 | |||
| 2868a5ff4f | |||
| d71877f915 | |||
| e0e896d4b5 | |||
| 0a280e9399 | |||
| a58ac967a6 | |||
| ebc6723f55 | |||
| 66fd20ace5 | |||
| de00212bb6 | |||
| f0b458cd17 | |||
| 7aa217828b | |||
| b27f8e45dc | |||
| 3e02f0a0ba | |||
| f759999826 | |||
| 049aa35286 | |||
| dd8be657c2 | |||
| 09752241ba | |||
| 83561a0ceb | |||
| b8ca83ddd1 | |||
| 4cae6c051b | |||
| e5f9467329 | |||
| 34c1f3c7de | |||
| c0f56be2a9 | |||
| c6f89431d9 | |||
| 80601451b6 | |||
| f74d1beb62 | |||
| b73ea7f086 | |||
| a3bf6975c4 | |||
| 08ebb52e9b | |||
| 47a6af2b25 | |||
| a84cfb5677 | |||
| 6e92c4915a | |||
| 2a18df61cd | |||
| fb28083ce6 | |||
| 476bda812e | |||
| 265f01eb06 | |||
| cb9ef3ee6a | |||
| a4487179a5 | |||
| f43f627037 | |||
| a7b799f154 | |||
| ee7cd8cd4c | |||
| 2d1fcf4a8d | |||
| ef14074164 | |||
| 49aa9cf6da | |||
| 8d5ef45e2b | |||
| 9ef177e735 | |||
| 7f1919ebc6 | |||
| 1aaaaba619 | |||
| b7dd9b7d35 | |||
| ff0af15ac7 | |||
| 8a7ff9e01d | |||
| e52e257d9b | |||
| 2d6654b234 | |||
| 1b0427e943 | |||
| 24e1881876 | |||
| bfff38a262 | |||
| 2aff31fe06 | |||
| 6ba2b767b0 | |||
| bbc292561f | |||
| e4b00117c3 | |||
| d25851ac17 | |||
| d02450c0d9 | |||
| 400b2109fb | |||
| 2a09f9ef0b | |||
| 0052d349d9 | |||
| 7c8a8b8b70 | |||
| 85835087ba | |||
| 5aac042c80 | |||
| 9dbd073073 | |||
| 035f6c8098 | |||
| d2a06654c3 | |||
| c028686298 | |||
| 858a02634e | |||
| 2682fd38f8 | |||
| 3ecb701504 | |||
| ac359a0e9e | |||
| df636cf4f6 | |||
| 791a363694 | |||
| 06eb8b5be8 | |||
| 4b4290e087 | |||
| c89f91215c | |||
| 65d07dbff2 | |||
| 2e4d28b910 | |||
| 51c63fa996 | |||
| f0a582dc92 | |||
| bee36a7a4f | |||
| 0edd54dee3 | |||
| 4a16cd891f | |||
| 23dd9d4c01 | |||
| df277d6b7b | |||
| eed2c304e4 | |||
| bd5953317e | |||
| b88f58cc21 | |||
| 9cbf3079f3 | |||
| 682ac25111 | |||
| 07da9a9dd2 | |||
| 395ae4d05a | |||
| 2ef35a8583 | |||
| 5807dbfdac | |||
| c4f26c3cad | |||
| 26d87b691c | |||
| 52e2b90eef | |||
| 62eb9ce598 | |||
| a9efda3bc3 | |||
| 8fe4dca4c2 | |||
| a05d3a47c6 | |||
| e9265eff35 | |||
| 8fa7bf795a | |||
| f045fb44de | |||
| 0d06840a1b | |||
| 624e540cb4 | |||
| 353333653f | |||
| 83fec57400 | |||
| 7ad0ec1600 | |||
| ec611aac45 | |||
| d7752961b8 | |||
| 8c5837e78a | |||
| b3f8df1783 | |||
| 0ecc6c196f | |||
| 77686547eb | |||
| 419cf6c12b | |||
| 326571b797 | |||
| c25c2693f5 | |||
| 8d8a1f552d | |||
| 638f5c7323 | |||
| 9ec5d33840 | |||
| 8d3ba2a8cd | |||
| 0c2838cc90 | |||
| 1e5edfca26 | |||
| b141e1ee68 | |||
| 2fd4836228 | |||
| 9ceab0f294 | |||
| d4c12dcef4 | |||
| 16bb920011 | |||
| 158fa85c46 | |||
| 34bd0e11e4 | |||
| 9e649a18c4 | |||
| 2bb45afe5f | |||
| 41a9098c80 | |||
| 36ef03a406 | |||
| fd6c713119 | |||
| 4a57d3ada1 | |||
| 53160c638a | |||
| a2077f7be1 | |||
| b245d44220 | |||
| b15f7bcf45 | |||
| f16cb8f7d2 | |||
| af915be142 | |||
| 2633f4fcef | |||
| 6c2330984e | |||
| 1e1d5ef442 | |||
| 248757ac3f | |||
| cdbfb2c08c | |||
| d84c917f19 | |||
| 1c7c82358d | |||
| c085694a8b | |||
| a57c2b9fe5 | |||
| 9fc87fe20f | |||
| 4761e25fe1 | |||
| 3ce50d80de | |||
| 8754b023d5 | |||
| 22c9829fea | |||
| 0f7b3e9f84 | |||
| 1a456828df | |||
| 5958c69d64 | |||
| b14aa901ba | |||
| 535dfb5099 | |||
| 19f1aad88c | |||
| 9009eccc8e | |||
| 7f3cdedef9 | |||
| 05095805f1 | |||
| 5430147572 | |||
| 9b9f031fe1 | |||
| 2ad63247c2 | |||
| 87f37e3d87 | |||
| cb8edd9b8f | |||
| 072ae7280e | |||
| e99c299c6f | |||
| 46cdbd514f | |||
| a3e0189f0b | |||
| 8f4344f413 | |||
| 7e28de1d24 | |||
| 624e685212 | |||
| a23765a1cb | |||
| 048502ecd1 | |||
| 1b072448c4 | |||
| 86997488fb | |||
| db6efec417 | |||
| 2cc60c6ee2 | |||
| 80c98b6a35 | |||
| f51c01dc88 | |||
| 7b8a35a311 | |||
| d9db5263e8 | |||
| 912a661c04 | |||
| d3edc62fc7 | |||
| 6b5de5fec8 | |||
| fed4c51aa4 | |||
| 1721209f85 | |||
| 289820188e | |||
| 4fa0a9064b | |||
| f751d97c29 | |||
| d7028252c2 | |||
| a2ee70a964 | |||
| 57b166d13a | |||
| 5c76f2b9a2 | |||
| e59f06ff92 | |||
| d3b96dd048 | |||
| 6544e8135c | |||
| 54ed415d75 | |||
| f34937d4ae | |||
| a4fc40875b | |||
| 56c1441494 | |||
| 08ce8213fc | |||
| 0a3692e459 | |||
| 341546508f | |||
| 444d583a49 | |||
| 130ad4451b | |||
| d24cf8e7ec | |||
| 2d750a14b9 | |||
| b25b9d815b | |||
| daf965f119 | |||
| 296cd83a55 | |||
| b297888eb4 | |||
| e2569ed6dd | |||
| 4718bfac25 | |||
| 2f0798de32 | |||
| 11b0a00593 | |||
| 1d4eb59e94 | |||
| d6c8838fe2 | |||
| 0b888ac54d | |||
| 75099abbc1 | |||
| 3c4f835d8b | |||
| ce842f7561 | |||
| 508af7eb72 | |||
| a4386a617a | |||
| 767ba713a6 | |||
| 020e85906d | |||
| d1ffdd57a5 | |||
| 9c9323e040 | |||
| 376cf09565 | |||
| 047129223e | |||
| 4b64168faa | |||
| 2bc6d08410 | |||
| 484bb6b11f | |||
| a0884e2a03 | |||
| 1e352fdf80 | |||
| 83dfbb84c2 | |||
| 9043064543 | |||
| a3649148c1 | |||
| 28966739f6 | |||
| 13cb32903a | |||
| dcd75e0384 | |||
| dfdd9e02ca | |||
| 275796c241 | |||
| 6ca99e33ab | |||
| 1909caf769 | |||
| 2d07eeb518 | |||
| 1c0da7cd49 | |||
| fd316c81c0 | |||
| edbd437d7b | |||
| 9be1f9c18b | |||
| 96f06d8026 | |||
| 935d8e8aea | |||
| 21b99b3810 | |||
| c051a1249d | |||
| 564697e175 | |||
| d4bf358eae | |||
| f84d5ba3a0 | |||
| b550ff038c | |||
| ac9c629c0b | |||
| 755e24315e | |||
| c6ecd31ea1 | |||
| e600b91a85 | |||
| abff279707 | |||
| fd18508266 | |||
| d7bc309eeb | |||
| 33c40e5ef8 | |||
| 062d926900 | |||
| 0ad0046a76 | |||
| b36cee93f7 | |||
| 2731e70fb7 | |||
| 276cf858fc | |||
| b2acff40c4 | |||
| e3333e716f | |||
| bb7b8c2fab | |||
| 389c7be264 | |||
| c2481e460b | |||
| c93e4a951a | |||
| aa094bf930 | |||
| 8a741952df | |||
| 0dcc55ee16 | |||
| 6c3a3b9e29 | |||
| e4eff8c837 | |||
| 527b895e3c | |||
| 4eb0a7b7c0 | |||
| f11462bf41 | |||
| dea1c31701 | |||
| 68ab43b3ab | |||
| cf72e3bba5 | |||
| 62743edf45 | |||
| eec3752b5a | |||
| 006fd6c733 | |||
| 7078e20200 | |||
| 52c01abbe9 | |||
| 6e26f39751 | |||
| 08aab9d4d8 | |||
| dc254bd562 | |||
| 6c3173f45b | |||
| 30cba7b935 | |||
| 9e27c0fe2c | |||
| 05c09eff19 | |||
| 258eb88684 | |||
| 9c73ef770f | |||
| 30d96dcb65 | |||
| 3a69fd50bb | |||
| 0f72729f0d | |||
| 0a8874180c | |||
| a6501474bb | |||
| 4cb93ddcde | |||
| c75c3f1ab1 | |||
| 281ad5fac9 | |||
| eb0301d74e | |||
| 4a76757dd2 | |||
| 1855dae35e | |||
| 4e7e715f10 | |||
| 5be1004292 | |||
| 1170ff58ee |
+5
-1
@@ -18,4 +18,8 @@ vite.config.js
|
|||||||
**/*.json
|
**/*.json
|
||||||
**/dev-dist
|
**/dev-dist
|
||||||
**/dist
|
**/dist
|
||||||
/src-tauri/target/**/*
|
/src-tauri/target/**/*
|
||||||
|
reverbGen.mjs
|
||||||
|
hydra.mjs
|
||||||
|
jsdoc-synonyms.js
|
||||||
|
packages/node/pattern.mjs
|
||||||
@@ -21,13 +21,13 @@ jobs:
|
|||||||
name: github-pages
|
name: github-pages
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v2
|
- uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 7
|
version: 8.11.0
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 20
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
if: matrix.platform == 'ubuntu-latest'
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf libasound2-dev
|
||||||
|
|
||||||
- name: Install app dependencies from lockfile and build web
|
- name: Install app dependencies from lockfile and build web
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18]
|
node-version: [20]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v2
|
- uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 7
|
version: 8.11.0
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|||||||
+87
-1
@@ -43,4 +43,90 @@ dev-dist
|
|||||||
Dirt-Samples
|
Dirt-Samples
|
||||||
tidal-drum-machines
|
tidal-drum-machines
|
||||||
webaudiofontdata
|
webaudiofontdata
|
||||||
src-tauri/target
|
src-tauri/target
|
||||||
|
|
||||||
|
# BEGIN JetBrains -> END JetBrains
|
||||||
|
|
||||||
|
# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
# END JetBrains -> BEGIN JetBrains
|
||||||
|
|
||||||
|
samples/*
|
||||||
|
!samples/README.md
|
||||||
|
|||||||
+3
-1
@@ -9,4 +9,6 @@ packages/xen/tunejs.js
|
|||||||
paper
|
paper
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
pnpm-workspace.yaml
|
pnpm-workspace.yaml
|
||||||
**/dev-dist
|
**/dev-dist
|
||||||
|
website/.astro
|
||||||
|
packages/node/pattern.mjs
|
||||||
+5
-5
@@ -13,7 +13,7 @@ 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 glanced through the
|
If you have any questions about strudel, make sure you've glanced through the
|
||||||
[docs](https://strudel.tidalcycles.org/learn/) to find out if it answers your question.
|
[docs](https://strudel.cc/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.
|
||||||
@@ -31,7 +31,7 @@ Use one of the Communication Channels listed above.
|
|||||||
|
|
||||||
## Improve the Docs
|
## Improve the Docs
|
||||||
|
|
||||||
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/workshop/getting-started/),
|
If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/),
|
||||||
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
|
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
|
||||||
|
|
||||||
## Propose a Feature
|
## Propose a Feature
|
||||||
@@ -66,7 +66,7 @@ To get the project up and running for development, make sure you have installed:
|
|||||||
|
|
||||||
- [git](https://git-scm.com/)
|
- [git](https://git-scm.com/)
|
||||||
- [node](https://nodejs.org/en/) >= 18
|
- [node](https://nodejs.org/en/) >= 18
|
||||||
- [pnpm](https://pnpm.io/) (`npm i pnpm -g`)
|
- [pnpm](https://pnpm.io/) (`curl -fsSL https://get.pnpm.io/install.sh | env PNPM_VERSION=8.11.0 sh -`)
|
||||||
|
|
||||||
then, do the following:
|
then, do the following:
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ Please report any problems you've had with the setup instructions!
|
|||||||
|
|
||||||
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
|
- You can format all files at once by running `pnpm codeformat` from the project root
|
||||||
- Run `pnpm format-check` from the project root to check if all files are well formatted
|
- 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
|
||||||
@@ -114,7 +114,7 @@ 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 `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,
|
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/<package-name>` to get the local version,
|
||||||
allowing to develop multiple packages at the same time.
|
allowing to develop multiple packages at the same time.
|
||||||
|
|
||||||
## Package Publishing
|
## Package Publishing
|
||||||
|
|||||||
@@ -2,38 +2,28 @@
|
|||||||
|
|
||||||
[](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 software is slowly stabilising, but please continue to tread carefully.
|
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is a bit more stable now, but please continue to tread carefully.
|
||||||
|
|
||||||
- Try it here: <https://strudel.tidalcycles.org/>
|
- Try it here: <https://strudel.cc>
|
||||||
- Docs: <https://strudel.tidalcycles.org/learn/>
|
- Docs: <https://strudel.cc/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>
|
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
||||||
|
- 2 Years of Strudel Blog Post: <https://strudel.cc/blog/#year-2>
|
||||||
|
|
||||||
## 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
|
||||||
pnpm run setup
|
pnpm i
|
||||||
pnpm run repl
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using Strudel In Your Project
|
## Using Strudel In Your Project
|
||||||
|
|
||||||
There are multiple npm packages you can use to use strudel, or only parts of it, in your project:
|
This project is organized into many [packages](./packages), which are also available on [npm](https://www.npmjs.com/search?q=%40strudel).
|
||||||
|
|
||||||
- [`core`](./packages/core/): tidal pattern engine
|
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start).
|
||||||
- [`mini`](./packages/mini): mini notation parser + core binding
|
|
||||||
- [`transpiler`](./packages/transpiler): user code transpiler
|
|
||||||
- [`webaudio`](./packages/webaudio): webaudio output
|
|
||||||
- [`osc`](./packages/osc): bindings to communicate via OSC
|
|
||||||
- [`midi`](./packages/midi): webmidi bindings
|
|
||||||
- [`serial`](./packages/serial): webserial bindings
|
|
||||||
- [`tonal`](./packages/tonal): tonal functions
|
|
||||||
- [`xen`](./packages/xen): microtonal / xenharmonic functions
|
|
||||||
- ... [and there are more](./packages/)
|
|
||||||
|
|
||||||
Click on the package names to find out more about each one.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
-1806
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 150 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
# examples
|
||||||
|
|
||||||
|
This folder contains usage examples for different scenarios.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# buildless examples
|
||||||
|
|
||||||
|
These examples show you how strudel can be used in a regular html file, without the need for a build tool.
|
||||||
|
|
||||||
|
Most examples are using [skypack](https://www.skypack.dev/)
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
/>
|
/>
|
||||||
<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.6.8');
|
const strudel = await import('https://cdn.skypack.dev/@strudel/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 = () => {
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
/>
|
/>
|
||||||
<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.6.8');
|
const strudel = await import('https://cdn.skypack.dev/@strudel/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
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
|
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
|
||||||
<button id="play">play</button>
|
<button id="play">play</button>
|
||||||
<button id="stop">stop</button>
|
<button id="stop">stop</button>
|
||||||
<script type="module">
|
<script>
|
||||||
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
|
strudel.initStrudel();
|
||||||
|
|
||||||
initStrudel();
|
|
||||||
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
|
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
|
||||||
document.getElementById('play').addEventListener('stop', () => hush());
|
document.getElementById('play').addEventListener('stop', () => hush());
|
||||||
</script>
|
</script>
|
||||||
+4
-4
@@ -1,12 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
|
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
|
||||||
<button id="a">A</button>
|
<button id="a">A</button>
|
||||||
<button id="b">B</button>
|
<button id="b">B</button>
|
||||||
<button id="c">C</button>
|
<button id="c">C</button>
|
||||||
<button id="stop">stop</button>
|
<button id="stop">stop</button>
|
||||||
<script type="module">
|
<script>
|
||||||
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
|
|
||||||
initStrudel({
|
initStrudel({
|
||||||
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
|
prebake: () => samples('github:tidalcycles/dirt-samples'),
|
||||||
});
|
});
|
||||||
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
|
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
|
||||||
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
|
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
@@ -16,38 +16,45 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="output"></div>
|
<div id="output"></div>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.6.8';
|
// TODO: refactor to use newer version without controls import
|
||||||
import { mini } from 'https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0';
|
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel/core@0.11.0';
|
||||||
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.6.0';
|
import { mini } from 'https://cdn.skypack.dev/@strudel/mini@0.11.0';
|
||||||
|
import { transpiler } from 'https://cdn.skypack.dev/@strudel/transpiler@0.11.0';
|
||||||
import {
|
import {
|
||||||
getAudioContext,
|
getAudioContext,
|
||||||
webaudioOutput,
|
webaudioOutput,
|
||||||
initAudioOnFirstClick,
|
initAudioOnFirstClick,
|
||||||
} from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0';
|
registerSynthSounds,
|
||||||
|
} from 'https://cdn.skypack.dev/@strudel/webaudio@0.11.0';
|
||||||
|
|
||||||
initAudioOnFirstClick();
|
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
const input = document.getElementById('text');
|
const input = document.getElementById('text');
|
||||||
input.innerHTML = getTune();
|
input.innerHTML = getTune();
|
||||||
|
|
||||||
evalScope(
|
const loadModules = evalScope(
|
||||||
controls,
|
controls,
|
||||||
import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8'),
|
import('https://cdn.skypack.dev/@strudel/core@0.11.0'),
|
||||||
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0'),
|
import('https://cdn.skypack.dev/@strudel/mini@0.11.0'),
|
||||||
import('https://cdn.skypack.dev/@strudel.cycles/tonal@0.6.0'),
|
import('https://cdn.skypack.dev/@strudel/tonal@0.11.0'),
|
||||||
import('https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0'),
|
import('https://cdn.skypack.dev/@strudel/webaudio@0.11.0'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const initAudio = Promise.all([initAudioOnFirstClick(), registerSynthSounds()]);
|
||||||
|
|
||||||
const { evaluate } = repl({
|
const { evaluate } = repl({
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: webaudioOutput,
|
||||||
getTime: () => ctx.currentTime,
|
getTime: () => ctx.currentTime,
|
||||||
transpiler,
|
transpiler,
|
||||||
});
|
});
|
||||||
document.getElementById('start').addEventListener('click', () => evaluate(input.value));
|
document.getElementById('start').addEventListener('click', async () => {
|
||||||
|
await loadModules;
|
||||||
|
await initAudio;
|
||||||
|
evaluate(input.value);
|
||||||
|
});
|
||||||
|
|
||||||
function getTune() {
|
function getTune() {
|
||||||
return `await samples('github:tidalcycles/Dirt-Samples/master')
|
return `samples('github:tidalcycles/dirt-samples')
|
||||||
|
setcps(1);
|
||||||
stack(
|
stack(
|
||||||
// amen
|
// amen
|
||||||
n("0 1 2 3 4 5 6 7")
|
n("0 1 2 3 4 5 6 7")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
|
<script src="https://unpkg.com/@strudel/embed@0.11.0"></script>
|
||||||
<!-- <script src="./embed.js"></script> -->
|
<!-- <script src="./embed.js"></script> -->
|
||||||
<strudel-repl>
|
<strudel-repl>
|
||||||
<!--
|
<!--
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script src="https://unpkg.com/@strudel/repl@1.0.2"></script>
|
||||||
|
<strudel-editor>
|
||||||
|
<!--
|
||||||
|
// @date 23-08-15
|
||||||
|
// "golf rolf" @by froos @license CC BY-NC-SA 4.0
|
||||||
|
|
||||||
|
setcps(1)
|
||||||
|
stack(
|
||||||
|
s("bd*2, ~ rim*<1!3 2>, hh*4").bank('RolandTR909')
|
||||||
|
.off(-1/8, set(speed("1.5").gain(.25)))
|
||||||
|
.mask("<0!16 1!64>")
|
||||||
|
,
|
||||||
|
note("g1(3,8)")
|
||||||
|
.s("gm_synth_bass_2:<0 2>")
|
||||||
|
.delay(".8:.25:.25")
|
||||||
|
.clip("<.5!16 2!32>")
|
||||||
|
.off(1/8, add(note("12?0.7")))
|
||||||
|
.lpf(sine.range(500,2000).slow(32)).lpq(8)
|
||||||
|
.add(note("0,.05"))
|
||||||
|
.mask("<0!8 1!32>")
|
||||||
|
,
|
||||||
|
n("<0 1 2 3 4>*8").scale('G4 minor')
|
||||||
|
.s("gm_lead_6_voice")
|
||||||
|
.clip(sine.range(.2,.8).slow(8))
|
||||||
|
.jux(rev)
|
||||||
|
.room(2)
|
||||||
|
.sometimes(add(note("12")))
|
||||||
|
.lpf(perlin.range(200,20000).slow(4))
|
||||||
|
).reset("<x@15 x(5,8)>")
|
||||||
|
-->
|
||||||
|
</strudel-editor>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
|
||||||
|
<button id="play">PLAY</button>
|
||||||
|
<script>
|
||||||
|
initStrudel({
|
||||||
|
prebake: () => samples('github:tidalcycles/dirt-samples'),
|
||||||
|
});
|
||||||
|
document.getElementById('play').addEventListener('click', () => s('bd sd').play());
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# codemirror-repl example
|
||||||
|
|
||||||
|
This folder demonstrates how to set up a full strudel repl with the `@strudel/codemirror` package. Run it using:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm i
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
+9
-10
@@ -1,11 +1,11 @@
|
|||||||
import { StrudelMirror } from '@strudel/codemirror';
|
import { StrudelMirror } from '@strudel/codemirror';
|
||||||
import { funk42 } from './tunes';
|
import { funk42 } from './tunes';
|
||||||
import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
|
import { drawPianoroll, evalScope } from '@strudel/core';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
import { initAudioOnFirstClick } from '@strudel/webaudio';
|
||||||
import { transpiler } from '@strudel.cycles/transpiler';
|
import { transpiler } from '@strudel/transpiler';
|
||||||
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
|
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel/webaudio';
|
||||||
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
|
import { registerSoundfonts } from '@strudel/soundfonts';
|
||||||
|
|
||||||
// init canvas
|
// init canvas
|
||||||
const canvas = document.getElementById('roll');
|
const canvas = document.getElementById('roll');
|
||||||
@@ -25,11 +25,10 @@ const editor = new StrudelMirror({
|
|||||||
prebake: async () => {
|
prebake: async () => {
|
||||||
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
|
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
|
||||||
const loadModules = evalScope(
|
const loadModules = evalScope(
|
||||||
controls,
|
import('@strudel/core'),
|
||||||
import('@strudel.cycles/core'),
|
import('@strudel/mini'),
|
||||||
import('@strudel.cycles/mini'),
|
import('@strudel/tonal'),
|
||||||
import('@strudel.cycles/tonal'),
|
import('@strudel/webaudio'),
|
||||||
import('@strudel.cycles/webaudio'),
|
|
||||||
);
|
);
|
||||||
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
|
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
|
||||||
},
|
},
|
||||||
@@ -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": "^5.0.10"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@strudel/codemirror": "workspace:*",
|
||||||
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/mini": "workspace:*",
|
||||||
|
"@strudel/soundfonts": "workspace:*",
|
||||||
|
"@strudel/tonal": "workspace:*",
|
||||||
|
"@strudel/transpiler": "workspace:*",
|
||||||
|
"@strudel/webaudio": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
|
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
|
||||||
await samples('github:felixroos/samples/main')
|
samples('github:felixroos/samples')
|
||||||
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||||
|
|
||||||
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
|
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
|
||||||
// make it 22 edo
|
// make it 22 edo
|
||||||
@@ -33,8 +33,8 @@ await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'githu
|
|||||||
|
|
||||||
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
|
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
|
||||||
|
|
||||||
await samples('github:felixroos/samples/main')
|
samples('github:felixroos/samples')
|
||||||
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||||
|
|
||||||
addVoicings('hip', {
|
addVoicings('hip', {
|
||||||
m11: ['2M 3m 4P 7m'],
|
m11: ['2M 3m 4P 7m'],
|
||||||
@@ -69,8 +69,8 @@ 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
|
// 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
|
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
|
||||||
|
|
||||||
await samples('github:felixroos/samples/main')
|
samples('github:felixroos/samples')
|
||||||
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||||
|
|
||||||
setcps(.5)
|
setcps(.5)
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# headless-repl demo
|
||||||
|
|
||||||
|
This demo shows how to use strudel in "headless mode".
|
||||||
|
Buttons A / B / C will switch between different patterns.
|
||||||
|
It showcases the usage of the `@strudel/web` package, using [vite](https://vitejs.dev/) as the dev server.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm i && cd examples/headless-repl
|
||||||
|
pnpm dev
|
||||||
|
# open http://localhost:5173/
|
||||||
|
```
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="https://strudel.tidalcycles.org/favicon.ico" />
|
<link rel="icon" type="image/svg+xml" href="https://strudel.cc/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>@strudel/web REPL Example</title>
|
<title>@strudel/web REPL Example</title>
|
||||||
</head>
|
</head>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<script type="module">
|
<script type="module">
|
||||||
import { initStrudel } from '@strudel/web';
|
import { initStrudel } from '@strudel/web';
|
||||||
initStrudel({
|
initStrudel({
|
||||||
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
|
prebake: () => samples('github:tidalcycles/dirt-samples'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
|
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
|
||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.3.2"
|
"vite": "^5.0.10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/web": "workspace:*"
|
"@strudel/web": "workspace:*"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
dist
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# minimal repl
|
||||||
|
|
||||||
|
This folder demonstrates how to set up a minimal strudel repl using vite and vanilla JS. Run it using:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're looking for a more feature rich alternative, have a look at the [../codemirror-repl](codemirror-repl example)
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
@@ -1,20 +1,15 @@
|
|||||||
import { controls, repl, evalScope } from '@strudel.cycles/core';
|
import { repl, evalScope } from '@strudel/core';
|
||||||
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
import { getAudioContext, webaudioOutput, initAudioOnFirstClick, registerSynthSounds } from '@strudel/webaudio';
|
||||||
import { transpiler } from '@strudel.cycles/transpiler';
|
import { transpiler } from '@strudel/transpiler';
|
||||||
import tune from './tune.mjs';
|
import tune from './tune.mjs';
|
||||||
|
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
const input = document.getElementById('text');
|
const input = document.getElementById('text');
|
||||||
input.innerHTML = tune;
|
input.innerHTML = tune;
|
||||||
initAudioOnFirstClick();
|
initAudioOnFirstClick();
|
||||||
|
registerSynthSounds();
|
||||||
|
|
||||||
evalScope(
|
evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/webaudio'), import('@strudel/tonal'));
|
||||||
controls,
|
|
||||||
import('@strudel.cycles/core'),
|
|
||||||
import('@strudel.cycles/mini'),
|
|
||||||
import('@strudel.cycles/webaudio'),
|
|
||||||
import('@strudel.cycles/tonal'),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { evaluate } = repl({
|
const { evaluate } = repl({
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: webaudioOutput,
|
||||||
+6
-6
@@ -10,13 +10,13 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.3.3"
|
"vite": "^5.0.10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"@strudel/webaudio": "workspace:*",
|
||||||
"@strudel.cycles/tonal": "workspace:*"
|
"@strudel/tonal": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
export default `await samples('github:tidalcycles/Dirt-Samples/master')
|
export default `samples('github:tidalcycles/dirt-samples')
|
||||||
|
setcps(1)
|
||||||
stack(
|
stack(
|
||||||
// amen
|
// amen
|
||||||
n("0 1 2 3 4 5 6 7")
|
n("0 1 2 3 4 5 6 7")
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# superdough demo
|
||||||
|
|
||||||
|
This demo shows how to use [superdough](https://www.npmjs.com/package/superdough) with [vite](https://vitejs.dev/).
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm i && cd examples/headless-repl
|
||||||
|
pnpm dev
|
||||||
|
# open http://localhost:5173/
|
||||||
|
```
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Superdough Example</title>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<button id="play">PLAAAAAAAY</button>
|
||||||
|
<script type="module">
|
||||||
|
import { superdough, samples, initAudioOnFirstClick, registerSynthSounds } from 'superdough';
|
||||||
|
|
||||||
|
const init = Promise.all([
|
||||||
|
initAudioOnFirstClick(),
|
||||||
|
samples('github:tidalcycles/dirt-samples'),
|
||||||
|
registerSynthSounds(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const loop = (t = 0) => {
|
||||||
|
// superdough(value, time, duration)
|
||||||
|
superdough({ s: 'bd', delay: 0.5 }, t);
|
||||||
|
superdough({ note: 'g1', s: 'sawtooth', cutoff: 600, resonance: 8 }, t, 0.125);
|
||||||
|
superdough({ note: 'g2', s: 'sawtooth', cutoff: 600, resonance: 8 }, t + 0.25, 0.125);
|
||||||
|
superdough({ s: 'hh' }, t + 0.25);
|
||||||
|
superdough({ s: 'sd', room: 0.5 }, t + 0.5);
|
||||||
|
superdough({ s: 'hh' }, t + 0.75);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('play').addEventListener('click', async () => {
|
||||||
|
await init;
|
||||||
|
let t = 0.1;
|
||||||
|
while (t < 16) {
|
||||||
|
loop(t++);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -12,6 +12,6 @@
|
|||||||
"superdough": "workspace:*"
|
"superdough": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.4.5"
|
"vite": "^5.0.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
// this barrel export is currently only used to find undocumented exports
|
// this barrel export is currently only used to find undocumented exports
|
||||||
|
export * from './packages/codemirror/index.mjs';
|
||||||
export * from './packages/core/index.mjs';
|
export * from './packages/core/index.mjs';
|
||||||
export * from './packages/csound/index.mjs';
|
export * from './packages/csound/index.mjs';
|
||||||
export * from './packages/embed/index.mjs';
|
|
||||||
export * from './packages/desktopbridge/index.mjs';
|
export * from './packages/desktopbridge/index.mjs';
|
||||||
|
export * from './packages/draw/index.mjs';
|
||||||
|
export * from './packages/embed/index.mjs';
|
||||||
|
export * from './packages/hydra/index.mjs';
|
||||||
export * from './packages/midi/index.mjs';
|
export * from './packages/midi/index.mjs';
|
||||||
export * from './packages/mini/index.mjs';
|
export * from './packages/mini/index.mjs';
|
||||||
export * from './packages/osc/index.mjs';
|
export * from './packages/osc/index.mjs';
|
||||||
export * from './packages/react/index.mjs';
|
export * from './packages/repl/index.mjs';
|
||||||
export * from './packages/serial/index.mjs';
|
export * from './packages/serial/index.mjs';
|
||||||
export * from './packages/soundfonts/index.mjs';
|
export * from './packages/soundfonts/index.mjs';
|
||||||
|
export * from './packages/superdough/index.mjs';
|
||||||
export * from './packages/tonal/index.mjs';
|
export * from './packages/tonal/index.mjs';
|
||||||
export * from './packages/transpiler/index.mjs';
|
export * from './packages/transpiler/index.mjs';
|
||||||
|
export * from './packages/web/index.mjs';
|
||||||
export * from './packages/webaudio/index.mjs';
|
export * from './packages/webaudio/index.mjs';
|
||||||
export * from './packages/xen/index.mjs';
|
export * from './packages/xen/index.mjs';
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
postcss.config.js - <short description TODO>
|
jsdoc-synonyms.js - Add support for @synonym tag
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/postcss.config.js>
|
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/midi/midi.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/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = {
|
function defineTags(dictionary) {
|
||||||
plugins: {
|
dictionary.defineTag('synonyms', {
|
||||||
tailwindcss: {},
|
mustHaveValue: true,
|
||||||
autoprefixer: {},
|
onTagged: function (doclet, tag) {
|
||||||
},
|
doclet.synonyms_text = tag.value;
|
||||||
};
|
doclet.synonyms = doclet.synonyms_text.split(/[ ,]+/);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { defineTags: defineTags };
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
|
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
|
||||||
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
|
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
|
||||||
},
|
},
|
||||||
"plugins": ["plugins/markdown"],
|
"plugins": ["plugins/markdown", "jsdoc/jsdoc-synonyms"],
|
||||||
"opts": {
|
"opts": {
|
||||||
"destination": "./out/",
|
"destination": "./out/",
|
||||||
"recurse": true
|
"recurse": true
|
||||||
@@ -65,7 +65,7 @@ async function getUndocumented(path, docs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// read doc.json file
|
// read doc.json file
|
||||||
const { docs } = JSON.parse(await readFile(resolve(__dirname, 'doc.json'), 'utf8'));
|
const { docs } = JSON.parse(await readFile(resolve(__dirname, '..', 'doc.json'), 'utf8'));
|
||||||
|
|
||||||
const paths = dependencyTree.toList({
|
const paths = dependencyTree.toList({
|
||||||
filename: 'index.mjs',
|
filename: 'index.mjs',
|
||||||
@@ -76,7 +76,9 @@ const paths = dependencyTree.toList({
|
|||||||
// const paths = ['../packages/core/pattern.mjs', '../packages/core/hap.mjs'].map((rel) => resolve(__dirname, rel));
|
// const paths = ['../packages/core/pattern.mjs', '../packages/core/hap.mjs'].map((rel) => resolve(__dirname, rel));
|
||||||
|
|
||||||
const undocumented = Object.fromEntries(
|
const undocumented = Object.fromEntries(
|
||||||
await Promise.all(paths.map(async (path) => [path, await getUndocumented(path, docs)])),
|
await Promise.all(
|
||||||
|
paths.map(async (path) => [path.replace(resolve(__dirname, '..'), ''), await getUndocumented(path, docs)]),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(JSON.stringify(undocumented, null, 2));
|
console.log(JSON.stringify(undocumented, null, 2));
|
||||||
+2
-4
@@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"packages": [
|
"packages": ["packages/*"],
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"version": "independent",
|
"version": "independent",
|
||||||
"npmClient": "pnpm",
|
"npmClient": "pnpm",
|
||||||
"useWorkspaces": true
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ If you want to automatically deploy your site on push, go to `deploy.yml` and ch
|
|||||||
## running locally
|
## running locally
|
||||||
|
|
||||||
- install dependencies with `npm run setup`
|
- install dependencies with `npm run setup`
|
||||||
- run dev server with `npm run repl` and open `http://localhost:3000/strudel/swatch/`
|
- run dev server with `npm run repl` and open `http://localhost:4321/strudel/swatch/`
|
||||||
|
|
||||||
## tests fail?
|
## tests fail?
|
||||||
|
|
||||||
|
|||||||
+22
-22
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/monorepo",
|
"name": "@strudel/monorepo",
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Port of tidalcycles to javascript",
|
"description": "Port of tidalcycles to javascript",
|
||||||
@@ -18,13 +18,14 @@
|
|||||||
"build": "npm run prebuild && cd website && npm run build",
|
"build": "npm run prebuild && cd website && npm run build",
|
||||||
"preview": "cd website && npm run preview",
|
"preview": "cd website && npm run preview",
|
||||||
"osc": "cd packages/osc && npm run server",
|
"osc": "cd packages/osc && npm run server",
|
||||||
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
"jsdoc": "jsdoc packages/ -c jsdoc/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/jsdoc.config.json",
|
||||||
"lint": "eslint . --ext mjs,js --quiet",
|
"lint": "eslint . --ext mjs,js --quiet",
|
||||||
"codeformat": "prettier --write .",
|
"codeformat": "prettier --write .",
|
||||||
"format-check": "prettier --check .",
|
"format-check": "prettier --check .",
|
||||||
"report-undocumented": "npm run jsdoc-json && node undocumented.mjs > undocumented.json",
|
"report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json",
|
||||||
"check": "npm run format-check && npm run lint && npm run test",
|
"check": "npm run format-check && npm run lint && npm run test",
|
||||||
|
"sampler": "cd samples && node ../packages/sampler/sample-server.mjs",
|
||||||
"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"
|
"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"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -43,30 +44,29 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"homepage": "https://strudel.cc",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel.cycles/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"@strudel/webaudio": "workspace:*",
|
||||||
"@strudel.cycles/xen": "workspace:*",
|
"@strudel/xen": "workspace:*"
|
||||||
"acorn": "^8.8.1",
|
|
||||||
"dependency-tree": "^9.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.4.0",
|
"@tauri-apps/cli": "^1.5.9",
|
||||||
"@vitest/ui": "^0.28.0",
|
"@vitest/ui": "^1.1.0",
|
||||||
"canvas": "^2.11.2",
|
"acorn": "^8.11.3",
|
||||||
"eslint": "^8.39.0",
|
"dependency-tree": "^10.0.9",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"jsdoc": "^4.0.2",
|
"jsdoc": "^4.0.2",
|
||||||
"jsdoc-json": "^2.0.2",
|
"jsdoc-json": "^2.0.2",
|
||||||
"jsdoc-to-markdown": "^8.0.0",
|
"jsdoc-to-markdown": "^8.0.0",
|
||||||
"lerna": "^6.6.1",
|
"lerna": "^8.0.1",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^3.1.1",
|
||||||
"rollup-plugin-visualizer": "^5.8.1",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"vitest": "^0.33.0"
|
"vitest": "^1.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-11
@@ -1,15 +1,5 @@
|
|||||||
# Packages
|
# Packages
|
||||||
|
|
||||||
Each folder represents one of the @strudel.cycles/* packages [published to npm](https://www.npmjs.com/org/strudel.cycles).
|
Each folder represents one of the @strudel/* packages [published to npm](https://www.npmjs.com/org/strudel).
|
||||||
|
|
||||||
To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs.
|
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
|
|
||||||
```
|
|
||||||
|
|||||||
+46
-39
@@ -1,5 +1,13 @@
|
|||||||
import { createRoot } from 'react-dom/client';
|
import jsdoc from '../../doc.json';
|
||||||
import jsdoc from '../../../../doc.json';
|
// import { javascriptLanguage } from '@codemirror/lang-javascript';
|
||||||
|
import { autocompletion } from '@codemirror/autocomplete';
|
||||||
|
import { h } from './html';
|
||||||
|
|
||||||
|
function plaintext(str) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.innerText = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||||
const getInnerText = (html) => {
|
const getInnerText = (html) => {
|
||||||
@@ -8,36 +16,32 @@ const getInnerText = (html) => {
|
|||||||
return div.textContent || div.innerText || '';
|
return div.textContent || div.innerText || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Autocomplete({ doc }) {
|
export function Autocomplete({ doc, label }) {
|
||||||
return (
|
return h`<div class="prose dark:prose-invert max-h-[400px] overflow-auto p-2">
|
||||||
<div className="prose dark:prose-invert max-h-[400px] overflow-auto">
|
<h1 class="pt-0 mt-0">${label || getDocLabel(doc)}</h1>
|
||||||
<h3 className="pt-0 mt-0">{getDocLabel(doc)}</h3>
|
${doc.description}
|
||||||
<div dangerouslySetInnerHTML={{ __html: doc.description }} />
|
<ul>
|
||||||
<ul>
|
${doc.params?.map(
|
||||||
{doc.params?.map(({ name, type, description }, i) => (
|
({ name, type, description }) =>
|
||||||
<li key={i}>
|
`<li>${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}</li>`,
|
||||||
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
|
)}
|
||||||
</li>
|
</ul>
|
||||||
))}
|
<div>
|
||||||
</ul>
|
${doc.examples?.map((example) => `<div><pre>${plaintext(example)}</pre></div>`)}
|
||||||
<div>
|
</div>
|
||||||
{doc.examples?.map((example, i) => (
|
</div>`[0];
|
||||||
<div key={i}>
|
/*
|
||||||
<pre
|
<pre
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
console.log('ola!');
|
console.log('ola!');
|
||||||
navigator.clipboard.writeText(example);
|
navigator.clipboard.writeText(example);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{example}
|
{example}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
*/
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsdocCompletions = jsdoc.docs
|
const jsdocCompletions = jsdoc.docs
|
||||||
@@ -52,13 +56,7 @@ const jsdocCompletions = jsdoc.docs
|
|||||||
.map((doc) /*: Completion */ => ({
|
.map((doc) /*: Completion */ => ({
|
||||||
label: getDocLabel(doc),
|
label: getDocLabel(doc),
|
||||||
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
||||||
info: () => {
|
info: () => Autocomplete({ doc }),
|
||||||
const node = document.createElement('div');
|
|
||||||
// if Autocomplete is non-interactive, it could also be rendered at build time..
|
|
||||||
// .. using renderToStaticMarkup
|
|
||||||
createRoot(node).render(<Autocomplete doc={doc} />);
|
|
||||||
return node;
|
|
||||||
},
|
|
||||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -75,3 +73,12 @@ export const strudelAutocomplete = (context /* : CompletionContext */) => {
|
|||||||
], */
|
], */
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function isAutoCompletionEnabled(on) {
|
||||||
|
return on
|
||||||
|
? [
|
||||||
|
autocompletion({ override: [strudelAutocomplete] }),
|
||||||
|
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
|
||||||
|
]
|
||||||
|
: []; // autocompletion({ override: [] })
|
||||||
|
}
|
||||||
@@ -1,37 +1,118 @@
|
|||||||
import { defaultKeymap } from '@codemirror/commands';
|
import { closeBrackets } from '@codemirror/autocomplete';
|
||||||
|
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
||||||
|
import { history } from '@codemirror/commands';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
|
||||||
import { EditorState } from '@codemirror/state';
|
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||||
import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from '@codemirror/view';
|
import {
|
||||||
import { Drawer, repl } from '@strudel.cycles/core';
|
EditorView,
|
||||||
import { flashField, flash } from './flash.mjs';
|
highlightActiveLineGutter,
|
||||||
import { highlightExtension, highlightMiniLocations } from './highlight.mjs';
|
highlightActiveLine,
|
||||||
import { oneDark } from './themes/one-dark';
|
keymap,
|
||||||
|
lineNumbers,
|
||||||
|
drawSelection,
|
||||||
|
} from '@codemirror/view';
|
||||||
|
import { Pattern, repl } from '@strudel/core';
|
||||||
|
import { Drawer, cleanupDraw } from '@strudel/draw';
|
||||||
|
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
||||||
|
import { isTooltipEnabled } from './tooltip.mjs';
|
||||||
|
import { flash, isFlashEnabled } from './flash.mjs';
|
||||||
|
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
||||||
|
import { keybindings } from './keybindings.mjs';
|
||||||
|
import { initTheme, activateTheme, theme } from './themes.mjs';
|
||||||
|
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||||
|
import { widgetPlugin, updateWidgets } from './widget.mjs';
|
||||||
|
import { persistentAtom } from '@nanostores/persistent';
|
||||||
|
|
||||||
|
const extensions = {
|
||||||
|
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||||
|
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
|
||||||
|
isBracketClosingEnabled: (on) => (on ? closeBrackets() : []),
|
||||||
|
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
|
||||||
|
theme,
|
||||||
|
isAutoCompletionEnabled,
|
||||||
|
isTooltipEnabled,
|
||||||
|
isPatternHighlightingEnabled,
|
||||||
|
isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
|
||||||
|
isFlashEnabled,
|
||||||
|
keybindings,
|
||||||
|
};
|
||||||
|
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||||
|
|
||||||
|
export const defaultSettings = {
|
||||||
|
keybindings: 'codemirror',
|
||||||
|
isBracketMatchingEnabled: false,
|
||||||
|
isBracketClosingEnabled: true,
|
||||||
|
isLineNumbersDisplayed: true,
|
||||||
|
isActiveLineHighlighted: false,
|
||||||
|
isAutoCompletionEnabled: false,
|
||||||
|
isPatternHighlightingEnabled: true,
|
||||||
|
isFlashEnabled: true,
|
||||||
|
isTooltipEnabled: false,
|
||||||
|
isLineWrappingEnabled: false,
|
||||||
|
theme: 'strudelTheme',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 18,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const codemirrorSettings = persistentAtom('codemirror-settings', defaultSettings, {
|
||||||
|
encode: JSON.stringify,
|
||||||
|
decode: JSON.parse,
|
||||||
|
});
|
||||||
|
|
||||||
// https://codemirror.net/docs/guide/
|
// https://codemirror.net/docs/guide/
|
||||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
|
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root }) {
|
||||||
|
const settings = codemirrorSettings.get();
|
||||||
|
const initialSettings = Object.keys(compartments).map((key) =>
|
||||||
|
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
||||||
|
);
|
||||||
|
initTheme(settings.theme);
|
||||||
let state = EditorState.create({
|
let state = EditorState.create({
|
||||||
doc: initialCode,
|
doc: initialCode,
|
||||||
extensions: [
|
extensions: [
|
||||||
theme,
|
/* search(),
|
||||||
|
highlightSelectionMatches(), */
|
||||||
|
...initialSettings,
|
||||||
javascript(),
|
javascript(),
|
||||||
lineNumbers(),
|
sliderPlugin,
|
||||||
highlightExtension,
|
widgetPlugin,
|
||||||
highlightActiveLineGutter(),
|
// indentOnInput(), // works without. already brought with javascript extension?
|
||||||
|
// bracketMatching(), // does not do anything
|
||||||
syntaxHighlighting(defaultHighlightStyle),
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
keymap.of(defaultKeymap),
|
history(),
|
||||||
flashField,
|
|
||||||
EditorView.updateListener.of((v) => onChange(v)),
|
EditorView.updateListener.of((v) => onChange(v)),
|
||||||
keymap.of([
|
drawSelection({ cursorBlinkRate: 0 }),
|
||||||
{
|
Prec.highest(
|
||||||
key: 'Ctrl-Enter',
|
keymap.of([
|
||||||
run: () => onEvaluate(),
|
{
|
||||||
|
key: 'Ctrl-Enter',
|
||||||
|
run: () => onEvaluate?.(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Alt-Enter',
|
||||||
|
run: () => onEvaluate?.(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Ctrl-.',
|
||||||
|
run: () => onStop?.(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Alt-.',
|
||||||
|
run: (_, e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onStop?.();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/* {
|
||||||
|
key: 'Ctrl-Shift-.',
|
||||||
|
run: () => (onPanic ? onPanic() : onStop?.()),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Ctrl-.',
|
key: 'Ctrl-Shift-Enter',
|
||||||
run: () => onStop(),
|
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
|
||||||
},
|
}, */
|
||||||
]),
|
]),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,45 +124,81 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, the
|
|||||||
|
|
||||||
export class StrudelMirror {
|
export class StrudelMirror {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
|
const {
|
||||||
|
root,
|
||||||
|
id,
|
||||||
|
initialCode = '',
|
||||||
|
onDraw,
|
||||||
|
drawContext,
|
||||||
|
drawTime = [0, 0],
|
||||||
|
autodraw,
|
||||||
|
prebake,
|
||||||
|
bgFill = true,
|
||||||
|
...replOptions
|
||||||
|
} = options;
|
||||||
this.code = initialCode;
|
this.code = initialCode;
|
||||||
|
this.root = root;
|
||||||
|
this.miniLocations = [];
|
||||||
|
this.widgets = [];
|
||||||
|
this.painters = [];
|
||||||
|
this.drawTime = drawTime;
|
||||||
|
this.drawContext = drawContext;
|
||||||
|
this.onDraw = onDraw || this.draw;
|
||||||
|
this.id = id || s4();
|
||||||
|
|
||||||
this.drawer = new Drawer((haps, time) => {
|
this.drawer = new Drawer((haps, time) => {
|
||||||
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
|
const currentFrame = haps.filter((hap) => hap.isActive(time));
|
||||||
this.highlight(currentFrame, time);
|
this.highlight(currentFrame, time);
|
||||||
onDraw?.(haps, time, currentFrame);
|
this.onDraw(haps, time, this.painters);
|
||||||
}, drawTime);
|
}, drawTime);
|
||||||
|
|
||||||
const prebaked = prebake();
|
this.prebaked = prebake();
|
||||||
prebaked.then(async () => {
|
autodraw && this.drawFirstFrame();
|
||||||
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({
|
this.repl = repl({
|
||||||
...replOptions,
|
...replOptions,
|
||||||
onToggle: async (started) => {
|
onToggle: (started) => {
|
||||||
replOptions?.onToggle?.(started);
|
replOptions?.onToggle?.(started);
|
||||||
const { scheduler } = await this.repl;
|
|
||||||
if (started) {
|
if (started) {
|
||||||
this.drawer.start(scheduler);
|
this.adjustDrawTime();
|
||||||
|
this.drawer.start(this.repl.scheduler);
|
||||||
|
// stop other repls when this one is started
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent('start-repl', {
|
||||||
|
detail: this.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.drawer.stop();
|
this.drawer.stop();
|
||||||
|
updateMiniLocations(this.editor, []);
|
||||||
|
cleanupDraw(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeEval: async () => {
|
beforeEval: async () => {
|
||||||
await prebaked;
|
cleanupDraw();
|
||||||
|
this.painters = [];
|
||||||
|
const self = this;
|
||||||
|
// this is similar to repl.mjs > injectPatternMethods
|
||||||
|
// maybe there is a solution without prototype hacking, but hey, it works
|
||||||
|
// we need to do this befor every eval to make sure it works with multiple StrudelMirror's side by side
|
||||||
|
Pattern.prototype.onPaint = function (onPaint) {
|
||||||
|
self.painters.push(onPaint);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
await this.prebaked;
|
||||||
|
await replOptions?.beforeEval?.();
|
||||||
},
|
},
|
||||||
afterEval: (options) => {
|
afterEval: (options) => {
|
||||||
|
// remember for when highlighting is toggled on
|
||||||
|
this.miniLocations = options.meta?.miniLocations;
|
||||||
|
this.widgets = options.meta?.widgets;
|
||||||
|
const sliders = this.widgets.filter((w) => w.type === 'slider');
|
||||||
|
updateSliderWidgets(this.editor, sliders);
|
||||||
|
const widgets = this.widgets.filter((w) => w.type !== 'slider');
|
||||||
|
updateWidgets(this.editor, widgets);
|
||||||
|
updateMiniLocations(this.editor, this.miniLocations);
|
||||||
replOptions?.afterEval?.(options);
|
replOptions?.afterEval?.(options);
|
||||||
|
this.adjustDrawTime();
|
||||||
this.drawer.invalidate();
|
this.drawer.invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -89,25 +206,154 @@ export class StrudelMirror {
|
|||||||
root,
|
root,
|
||||||
initialCode,
|
initialCode,
|
||||||
onChange: (v) => {
|
onChange: (v) => {
|
||||||
this.code = v.state.doc.toString();
|
if (v.docChanged) {
|
||||||
|
this.code = v.state.doc.toString();
|
||||||
|
this.repl.setCode?.(this.code);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onEvaluate: () => this.evaluate(),
|
onEvaluate: () => this.evaluate(),
|
||||||
onStop: () => this.stop(),
|
onStop: () => this.stop(),
|
||||||
});
|
});
|
||||||
|
const cmEditor = this.root.querySelector('.cm-editor');
|
||||||
|
if (cmEditor) {
|
||||||
|
this.root.style.display = 'block';
|
||||||
|
if (bgFill) {
|
||||||
|
this.root.style.backgroundColor = 'var(--background)';
|
||||||
|
}
|
||||||
|
cmEditor.style.backgroundColor = 'transparent';
|
||||||
|
}
|
||||||
|
const settings = codemirrorSettings.get();
|
||||||
|
this.setFontSize(settings.fontSize);
|
||||||
|
this.setFontFamily(settings.fontFamily);
|
||||||
|
|
||||||
|
// stop this repl when another repl is started
|
||||||
|
this.onStartRepl = (e) => {
|
||||||
|
if (e.detail !== this.id) {
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('start-repl', this.onStartRepl);
|
||||||
|
}
|
||||||
|
// adjusts draw time depending on if there are painters
|
||||||
|
adjustDrawTime() {
|
||||||
|
// when no painters are set, [0,0] is enough (just highlighting)
|
||||||
|
this.drawer.setDrawTime(this.painters.length ? this.drawTime : [0, 0]);
|
||||||
|
}
|
||||||
|
draw(haps, time) {
|
||||||
|
this.painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
|
||||||
|
}
|
||||||
|
async drawFirstFrame() {
|
||||||
|
if (!this.onDraw) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// draw first frame instantly
|
||||||
|
await this.prebaked;
|
||||||
|
try {
|
||||||
|
await this.repl.evaluate(this.code, false);
|
||||||
|
this.drawer.invalidate(this.repl.scheduler, -0.001);
|
||||||
|
// draw at -0.001 to avoid haps at 0 to be visualized as active
|
||||||
|
this.onDraw?.(this.drawer.visibleHaps, -0.001, this.painters);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('first frame could not be painted');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async evaluate() {
|
async evaluate() {
|
||||||
const { evaluate } = await this.repl;
|
|
||||||
this.flash();
|
this.flash();
|
||||||
await evaluate(this.code);
|
await this.repl.evaluate(this.code);
|
||||||
}
|
}
|
||||||
async stop() {
|
async stop() {
|
||||||
const { scheduler } = await this.repl;
|
this.repl.scheduler.stop();
|
||||||
scheduler.stop();
|
}
|
||||||
|
async toggle() {
|
||||||
|
if (this.repl.scheduler.started) {
|
||||||
|
this.repl.stop();
|
||||||
|
} else {
|
||||||
|
this.evaluate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
flash(ms) {
|
flash(ms) {
|
||||||
flash(this.editor, ms);
|
flash(this.editor, ms);
|
||||||
}
|
}
|
||||||
highlight(haps, time) {
|
highlight(haps, time) {
|
||||||
highlightMiniLocations(this.editor.view, time, haps);
|
highlightMiniLocations(this.editor, time, haps);
|
||||||
|
}
|
||||||
|
setFontSize(size) {
|
||||||
|
this.root.style.fontSize = size + 'px';
|
||||||
|
}
|
||||||
|
setFontFamily(family) {
|
||||||
|
this.root.style.fontFamily = family;
|
||||||
|
const scroller = this.root.querySelector('.cm-scroller');
|
||||||
|
if (scroller) {
|
||||||
|
scroller.style.fontFamily = family;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reconfigureExtension(key, value) {
|
||||||
|
if (!extensions[key]) {
|
||||||
|
console.warn(`extension ${key} is not known`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
value = parseBooleans(value);
|
||||||
|
const newValue = extensions[key](value, this);
|
||||||
|
this.editor.dispatch({
|
||||||
|
effects: compartments[key].reconfigure(newValue),
|
||||||
|
});
|
||||||
|
if (key === 'theme') {
|
||||||
|
activateTheme(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLineWrappingEnabled(enabled) {
|
||||||
|
this.reconfigureExtension('isLineWrappingEnabled', enabled);
|
||||||
|
}
|
||||||
|
setBracketMatchingEnabled(enabled) {
|
||||||
|
this.reconfigureExtension('isBracketMatchingEnabled', enabled);
|
||||||
|
}
|
||||||
|
setLineNumbersDisplayed(enabled) {
|
||||||
|
this.reconfigureExtension('isLineNumbersDisplayed', enabled);
|
||||||
|
}
|
||||||
|
setBracketClosingEnabled(enabled) {
|
||||||
|
this.reconfigureExtension('isBracketClosingEnabled', enabled);
|
||||||
|
}
|
||||||
|
setTheme(theme) {
|
||||||
|
this.reconfigureExtension('theme', theme);
|
||||||
|
}
|
||||||
|
setAutocompletionEnabled(enabled) {
|
||||||
|
this.reconfigureExtension('isAutoCompletionEnabled', enabled);
|
||||||
|
}
|
||||||
|
updateSettings(settings) {
|
||||||
|
this.setFontSize(settings.fontSize);
|
||||||
|
this.setFontFamily(settings.fontFamily);
|
||||||
|
for (let key in extensions) {
|
||||||
|
this.reconfigureExtension(key, settings[key]);
|
||||||
|
}
|
||||||
|
const updated = { ...codemirrorSettings.get(), ...settings };
|
||||||
|
codemirrorSettings.set(updated);
|
||||||
|
}
|
||||||
|
changeSetting(key, value) {
|
||||||
|
if (extensions[key]) {
|
||||||
|
this.reconfigureExtension(key, value);
|
||||||
|
return;
|
||||||
|
} else if (key === 'fontFamily') {
|
||||||
|
this.setFontFamily(value);
|
||||||
|
} else if (key === 'fontSize') {
|
||||||
|
this.setFontSize(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCode(code) {
|
||||||
|
const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
|
||||||
|
this.editor.dispatch({ changes });
|
||||||
|
}
|
||||||
|
clear() {
|
||||||
|
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseBooleans(value) {
|
||||||
|
return { true: true, false: false }[value] ?? value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to generate repl ids
|
||||||
|
function s4() {
|
||||||
|
return Math.floor((1 + Math.random()) * 0x10000)
|
||||||
|
.toString(16)
|
||||||
|
.substring(1);
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,3 +33,5 @@ export const flash = (view, ms = 200) => {
|
|||||||
view.dispatch({ effects: setFlash.of(false) });
|
view.dispatch({ effects: setFlash.of(false) });
|
||||||
}, ms);
|
}, ms);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isFlashEnabled = (on) => (on ? flashField : []);
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ const visibleMiniLocations = StateField.define({
|
|||||||
// this is why we need to find a way to update the existing decorations, showing the ones that have an active range
|
// this is why we need to find a way to update the existing decorations, showing the ones that have an active range
|
||||||
const haps = new Map();
|
const haps = new Map();
|
||||||
for (let hap of e.value.haps) {
|
for (let hap of e.value.haps) {
|
||||||
|
if (!hap.context?.locations || !hap.whole) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for (let { start, end } of hap.context.locations) {
|
for (let { start, end } of hap.context.locations) {
|
||||||
let id = `${start}:${end}`;
|
let id = `${start}:${end}`;
|
||||||
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
|
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
|
||||||
@@ -64,7 +67,6 @@ const visibleMiniLocations = StateField.define({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
visible = { atTime: e.value.atTime, haps };
|
visible = { atTime: e.value.atTime, haps };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +92,7 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
|
|||||||
|
|
||||||
if (haps.has(id)) {
|
if (haps.has(id)) {
|
||||||
const hap = haps.get(id);
|
const hap = haps.get(id);
|
||||||
const color = hap.context.color ?? 'var(--foreground)';
|
const color = hap.value?.color ?? 'var(--foreground)';
|
||||||
// Get explicit channels for color values
|
// Get explicit channels for color values
|
||||||
/*
|
/*
|
||||||
const swatch = document.createElement('div');
|
const swatch = document.createElement('div');
|
||||||
@@ -124,3 +126,12 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
|
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
|
||||||
|
|
||||||
|
export const isPatternHighlightingEnabled = (on, config) => {
|
||||||
|
on &&
|
||||||
|
config &&
|
||||||
|
setTimeout(() => {
|
||||||
|
updateMiniLocations(config.editor, config.miniLocations);
|
||||||
|
}, 100);
|
||||||
|
return on ? highlightExtension : [];
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
|
||||||
|
export let html = (string) => {
|
||||||
|
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
|
||||||
|
};
|
||||||
|
let parseChunk = (chunk) => {
|
||||||
|
if (Array.isArray(chunk)) return chunk.flat().join('');
|
||||||
|
if (chunk === undefined) return '';
|
||||||
|
return chunk;
|
||||||
|
};
|
||||||
|
export let h = (strings, ...vars) => {
|
||||||
|
let string = '';
|
||||||
|
for (let i in strings) {
|
||||||
|
string += parseChunk(strings[i]);
|
||||||
|
string += parseChunk(vars[i]);
|
||||||
|
}
|
||||||
|
return html(string);
|
||||||
|
};
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
export * from './codemirror.mjs';
|
export * from './codemirror.mjs';
|
||||||
export * from './highlight.mjs';
|
export * from './highlight.mjs';
|
||||||
export * from './flash.mjs';
|
export * from './flash.mjs';
|
||||||
|
export * from './slider.mjs';
|
||||||
|
export * from './themes.mjs';
|
||||||
|
export * from './widget.mjs';
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Prec } from '@codemirror/state';
|
||||||
|
import { keymap, ViewPlugin } from '@codemirror/view';
|
||||||
|
// import { searchKeymap } from '@codemirror/search';
|
||||||
|
import { emacs } from '@replit/codemirror-emacs';
|
||||||
|
import { vim } from '@replit/codemirror-vim';
|
||||||
|
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||||
|
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
|
||||||
|
|
||||||
|
const vscodePlugin = ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
constructor() {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: () => {
|
||||||
|
return Prec.highest(keymap.of([...vscodeKeymap]));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
||||||
|
|
||||||
|
const keymaps = {
|
||||||
|
vim,
|
||||||
|
emacs,
|
||||||
|
vscode: vscodeExtension,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function keybindings(name) {
|
||||||
|
const active = keymaps[name];
|
||||||
|
return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []];
|
||||||
|
// keymap.of(searchKeymap),
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/codemirror",
|
"name": "@strudel/codemirror",
|
||||||
"version": "0.9.0",
|
"version": "1.0.1",
|
||||||
"description": "Codemirror Extensions for Strudel",
|
"description": "Codemirror Extensions for Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.mjs"
|
||||||
"module": "dist/index.mjs"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@@ -33,15 +32,26 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/commands": "^6.2.4",
|
"@codemirror/autocomplete": "^6.11.1",
|
||||||
"@codemirror/lang-javascript": "^6.1.7",
|
"@codemirror/commands": "^6.3.3",
|
||||||
"@codemirror/language": "^6.6.0",
|
"@codemirror/lang-javascript": "^6.2.1",
|
||||||
"@codemirror/state": "^6.2.0",
|
"@codemirror/language": "^6.10.0",
|
||||||
"@codemirror/view": "^6.10.0",
|
"@codemirror/search": "^6.5.5",
|
||||||
"@lezer/highlight": "^1.1.4",
|
"@codemirror/state": "^6.4.0",
|
||||||
"@strudel.cycles/core": "workspace:*"
|
"@codemirror/view": "^6.23.0",
|
||||||
|
"@lezer/highlight": "^1.2.0",
|
||||||
|
"@nanostores/persistent": "^0.9.1",
|
||||||
|
"@replit/codemirror-emacs": "^6.0.1",
|
||||||
|
"@replit/codemirror-vim": "^6.1.0",
|
||||||
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||||
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/draw": "workspace:*",
|
||||||
|
"@strudel/transpiler": "workspace:*",
|
||||||
|
"@uiw/codemirror-themes": "^4.21.21",
|
||||||
|
"@uiw/codemirror-themes-all": "^4.21.21",
|
||||||
|
"nanostores": "^0.9.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.3.3"
|
"vite": "^5.0.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { ref, pure } from '@strudel/core';
|
||||||
|
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
|
||||||
|
import { StateEffect } from '@codemirror/state';
|
||||||
|
|
||||||
|
export let sliderValues = {};
|
||||||
|
const getSliderID = (from) => `slider_${from}`;
|
||||||
|
|
||||||
|
export class SliderWidget extends WidgetType {
|
||||||
|
constructor(value, min, max, from, to, step, view) {
|
||||||
|
super();
|
||||||
|
this.value = value;
|
||||||
|
this.min = min;
|
||||||
|
this.max = max;
|
||||||
|
this.from = from;
|
||||||
|
this.originalFrom = from;
|
||||||
|
this.to = to;
|
||||||
|
this.step = step;
|
||||||
|
this.view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
eq() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
toDOM() {
|
||||||
|
let wrap = document.createElement('span');
|
||||||
|
wrap.setAttribute('aria-hidden', 'true');
|
||||||
|
wrap.className = 'cm-slider'; // inline-flex items-center
|
||||||
|
let slider = wrap.appendChild(document.createElement('input'));
|
||||||
|
slider.type = 'range';
|
||||||
|
slider.min = this.min;
|
||||||
|
slider.max = this.max;
|
||||||
|
slider.step = this.step ?? (this.max - this.min) / 1000;
|
||||||
|
slider.originalValue = this.value;
|
||||||
|
// to make sure the code stays in sync, let's save the original value
|
||||||
|
// becuase .value automatically clamps values so it'll desync with the code
|
||||||
|
slider.value = slider.originalValue;
|
||||||
|
slider.from = this.from;
|
||||||
|
slider.originalFrom = this.originalFrom;
|
||||||
|
slider.to = this.to;
|
||||||
|
slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
|
||||||
|
this.slider = slider;
|
||||||
|
slider.addEventListener('input', (e) => {
|
||||||
|
const next = e.target.value;
|
||||||
|
let insert = next;
|
||||||
|
//let insert = next.toFixed(2);
|
||||||
|
const to = slider.from + slider.originalValue.length;
|
||||||
|
let change = { from: slider.from, to, insert };
|
||||||
|
slider.originalValue = insert;
|
||||||
|
slider.value = insert;
|
||||||
|
this.view.dispatch({ changes: change });
|
||||||
|
const id = getSliderID(slider.originalFrom); // matches id generated in transpiler
|
||||||
|
window.postMessage({ type: 'cm-slider', value: Number(next), id });
|
||||||
|
});
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreEvent(e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setSliderWidgets = StateEffect.define();
|
||||||
|
|
||||||
|
export const updateSliderWidgets = (view, widgets) => {
|
||||||
|
view.dispatch({ effects: setSliderWidgets.of(widgets) });
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSliders(widgetConfigs, view) {
|
||||||
|
return widgetConfigs
|
||||||
|
.filter((w) => w.type === 'slider')
|
||||||
|
.map(({ from, to, value, min, max, step }) => {
|
||||||
|
return Decoration.widget({
|
||||||
|
widget: new SliderWidget(value, min, max, from, to, step, view),
|
||||||
|
side: 0,
|
||||||
|
}).range(from /* , to */);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sliderPlugin = ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
decorations; //: DecorationSet
|
||||||
|
|
||||||
|
constructor(view /* : EditorView */) {
|
||||||
|
this.decorations = Decoration.set([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(update /* : ViewUpdate */) {
|
||||||
|
update.transactions.forEach((tr) => {
|
||||||
|
if (tr.docChanged) {
|
||||||
|
this.decorations = this.decorations.map(tr.changes);
|
||||||
|
const iterator = this.decorations.iter();
|
||||||
|
while (iterator.value) {
|
||||||
|
// when the widgets are moved, we need to tell the dom node the current position
|
||||||
|
// this is important because the updateSliderValue function has to work with the dom node
|
||||||
|
if (iterator.value?.widget?.slider) {
|
||||||
|
iterator.value.widget.slider.from = iterator.from;
|
||||||
|
iterator.value.widget.slider.to = iterator.to;
|
||||||
|
}
|
||||||
|
iterator.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let e of tr.effects) {
|
||||||
|
if (e.is(setSliderWidgets)) {
|
||||||
|
this.decorations = Decoration.set(getSliders(e.value, update.view));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decorations: (v) => v.decorations,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a slider widget to allow the user manipulate a value
|
||||||
|
*
|
||||||
|
* @name slider
|
||||||
|
* @param {number} value Initial value
|
||||||
|
* @param {number} min Minimum value - optional, defaults to 0
|
||||||
|
* @param {number} max Maximum value - optional, defaults to 1
|
||||||
|
* @param {number} step Step size - optional
|
||||||
|
*/
|
||||||
|
export let slider = (value) => {
|
||||||
|
console.warn('slider will only work when the transpiler is used... passing value as is');
|
||||||
|
return pure(value);
|
||||||
|
};
|
||||||
|
// function transpiled from slider = (value, min, max)
|
||||||
|
export let sliderWithID = (id, value, min, max) => {
|
||||||
|
sliderValues[id] = value; // sync state at eval time (code -> state)
|
||||||
|
return ref(() => sliderValues[id]); // use state at query time
|
||||||
|
};
|
||||||
|
// update state when sliders are moved
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('message', (e) => {
|
||||||
|
if (e.data.type === 'cm-slider') {
|
||||||
|
if (sliderValues[e.data.id] !== undefined) {
|
||||||
|
// update state when slider is moved
|
||||||
|
sliderValues[e.data.id] = e.data.value;
|
||||||
|
} else {
|
||||||
|
console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -30,13 +30,14 @@ import {
|
|||||||
xcodeLight,
|
xcodeLight,
|
||||||
} from '@uiw/codemirror-themes-all';
|
} from '@uiw/codemirror-themes-all';
|
||||||
|
|
||||||
import strudelTheme from '@strudel.cycles/react/src/themes/strudel-theme';
|
import strudelTheme from './themes/strudel-theme';
|
||||||
import bluescreen, { settings as bluescreenSettings } from '@strudel.cycles/react/src/themes/bluescreen';
|
import bluescreen, { settings as bluescreenSettings } from './themes/bluescreen';
|
||||||
import blackscreen, { settings as blackscreenSettings } from '@strudel.cycles/react/src/themes/blackscreen';
|
import blackscreen, { settings as blackscreenSettings } from './themes/blackscreen';
|
||||||
import whitescreen, { settings as whitescreenSettings } from '@strudel.cycles/react/src/themes/whitescreen';
|
import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen';
|
||||||
import teletext, { settings as teletextSettings } from '@strudel.cycles/react/src/themes/teletext';
|
import teletext, { settings as teletextSettings } from './themes/teletext';
|
||||||
import algoboy, { settings as algoboySettings } from '@strudel.cycles/react/src/themes/algoboy';
|
import algoboy, { settings as algoboySettings } from './themes/algoboy';
|
||||||
import terminal, { settings as terminalSettings } from '@strudel.cycles/react/src/themes/terminal';
|
import terminal, { settings as terminalSettings } from './themes/terminal';
|
||||||
|
import { setTheme } from '@strudel/draw';
|
||||||
|
|
||||||
export const themes = {
|
export const themes = {
|
||||||
strudelTheme,
|
strudelTheme,
|
||||||
@@ -473,6 +474,9 @@ function stringifySafe(json) {
|
|||||||
return JSON.stringify(json, getCircularReplacer());
|
return JSON.stringify(json, getCircularReplacer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const theme = (theme) => themes[theme] || themes.strudelTheme;
|
||||||
|
|
||||||
|
// css style injection helpers
|
||||||
export function injectStyle(rule) {
|
export function injectStyle(rule) {
|
||||||
const newStyle = document.createElement('style');
|
const newStyle = document.createElement('style');
|
||||||
document.head.appendChild(newStyle);
|
document.head.appendChild(newStyle);
|
||||||
@@ -480,3 +484,46 @@ export function injectStyle(rule) {
|
|||||||
const ruleIndex = styleSheet.insertRule(rule, 0);
|
const ruleIndex = styleSheet.insertRule(rule, 0);
|
||||||
return () => styleSheet.deleteRule(ruleIndex);
|
return () => styleSheet.deleteRule(ruleIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentTheme,
|
||||||
|
resetThemeStyle,
|
||||||
|
themeStyle,
|
||||||
|
styleID = 'strudel-theme-vars';
|
||||||
|
export function initTheme(theme) {
|
||||||
|
if (!document.getElementById(styleID)) {
|
||||||
|
themeStyle = document.createElement('style');
|
||||||
|
themeStyle.id = styleID;
|
||||||
|
document.head.append(themeStyle);
|
||||||
|
}
|
||||||
|
activateTheme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activateTheme(name) {
|
||||||
|
if (currentTheme === name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentTheme = name;
|
||||||
|
if (!settings[name]) {
|
||||||
|
console.warn('theme', name, 'has no settings.. defaulting to strudelTheme settings');
|
||||||
|
}
|
||||||
|
const themeSettings = settings[name] || settings.strudelTheme;
|
||||||
|
// set css variables
|
||||||
|
themeStyle.innerHTML = `:root {
|
||||||
|
${Object.entries(themeSettings)
|
||||||
|
// important to override fallback
|
||||||
|
.map(([key, value]) => `--${key}: ${value} !important;`)
|
||||||
|
.join('\n')}
|
||||||
|
}`;
|
||||||
|
setTheme(themeSettings);
|
||||||
|
// tailwind dark mode
|
||||||
|
if (themeSettings.light) {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
resetThemeStyle?.();
|
||||||
|
resetThemeStyle = undefined;
|
||||||
|
if (themeSettings.customStyle) {
|
||||||
|
resetThemeStyle = injectStyle(themeSettings.customStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ export default createTheme({
|
|||||||
theme: 'light',
|
theme: 'light',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
|
{ tag: t.labelName, color: '#0f380f' },
|
||||||
{ tag: t.keyword, color: '#0f380f' },
|
{ tag: t.keyword, color: '#0f380f' },
|
||||||
{ tag: t.operator, color: '#0f380f' },
|
{ tag: t.operator, color: '#0f380f' },
|
||||||
{ tag: t.special(t.variableName), color: '#0f380f' },
|
{ tag: t.special(t.variableName), color: '#0f380f' },
|
||||||
+1
@@ -15,6 +15,7 @@ export default createTheme({
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
|
{ tag: t.labelName, color: 'white' },
|
||||||
{ tag: t.keyword, color: 'white' },
|
{ tag: t.keyword, color: 'white' },
|
||||||
{ tag: t.operator, color: 'white' },
|
{ tag: t.operator, color: 'white' },
|
||||||
{ tag: t.special(t.variableName), color: 'white' },
|
{ tag: t.special(t.variableName), color: 'white' },
|
||||||
+1
@@ -18,6 +18,7 @@ export default createTheme({
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
|
{ tag: t.labelName, color: 'white' },
|
||||||
{ tag: t.keyword, color: 'white' },
|
{ tag: t.keyword, color: 'white' },
|
||||||
{ tag: t.operator, color: 'white' },
|
{ tag: t.operator, color: 'white' },
|
||||||
{ tag: t.special(t.variableName), color: 'white' },
|
{ tag: t.special(t.variableName), color: 'white' },
|
||||||
-139
@@ -1,139 +0,0 @@
|
|||||||
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)];
|
|
||||||
+1
@@ -15,6 +15,7 @@ export default createTheme({
|
|||||||
gutterForeground: '#8a919966',
|
gutterForeground: '#8a919966',
|
||||||
},
|
},
|
||||||
styles: [
|
styles: [
|
||||||
|
{ tag: t.labelName, color: '#89ddff' },
|
||||||
{ tag: t.keyword, color: '#c792ea' },
|
{ tag: t.keyword, color: '#c792ea' },
|
||||||
{ tag: t.operator, color: '#89ddff' },
|
{ tag: t.operator, color: '#89ddff' },
|
||||||
{ tag: t.special(t.variableName), color: '#eeffff' },
|
{ tag: t.special(t.variableName), color: '#eeffff' },
|
||||||
+1
@@ -27,6 +27,7 @@ export default createTheme({
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
|
{ tag: t.labelName, color: colorB },
|
||||||
{ tag: t.keyword, color: colorA },
|
{ tag: t.keyword, color: colorA },
|
||||||
{ tag: t.operator, color: mini },
|
{ tag: t.operator, color: mini },
|
||||||
{ tag: t.special(t.variableName), color: colorA },
|
{ tag: t.special(t.variableName), color: colorA },
|
||||||
+1
@@ -14,6 +14,7 @@ export default createTheme({
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
|
{ tag: t.labelName, color: '#41FF00' },
|
||||||
{ tag: t.keyword, color: '#41FF00' },
|
{ tag: t.keyword, color: '#41FF00' },
|
||||||
{ tag: t.operator, color: '#41FF00' },
|
{ tag: t.operator, color: '#41FF00' },
|
||||||
{ tag: t.special(t.variableName), color: '#41FF00' },
|
{ tag: t.special(t.variableName), color: '#41FF00' },
|
||||||
+1
@@ -16,6 +16,7 @@ export default createTheme({
|
|||||||
theme: 'light',
|
theme: 'light',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
|
{ tag: t.labelName, color: 'black' },
|
||||||
{ tag: t.keyword, color: 'black' },
|
{ tag: t.keyword, color: 'black' },
|
||||||
{ tag: t.operator, color: 'black' },
|
{ tag: t.operator, color: 'black' },
|
||||||
{ tag: t.special(t.variableName), color: 'black' },
|
{ tag: t.special(t.variableName), color: 'black' },
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { hoverTooltip } from '@codemirror/view';
|
||||||
|
import jsdoc from '../../doc.json';
|
||||||
|
import { Autocomplete } from './autocomplete.mjs';
|
||||||
|
|
||||||
|
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||||
|
|
||||||
|
let ctrlDown = false;
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// Record Control key event to trigger or block the tooltip depending on the state
|
||||||
|
window.addEventListener(
|
||||||
|
'keyup',
|
||||||
|
function (e) {
|
||||||
|
if (e.key == 'Control') {
|
||||||
|
ctrlDown = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
window.addEventListener(
|
||||||
|
'keydown',
|
||||||
|
function (e) {
|
||||||
|
if (e.key == 'Control') {
|
||||||
|
ctrlDown = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const strudelTooltip = hoverTooltip(
|
||||||
|
(view, pos, side) => {
|
||||||
|
// Word selection from CodeMirror Hover Tooltip example https://codemirror.net/examples/tooltip/#hover-tooltips
|
||||||
|
if (!ctrlDown) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let { from, to, text } = view.state.doc.lineAt(pos);
|
||||||
|
let start = pos,
|
||||||
|
end = pos;
|
||||||
|
while (start > from && /\w/.test(text[start - from - 1])) {
|
||||||
|
start--;
|
||||||
|
}
|
||||||
|
while (end < to && /\w/.test(text[end - from])) {
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
if ((start == pos && side < 0) || (end == pos && side > 0)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let word = text.slice(start - from, end - from);
|
||||||
|
// Get entry from Strudel documentation
|
||||||
|
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
||||||
|
if (!entry) {
|
||||||
|
// Try for synonyms
|
||||||
|
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||||
|
if (!entry) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pos: start,
|
||||||
|
end,
|
||||||
|
above: false,
|
||||||
|
arrow: true,
|
||||||
|
create(view) {
|
||||||
|
let dom = document.createElement('div');
|
||||||
|
dom.className = 'strudel-tooltip';
|
||||||
|
const ac = Autocomplete({ doc: entry, label: word });
|
||||||
|
dom.appendChild(ac);
|
||||||
|
return { dom };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ hoverTime: 10 },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const isTooltipEnabled = (on) => (on ? strudelTooltip : []);
|
||||||
@@ -8,8 +8,8 @@ export default defineConfig({
|
|||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'index.mjs'),
|
entry: resolve(__dirname, 'index.mjs'),
|
||||||
formats: ['es', 'cjs'],
|
formats: ['es'],
|
||||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: [...Object.keys(dependencies)],
|
external: [...Object.keys(dependencies)],
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { StateEffect, StateField } from '@codemirror/state';
|
||||||
|
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
||||||
|
import { getWidgetID, registerWidgetType } from '@strudel/transpiler';
|
||||||
|
import { Pattern } from '@strudel/core';
|
||||||
|
|
||||||
|
export const addWidget = StateEffect.define({
|
||||||
|
map: ({ from, to }, change) => {
|
||||||
|
return { from: change.mapPos(from), to: change.mapPos(to) };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateWidgets = (view, widgets) => {
|
||||||
|
view.dispatch({ effects: addWidget.of(widgets) });
|
||||||
|
};
|
||||||
|
|
||||||
|
function getWidgets(widgetConfigs) {
|
||||||
|
return (
|
||||||
|
widgetConfigs
|
||||||
|
// codemirror throws an error if we don't sort
|
||||||
|
.sort((a, b) => a.to - b.to)
|
||||||
|
.map((widgetConfig) => {
|
||||||
|
return Decoration.widget({
|
||||||
|
widget: new BlockWidget(widgetConfig),
|
||||||
|
side: 0,
|
||||||
|
block: true,
|
||||||
|
}).range(widgetConfig.to);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgetField = StateField.define(
|
||||||
|
/* <DecorationSet> */ {
|
||||||
|
create() {
|
||||||
|
return Decoration.none;
|
||||||
|
},
|
||||||
|
update(widgets, tr) {
|
||||||
|
widgets = widgets.map(tr.changes);
|
||||||
|
for (let e of tr.effects) {
|
||||||
|
if (e.is(addWidget)) {
|
||||||
|
try {
|
||||||
|
widgets = widgets.update({
|
||||||
|
filter: () => false,
|
||||||
|
add: getWidgets(e.value),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('err', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return widgets;
|
||||||
|
},
|
||||||
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const widgetElements = {};
|
||||||
|
export function setWidget(id, el) {
|
||||||
|
widgetElements[id] = el;
|
||||||
|
el.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BlockWidget extends WidgetType {
|
||||||
|
constructor(widgetConfig) {
|
||||||
|
super();
|
||||||
|
this.widgetConfig = widgetConfig;
|
||||||
|
}
|
||||||
|
eq() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
toDOM() {
|
||||||
|
const id = getWidgetID(this.widgetConfig);
|
||||||
|
const el = widgetElements[id];
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
ignoreEvent(e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const widgetPlugin = [widgetField];
|
||||||
|
|
||||||
|
// widget implementer API to create a new widget type
|
||||||
|
export function registerWidget(type, fn) {
|
||||||
|
registerWidgetType(type);
|
||||||
|
if (fn) {
|
||||||
|
Pattern.prototype[type] = function (id, options = { fold: 1 }) {
|
||||||
|
// fn is expected to create a dom element and call setWidget(id, el);
|
||||||
|
// fn should also return the pattern
|
||||||
|
return fn(id, options, this);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wire up @strudel/draw functions
|
||||||
|
|
||||||
|
function getCanvasWidget(id, options = {}) {
|
||||||
|
const { width = 500, height = 60, pixelRatio = window.devicePixelRatio } = options;
|
||||||
|
let canvas = document.getElementById(id) || document.createElement('canvas');
|
||||||
|
canvas.width = width * pixelRatio;
|
||||||
|
canvas.height = height * pixelRatio;
|
||||||
|
canvas.style.width = width + 'px';
|
||||||
|
canvas.style.height = height + 'px';
|
||||||
|
setWidget(id, canvas);
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerWidget('_pianoroll', (id, options = {}, pat) => {
|
||||||
|
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||||
|
return pat.tag(id).pianoroll({ fold: 1, ...options, ctx, id });
|
||||||
|
});
|
||||||
|
|
||||||
|
registerWidget('_punchcard', (id, options = {}, pat) => {
|
||||||
|
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||||
|
return pat.tag(id).punchcard({ fold: 1, ...options, ctx, id });
|
||||||
|
});
|
||||||
|
|
||||||
|
registerWidget('_spiral', (id, options = {}, pat) => {
|
||||||
|
let _size = options.size || 275;
|
||||||
|
options = { width: _size, height: _size, ...options, size: _size / 5 };
|
||||||
|
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||||
|
return pat.tag(id).spiral({ ...options, ctx, id });
|
||||||
|
});
|
||||||
|
|
||||||
|
registerWidget('_scope', (id, options = {}, pat) => {
|
||||||
|
options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options };
|
||||||
|
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||||
|
return pat.tag(id).scope({ ...options, ctx, id });
|
||||||
|
});
|
||||||
|
|
||||||
|
registerWidget('_pitchwheel', (id, options = {}, pat) => {
|
||||||
|
let _size = options.size || 200;
|
||||||
|
options = { width: _size, height: _size, ...options, size: _size / 5 };
|
||||||
|
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||||
|
return pat.pitchwheel({ ...options, ctx, id });
|
||||||
|
});
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
# @strudel.cycles/core
|
# @strudel/core
|
||||||
|
|
||||||
This package contains the bare essence of strudel.
|
This package contains the bare essence of strudel.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm i @strudel.cycles/core --save
|
npm i @strudel/core --save
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { sequence } from '@strudel.cycles/core';
|
import { sequence } from '@strudel/core';
|
||||||
|
|
||||||
const pattern = sequence('a', ['b', 'c']);
|
const pattern = sequence('a', ['b', 'c']);
|
||||||
|
|
||||||
@@ -33,7 +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-forked-9ywhv7?file=/src/index.js).
|
- [play with @strudel/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/main/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/main/packages/core/examples/vanilla.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/)
|
- [open minimal vite example](./examples/vite-vanilla-repl/)
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
// TODO: swap below line with above one when firefox supports esm imports in service workers
|
||||||
|
// see https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker?retiredLocale=de#browser_compatibility
|
||||||
|
// import createClock from './zyklus.mjs';
|
||||||
|
|
||||||
|
function getTime() {
|
||||||
|
const precision = 10 ** 4;
|
||||||
|
const seconds = performance.now() / 1000;
|
||||||
|
return Math.round(seconds * precision) / precision;
|
||||||
|
}
|
||||||
|
|
||||||
|
let num_cycles_at_cps_change = 0;
|
||||||
|
let num_ticks_since_cps_change = 0;
|
||||||
|
let num_seconds_at_cps_change = 0;
|
||||||
|
let cps = 0.5;
|
||||||
|
// {id: {started: boolean}}
|
||||||
|
const clients = new Map();
|
||||||
|
const duration = 0.1;
|
||||||
|
const channel = new BroadcastChannel('strudeltick');
|
||||||
|
|
||||||
|
const sendMessage = (type, payload) => {
|
||||||
|
channel.postMessage({ type, payload });
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendTick = (phase, duration, tick, time) => {
|
||||||
|
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
|
||||||
|
|
||||||
|
const tickdeadline = phase - time;
|
||||||
|
const lastTick = time + tickdeadline;
|
||||||
|
const num_cycles_since_cps_change = num_seconds_since_cps_change * cps;
|
||||||
|
|
||||||
|
const begin = num_cycles_at_cps_change + num_cycles_since_cps_change;
|
||||||
|
const secondsSinceLastTick = time - lastTick - duration;
|
||||||
|
|
||||||
|
const eventLength = duration * cps;
|
||||||
|
const end = begin + eventLength;
|
||||||
|
|
||||||
|
const cycle = begin + secondsSinceLastTick * cps;
|
||||||
|
|
||||||
|
sendMessage('tick', {
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
cps,
|
||||||
|
tickdeadline,
|
||||||
|
num_cycles_at_cps_change,
|
||||||
|
num_seconds_at_cps_change,
|
||||||
|
num_seconds_since_cps_change,
|
||||||
|
cycle,
|
||||||
|
});
|
||||||
|
num_ticks_since_cps_change++;
|
||||||
|
};
|
||||||
|
|
||||||
|
//create clock method from zyklus
|
||||||
|
const clock = createClock(getTime, sendTick, duration);
|
||||||
|
let started = false;
|
||||||
|
|
||||||
|
const startClock = (id) => {
|
||||||
|
clients.set(id, { started: true });
|
||||||
|
if (started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clock.start();
|
||||||
|
started = true;
|
||||||
|
};
|
||||||
|
const stopClock = async (id) => {
|
||||||
|
clients.set(id, { started: false });
|
||||||
|
|
||||||
|
const otherClientStarted = Array.from(clients.values()).some((c) => c.started);
|
||||||
|
//dont stop the clock if other instances are running...
|
||||||
|
if (!started || otherClientStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clock.stop();
|
||||||
|
setCycle(0);
|
||||||
|
started = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCycle = (cycle) => {
|
||||||
|
num_ticks_since_cps_change = 0;
|
||||||
|
num_cycles_at_cps_change = cycle;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processMessage = (message) => {
|
||||||
|
const { type, payload } = message;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'cpschange': {
|
||||||
|
if (payload.cps !== cps) {
|
||||||
|
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
|
||||||
|
num_cycles_at_cps_change = num_cycles_at_cps_change + num_seconds_since_cps_change * cps;
|
||||||
|
num_seconds_at_cps_change = num_seconds_at_cps_change + num_seconds_since_cps_change;
|
||||||
|
cps = payload.cps;
|
||||||
|
num_ticks_since_cps_change = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'setcycle': {
|
||||||
|
setCycle(payload.cycle);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'toggle': {
|
||||||
|
if (payload.started) {
|
||||||
|
startClock(message.id);
|
||||||
|
} else {
|
||||||
|
stopClock(message.id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.onconnect = function (e) {
|
||||||
|
// the incoming port
|
||||||
|
const port = e.ports[0];
|
||||||
|
|
||||||
|
port.addEventListener('message', function (e) {
|
||||||
|
processMessage(e.data);
|
||||||
|
});
|
||||||
|
port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
|
||||||
|
};
|
||||||
|
|
||||||
|
// used to consistently schedule events, for use in a service worker - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/clockworker.mjs>
|
||||||
|
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, t);
|
||||||
|
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 };
|
||||||
|
}
|
||||||
+1545
-1190
File diff suppressed because it is too large
Load Diff
+46
-16
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
cyclist.mjs - <short description TODO>
|
cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.mjs>
|
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/>.
|
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/>.
|
||||||
*/
|
*/
|
||||||
@@ -8,38 +8,55 @@ import createClock from './zyklus.mjs';
|
|||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
export class Cyclist {
|
export class Cyclist {
|
||||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) {
|
||||||
this.started = false;
|
this.started = false;
|
||||||
this.cps = 1;
|
this.cps = 0.5;
|
||||||
|
this.num_ticks_since_cps_change = 0;
|
||||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||||
this.lastBegin = 0; // query begin of last tick
|
this.lastBegin = 0; // query begin of last tick
|
||||||
this.lastEnd = 0; // query end of last tick
|
this.lastEnd = 0; // query end of last tick
|
||||||
this.getTime = getTime; // get absolute time
|
this.getTime = getTime; // get absolute time
|
||||||
|
this.num_cycles_at_cps_change = 0;
|
||||||
|
this.seconds_at_cps_change; // clock phase when cps was changed
|
||||||
this.onToggle = onToggle;
|
this.onToggle = onToggle;
|
||||||
this.latency = latency; // fixed trigger time offset
|
this.latency = latency; // fixed trigger time offset
|
||||||
const round = (x) => Math.round(x * 1000) / 1000;
|
|
||||||
this.clock = createClock(
|
this.clock = createClock(
|
||||||
getTime,
|
getTime,
|
||||||
// called slightly before each cycle
|
// called slightly before each cycle
|
||||||
(phase, duration, tick) => {
|
(phase, duration, _, t) => {
|
||||||
if (tick === 0) {
|
if (this.num_ticks_since_cps_change === 0) {
|
||||||
this.origin = phase;
|
this.num_cycles_at_cps_change = this.lastEnd;
|
||||||
|
this.seconds_at_cps_change = phase;
|
||||||
}
|
}
|
||||||
|
this.num_ticks_since_cps_change++;
|
||||||
|
const seconds_since_cps_change = this.num_ticks_since_cps_change * duration;
|
||||||
|
const num_cycles_since_cps_change = seconds_since_cps_change * this.cps;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const time = getTime();
|
|
||||||
const begin = this.lastEnd;
|
const begin = this.lastEnd;
|
||||||
this.lastBegin = begin;
|
this.lastBegin = begin;
|
||||||
const end = round(begin + duration * this.cps);
|
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
|
||||||
this.lastEnd = end;
|
this.lastEnd = end;
|
||||||
const haps = this.pattern.queryArc(begin, end);
|
this.lastTick = phase;
|
||||||
const tickdeadline = phase - time; // time left till phase begins
|
|
||||||
this.lastTick = time + tickdeadline;
|
if (phase < t) {
|
||||||
|
// avoid querying haps that are in the past anyway
|
||||||
|
console.log(`skip query: too late`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// query the pattern for events
|
||||||
|
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||||
|
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
if (hap.part.begin.equals(hap.whole.begin)) {
|
if (hap.hasOnset()) {
|
||||||
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
|
const targetTime =
|
||||||
|
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
||||||
const duration = hap.duration / this.cps;
|
const duration = hap.duration / this.cps;
|
||||||
onTrigger?.(hap, deadline, duration, this.cps);
|
// the following line is dumb and only here for backwards compatibility
|
||||||
|
// see https://github.com/tidalcycles/strudel/pull/1004
|
||||||
|
const deadline = targetTime - phase;
|
||||||
|
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -48,9 +65,16 @@ export class Cyclist {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
interval, // duration of each cycle
|
interval, // duration of each cycle
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
setInterval,
|
||||||
|
clearInterval,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
now() {
|
now() {
|
||||||
|
if (!this.started) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
|
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
|
||||||
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
|
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
|
||||||
}
|
}
|
||||||
@@ -59,6 +83,8 @@ export class Cyclist {
|
|||||||
this.onToggle?.(v);
|
this.onToggle?.(v);
|
||||||
}
|
}
|
||||||
start() {
|
start() {
|
||||||
|
this.num_ticks_since_cps_change = 0;
|
||||||
|
this.num_cycles_at_cps_change = 0;
|
||||||
if (!this.pattern) {
|
if (!this.pattern) {
|
||||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||||
}
|
}
|
||||||
@@ -83,8 +109,12 @@ export class Cyclist {
|
|||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setCps(cps = 1) {
|
setCps(cps = 0.5) {
|
||||||
|
if (this.cps === cps) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.cps = cps;
|
this.cps = cps;
|
||||||
|
this.num_ticks_since_cps_change = 0;
|
||||||
}
|
}
|
||||||
log(begin, end, haps) {
|
log(begin, end, haps) {
|
||||||
const onsets = haps.filter((h) => h.hasOnset());
|
const onsets = haps.filter((h) => h.hasOnset());
|
||||||
|
|||||||
@@ -41,11 +41,17 @@ const _bjork = function (n, x) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const bjork = function (ons, steps) {
|
export const bjork = function (ons, steps) {
|
||||||
|
const inverted = ons < 0;
|
||||||
|
ons = Math.abs(ons);
|
||||||
const offs = steps - ons;
|
const offs = steps - ons;
|
||||||
const x = Array(ons).fill([1]);
|
const x = Array(ons).fill([1]);
|
||||||
const y = Array(offs).fill([0]);
|
const y = Array(offs).fill([0]);
|
||||||
const result = _bjork([ons, offs], [x, y]);
|
const result = _bjork([ons, offs], [x, y]);
|
||||||
return flatten(result[1][0]).concat(flatten(result[1][1]));
|
const p = flatten(result[1][0]).concat(flatten(result[1][1]));
|
||||||
|
if (inverted) {
|
||||||
|
return p.map((x) => (x === 0 ? 1 : 0));
|
||||||
|
}
|
||||||
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,8 +151,10 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
|
|||||||
* so there will be no gaps.
|
* so there will be no gaps.
|
||||||
* @name euclidLegato
|
* @name euclidLegato
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
|
* @param {number} pulses the number of onsets / beats
|
||||||
|
* @param {number} steps the number of steps to fill
|
||||||
* @example
|
* @example
|
||||||
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
|
* note("c3").euclidLegato(3,8)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const _euclidLegato = function (pulses, steps, rotation, pat) {
|
const _euclidLegato = function (pulses, steps, rotation, pat) {
|
||||||
@@ -166,6 +174,18 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
|
|||||||
return _euclidLegato(pulses, steps, 0, pat);
|
return _euclidLegato(pulses, steps, 0, pat);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to `euclid`, but each pulse is held until the next pulse,
|
||||||
|
* so there will be no gaps, and has an additional parameter for 'rotating'
|
||||||
|
* the resulting sequence
|
||||||
|
* @name euclidLegatoRot
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {number} pulses the number of onsets / beats
|
||||||
|
* @param {number} steps the number of steps to fill
|
||||||
|
* @param {number} rotation offset in steps
|
||||||
|
* @example
|
||||||
|
* note("c3").euclidLegatoRot(3,5,2)
|
||||||
|
*/
|
||||||
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
||||||
return _euclidLegato(pulses, steps, rotation, pat);
|
return _euclidLegato(pulses, steps, rotation, pat);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const evalScope = async (...args) => {
|
|||||||
globalThis[name] = value;
|
globalThis[name] = value;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
return modules;
|
||||||
};
|
};
|
||||||
|
|
||||||
function safeEval(str, options = {}) {
|
function safeEval(str, options = {}) {
|
||||||
@@ -47,10 +48,5 @@ export const evaluate = async (code, transpiler) => {
|
|||||||
// if no transpiler is given, we expect a single instruction (!wrapExpression)
|
// if no transpiler is given, we expect a single instruction (!wrapExpression)
|
||||||
const options = { wrapExpression: !!transpiler };
|
const options = { wrapExpression: !!transpiler };
|
||||||
let evaluated = await safeEval(code, options);
|
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, meta };
|
return { mode: 'javascript', pattern: evaluated, meta };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"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:*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
!dist
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
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
@@ -1 +0,0 @@
|
|||||||
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};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import Fraction from 'fraction.js';
|
import Fraction from 'fraction.js';
|
||||||
import { TimeSpan } from './timespan.mjs';
|
import { TimeSpan } from './timespan.mjs';
|
||||||
|
import { removeUndefineds } from './util.mjs';
|
||||||
|
|
||||||
// Returns the start of the cycle.
|
// Returns the start of the cycle.
|
||||||
Fraction.prototype.sam = function () {
|
Fraction.prototype.sam = function () {
|
||||||
@@ -47,14 +48,39 @@ Fraction.prototype.eq = function (other) {
|
|||||||
return this.compare(other) == 0;
|
return this.compare(other) == 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Fraction.prototype.ne = function (other) {
|
||||||
|
return this.compare(other) != 0;
|
||||||
|
};
|
||||||
|
|
||||||
Fraction.prototype.max = function (other) {
|
Fraction.prototype.max = function (other) {
|
||||||
return this.gt(other) ? this : other;
|
return this.gt(other) ? this : other;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Fraction.prototype.maximum = function (...others) {
|
||||||
|
others = others.map((x) => new Fraction(x));
|
||||||
|
return others.reduce((max, other) => other.max(max), this);
|
||||||
|
};
|
||||||
|
|
||||||
Fraction.prototype.min = function (other) {
|
Fraction.prototype.min = function (other) {
|
||||||
return this.lt(other) ? this : other;
|
return this.lt(other) ? this : other;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Fraction.prototype.mulmaybe = function (other) {
|
||||||
|
return other !== undefined ? this.mul(other) : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
Fraction.prototype.divmaybe = function (other) {
|
||||||
|
return other !== undefined ? this.div(other) : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
Fraction.prototype.addmaybe = function (other) {
|
||||||
|
return other !== undefined ? this.add(other) : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
Fraction.prototype.submaybe = function (other) {
|
||||||
|
return other !== undefined ? this.sub(other) : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
Fraction.prototype.show = function (/* excludeWhole = false */) {
|
Fraction.prototype.show = function (/* excludeWhole = false */) {
|
||||||
// return this.toFraction(excludeWhole);
|
// return this.toFraction(excludeWhole);
|
||||||
return this.s * this.n + '/' + this.d;
|
return this.s * this.n + '/' + this.d;
|
||||||
@@ -80,9 +106,26 @@ const fraction = (n) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const gcd = (...fractions) => {
|
export const gcd = (...fractions) => {
|
||||||
|
fractions = removeUndefineds(fractions);
|
||||||
|
if (fractions.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
|
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const lcm = (...fractions) => {
|
||||||
|
fractions = removeUndefineds(fractions);
|
||||||
|
if (fractions.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fractions.reduce(
|
||||||
|
(lcm, fraction) => (lcm === undefined || fraction === undefined ? undefined : lcm.lcm(fraction)),
|
||||||
|
fraction(1),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
fraction._original = Fraction;
|
fraction._original = Fraction;
|
||||||
|
|
||||||
export default fraction;
|
export default fraction;
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
/*
|
|
||||||
gist.js - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/gist.js>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 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
|
|
||||||
export default (route, cache = true) =>
|
|
||||||
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${cache ? '' : Date.now()}`)
|
|
||||||
.then((res) => res.text())
|
|
||||||
.then((code) => eval(code));
|
|
||||||
+36
-1
@@ -3,6 +3,7 @@ hap.mjs - <short description TODO>
|
|||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/hap.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/hap.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 Fraction from './fraction.mjs';
|
||||||
|
|
||||||
export class Hap {
|
export class Hap {
|
||||||
/*
|
/*
|
||||||
@@ -32,13 +33,43 @@ export class Hap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get duration() {
|
get duration() {
|
||||||
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
|
let duration;
|
||||||
|
if (typeof this.value?.duration === 'number') {
|
||||||
|
duration = Fraction(this.value.duration);
|
||||||
|
} else {
|
||||||
|
duration = this.whole.end.sub(this.whole.begin);
|
||||||
|
}
|
||||||
|
if (typeof this.value?.clip === 'number') {
|
||||||
|
return duration.mul(this.value.clip);
|
||||||
|
}
|
||||||
|
return duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
get endClipped() {
|
get endClipped() {
|
||||||
return this.whole.begin.add(this.duration);
|
return this.whole.begin.add(this.duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isActive(currentTime) {
|
||||||
|
return this.whole.begin <= currentTime && this.endClipped >= currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
isInPast(currentTime) {
|
||||||
|
return currentTime > this.endClipped;
|
||||||
|
}
|
||||||
|
isInNearPast(margin, currentTime) {
|
||||||
|
return currentTime - margin <= this.endClipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
isInFuture(currentTime) {
|
||||||
|
return currentTime < this.whole.begin;
|
||||||
|
}
|
||||||
|
isInNearFuture(margin, currentTime) {
|
||||||
|
return currentTime < this.whole.begin && currentTime > this.whole.begin - margin;
|
||||||
|
}
|
||||||
|
isWithinTime(min, max) {
|
||||||
|
return this.whole.begin <= max && this.endClipped >= min;
|
||||||
|
}
|
||||||
|
|
||||||
wholeOrPart() {
|
wholeOrPart() {
|
||||||
return this.whole ? this.whole : this.part;
|
return this.whole ? this.whole : this.part;
|
||||||
}
|
}
|
||||||
@@ -60,6 +91,10 @@ export class Hap {
|
|||||||
return this.whole != undefined && this.whole.begin.equals(this.part.begin);
|
return this.whole != undefined && this.whole.begin.equals(this.part.begin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasTag(tag) {
|
||||||
|
return this.context.tags?.includes(tag);
|
||||||
|
}
|
||||||
|
|
||||||
resolveState(state) {
|
resolveState(state) {
|
||||||
if (this.stateful && this.hasOnset()) {
|
if (this.stateful && this.hasOnset()) {
|
||||||
console.log('stateful');
|
console.log('stateful');
|
||||||
|
|||||||
+7
-10
@@ -4,11 +4,13 @@ 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 controls from './controls.mjs';
|
import * as controls from './controls.mjs'; // legacy
|
||||||
export * from './euclid.mjs';
|
export * from './euclid.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
|
import createClock from './zyklus.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
export { Fraction, controls };
|
export { Fraction, controls, createClock };
|
||||||
|
export * from './controls.mjs';
|
||||||
export * from './hap.mjs';
|
export * from './hap.mjs';
|
||||||
export * from './pattern.mjs';
|
export * from './pattern.mjs';
|
||||||
export * from './signal.mjs';
|
export * from './signal.mjs';
|
||||||
@@ -21,22 +23,17 @@ export * from './repl.mjs';
|
|||||||
export * from './cyclist.mjs';
|
export * from './cyclist.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './time.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 * from './ui.mjs';
|
||||||
export { default as drawLine } from './drawLine.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)
|
// below won't work with runtime.mjs (json import fails)
|
||||||
/* import * as p from './package.json';
|
/* import * as p from './package.json';
|
||||||
export const version = p.version; */
|
export const version = p.version; */
|
||||||
logger('🌀 @strudel.cycles/core loaded 🌀');
|
logger('🌀 @strudel/core loaded 🌀');
|
||||||
if (globalThis._strudelLoaded) {
|
if (globalThis._strudelLoaded) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`@strudel.cycles/core was loaded more than once...
|
`@strudel/core was loaded more than once...
|
||||||
This might happen when you have multiple versions of strudel installed.
|
This might happen when you have multiple versions of strudel installed.
|
||||||
Please check with "npm ls @strudel.cycles/core".`,
|
Please check with "npm ls @strudel/core".`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
globalThis._strudelLoaded = true;
|
globalThis._strudelLoaded = true;
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
export const logKey = 'strudel.log';
|
export const logKey = 'strudel.log';
|
||||||
|
|
||||||
|
let debounce = 1000,
|
||||||
|
lastMessage,
|
||||||
|
lastTime;
|
||||||
|
|
||||||
export function logger(message, type, data = {}) {
|
export function logger(message, type, data = {}) {
|
||||||
|
let t = performance.now();
|
||||||
|
if (lastMessage === message && t - lastTime < debounce) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastMessage = message;
|
||||||
|
lastTime = t;
|
||||||
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
||||||
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/*
|
||||||
|
neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances.
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.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 { logger } from './logger.mjs';
|
||||||
|
|
||||||
|
export class NeoCyclist {
|
||||||
|
constructor({ onTrigger, onToggle, getTime }) {
|
||||||
|
this.started = false;
|
||||||
|
this.cps = 0.5;
|
||||||
|
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||||
|
this.getTime = getTime; // get absolute time
|
||||||
|
this.time_at_last_tick_message = 0;
|
||||||
|
|
||||||
|
this.num_cycles_at_cps_change = 0;
|
||||||
|
this.onToggle = onToggle;
|
||||||
|
this.latency = 0.1; // fixed trigger time offset
|
||||||
|
this.cycle = 0;
|
||||||
|
this.id = Math.round(Date.now() * Math.random());
|
||||||
|
this.worker_time_dif;
|
||||||
|
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
|
||||||
|
this.worker.port.start();
|
||||||
|
|
||||||
|
this.channel = new BroadcastChannel('strudeltick');
|
||||||
|
let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif
|
||||||
|
const maxWeight = 20;
|
||||||
|
const precision = 10 ** 3; //round off time diff to prevent accumulating outliers
|
||||||
|
|
||||||
|
// the clock of the worker and the audio context clock can drift apart over time
|
||||||
|
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
|
||||||
|
// we need to keep a rolling weighted average of the time difference between the worker clock and audio context clock
|
||||||
|
// in order to schedule events consistently.
|
||||||
|
const setTimeReference = (num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline) => {
|
||||||
|
const time_dif = getTime() - (num_seconds_at_cps_change + num_seconds_since_cps_change) + tickdeadline;
|
||||||
|
if (this.worker_time_dif == null) {
|
||||||
|
this.worker_time_dif = time_dif;
|
||||||
|
} else {
|
||||||
|
const w = 1; //weight of new time diff;
|
||||||
|
const new_dif =
|
||||||
|
Math.round(((this.worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision;
|
||||||
|
|
||||||
|
if (new_dif != this.worker_time_dif) {
|
||||||
|
// reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens
|
||||||
|
weight = 4;
|
||||||
|
}
|
||||||
|
this.worker_time_dif = new_dif;
|
||||||
|
}
|
||||||
|
weight = Math.min(weight + 1, maxWeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tickCallback = (payload) => {
|
||||||
|
const {
|
||||||
|
num_cycles_at_cps_change,
|
||||||
|
cps,
|
||||||
|
num_seconds_at_cps_change,
|
||||||
|
num_seconds_since_cps_change,
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
tickdeadline,
|
||||||
|
cycle,
|
||||||
|
} = payload;
|
||||||
|
this.cps = cps;
|
||||||
|
this.cycle = cycle;
|
||||||
|
|
||||||
|
setTimeReference(num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline);
|
||||||
|
|
||||||
|
processHaps(begin, end, num_cycles_at_cps_change, num_seconds_at_cps_change);
|
||||||
|
|
||||||
|
this.time_at_last_tick_message = this.getTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
const processHaps = (begin, end, num_cycles_at_cps_change, seconds_at_cps_change) => {
|
||||||
|
if (this.started === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||||
|
|
||||||
|
haps.forEach((hap) => {
|
||||||
|
if (hap.hasOnset()) {
|
||||||
|
const targetTime =
|
||||||
|
(hap.whole.begin - num_cycles_at_cps_change) / this.cps +
|
||||||
|
seconds_at_cps_change +
|
||||||
|
this.latency +
|
||||||
|
this.worker_time_dif;
|
||||||
|
const duration = hap.duration / this.cps;
|
||||||
|
onTrigger?.(hap, 0, duration, this.cps, targetTime);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// receive messages from worker clock and process them
|
||||||
|
this.channel.onmessage = (message) => {
|
||||||
|
if (!this.started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { payload, type } = message.data;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'tick': {
|
||||||
|
tickCallback(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
sendMessage(type, payload) {
|
||||||
|
this.worker.port.postMessage({ type, payload, id: this.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
now() {
|
||||||
|
const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps;
|
||||||
|
return this.cycle + gap;
|
||||||
|
}
|
||||||
|
setCps(cps = 1) {
|
||||||
|
this.sendMessage('cpschange', { cps });
|
||||||
|
}
|
||||||
|
setCycle(cycle) {
|
||||||
|
this.sendMessage('setcycle', { cycle });
|
||||||
|
}
|
||||||
|
setStarted(started) {
|
||||||
|
this.sendMessage('toggle', { started });
|
||||||
|
this.started = started;
|
||||||
|
this.onToggle?.(started);
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
logger('[cyclist] start');
|
||||||
|
this.setStarted(true);
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
this.worker_time_dif = null;
|
||||||
|
logger('[cyclist] stop');
|
||||||
|
this.setStarted(false);
|
||||||
|
}
|
||||||
|
setPattern(pat, autostart = false) {
|
||||||
|
this.pattern = pat;
|
||||||
|
if (autostart && !this.started) {
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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('')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/core",
|
"name": "@strudel/core",
|
||||||
"version": "0.9.0",
|
"version": "1.0.1",
|
||||||
"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": {
|
"publishConfig": {
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.mjs"
|
||||||
"module": "dist/index.mjs"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
@@ -29,13 +28,13 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"homepage": "https://strudel.cc",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fraction.js": "^4.2.0"
|
"fraction.js": "^4.3.7"
|
||||||
},
|
},
|
||||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.3.3",
|
"vite": "^5.0.10",
|
||||||
"vitest": "^0.33.0"
|
"vitest": "^1.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+971
-342
File diff suppressed because it is too large
Load Diff
+142
-44
@@ -1,14 +1,13 @@
|
|||||||
|
import { NeoCyclist } from './neocyclist.mjs';
|
||||||
import { Cyclist } from './cyclist.mjs';
|
import { Cyclist } from './cyclist.mjs';
|
||||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { setTime } from './time.mjs';
|
import { setTime } from './time.mjs';
|
||||||
import { evalScope } from './evaluate.mjs';
|
import { evalScope } from './evaluate.mjs';
|
||||||
import { register } from './pattern.mjs';
|
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||||
|
|
||||||
export function repl({
|
export function repl({
|
||||||
interval,
|
|
||||||
defaultOutput,
|
defaultOutput,
|
||||||
onSchedulerError,
|
|
||||||
onEvalError,
|
onEvalError,
|
||||||
beforeEval,
|
beforeEval,
|
||||||
afterEval,
|
afterEval,
|
||||||
@@ -16,79 +15,178 @@ export function repl({
|
|||||||
transpiler,
|
transpiler,
|
||||||
onToggle,
|
onToggle,
|
||||||
editPattern,
|
editPattern,
|
||||||
|
onUpdateState,
|
||||||
|
sync = false,
|
||||||
|
setInterval,
|
||||||
|
clearInterval,
|
||||||
}) {
|
}) {
|
||||||
const scheduler = new Cyclist({
|
const state = {
|
||||||
interval,
|
schedulerError: undefined,
|
||||||
|
evalError: undefined,
|
||||||
|
code: '// LOADING',
|
||||||
|
activeCode: '// LOADING',
|
||||||
|
pattern: undefined,
|
||||||
|
miniLocations: [],
|
||||||
|
widgets: [],
|
||||||
|
pending: false,
|
||||||
|
started: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateState = (update) => {
|
||||||
|
Object.assign(state, update);
|
||||||
|
state.isDirty = state.code !== state.activeCode;
|
||||||
|
state.error = state.evalError || state.schedulerError;
|
||||||
|
onUpdateState?.(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
const schedulerOptions = {
|
||||||
onTrigger: getTrigger({ defaultOutput, getTime }),
|
onTrigger: getTrigger({ defaultOutput, getTime }),
|
||||||
onError: onSchedulerError,
|
|
||||||
getTime,
|
getTime,
|
||||||
onToggle,
|
onToggle: (started) => {
|
||||||
});
|
updateState({ started });
|
||||||
|
onToggle?.(started);
|
||||||
|
},
|
||||||
|
setInterval,
|
||||||
|
clearInterval,
|
||||||
|
};
|
||||||
|
|
||||||
|
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
||||||
|
const scheduler =
|
||||||
|
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
||||||
|
let pPatterns = {};
|
||||||
|
let anonymousIndex = 0;
|
||||||
|
let allTransform;
|
||||||
|
|
||||||
|
const hush = function () {
|
||||||
|
pPatterns = {};
|
||||||
|
anonymousIndex = 0;
|
||||||
|
allTransform = undefined;
|
||||||
|
return silence;
|
||||||
|
};
|
||||||
|
|
||||||
const setPattern = (pattern, autostart = true) => {
|
const setPattern = (pattern, autostart = true) => {
|
||||||
pattern = editPattern?.(pattern) || pattern;
|
pattern = editPattern?.(pattern) || pattern;
|
||||||
scheduler.setPattern(pattern, autostart);
|
scheduler.setPattern(pattern, autostart);
|
||||||
};
|
};
|
||||||
setTime(() => scheduler.now()); // TODO: refactor?
|
setTime(() => scheduler.now()); // TODO: refactor?
|
||||||
const evaluate = async (code, autostart = true) => {
|
|
||||||
|
const stop = () => scheduler.stop();
|
||||||
|
const start = () => scheduler.start();
|
||||||
|
const pause = () => scheduler.pause();
|
||||||
|
const toggle = () => scheduler.toggle();
|
||||||
|
const setCps = (cps) => scheduler.setCps(cps);
|
||||||
|
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
||||||
|
const all = function (transform) {
|
||||||
|
allTransform = transform;
|
||||||
|
return silence;
|
||||||
|
};
|
||||||
|
|
||||||
|
// set pattern methods that use this repl via closure
|
||||||
|
const injectPatternMethods = () => {
|
||||||
|
Pattern.prototype.p = function (id) {
|
||||||
|
if (id.startsWith('_') || id.endsWith('_')) {
|
||||||
|
// allows muting a pattern x with x_ or _x
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
|
if (id === '$') {
|
||||||
|
// allows adding anonymous patterns with $:
|
||||||
|
id = `$${anonymousIndex}`;
|
||||||
|
anonymousIndex++;
|
||||||
|
}
|
||||||
|
pPatterns[id] = this;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
Pattern.prototype.q = function (id) {
|
||||||
|
return silence;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
for (let i = 1; i < 10; ++i) {
|
||||||
|
Object.defineProperty(Pattern.prototype, `d${i}`, {
|
||||||
|
get() {
|
||||||
|
return this.p(i);
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(Pattern.prototype, `p${i}`, {
|
||||||
|
get() {
|
||||||
|
return this.p(i);
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Pattern.prototype[`q${i}`] = silence;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('injectPatternMethods: error:', err);
|
||||||
|
}
|
||||||
|
const cpm = register('cpm', function (cpm, pat) {
|
||||||
|
return pat._fast(cpm / 60 / scheduler.cps);
|
||||||
|
});
|
||||||
|
return evalScope({
|
||||||
|
all,
|
||||||
|
hush,
|
||||||
|
cpm,
|
||||||
|
setCps,
|
||||||
|
setcps: setCps,
|
||||||
|
setCpm,
|
||||||
|
setcpm: setCpm,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const evaluate = async (code, autostart = true, shouldHush = true) => {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error('no code to evaluate');
|
throw new Error('no code to evaluate');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
updateState({ code, pending: true });
|
||||||
|
await injectPatternMethods();
|
||||||
await beforeEval?.({ code });
|
await beforeEval?.({ code });
|
||||||
|
shouldHush && hush();
|
||||||
let { pattern, meta } = await _evaluate(code, transpiler);
|
let { pattern, meta } = await _evaluate(code, transpiler);
|
||||||
|
if (Object.keys(pPatterns).length) {
|
||||||
|
pattern = stack(...Object.values(pPatterns));
|
||||||
|
}
|
||||||
|
if (allTransform) {
|
||||||
|
pattern = allTransform(pattern);
|
||||||
|
}
|
||||||
|
if (!isPattern(pattern)) {
|
||||||
|
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||||
|
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||||
|
}
|
||||||
logger(`[eval] code updated`);
|
logger(`[eval] code updated`);
|
||||||
setPattern(pattern, autostart);
|
setPattern(pattern, autostart);
|
||||||
|
updateState({
|
||||||
|
miniLocations: meta?.miniLocations || [],
|
||||||
|
widgets: meta?.widgets || [],
|
||||||
|
activeCode: code,
|
||||||
|
pattern,
|
||||||
|
evalError: undefined,
|
||||||
|
schedulerError: undefined,
|
||||||
|
pending: false,
|
||||||
|
});
|
||||||
afterEval?.({ code, pattern, meta });
|
afterEval?.({ code, pattern, meta });
|
||||||
return pattern;
|
return pattern;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.warn(`[repl] eval error: ${err.message}`);
|
|
||||||
logger(`[eval] error: ${err.message}`, 'error');
|
logger(`[eval] error: ${err.message}`, 'error');
|
||||||
|
console.error(err);
|
||||||
|
updateState({ evalError: err, pending: false });
|
||||||
onEvalError?.(err);
|
onEvalError?.(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const stop = () => scheduler.stop();
|
const setCode = (code) => updateState({ code });
|
||||||
const start = () => scheduler.start();
|
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
||||||
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 =
|
export const getTrigger =
|
||||||
({ getTime, defaultOutput }) =>
|
({ getTime, defaultOutput }) =>
|
||||||
async (hap, deadline, duration, cps) => {
|
async (hap, deadline, duration, cps, t) => {
|
||||||
|
// TODO: get rid of deadline after https://github.com/tidalcycles/strudel/pull/1004
|
||||||
try {
|
try {
|
||||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||||
await defaultOutput(hap, deadline, duration, cps);
|
await defaultOutput(hap, deadline, duration, cps, t);
|
||||||
}
|
}
|
||||||
if (hap.context.onTrigger) {
|
if (hap.context.onTrigger) {
|
||||||
// call signature of output / onTrigger is different...
|
// call signature of output / onTrigger is different...
|
||||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger(`[cyclist] error: ${err.message}`, 'error');
|
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||||
|
|||||||
+290
-23
@@ -7,7 +7,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, register } 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, _mod, clamp, objectMap } from './util.mjs';
|
||||||
|
|
||||||
export function steady(value) {
|
export function steady(value) {
|
||||||
// A continuous value
|
// A continuous value
|
||||||
@@ -27,9 +27,11 @@ export const isaw2 = isaw.toBipolar();
|
|||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
|
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
|
||||||
|
* .clip(saw.slow(2))
|
||||||
* @example
|
* @example
|
||||||
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
* n(saw.range(0,8).segment(8))
|
||||||
|
* .scale('C major')
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const saw = signal((t) => t % 1);
|
export const saw = signal((t) => t % 1);
|
||||||
@@ -42,7 +44,8 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
|||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
|
* n(sine.segment(16).range(0,15))
|
||||||
|
* .scale("C:minor")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const sine = sine2.fromBipolar();
|
export const sine = sine2.fromBipolar();
|
||||||
@@ -52,7 +55,8 @@ export const sine = sine2.fromBipolar();
|
|||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor').note()
|
* n(stack(sine,cosine).segment(16).range(0,15))
|
||||||
|
* .scale("C:minor")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const cosine = sine._early(Fraction(1).div(4));
|
export const cosine = sine._early(Fraction(1).div(4));
|
||||||
@@ -63,7 +67,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
|
|||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* square.segment(2).range(0,7).scale('C minor').note()
|
* n(square.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
export const square = signal((t) => Math.floor((t * 2) % 2));
|
||||||
@@ -74,7 +78,7 @@ export const square2 = square.toBipolar();
|
|||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* tri.segment(8).range(0,7).scale('C minor').note()
|
* n(tri.segment(8).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const tri = fastcat(isaw, saw);
|
export const tri = fastcat(isaw, saw);
|
||||||
@@ -101,6 +105,7 @@ const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
|
|||||||
|
|
||||||
const timeToRandsPrime = (seed, n) => {
|
const timeToRandsPrime = (seed, n) => {
|
||||||
const result = [];
|
const result = [];
|
||||||
|
// eslint-disable-next-line
|
||||||
for (let i = 0; i < n; ++n) {
|
for (let i = 0; i < n; ++n) {
|
||||||
result.push(intSeedToRand(seed));
|
result.push(intSeedToRand(seed));
|
||||||
seed = xorwise(seed);
|
seed = xorwise(seed);
|
||||||
@@ -117,8 +122,8 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
|||||||
/**
|
/**
|
||||||
* A discrete pattern of numbers from 0 to n-1
|
* A discrete pattern of numbers from 0 to n-1
|
||||||
* @example
|
* @example
|
||||||
* run(4).scale('C4 major').note()
|
* n(run(4)).scale("C4:pentatonic")
|
||||||
* // "0 1 2 3".scale('C4 major').note()
|
* // n("0 1 2 3").scale("C4:pentatonic")
|
||||||
*/
|
*/
|
||||||
export const run = (n) => saw.range(0, n).floor().segment(n);
|
export const run = (n) => saw.range(0, n).floor().segment(n);
|
||||||
|
|
||||||
@@ -128,7 +133,7 @@ export const run = (n) => saw.range(0, n).floor().segment(n);
|
|||||||
* @name rand
|
* @name rand
|
||||||
* @example
|
* @example
|
||||||
* // randomly change the cutoff
|
* // randomly change the cutoff
|
||||||
* s("bd sd,hh*4").cutoff(rand.range(500,2000))
|
* s("bd*4,hh*8").cutoff(rand.range(500,8000))
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const rand = signal(timeToRand);
|
export const rand = signal(timeToRand);
|
||||||
@@ -138,7 +143,24 @@ export const rand = signal(timeToRand);
|
|||||||
export const rand2 = rand.toBipolar();
|
export const rand2 = rand.toBipolar();
|
||||||
|
|
||||||
export const _brandBy = (p) => rand.fmap((x) => x < p);
|
export const _brandBy = (p) => rand.fmap((x) => x < p);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A continuous pattern of 0 or 1 (binary random), with a probability for the value being 1
|
||||||
|
*
|
||||||
|
* @name brandBy
|
||||||
|
* @param {number} probability - a number between 0 and 1
|
||||||
|
* @example
|
||||||
|
* s("hh*10").pan(brandBy(0.2))
|
||||||
|
*/
|
||||||
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A continuous pattern of 0 or 1 (binary random)
|
||||||
|
*
|
||||||
|
* @name brand
|
||||||
|
* @example
|
||||||
|
* s("hh*10").pan(brand)
|
||||||
|
*/
|
||||||
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));
|
||||||
@@ -150,17 +172,219 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
|||||||
* @param {number} n max value (exclusive)
|
* @param {number} n max value (exclusive)
|
||||||
* @example
|
* @example
|
||||||
* // randomly select scale notes from 0 - 7 (= C to C)
|
* // randomly select scale notes from 0 - 7 (= C to C)
|
||||||
* irand(8).struct("x(3,8)").scale('C minor').note()
|
* n(irand(8)).struct("x x*2 x x*3").scale("C:minor")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
||||||
|
|
||||||
|
const _pick = function (lookup, pat, modulo = true) {
|
||||||
|
const array = Array.isArray(lookup);
|
||||||
|
const len = Object.keys(lookup).length;
|
||||||
|
|
||||||
|
lookup = objectMap(lookup, reify);
|
||||||
|
|
||||||
|
if (len === 0) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
|
return pat.fmap((i) => {
|
||||||
|
let key = i;
|
||||||
|
if (array) {
|
||||||
|
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
|
||||||
|
}
|
||||||
|
return lookup[key];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
||||||
|
* Similar to `inhabit`, but maintains the structure of the original patterns.
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {*} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
* @example
|
||||||
|
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
|
||||||
|
* @example
|
||||||
|
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
|
||||||
|
* @example
|
||||||
|
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
|
||||||
|
* @example
|
||||||
|
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const pick = function (lookup, pat) {
|
||||||
|
// backward compatibility - the args used to be flipped
|
||||||
|
if (Array.isArray(pat)) {
|
||||||
|
[pat, lookup] = [lookup, pat];
|
||||||
|
}
|
||||||
|
return __pick(lookup, pat);
|
||||||
|
};
|
||||||
|
|
||||||
|
const __pick = register('pick', function (lookup, pat) {
|
||||||
|
return _pick(lookup, pat, false).innerJoin();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** * The same as `pick`, but if you pick a number greater than the size of the list,
|
||||||
|
* it wraps around, rather than sticking at the maximum value.
|
||||||
|
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||||
|
* second one.
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {*} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const pickmod = register('pickmod', function (lookup, pat) {
|
||||||
|
return _pick(lookup, pat, true).innerJoin();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {Pattern} lookup a pattern of indices
|
||||||
|
* @param {function[]} funcs the array of functions from which to pull
|
||||||
|
* @returns {Pattern}
|
||||||
|
* @example
|
||||||
|
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
|
||||||
|
* @example
|
||||||
|
* note("<c2 d2>(3,8)").s("square")
|
||||||
|
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
|
||||||
|
*/
|
||||||
|
export const pickF = register('pickF', function (lookup, funcs, pat) {
|
||||||
|
return pat.apply(pick(lookup, funcs));
|
||||||
|
});
|
||||||
|
|
||||||
|
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
|
||||||
|
* it wraps around, rather than sticking at the maximum value.
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {Pattern} lookup a pattern of indices
|
||||||
|
* @param {function[]} funcs the array of functions from which to pull
|
||||||
|
* @returns {Pattern}
|
||||||
|
*/
|
||||||
|
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
|
||||||
|
return pat.apply(pickmod(lookup, funcs));
|
||||||
|
});
|
||||||
|
|
||||||
|
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {*} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
*/
|
||||||
|
export const pickOut = register('pickOut', function (lookup, pat) {
|
||||||
|
return _pick(lookup, pat, false).outerJoin();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
|
||||||
|
* it wraps around, rather than sticking at the maximum value.
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {*} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
*/
|
||||||
|
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
|
||||||
|
return _pick(lookup, pat, true).outerJoin();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {*} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
*/
|
||||||
|
export const pickRestart = register('pickRestart', function (lookup, pat) {
|
||||||
|
return _pick(lookup, pat, false).restartJoin();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
|
||||||
|
* it wraps around, rather than sticking at the maximum value.
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {*} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
*/
|
||||||
|
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
|
||||||
|
return _pick(lookup, pat, true).restartJoin();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {*} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
*/
|
||||||
|
export const pickReset = register('pickReset', function (lookup, pat) {
|
||||||
|
return _pick(lookup, pat, false).resetJoin();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
|
||||||
|
* it wraps around, rather than sticking at the maximum value.
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {*} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
*/
|
||||||
|
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
|
||||||
|
return _pick(lookup, pat, true).resetJoin();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
||||||
|
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
|
||||||
|
* @name inhabit
|
||||||
|
* @synonyms pickSqueeze
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {*} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
* @example
|
||||||
|
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
|
||||||
|
b: s("cp sd")
|
||||||
|
})
|
||||||
|
* @example
|
||||||
|
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
|
||||||
|
*/
|
||||||
|
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
|
||||||
|
return _pick(lookup, pat, false).squeezeJoin();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
|
||||||
|
* it wraps around, rather than sticking at the maximum value.
|
||||||
|
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||||
|
* second one.
|
||||||
|
* @name inhabitmod
|
||||||
|
* @synonyms pickmodSqueeze
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {*} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
|
||||||
|
return _pick(lookup, pat, true).squeezeJoin();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick from the list of values (or patterns of values) via the index using the given
|
||||||
|
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
|
||||||
|
* @param {Pattern} pat
|
||||||
|
* @param {*} xs
|
||||||
|
* @returns {Pattern}
|
||||||
|
* @example
|
||||||
|
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const squeeze = (pat, xs) => {
|
||||||
|
xs = xs.map(reify);
|
||||||
|
if (xs.length == 0) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
|
return pat
|
||||||
|
.fmap((i) => {
|
||||||
|
const key = _mod(Math.round(i), xs.length);
|
||||||
|
return xs[key];
|
||||||
|
})
|
||||||
|
.squeezeJoin();
|
||||||
|
};
|
||||||
|
|
||||||
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.range(0, xs.length).fmap((i) => xs[Math.floor(i)]);
|
|
||||||
|
return pat.range(0, xs.length).fmap((i) => {
|
||||||
|
const key = Math.min(Math.max(Math.floor(i), 0), xs.length - 1);
|
||||||
|
return xs[key];
|
||||||
|
});
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Choose from the list of values (or patterns of values) using the given
|
* Choose from the list of values (or patterns of values) using the given
|
||||||
@@ -168,6 +392,8 @@ export const __chooseWith = (pat, xs) => {
|
|||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
|
* @example
|
||||||
|
* note("c2 g2!2 d2 f1").s(chooseWith(sine.fast(2), ["sawtooth", "triangle", "bd:6"]))
|
||||||
*/
|
*/
|
||||||
export const chooseWith = (pat, xs) => {
|
export const chooseWith = (pat, xs) => {
|
||||||
return __chooseWith(pat, xs).outerJoin();
|
return __chooseWith(pat, xs).outerJoin();
|
||||||
@@ -188,6 +414,8 @@ export const chooseInWith = (pat, xs) => {
|
|||||||
* Chooses randomly from the given list of elements.
|
* Chooses randomly from the given list of elements.
|
||||||
* @param {...any} xs values / patterns to choose from.
|
* @param {...any} xs values / patterns to choose from.
|
||||||
* @returns {Pattern} - a continuous pattern.
|
* @returns {Pattern} - a continuous pattern.
|
||||||
|
* @example
|
||||||
|
* note("c2 g2!2 d2 f1").s(choose("sine", "triangle", "bd:6"))
|
||||||
*/
|
*/
|
||||||
export const choose = (...xs) => chooseWith(rand, xs);
|
export const choose = (...xs) => chooseWith(rand, xs);
|
||||||
|
|
||||||
@@ -214,11 +442,12 @@ Pattern.prototype.choose2 = function (...xs) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Picks one of the elements at random each cycle.
|
* Picks one of the elements at random each cycle.
|
||||||
|
* @synonyms randcat
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* chooseCycles("bd", "hh", "sd").s().fast(4)
|
* chooseCycles("bd", "hh", "sd").s().fast(8)
|
||||||
* @example
|
* @example
|
||||||
* "bd | hh | sd".s().fast(4)
|
* s("bd | hh | sd").fast(8)
|
||||||
*/
|
*/
|
||||||
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
|
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
|
||||||
|
|
||||||
@@ -242,9 +471,27 @@ const _wchooseWith = function (pat, ...pairs) {
|
|||||||
|
|
||||||
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
|
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses randomly from the given list of elements by giving a probability to each element
|
||||||
|
* @param {...any} pairs arrays of value and weight
|
||||||
|
* @returns {Pattern} - a continuous pattern.
|
||||||
|
* @example
|
||||||
|
* note("c2 g2!2 d2 f1").s(wchoose(["sine",10], ["triangle",1], ["bd:6",1]))
|
||||||
|
*/
|
||||||
export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
||||||
|
|
||||||
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
|
/**
|
||||||
|
* Picks one of the elements at random each cycle by giving a probability to each element
|
||||||
|
* @synonyms wrandcat
|
||||||
|
* @returns {Pattern}
|
||||||
|
* @example
|
||||||
|
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
|
||||||
|
* @example
|
||||||
|
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
|
||||||
|
*/
|
||||||
|
export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pairs).innerJoin();
|
||||||
|
|
||||||
|
export const wrandcat = wchooseCycles;
|
||||||
|
|
||||||
// this function expects pat to be a pattern of floats...
|
// this function expects pat to be a pattern of floats...
|
||||||
export const perlinWith = (pat) => {
|
export const perlinWith = (pat) => {
|
||||||
@@ -261,7 +508,7 @@ export const perlinWith = (pat) => {
|
|||||||
* @name perlin
|
* @name perlin
|
||||||
* @example
|
* @example
|
||||||
* // randomly change the cutoff
|
* // randomly change the cutoff
|
||||||
* s("bd sd,hh*4").cutoff(perlin.range(500,2000))
|
* s("bd*4,hh*8").cutoff(perlin.range(500,8000))
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
||||||
@@ -303,7 +550,7 @@ export const degradeBy = register('degradeBy', function (x, pat) {
|
|||||||
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inverse of {@link Pattern#degradeBy}: Randomly removes events from the pattern by a given amount.
|
* Inverse of `degradeBy`: Randomly removes events from the pattern by a given amount.
|
||||||
* 0 = 100% chance of removal
|
* 0 = 100% chance of removal
|
||||||
* 1 = 0% 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).
|
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
|
||||||
@@ -314,6 +561,11 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
|||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* s("hh*8").undegradeBy(0.2)
|
* s("hh*8").undegradeBy(0.2)
|
||||||
|
* @example
|
||||||
|
* s("hh*10").layer(
|
||||||
|
* x => x.degradeBy(0.2).pan(0),
|
||||||
|
* x => x.undegradeBy(0.8).pan(1)
|
||||||
|
* )
|
||||||
*/
|
*/
|
||||||
export const undegradeBy = register('undegradeBy', function (x, pat) {
|
export const undegradeBy = register('undegradeBy', function (x, pat) {
|
||||||
return pat._degradeByWith(
|
return pat._degradeByWith(
|
||||||
@@ -322,12 +574,27 @@ export const undegradeBy = register('undegradeBy', function (x, pat) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
|
||||||
|
* Events that would be removed by degrade are let through by undegrade and vice versa (see second example).
|
||||||
|
*
|
||||||
|
* @name undegrade
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").undegrade()
|
||||||
|
* @example
|
||||||
|
* s("hh*10").layer(
|
||||||
|
* x => x.degrade().pan(0),
|
||||||
|
* x => x.undegrade().pan(1)
|
||||||
|
* )
|
||||||
|
*/
|
||||||
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Randomly applies the given function by the given probability.
|
* Randomly applies the given function by the given probability.
|
||||||
* Similar to {@link Pattern#someCyclesBy}
|
* Similar to `someCyclesBy`
|
||||||
*
|
*
|
||||||
* @name sometimesBy
|
* @name sometimesBy
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
@@ -335,7 +602,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
|||||||
* @param {function} function - the transformation to apply
|
* @param {function} function - the transformation to apply
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
|
* s("hh*8").sometimesBy(.4, x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
||||||
@@ -353,7 +620,7 @@ export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
|||||||
* @param {function} function - the transformation to apply
|
* @param {function} function - the transformation to apply
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* s("hh*4").sometimes(x=>x.speed("0.5"))
|
* s("hh*8").sometimes(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
export const sometimes = register('sometimes', function (func, pat) {
|
export const sometimes = register('sometimes', function (func, pat) {
|
||||||
return pat._sometimesBy(0.5, func);
|
return pat._sometimesBy(0.5, func);
|
||||||
@@ -362,7 +629,7 @@ export const sometimes = register('sometimes', function (func, pat) {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Randomly applies the given function by the given probability on a cycle by cycle basis.
|
* Randomly applies the given function by the given probability on a cycle by cycle basis.
|
||||||
* Similar to {@link Pattern#sometimesBy}
|
* Similar to `sometimesBy`
|
||||||
*
|
*
|
||||||
* @name someCyclesBy
|
* @name someCyclesBy
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
@@ -370,7 +637,7 @@ export const sometimes = register('sometimes', function (func, pat) {
|
|||||||
* @param {function} function - the transformation to apply
|
* @param {function} function - the transformation to apply
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
|
* s("bd,hh*8").someCyclesBy(.3, x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
|
export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
|
||||||
@@ -392,7 +659,7 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat)
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
|
* s("bd,hh*8").someCycles(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
export const someCycles = register('someCycles', function (func, pat) {
|
export const someCycles = register('someCycles', function (func, pat) {
|
||||||
return pat._someCyclesBy(0.5, func);
|
return pat._someCyclesBy(0.5, func);
|
||||||
|
|||||||
@@ -4,25 +4,39 @@ Copyright (C) 2023 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 controls from '../controls.mjs';
|
import { s, pan } from '../controls.mjs';
|
||||||
import { mini } from '../../mini/mini.mjs';
|
import { mini } from '../../mini/mini.mjs';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import Fraction from '../fraction.mjs';
|
||||||
|
|
||||||
describe('controls', () => {
|
describe('controls', () => {
|
||||||
it('should support controls', () => {
|
it('should support controls', () => {
|
||||||
expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
|
expect(s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
|
||||||
});
|
});
|
||||||
it('should support compound controls', () => {
|
it('should support compound controls', () => {
|
||||||
expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
|
expect(s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
|
||||||
expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
|
expect(s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
|
||||||
{ s: 'bd', n: 3 },
|
{ s: 'bd', n: 3 },
|
||||||
{ s: 'sd', n: 4, gain: 1.4 },
|
{ s: 'sd', n: 4, gain: 1.4 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('should support ignore extra elements in compound controls', () => {
|
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([
|
expect(s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
|
||||||
{ s: 'bd', n: 3, gain: 0.4 },
|
{ s: 'bd', n: 3, gain: 0.4 },
|
||||||
{ s: 'sd', n: 4, gain: 0.5 },
|
{ s: 'sd', n: 4, gain: 0.5 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
it('should support nested controls', () => {
|
||||||
|
expect(s(mini('bd').pan(1)).firstCycleValues).toEqual([{ s: 'bd', pan: 1 }]);
|
||||||
|
expect(s(mini('bd:1').pan(1)).firstCycleValues).toEqual([{ s: 'bd', n: 1, pan: 1 }]);
|
||||||
|
});
|
||||||
|
it('preserves tactus of the left pattern', () => {
|
||||||
|
expect(s(mini('bd cp mt').pan(mini('1 2 3 4'))).tactus).toEqual(Fraction(3));
|
||||||
|
});
|
||||||
|
it('preserves tactus of the right pattern for .out', () => {
|
||||||
|
expect(s(mini('bd cp mt').set.out(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(4));
|
||||||
|
});
|
||||||
|
it('combines tactus of the pattern for .mix as lcm', () => {
|
||||||
|
expect(s(mini('bd cp mt').set.mix(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(12));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ import {
|
|||||||
slowcat,
|
slowcat,
|
||||||
cat,
|
cat,
|
||||||
sequence,
|
sequence,
|
||||||
polymeter,
|
palindrome,
|
||||||
polymeterSteps,
|
s_polymeter,
|
||||||
|
s_polymeterSteps,
|
||||||
polyrhythm,
|
polyrhythm,
|
||||||
silence,
|
silence,
|
||||||
fast,
|
fast,
|
||||||
@@ -45,13 +46,18 @@ import {
|
|||||||
rev,
|
rev,
|
||||||
time,
|
time,
|
||||||
run,
|
run,
|
||||||
|
pick,
|
||||||
|
stackLeft,
|
||||||
|
stackRight,
|
||||||
|
stackCentre,
|
||||||
|
s_cat,
|
||||||
|
calculateTactus,
|
||||||
} from '../index.mjs';
|
} from '../index.mjs';
|
||||||
|
|
||||||
import { steady } from '../signal.mjs';
|
import { steady } from '../signal.mjs';
|
||||||
|
|
||||||
import controls from '../controls.mjs';
|
import { n, s } from '../controls.mjs';
|
||||||
|
|
||||||
const { n, s } = controls;
|
|
||||||
const st = (begin, end) => new State(ts(begin, end));
|
const st = (begin, end) => new State(ts(begin, end));
|
||||||
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
||||||
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
|
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
|
||||||
@@ -180,15 +186,19 @@ describe('Pattern', () => {
|
|||||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
|
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('can Trig() structure', () => {
|
it('can Reset() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trig(20, 30).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||||
|
.add.reset(20, 30)
|
||||||
|
.early(2),
|
||||||
sequence(26, 27, 36, 37),
|
sequence(26, 27, 36, 37),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Trigzero() structure', () => {
|
it('can Restart() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trigzero(20, 30).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||||
|
.add.restart(20, 30)
|
||||||
|
.early(2),
|
||||||
sequence(21, 22, 31, 32),
|
sequence(21, 22, 31, 32),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -228,15 +238,19 @@ describe('Pattern', () => {
|
|||||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('can Trig() structure', () => {
|
it('can Reset() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trig(20, 30).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||||
|
.keep.reset(20, 30)
|
||||||
|
.early(2),
|
||||||
sequence(6, 7, 6, 7),
|
sequence(6, 7, 6, 7),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Trigzero() structure', () => {
|
it('can Restart() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trigzero(20, 30).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||||
|
.keep.restart(20, 30)
|
||||||
|
.early(2),
|
||||||
sequence(1, 2, 1, 2),
|
sequence(1, 2, 1, 2),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -270,15 +284,19 @@ describe('Pattern', () => {
|
|||||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('can Trig() structure', () => {
|
it('can Reset() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trig(false, true).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||||
|
.keepif.reset(false, true)
|
||||||
|
.early(2),
|
||||||
sequence(silence, silence, 6, 7),
|
sequence(silence, silence, 6, 7),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Trigzero() structure', () => {
|
it('can Restart() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trigzero(false, true).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||||
|
.keepif.restart(false, true)
|
||||||
|
.early(2),
|
||||||
sequence(silence, silence, 1, 2),
|
sequence(silence, silence, 1, 2),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -571,6 +589,18 @@ describe('Pattern', () => {
|
|||||||
expect(sequence(1, 2, 3).firstCycle()).toStrictEqual(fastcat(1, 2, 3).firstCycle());
|
expect(sequence(1, 2, 3).firstCycle()).toStrictEqual(fastcat(1, 2, 3).firstCycle());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('palindrome()', () => {
|
||||||
|
it('Can create palindrome', () => {
|
||||||
|
expect(
|
||||||
|
fastcat('a', 'b', 'c')
|
||||||
|
.palindrome()
|
||||||
|
.fast(2)
|
||||||
|
.firstCycle()
|
||||||
|
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||||
|
.map((a) => a.value),
|
||||||
|
).toStrictEqual(['a', 'b', 'c', 'c', 'b', 'a']);
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('polyrhythm()', () => {
|
describe('polyrhythm()', () => {
|
||||||
it('Can layer up cycles', () => {
|
it('Can layer up cycles', () => {
|
||||||
expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual(
|
expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual(
|
||||||
@@ -578,16 +608,19 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('polymeter()', () => {
|
describe('s_polymeter()', () => {
|
||||||
it('Can layer up cycles, stepwise', () => {
|
it('Can layer up cycles, stepwise, with lists', () => {
|
||||||
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
|
expect(s_polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
|
||||||
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
|
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
|
expect(s_polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
|
||||||
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
|
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('Can layer up cycles, stepwise, with weighted patterns', () => {
|
||||||
|
sameFirst(s_polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('firstOf()', () => {
|
describe('firstOf()', () => {
|
||||||
@@ -638,7 +671,11 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
describe('struct()', () => {
|
describe('struct()', () => {
|
||||||
it('Can restructure a discrete pattern', () => {
|
it('Can restructure a discrete pattern', () => {
|
||||||
expect(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle()).toStrictEqual([
|
expect(
|
||||||
|
sequence('a', 'b')
|
||||||
|
.struct(sequence(true, true, true))
|
||||||
|
.firstCycle(),
|
||||||
|
).toStrictEqual([
|
||||||
hap(ts(0, third), ts(0, third), 'a'),
|
hap(ts(0, third), ts(0, third), 'a'),
|
||||||
hap(ts(third, twothirds), ts(third, 0.5), 'a'),
|
hap(ts(third, twothirds), ts(third, 0.5), 'a'),
|
||||||
hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
|
hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
|
||||||
@@ -669,7 +706,11 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
describe('mask()', () => {
|
describe('mask()', () => {
|
||||||
it('Can fragment a pattern', () => {
|
it('Can fragment a pattern', () => {
|
||||||
expect(sequence('a', 'b').mask(sequence(true, true, true)).firstCycle()).toStrictEqual([
|
expect(
|
||||||
|
sequence('a', 'b')
|
||||||
|
.mask(sequence(true, true, true))
|
||||||
|
.firstCycle(),
|
||||||
|
).toStrictEqual([
|
||||||
hap(ts(0, 0.5), ts(0, third), 'a'),
|
hap(ts(0, 0.5), ts(0, third), 'a'),
|
||||||
hap(ts(0, 0.5), ts(third, 0.5), 'a'),
|
hap(ts(0, 0.5), ts(third, 0.5), 'a'),
|
||||||
hap(ts(0.5, 1), ts(0.5, twothirds), 'b'),
|
hap(ts(0.5, 1), ts(0.5, twothirds), 'b'),
|
||||||
@@ -938,9 +979,11 @@ describe('Pattern', () => {
|
|||||||
expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1);
|
expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1);
|
||||||
});
|
});
|
||||||
it('Doesnt merge two overlapping haps', () => {
|
it('Doesnt merge two overlapping haps', () => {
|
||||||
expect(stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(
|
expect(
|
||||||
2,
|
stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1))
|
||||||
);
|
.defragmentHaps()
|
||||||
|
.firstCycle().length,
|
||||||
|
).toStrictEqual(2);
|
||||||
});
|
});
|
||||||
it('Doesnt merge two touching haps with different values', () => {
|
it('Doesnt merge two touching haps with different values', () => {
|
||||||
expect(stack(pure('a').mask(1, 0), pure('b').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
|
expect(stack(pure('a').mask(1, 0), pure('b').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
|
||||||
@@ -1003,4 +1046,167 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('chunk', () => {
|
||||||
|
it('Processes each cycle of the source pattern multiple times, once for each chunk', () => {
|
||||||
|
expect(sequence(0, 1, 2, 3).slow(2).chunk(2, add(10)).fast(4).firstCycleValues).toStrictEqual([
|
||||||
|
10, 1, 0, 11, 12, 3, 2, 13,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('fastChunk', () => {
|
||||||
|
it('Unlike chunk, cycles of the source pattern proceed cycle-by-cycle', () => {
|
||||||
|
expect(sequence(0, 1, 2, 3).slow(2).fastChunk(2, add(10)).fast(4).firstCycleValues).toStrictEqual([
|
||||||
|
10, 1, 2, 13, 10, 1, 2, 13,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('repeatCycles', () => {
|
||||||
|
it('Repeats each cycle of the source pattern the given number of times', () => {
|
||||||
|
expect(slowcat(0, 1).repeatCycles(2).fast(6).firstCycleValues).toStrictEqual([0, 0, 1, 1, 0, 0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('inhabit', () => {
|
||||||
|
it('Can pattern named patterns', () => {
|
||||||
|
expect(
|
||||||
|
sameFirst(
|
||||||
|
sequence('a', 'b', stack('a', 'b')).inhabit({ a: sequence(1, 2), b: sequence(10, 20, 30) }),
|
||||||
|
sequence([1, 2], [10, 20, 30], stack([1, 2], [10, 20, 30])),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('Can pattern indexed patterns', () => {
|
||||||
|
expect(
|
||||||
|
sameFirst(
|
||||||
|
sequence('0', '1', stack('0', '1')).inhabit([sequence(1, 2), sequence(10, 20, 30)]),
|
||||||
|
sequence([1, 2], [10, 20, 30], stack([1, 2], [10, 20, 30])),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('pick', () => {
|
||||||
|
it('Can pattern named patterns', () => {
|
||||||
|
expect(
|
||||||
|
sameFirst(
|
||||||
|
sequence('a', 'b', 'a', stack('a', 'b')).pick({ a: sequence(1, 2, 3, 4), b: sequence(10, 20, 30, 40) }),
|
||||||
|
sequence(1, 20, 3, stack(4, 40)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('Can pattern indexed patterns', () => {
|
||||||
|
expect(
|
||||||
|
sameFirst(
|
||||||
|
sequence(0, 1, 0, stack(0, 1)).pick([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]),
|
||||||
|
sequence(1, 20, 3, stack(4, 40)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('Clamps indexes', () => {
|
||||||
|
expect(
|
||||||
|
sameFirst(sequence(0, 1, 2, 3).pick([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]), sequence(1, 20, 30, 40)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('Is backwards compatible', () => {
|
||||||
|
expect(
|
||||||
|
sameFirst(
|
||||||
|
pick([sequence('a', 'b'), sequence('c', 'd')], sequence(0, 1)),
|
||||||
|
pick(sequence(0, 1), [sequence('a', 'b'), sequence('c', 'd')]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('pickmod', () => {
|
||||||
|
it('Wraps indexes', () => {
|
||||||
|
expect(
|
||||||
|
sameFirst(
|
||||||
|
sequence(0, 1, 2, 3).pickmod([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]),
|
||||||
|
sequence(1, 20, 3, 40),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('tactus', () => {
|
||||||
|
it('Is correctly preserved/calculated through transformations', () => {
|
||||||
|
expect(sequence(0, 1, 2, 3).linger(4).tactus).toStrictEqual(Fraction(4));
|
||||||
|
expect(sequence(0, 1, 2, 3).iter(4).tactus).toStrictEqual(Fraction(4));
|
||||||
|
expect(sequence(0, 1, 2, 3).fast(4).tactus).toStrictEqual(Fraction(4));
|
||||||
|
expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(4));
|
||||||
|
expect(sequence(0, 1, 2, 3).rev().tactus).toStrictEqual(Fraction(4));
|
||||||
|
expect(sequence(1).segment(10).tactus).toStrictEqual(Fraction(10));
|
||||||
|
expect(sequence(1, 0, 1).invert().tactus).toStrictEqual(Fraction(3));
|
||||||
|
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4).tactus).toStrictEqual(Fraction(8));
|
||||||
|
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4).tactus).toStrictEqual(Fraction(8));
|
||||||
|
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
|
||||||
|
Fraction(4),
|
||||||
|
);
|
||||||
|
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
|
||||||
|
Fraction(4),
|
||||||
|
);
|
||||||
|
expect(sequence({ n: 0 }, { n: 1 }, { n: 2 }).chop(4).tactus).toStrictEqual(Fraction(12));
|
||||||
|
expect(
|
||||||
|
pure((x) => x + 1)
|
||||||
|
.setTactus(3)
|
||||||
|
.appBoth(pure(1).setTactus(2)).tactus,
|
||||||
|
).toStrictEqual(Fraction(6));
|
||||||
|
expect(
|
||||||
|
pure((x) => x + 1)
|
||||||
|
.setTactus(undefined)
|
||||||
|
.appBoth(pure(1).setTactus(2)).tactus,
|
||||||
|
).toStrictEqual(Fraction(2));
|
||||||
|
expect(
|
||||||
|
pure((x) => x + 1)
|
||||||
|
.setTactus(3)
|
||||||
|
.appBoth(pure(1).setTactus(undefined)).tactus,
|
||||||
|
).toStrictEqual(Fraction(3));
|
||||||
|
expect(stack(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(6));
|
||||||
|
expect(stack(fastcat(0, 1, 2), fastcat(3, 4).setTactus(undefined)).tactus).toStrictEqual(Fraction(3));
|
||||||
|
expect(stackLeft(fastcat(0, 1, 2, 3), fastcat(3, 4)).tactus).toStrictEqual(Fraction(4));
|
||||||
|
expect(stackRight(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3));
|
||||||
|
// maybe this should double when they are either all even or all odd
|
||||||
|
expect(stackCentre(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3));
|
||||||
|
expect(fastcat(0, 1).ply(3).tactus).toStrictEqual(Fraction(6));
|
||||||
|
expect(fastcat(0, 1).setTactus(undefined).ply(3).tactus).toStrictEqual(undefined);
|
||||||
|
expect(fastcat(0, 1).fast(3).tactus).toStrictEqual(Fraction(2));
|
||||||
|
expect(fastcat(0, 1).setTactus(undefined).fast(3).tactus).toStrictEqual(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('s_cat', () => {
|
||||||
|
it('can cat', () => {
|
||||||
|
expect(sameFirst(s_cat(fastcat(0, 1, 2, 3), fastcat(4, 5)), fastcat(0, 1, 2, 3, 4, 5)));
|
||||||
|
expect(sameFirst(s_cat(pure(1), pure(2), pure(3)), fastcat(1, 2, 3)));
|
||||||
|
});
|
||||||
|
it('calculates undefined tactuses as the average', () => {
|
||||||
|
expect(sameFirst(s_cat(pure(1), pure(2), pure(3).setTactus(undefined)), fastcat(1, 2, 3)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('s_taper', () => {
|
||||||
|
it('can taper', () => {
|
||||||
|
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_taper(1, 5), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0)));
|
||||||
|
});
|
||||||
|
it('can taper backwards', () => {
|
||||||
|
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_taper(-1, 5), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('s_add and s_sub', () => {
|
||||||
|
it('can add from the left', () => {
|
||||||
|
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(2), sequence(0, 1)));
|
||||||
|
});
|
||||||
|
it('can sub to the left', () => {
|
||||||
|
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(2), sequence(0, 1, 2)));
|
||||||
|
});
|
||||||
|
it('can add from the right', () => {
|
||||||
|
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(-2), sequence(3, 4)));
|
||||||
|
});
|
||||||
|
it('can sub to the right', () => {
|
||||||
|
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(-2), sequence(2, 3, 4)));
|
||||||
|
});
|
||||||
|
it('can subtract nothing', () => {
|
||||||
|
expect(sameFirst(pure('a').s_sub(0), pure('a')));
|
||||||
|
});
|
||||||
|
it('can subtract nothing, repeatedly', () => {
|
||||||
|
expect(sameFirst(pure('a').s_sub(0, 0), fastcat('a', 'a')));
|
||||||
|
for (var i = 0; i < 100; ++i) {
|
||||||
|
expect(sameFirst(pure('a').s_sub(...Array(i).fill(0)), fastcat(...Array(i).fill('a'))));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
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';
|
import { n } from '../controls.mjs';
|
||||||
const { n } = controls;
|
|
||||||
|
|
||||||
describe('Value', () => {
|
describe('Value', () => {
|
||||||
it('unionWith', () => {
|
it('unionWith', () => {
|
||||||
@@ -23,8 +22,4 @@ describe('Value', () => {
|
|||||||
expect(valued(mul).ap(3).ap(3).value).toEqual(9);
|
expect(valued(mul).ap(3).ap(3).value).toEqual(9);
|
||||||
expect(valued(3).mul(3).value).toEqual(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 }]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,19 +4,6 @@ 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 { getTime } from './time.mjs';
|
|
||||||
|
|
||||||
function frame(callback) {
|
|
||||||
if (window.strudelAnimation) {
|
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
|
||||||
}
|
|
||||||
const animate = (animationTime) => {
|
|
||||||
callback(animationTime, getTime());
|
|
||||||
window.strudelAnimation = requestAnimationFrame(animate);
|
|
||||||
};
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const backgroundImage = function (src, animateOptions = {}) {
|
export const backgroundImage = function (src, animateOptions = {}) {
|
||||||
const container = document.getElementById('code');
|
const container = document.getElementById('code');
|
||||||
const bg = 'background-image:url(' + src + ');background-size:contain;';
|
const bg = 'background-image:url(' + src + ');background-size:contain;';
|
||||||
@@ -35,18 +22,11 @@ export const backgroundImage = function (src, animateOptions = {}) {
|
|||||||
if (funcOptions.length === 0) {
|
if (funcOptions.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
frame((_, t) =>
|
|
||||||
funcOptions.forEach(([option, value]) => {
|
|
||||||
handleOption(option, value(t));
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanupUi = () => {
|
export const cleanupUi = () => {
|
||||||
const container = document.getElementById('code');
|
const container = document.getElementById('code');
|
||||||
if (container) {
|
if (container) {
|
||||||
container.style = '';
|
container.style = '';
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+114
-8
@@ -4,6 +4,8 @@ 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 { logger } from './logger.mjs';
|
||||||
|
|
||||||
// returns true if the given string is a note
|
// returns true if the given string is a note
|
||||||
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
||||||
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
|
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
|
||||||
@@ -59,6 +61,11 @@ export const valueToMidi = (value, fallbackValue) => {
|
|||||||
return fallbackValue;
|
return fallbackValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// used to schedule external event like midi and osc out
|
||||||
|
export const getEventOffsetMs = (targetTimeSeconds, currentTimeSeconds) => {
|
||||||
|
return (targetTimeSeconds - currentTimeSeconds) * 1000;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
@@ -84,6 +91,18 @@ export const midi2note = (n) => {
|
|||||||
// 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)
|
// 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 _mod = (n, m) => ((n % m) + m) % m;
|
||||||
|
|
||||||
|
export function nanFallback(value, fallback = 0) {
|
||||||
|
if (isNaN(Number(value))) {
|
||||||
|
logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
// round to nearest int, negative numbers will output a subtracted index
|
||||||
|
export const getSoundIndex = (n, numSounds) => {
|
||||||
|
return _mod(Math.round(nanFallback(n ?? 0, 0)), numSounds);
|
||||||
|
};
|
||||||
|
|
||||||
export const getPlayableNoteValue = (hap) => {
|
export const getPlayableNoteValue = (hap) => {
|
||||||
let { value, context } = hap;
|
let { value, context } = hap;
|
||||||
let note = value;
|
let note = value;
|
||||||
@@ -217,6 +236,14 @@ export const splitAt = function (index, value) {
|
|||||||
|
|
||||||
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||||
|
|
||||||
|
export const pairs = function (xs) {
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < xs.length - 1; ++i) {
|
||||||
|
result.push([xs[i], xs[i + 1]]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||||
|
|
||||||
/* solmization, not used yet */
|
/* solmization, not used yet */
|
||||||
@@ -262,15 +289,94 @@ export const sol2note = (n, notation = 'letters') => {
|
|||||||
notation === 'solfeggio'
|
notation === 'solfeggio'
|
||||||
? solfeggio /*check if its is any of the following*/
|
? solfeggio /*check if its is any of the following*/
|
||||||
: notation === 'indian'
|
: notation === 'indian'
|
||||||
? indian
|
? indian
|
||||||
: notation === 'german'
|
: notation === 'german'
|
||||||
? german
|
? german
|
||||||
: notation === 'byzantine'
|
: notation === 'byzantine'
|
||||||
? byzantine
|
? byzantine
|
||||||
: notation === 'japanese'
|
: notation === 'japanese'
|
||||||
? japanese
|
? japanese
|
||||||
: english; /*if not use standard version*/
|
: english; /*if not use standard version*/
|
||||||
const note = pc[n % 12]; /*calculating the midi value to the note*/
|
const note = pc[n % 12]; /*calculating the midi value to the note*/
|
||||||
const oct = Math.floor(n / 12) - 1;
|
const oct = Math.floor(n / 12) - 1;
|
||||||
return note + oct;
|
return note + oct;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Remove duplicates from list
|
||||||
|
export function uniq(a) {
|
||||||
|
var seen = {};
|
||||||
|
return a.filter(function (item) {
|
||||||
|
return seen.hasOwn(item) ? false : (seen[item] = true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicates from list, sorting in the process. Mutates argument!
|
||||||
|
export function uniqsort(a) {
|
||||||
|
return a.sort().filter(function (item, pos, ary) {
|
||||||
|
return !pos || item != ary[pos - 1];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// rational version
|
||||||
|
export function uniqsortr(a) {
|
||||||
|
return a
|
||||||
|
.sort((x, y) => x.compare(y))
|
||||||
|
.filter(function (item, pos, ary) {
|
||||||
|
return !pos || item.ne(ary[pos - 1]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// code hashing helpers
|
||||||
|
|
||||||
|
export function unicodeToBase64(text) {
|
||||||
|
const utf8Bytes = new TextEncoder().encode(text);
|
||||||
|
const base64String = btoa(String.fromCharCode(...utf8Bytes));
|
||||||
|
return base64String;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64ToUnicode(base64String) {
|
||||||
|
const utf8Bytes = new Uint8Array(
|
||||||
|
atob(base64String)
|
||||||
|
.split('')
|
||||||
|
.map((char) => char.charCodeAt(0)),
|
||||||
|
);
|
||||||
|
const decodedText = new TextDecoder().decode(utf8Bytes);
|
||||||
|
return decodedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function code2hash(code) {
|
||||||
|
return encodeURIComponent(unicodeToBase64(code));
|
||||||
|
//return '#' + encodeURIComponent(btoa(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hash2code(hash) {
|
||||||
|
return base64ToUnicode(decodeURIComponent(hash));
|
||||||
|
//return atob(decodeURIComponent(codeParam || ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function objectMap(obj, fn) {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(fn);
|
||||||
|
}
|
||||||
|
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Floating point versions, see Fraction for rational versions
|
||||||
|
// // greatest common divisor
|
||||||
|
// export const gcd = function (x, y, ...z) {
|
||||||
|
// if (!y && z.length > 0) {
|
||||||
|
// return gcd(x, ...z);
|
||||||
|
// }
|
||||||
|
// if (!y) {
|
||||||
|
// return x;
|
||||||
|
// }
|
||||||
|
// return gcd(y, x % y, ...z);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // lowest common multiple
|
||||||
|
// export const lcm = function (x, y, ...z) {
|
||||||
|
// if (z.length == 0) {
|
||||||
|
// return (x * y) / gcd(x, y);
|
||||||
|
// }
|
||||||
|
// return lcm((x * y) / gcd(x, y), ...z);
|
||||||
|
// };
|
||||||
|
|||||||
@@ -5,14 +5,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';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
export function unionWithObj(a, b, func) {
|
export function unionWithObj(a, b, func) {
|
||||||
if (typeof b?.value === 'number') {
|
if (b?.value !== undefined && Object.keys(b).length === 1) {
|
||||||
// https://github.com/tidalcycles/strudel/issues/262
|
// https://github.com/tidalcycles/strudel/issues/1026
|
||||||
const numKeys = Object.keys(a).filter((k) => typeof a[k] === 'number');
|
logger(`[warn]: Can't do arithmetic on control pattern.`);
|
||||||
const numerals = Object.fromEntries(numKeys.map((k) => [k, b.value]));
|
return a;
|
||||||
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])])));
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user