mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-18 16:46:10 -04:00
Compare commits
840 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 193324b2b3 | |||
| f3a2cff5db | |||
| 97c8ab0f71 | |||
| 28534ea547 | |||
| 1a75b79a33 | |||
| 75dfb0af35 | |||
| 073fa09648 | |||
| 57157fde9c | |||
| cbae355896 | |||
| 924e8a764a | |||
| 40e629cb0e | |||
| 99ec261f79 | |||
| 9daea5bbc2 | |||
| 4704c03d48 | |||
| 9dbf52bedb | |||
| cfd04ab7fa | |||
| 33dbf75e3b | |||
| 8ffaeeb171 | |||
| 451828e5d4 | |||
| 176877f2b8 | |||
| 61b0d08b5f | |||
| f50a6ce1ae | |||
| 8549d6ea8b | |||
| 90a90a87a5 | |||
| 760bdea521 | |||
| 0fd783e83f | |||
| 42caa9058b | |||
| 9ab315ac2c | |||
| a08ad07537 | |||
| c6ce763f7c | |||
| 0393205eda | |||
| 5c05a5a63e | |||
| b01d9f849b | |||
| 0ab904275c | |||
| 5f82a86801 | |||
| 13a5d8de86 | |||
| f024b71808 | |||
| cd35a17904 | |||
| be3f68ed06 | |||
| 64b8fd9584 | |||
| b001d1d0f1 | |||
| 64ad75c79e | |||
| 12eb361d69 | |||
| dd51bd0a2f | |||
| 0fb478a7c8 | |||
| 9694ecf55b | |||
| fe80478b8a | |||
| a253c26bee | |||
| a500f983f5 | |||
| 3579b6f8f3 | |||
| 14cb954213 | |||
| 6170eef441 | |||
| ddb9af4d57 | |||
| 6db2c0bf93 | |||
| 46f70e6a5e | |||
| 0b994a1b0f | |||
| 0326244956 | |||
| ea1b2252fc | |||
| 0da25ea7cc | |||
| 3209afa4ff | |||
| 33e2631e5d | |||
| f02a41b675 | |||
| f497d7b212 | |||
| 0f18f5eb81 | |||
| 8da81104aa | |||
| 97cbdc6b0f | |||
| 7f53f4e36b | |||
| c350f262e6 | |||
| 3e727893bf | |||
| 2a2c7437e4 | |||
| 4f0885d3af | |||
| 5656cd2fc6 | |||
| b6e42876ad | |||
| 3671344867 | |||
| 30b149053c | |||
| cdd40df9d3 | |||
| 9d661808ba | |||
| 6a5d849b3c | |||
| 25bbd73a90 | |||
| 3d27a5f8c7 | |||
| 44ef372399 | |||
| fd09dff381 | |||
| 10580a0bec | |||
| f3f87b0a46 | |||
| ac606bcf71 | |||
| bb50df07e7 | |||
| f7ac033e1c | |||
| a1043352c1 | |||
| e15fecf6cf | |||
| 3e99f345ef | |||
| 0a77770ef3 | |||
| 379b8392dd | |||
| 0fdae70461 | |||
| 087423c754 | |||
| ce64c98fb7 | |||
| e11a8a9dbb | |||
| ef4a68a24d | |||
| 0ec65c85d3 | |||
| 74326f226d | |||
| 554e0d7a39 | |||
| 1b99ac92dd | |||
| ff0b77908a | |||
| 83d19638ba | |||
| a7d484b1a8 | |||
| d87beb37b8 | |||
| dcdb383960 | |||
| 478a7e95ea | |||
| f7b3b582cc | |||
| d1719dc277 | |||
| 9d8c812aa3 | |||
| 2e390acce9 | |||
| e04797d891 | |||
| fb6195b081 | |||
| c6491a3ccd | |||
| 014ae2107e | |||
| 1457eb9d19 | |||
| 6f5f8cc7d8 | |||
| 9ec3fee455 | |||
| 2d52168c26 | |||
| aa5dd7920f | |||
| d60956e82a | |||
| 91fd3fb5ca | |||
| 46f3b662a7 | |||
| 54a61dcdae | |||
| b58f5232c7 | |||
| 9a5bda862c | |||
| 7f67adb639 | |||
| 48fd943622 | |||
| 08350d00be | |||
| 84bf4a2686 | |||
| 2823456e96 | |||
| a373629fc2 | |||
| 0aa2f72a3a | |||
| c032f6dde1 | |||
| f448d8f334 | |||
| a5d4a43753 | |||
| 52e01321ec | |||
| 19692e0122 | |||
| cd1da3cb81 | |||
| ffaa8f4ab3 | |||
| f3da9d680d | |||
| 6118bd24f2 | |||
| fddbb2e2ac | |||
| cb7156b091 | |||
| b5914a3f12 | |||
| 348d576ff3 | |||
| 7507654749 | |||
| c25aad0c4a | |||
| 4aa32415cd | |||
| cde23bbf98 | |||
| 555fe0b629 | |||
| be44c28fd3 | |||
| 657091307e | |||
| 458dc6e07d | |||
| cfaecf9fcb | |||
| 902f722d4c | |||
| ea2d8b2773 | |||
| b319754dc3 | |||
| 35f2040717 | |||
| 4121fc926e | |||
| fd9ab987e6 | |||
| 6c3a235b08 | |||
| 07cb861385 | |||
| 217e348fc3 | |||
| ad917afba4 | |||
| 9539c6d317 | |||
| e946e1e01e | |||
| fea1e71a99 | |||
| 9731eb8c5b | |||
| e058ae54ef | |||
| ba6714f384 | |||
| 0a0a761e23 | |||
| 88e8368022 | |||
| 468ccc6288 | |||
| 77e0237148 | |||
| ffdfb07f72 | |||
| 203ed88024 | |||
| 9923bcd16f | |||
| 0179ec051f | |||
| 05b6353355 | |||
| 47c6ed8829 | |||
| ff9ebd64e3 | |||
| cd2b098b81 | |||
| 7000a4cead | |||
| 5aa983b45b | |||
| 800989419b | |||
| 362679954a | |||
| 2732d64beb | |||
| 88684d6d9e | |||
| 9afddf0850 | |||
| a2d7d6e81f | |||
| 49bcb5e19f | |||
| 3744ea6218 | |||
| b2c6d87633 | |||
| e4f538b674 | |||
| ea0e0b4396 | |||
| 1ac784dc7a | |||
| d980c9ca4c | |||
| 6a2a60df0d | |||
| ee9b89b6d5 | |||
| 68cc109933 | |||
| 1015bb0fdc | |||
| 7da94dd109 | |||
| 171808cf2c | |||
| 78d14cdfea | |||
| 19982a2ffb | |||
| 79904560fc | |||
| 28aee1c58d | |||
| d4512f360b | |||
| 48d0ffe868 | |||
| 62af12f3b9 | |||
| efa4bfef2f | |||
| fc3ead4c01 | |||
| ca9c813b40 | |||
| e3b0f47c40 | |||
| bf13e42092 | |||
| 4345fda9bc | |||
| 4059e9faa9 | |||
| 95fcce1bcf | |||
| 86cde9716a | |||
| 6e315a50ed | |||
| 500c0b6534 | |||
| db75dca118 | |||
| d5f37e2cbe | |||
| a120a9895b | |||
| e9c1db020f | |||
| 08cd319a0b | |||
| 896ec69329 | |||
| 32e64bbb6e | |||
| 0974540e5f | |||
| e06963e616 | |||
| a675815529 | |||
| 3d48135b32 | |||
| 8f09e7f097 | |||
| e8b09f1124 | |||
| 57ba353594 | |||
| b695e090cd | |||
| 69ecb7b54f | |||
| f57fe182ed | |||
| 10ee11c886 | |||
| 45842b1f05 | |||
| 9ca5f9ad48 | |||
| bb00036a30 | |||
| 4a2a540500 | |||
| 09217216dc | |||
| 22a64c4931 | |||
| 93f520b941 | |||
| c84714dc3a | |||
| a7aa09a13e | |||
| efa1cdfcb2 | |||
| 70695daae8 | |||
| 62fdba0600 | |||
| 5733e00908 | |||
| e56abaefd7 | |||
| c71e893e8e | |||
| 3992356517 | |||
| dbb84ef129 | |||
| 8781fa2e33 | |||
| 605a1365d3 | |||
| ebb1bbc966 | |||
| d4518f7a6e | |||
| 68c9008019 | |||
| cafde1e910 | |||
| 41f5f3fe6e | |||
| cf12ecfa34 | |||
| 4fa668927f | |||
| 24d2b5b4bd | |||
| 4da2e003e1 | |||
| 205debde9c | |||
| ba6c7b0901 | |||
| 8e5d02fd25 | |||
| 11d3251c67 | |||
| 9ec5787b4d | |||
| 027c669ad7 | |||
| 405c12ba3f | |||
| 64fd53a8e5 | |||
| 069d63e3cf | |||
| 66049e0e25 | |||
| db7ef01dbb | |||
| 1a6b649fd6 | |||
| 548619775e | |||
| 4b79a75f39 | |||
| 28912fff97 | |||
| 98a83ddcc0 | |||
| e9d965ce7a | |||
| b7624813cc | |||
| 734e451bd9 | |||
| 7a96436085 | |||
| 670af8fb1b | |||
| 0abd062e5c | |||
| 1286468d5a | |||
| 0f3da5be4a | |||
| 9170269e06 | |||
| 8bb460701f | |||
| c0a7173ca4 | |||
| 949e15d8c6 | |||
| 0caedeb481 | |||
| 83ca8d95be | |||
| 8483868783 | |||
| 6961388545 | |||
| 8ef1ae5202 | |||
| 26b8f93cea | |||
| a576523976 | |||
| cf21c4085b | |||
| 204f655d6d | |||
| fb14865785 | |||
| 5cd052cc59 | |||
| af49c4117c | |||
| dd736130a0 | |||
| cee8bda676 | |||
| 6420a72ceb | |||
| 380e7c7899 | |||
| 16b4058a8b | |||
| e3fba0257f | |||
| cb8b65f6f3 | |||
| 3944a8ed54 | |||
| 2ceafe6379 | |||
| 9f058bd88a | |||
| 1dd0ff13d8 | |||
| 6061f10230 | |||
| ea59fafb52 | |||
| 2ff71126de | |||
| 6d522e1743 | |||
| b37f3c819d | |||
| 1b41994e5f | |||
| 4fd4b0d713 | |||
| 7b43ed702e | |||
| 56cd77942b | |||
| 37dc723b4b | |||
| d7ea37c917 | |||
| c1d2bf9b9f | |||
| 36f837730a | |||
| b8f0a1dd82 | |||
| 2d1b62a978 | |||
| 0792d0d59d | |||
| 8c6da1a658 | |||
| 26be10f937 | |||
| ef4d9a13ed | |||
| 320d987ede | |||
| 51a4c4d13b | |||
| aaf3ae5f9c | |||
| 858f4f74b3 | |||
| 3feed90039 | |||
| b46330fc7a | |||
| 5ca7ce1f75 | |||
| d1d9b37ec7 | |||
| 823115c4ec | |||
| 8fe3fda27f | |||
| 63fffd7493 | |||
| 243a350e2b | |||
| 8c09e7ba36 | |||
| d89502f91f | |||
| 847602f933 | |||
| 8fcf8fe39c | |||
| d59eec9296 | |||
| b9122ebc34 | |||
| 38e066ee49 | |||
| 232a04a1a2 | |||
| dcb145635c | |||
| c9b73c68a6 | |||
| 08f7925ea9 | |||
| ed79ec7940 | |||
| 869a44dd04 | |||
| 6a9e9ebc4c | |||
| f18a2a11fa | |||
| 9aaaa755c7 | |||
| 818cd9044b | |||
| 006ca12b6d | |||
| b5f9ab7c20 | |||
| 0981f95f16 | |||
| f64ef32eaa | |||
| 7a19659c9e | |||
| b1202fcd55 | |||
| b8f991eddb | |||
| d0013f55a2 | |||
| 7efaff4f6f | |||
| bc7a84e462 | |||
| 61657bd1b1 | |||
| fc63f4f89c | |||
| 2c44c252d7 | |||
| 613fc6e3ef | |||
| 0a9604d10e | |||
| 1cabedc65e | |||
| 4e5a0d9e02 | |||
| bf373d894d | |||
| e6bbad84c1 | |||
| 4de477d46c | |||
| 264cab3c9b | |||
| 7f9ef59bea | |||
| 8ebbef4cbb | |||
| 0c98a84242 | |||
| 2bbdd21b2f | |||
| 794cfe949b | |||
| 6a5010a2d6 | |||
| 61559fee9d | |||
| 2118a20408 | |||
| b0bc20cb83 | |||
| 2cd77b075b | |||
| 03450c6085 | |||
| 5d3c522da2 | |||
| bf1ba6efc0 | |||
| 67ad82d52e | |||
| f437d976f8 | |||
| c94443faaa | |||
| c6d57f6a56 | |||
| 09b06da403 | |||
| df73ce8a60 | |||
| 8b22c2e04f | |||
| b4f4d6cea2 | |||
| cdf7b2acf1 | |||
| 94651731a5 | |||
| 56424d29a3 | |||
| c5083b2aef | |||
| 1b06795ffe | |||
| 6679cd60ce | |||
| 843a3c16ba | |||
| df40204a57 | |||
| 210910eb6e | |||
| ef427758da | |||
| def6bbf683 | |||
| 5f37a4a21c | |||
| dc869987f9 | |||
| b98d2201e5 | |||
| 8dd6440bb8 | |||
| a2d83c48eb | |||
| a0ae8f4a07 | |||
| 719dcbcd4d | |||
| caa207e868 | |||
| 306004ecd2 | |||
| bcac3c5b84 | |||
| ccc90108df | |||
| b9781915a4 | |||
| 1890e65b75 | |||
| 2f4da3ed52 | |||
| 27a6ac0c0b | |||
| b81a66f07e | |||
| bc43fc9585 | |||
| 4f7c013c78 | |||
| 09b15a07c6 | |||
| 4340f024d3 | |||
| 98e9780819 | |||
| a45d63595f | |||
| 5fbd1453b4 | |||
| f827201ee0 | |||
| 485a96962a | |||
| 98a104f731 | |||
| ff987f14d7 | |||
| 4dd08268dc | |||
| c18a13d4fc | |||
| 7a79995727 | |||
| 01eca562c5 | |||
| f491d0c0c4 | |||
| edf2287e13 | |||
| fcaab633c1 | |||
| 33b6579b5d | |||
| 72a8e4bc22 | |||
| c67b02b7b0 | |||
| 486cd00384 | |||
| caea7bb39d | |||
| e8fb7ee683 | |||
| e4ee4211c8 | |||
| 3fa3ebee14 | |||
| 8e531f400e | |||
| 5727c0dc8a | |||
| f599088cdf | |||
| d78da55f54 | |||
| 9052ff71e6 | |||
| 713da887ca | |||
| ad5950697e | |||
| 85d0d06cab | |||
| be6ec558e3 | |||
| dd2f60810a | |||
| 6c0b86d8f4 | |||
| 517ada6dbf | |||
| 0a098e3008 | |||
| c40078ffc9 | |||
| 729e0afdbe | |||
| eab86840d3 | |||
| 43aa2247f1 | |||
| afe17a507e | |||
| e621020474 | |||
| ac2aa7f288 | |||
| 805bd7e930 | |||
| f9bf2122b7 | |||
| 4e3e533e13 | |||
| 611c429912 | |||
| e2898ee5bf | |||
| e1a532500e | |||
| 4cf412b93d | |||
| 54c9c434e0 | |||
| de19f3e5fe | |||
| 8304993481 | |||
| 4c838aeaca | |||
| 4ec1145dce | |||
| 547d925065 | |||
| 874633051b | |||
| 2293f1ba15 | |||
| 483c3f61b5 | |||
| 83263968f2 | |||
| f279c61792 | |||
| 9717c696d8 | |||
| 94a594c777 | |||
| f1ae8a17cf | |||
| add9c43827 | |||
| d3f6a5c296 | |||
| 4aea78b7c0 | |||
| 4313fe670d | |||
| 2e8a1b67ea | |||
| e73044c533 | |||
| 418f09e364 | |||
| 08f207a396 | |||
| 4f57060708 | |||
| 1507724ecd | |||
| 4bc2f64b24 | |||
| 839ba31dfb | |||
| 2341452d91 | |||
| 7ab263b514 | |||
| 45b5b5960f | |||
| 1b8ec1f01c | |||
| c92cb1c096 | |||
| 8f31fbe275 | |||
| aa76419eda | |||
| 21503c0c38 | |||
| d957b19f55 | |||
| 122c4242ce | |||
| e769ed2fd8 | |||
| c722904831 | |||
| c93faa9564 | |||
| ce6495a46d | |||
| 04f2b905af | |||
| b668a2c0d2 | |||
| 5fc8f10602 | |||
| a27f399a9e | |||
| ff5d14fb63 | |||
| 27fb2d2d5b | |||
| 0b9d59bf0e | |||
| 23e059a065 | |||
| 45c7b29a96 | |||
| 3e6c3fda60 | |||
| 1a1363ef77 | |||
| b5c51e231b | |||
| 22711dd7be | |||
| 1ba4d3d5d1 | |||
| 823ab73585 | |||
| 66c444efac | |||
| b688b8921a | |||
| 00550ddab5 | |||
| 42fedec088 | |||
| ff3bc0e8c2 | |||
| dde7dff870 | |||
| 25ffc9cf96 | |||
| eca92cf5b5 | |||
| 372bdf8be7 | |||
| c7c90b0647 | |||
| 14c2da4fa2 | |||
| 64275cfd22 | |||
| f0b099d9f7 | |||
| c99d957bc8 | |||
| 51b14d55ec | |||
| f92d9994df | |||
| 431e1b87bd | |||
| 3ec2fc6030 | |||
| 99f416bf6b | |||
| 89acc4870b | |||
| 314c739c3a | |||
| 30b18fc997 | |||
| 168ac60853 | |||
| dff0c0c3b0 | |||
| ea3e9de7ab | |||
| c93fd900bc | |||
| a761aa9a37 | |||
| f37eb92d93 | |||
| 2c7cc5d325 | |||
| 16659c82d2 | |||
| 4ed3ca4128 | |||
| 9b2899c8fb | |||
| fada7fc9cc | |||
| 2be5bca42b | |||
| b1ebf6367c | |||
| 57e373f578 | |||
| 5c01874a32 | |||
| dd89e11633 | |||
| 4e51f19780 | |||
| 33d37d8dee | |||
| 60ce34af43 | |||
| 2602ff6e34 | |||
| ef5cd36a37 | |||
| 7d47f5aba3 | |||
| 33cb506c71 | |||
| 5fb36acb04 | |||
| 5f985f359b | |||
| 09045d5387 | |||
| 371af755df | |||
| 0485632e22 | |||
| c3d6de2412 | |||
| 36d49b11d9 | |||
| 866277510e | |||
| da8e00eb4e | |||
| cd30b2af3f | |||
| 38b3337d35 | |||
| 84d34aa12b | |||
| 96b7b68900 | |||
| 2a6ffbf5a4 | |||
| 1ac99e260b | |||
| 519062c8d0 | |||
| 78d7cdf662 | |||
| a191b1e811 | |||
| b372161874 | |||
| 5bbec7b17c | |||
| 54b847a8ef | |||
| a40a0f730f | |||
| a677b94297 | |||
| d102528d0a | |||
| 854692f975 | |||
| bdc321df4d | |||
| 230e8e9d4d | |||
| 96c49689ec | |||
| 698f560d56 | |||
| 67ee493e56 | |||
| 4fc7ef698a | |||
| 89434fc3bd | |||
| 6129f9afe8 | |||
| e5f38dbbc6 | |||
| 5534db95bc | |||
| 37c45424fd | |||
| ed44f4ed0c | |||
| f829f35cfa | |||
| 3033acadbb | |||
| 5c2a8a68c0 | |||
| e9544f8f92 | |||
| 1a6743dba9 | |||
| 06d6621c14 | |||
| d7b82edae3 | |||
| 5eae156134 | |||
| d8843e6057 | |||
| cf7c734778 | |||
| 240a8930ef | |||
| 4f3046593a | |||
| a0bd5bdd35 | |||
| e3bc63e4fa | |||
| fbf7c6293f | |||
| e128b02da1 | |||
| 2168dbc6fc | |||
| 97db895354 | |||
| 48fc68bfdb | |||
| 3741ffbb59 | |||
| caf58492f2 | |||
| 0c5bdd0205 | |||
| ae6da0d6ef | |||
| bdbb4cb495 | |||
| 566c358e8d | |||
| 324cffe6df | |||
| dadb8119e8 | |||
| 335d54b5f4 | |||
| 0fd851999f | |||
| 50f88798d8 | |||
| 8033cfc49d | |||
| 882f450ee8 | |||
| 4d60a582fe | |||
| 7ef3d33d0e | |||
| 24328ee8ae | |||
| d45c380389 | |||
| fbb3d144a1 | |||
| 7e7e299bce | |||
| d18116fcbe | |||
| e38513f077 | |||
| 08c4c641ec | |||
| 5d7e46b246 | |||
| dac906a79d | |||
| ecfeac4d63 | |||
| db1fc399a1 | |||
| 60616a5228 | |||
| 699c8d5b60 | |||
| 4264f013af | |||
| ecc4e1803c | |||
| 5c230c2d65 | |||
| 6af681354b | |||
| 5f381cf153 | |||
| 2ed290a214 | |||
| cf11472348 | |||
| 2c14514c56 | |||
| 053e6d8325 | |||
| 603c55fda7 | |||
| 113adcc5cc | |||
| a5bf8f6732 | |||
| c4b482634f | |||
| 4b083d6f81 | |||
| 04973d153b | |||
| 69ce7c78a4 | |||
| b64cf197f2 | |||
| a8b0279853 | |||
| e715e2ba2f | |||
| 985de36647 | |||
| b7b576e38b | |||
| 5018669ded | |||
| c5342d3eb0 | |||
| 0e7a0b5754 | |||
| 933ef02df0 | |||
| 513e0d748d | |||
| 852f01fddd | |||
| 9ebada63e9 | |||
| dbda70eee9 | |||
| 18bd10dfed | |||
| f76d03a0dc | |||
| 86ba5c9c1a | |||
| 5b56fbed97 | |||
| dfdd918ea0 | |||
| df2c7ec1a1 | |||
| 0d179f2042 | |||
| 63c6a9735c | |||
| f7e3f7d187 | |||
| cc54da5980 | |||
| ca82796081 | |||
| d092068bd2 | |||
| 0c1529237c | |||
| 6dbb2cb98a | |||
| ead763b909 | |||
| 415139851e | |||
| 5c99b72e2f | |||
| 82cb6c93cf | |||
| d21734ccf2 | |||
| 91934006d5 | |||
| 014c258c57 | |||
| 5e76bc3f2f | |||
| 7f6940f3a5 | |||
| 8d5682deb0 | |||
| 819abbfa7a | |||
| 227ad332b8 | |||
| 17c83e1a85 | |||
| 5d699496ca | |||
| e4f4fe81ae | |||
| abe28367fb | |||
| cc394d9f74 | |||
| 476b51ee31 | |||
| ec823ade14 | |||
| 0c3777f7d7 | |||
| a679e47c0d | |||
| 1e82735241 | |||
| a09ec6b0b5 | |||
| 0134a50ffa | |||
| 1da005e811 | |||
| 8ab912429b | |||
| 88d12223a5 | |||
| 6c363d9c4b | |||
| 6d3d9659e2 | |||
| 8977c5cef4 | |||
| d3645f6c63 | |||
| dbb057621d | |||
| 3a65bc0650 | |||
| 1eb6ff8c17 | |||
| 505cc01a18 | |||
| fdc4bb455f | |||
| c1a27aa6e4 | |||
| a14c7233d4 | |||
| c786d77b07 | |||
| e4913dfb73 | |||
| dfd33bab8e | |||
| 19b8f073b1 | |||
| b38b16f70e | |||
| dd69e4caff | |||
| 762a17d949 | |||
| d90cb23f22 | |||
| 1ccc391c8e | |||
| cafcead62e | |||
| 71ca46b21b | |||
| acf339ad13 | |||
| de4726ec8d | |||
| e58aff41f9 | |||
| 3c8acf9e0e | |||
| 782e8522c5 | |||
| dd4464cb08 | |||
| 3fdfd12448 | |||
| a11d23ba62 | |||
| 6bab362171 | |||
| 5d28c5c825 | |||
| b9173ebe50 | |||
| 0a1577ff43 | |||
| a616d6fd3e | |||
| 149b2ab9f9 | |||
| 4406dd88d1 | |||
| 48a950faf8 | |||
| 0ffbb5cb2c | |||
| f4c31bcf2c | |||
| f0e412a443 | |||
| ce91d36b7a | |||
| a022e5bb93 | |||
| 8e61673bd6 | |||
| 88e39d22f1 | |||
| 1441374f6e | |||
| d1cedaa3e8 | |||
| 5b721f6453 | |||
| a5f05b4715 | |||
| 96f49f117a | |||
| 88e1211762 | |||
| 59e1664223 | |||
| 42922a0d75 | |||
| 4f8a2a0900 | |||
| 9a2a26e904 | |||
| 067c3433b6 | |||
| d0b07c4890 | |||
| 24f8f48a2f | |||
| acafcc9888 | |||
| ec4cfe6f86 | |||
| 7b0c80c8be | |||
| 29029714ee | |||
| cd1203d1e2 | |||
| 4dfc34515c | |||
| 11a9f3ced3 | |||
| 5be1a7000c | |||
| 9059c1b850 | |||
| ec60fe42be | |||
| 275931969e | |||
| e37e0f189a | |||
| 8103943396 | |||
| 9497c99e9e | |||
| 2172d1a767 | |||
| e72c47869a | |||
| 5ea2124e91 | |||
| 0c08919bfa | |||
| a7562c3803 | |||
| 5f4d8f6862 | |||
| 9c7d0951b7 | |||
| 75e57c75cf | |||
| 04502b05d4 | |||
| e3e3a8404a | |||
| 48bf17614f | |||
| 1941b1ff65 | |||
| 46aa3d275a | |||
| d667e790b9 | |||
| bc345b569b | |||
| ee84b4e013 | |||
| e77129fa51 | |||
| 591855919e | |||
| 7af6188a8a | |||
| 1cd31cfce9 | |||
| 227dda921c | |||
| bdf2047ea1 | |||
| 0522996b7f | |||
| ad3a324719 | |||
| 8c9e178206 |
@@ -0,0 +1,20 @@
|
|||||||
|
krill-parser.js
|
||||||
|
krill.pegjs
|
||||||
|
.eslintrc.json
|
||||||
|
server.js
|
||||||
|
tidal-sniffer.js
|
||||||
|
*.jsx
|
||||||
|
tunejs.js
|
||||||
|
out/**
|
||||||
|
postcss.config.js
|
||||||
|
postcss.config.cjs
|
||||||
|
tailwind.config.js
|
||||||
|
tailwind.config.cjs
|
||||||
|
vite.config.js
|
||||||
|
/**/dist/**/*
|
||||||
|
!**/*.mjs
|
||||||
|
**/*.tsx
|
||||||
|
**/*.ts
|
||||||
|
**/*.json
|
||||||
|
**/dev-dist
|
||||||
|
**/dist
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": ["eslint:recommended"],
|
||||||
|
"overrides": [],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": ["import"],
|
||||||
|
"rules": {
|
||||||
|
"no-unused-vars": ["warn", { "destructuredArrayIgnorePattern": ".", "ignoreRestSiblings": false }],
|
||||||
|
"import/no-extraneous-dependencies": ["error", {"devDependencies": true}]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Build and Deploy
|
||||||
|
|
||||||
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
deployments: write
|
||||||
|
|
||||||
|
# Allow one concurrent deployment
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 7
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: "pnpm"
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v2
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v1
|
||||||
|
with:
|
||||||
|
# Upload entire repository
|
||||||
|
path: "./website/dist"
|
||||||
|
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v1
|
||||||
@@ -7,13 +7,18 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [16, 17]
|
node-version: [18]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v3
|
- uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
version: 7
|
||||||
cache: 'npm'
|
- uses: actions/setup-node@v3
|
||||||
- run: npm install
|
with:
|
||||||
- run: npm test
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'pnpm'
|
||||||
|
- run: pnpm install
|
||||||
|
- run: pnpm run format-check
|
||||||
|
- run: pnpm run lint
|
||||||
|
- run: pnpm test
|
||||||
|
|||||||
+5
-1
@@ -35,4 +35,8 @@ tutorial.rendered.mdx
|
|||||||
doc.json
|
doc.json
|
||||||
talk/public/EmuSP12
|
talk/public/EmuSP12
|
||||||
talk/public/samples
|
talk/public/samples
|
||||||
server/samples/old
|
server/samples/old
|
||||||
|
repl/stats.html
|
||||||
|
coverage
|
||||||
|
public/icons/apple-splash-*
|
||||||
|
dev-dist
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
*.md
|
||||||
|
*.json
|
||||||
|
*.yml
|
||||||
|
*.
|
||||||
|
**/out
|
||||||
|
**/dist
|
||||||
|
packages/mini/krill-parser.js
|
||||||
|
packages/xen/tunejs.js
|
||||||
|
paper
|
||||||
|
pnpm-lock.yaml
|
||||||
|
pnpm-workspace.yaml
|
||||||
|
**/dev-dist
|
||||||
+14
-15
@@ -1,16 +1,15 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"jsxSingleQuote": false,
|
"jsxSingleQuote": false,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"jsxBracketSameLine": false,
|
"bracketSameLine": false,
|
||||||
"arrowParens": "always",
|
"arrowParens": "always",
|
||||||
"proseWrap": "preserve",
|
"proseWrap": "preserve",
|
||||||
"htmlWhitespaceSensitivity": "css",
|
"htmlWhitespaceSensitivity": "css",
|
||||||
"endOfLine": "lf"
|
"endOfLine": "lf"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+5
-1
@@ -2,5 +2,9 @@
|
|||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"subspan",
|
"subspan",
|
||||||
"vals"
|
"vals"
|
||||||
]
|
],
|
||||||
|
"yaml.schemas": {
|
||||||
|
"https://json.schemastore.org/github-workflow.json": "file:///home/felix/projects/strudel/.github/workflows/deploy.yml"
|
||||||
|
},
|
||||||
|
"testing.automaticallyOpenPeekView": "never"
|
||||||
}
|
}
|
||||||
+53
-24
@@ -12,8 +12,8 @@ To get in touch with the contributors, either
|
|||||||
|
|
||||||
## Ask a Question
|
## Ask a Question
|
||||||
|
|
||||||
If you have any questions about strudel, make sure you've read the
|
If you have any questions about strudel, make sure you've glanced through the
|
||||||
[tutorial](https://strudel.tidalcycles.org/tutorial/) to find out if it answers your question.
|
[docs](https://strudel.tidalcycles.org/learn/) to find out if it answers your question.
|
||||||
If not, use one of the Communication Channels above!
|
If not, use one of the Communication Channels above!
|
||||||
|
|
||||||
Don't be afraid to ask! Your question might be of great value for other people too.
|
Don't be afraid to ask! Your question might be of great value for other people too.
|
||||||
@@ -29,13 +29,10 @@ If you made some music with strudel, you can give back some love and share what
|
|||||||
Your creation could also be part of the random selection in the REPL if you want.
|
Your creation could also be part of the random selection in the REPL if you want.
|
||||||
Use one of the Communication Channels listed above.
|
Use one of the Communication Channels listed above.
|
||||||
|
|
||||||
## Improve the Tutorial
|
## Improve the Docs
|
||||||
|
|
||||||
If you find some weak spots in the [tutorial](https://strudel.tidalcycles.org/),
|
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/learn/getting-started),
|
||||||
you are welcome to improve them by editing [this file](https://github.com/tidalcycles/strudel/blob/main/tutorial/tutorial.mdx).
|
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
|
||||||
|
|
||||||
|
|
||||||
This will even work without setting up a development environment, only a github account is required.
|
|
||||||
|
|
||||||
## Propose a Feature
|
## Propose a Feature
|
||||||
|
|
||||||
@@ -52,40 +49,43 @@ Please check that it has not been reported before.
|
|||||||
To fix a bug that has been reported,
|
To fix a bug that has been reported,
|
||||||
|
|
||||||
1. check that nobody else is already fixing it and respond to the issue to let people know you're on it
|
1. check that nobody else is already fixing it and respond to the issue to let people know you're on it
|
||||||
3. fork the repository
|
2. fork the repository
|
||||||
4. make sure you've setup the project (see below)
|
3. make sure you've setup the project (see below)
|
||||||
5. hopefully fix the bug
|
4. hopefully fix the bug
|
||||||
6. make sure the tests pass
|
5. make sure the tests pass
|
||||||
7. send a pull request
|
6. send a pull request
|
||||||
|
|
||||||
## Write Tests
|
## Write Tests
|
||||||
|
|
||||||
There are still many tests that have not been written yet! Reading and writing tests is a great opportunity to get familiar with the codebase.
|
There are still many tests that have not been written yet! Reading and writing tests is a great opportunity to get familiar with the codebase.
|
||||||
You can find the tests in each package in the `test` folder. To run all tests, run `npm test` from the root folder.
|
You can find the tests in each package in the `test` folder. To run all tests, run `pnpm test` from the root folder.
|
||||||
|
|
||||||
## Project Setup
|
## Project Setup
|
||||||
|
|
||||||
To get the project up and running for development, make sure you have installed:
|
To get the project up and running for development, make sure you have installed:
|
||||||
|
|
||||||
- git
|
- [git](https://git-scm.com/)
|
||||||
- node, preferably v16
|
- [node](https://nodejs.org/en/) >= 18
|
||||||
|
- [pnpm](https://pnpm.io/) (`npm i pnpm -g`)
|
||||||
|
|
||||||
then, do the following:
|
then, do the following:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/tidalcycles/strudel.git && cd strudel
|
git clone https://github.com/tidalcycles/strudel.git && cd strudel
|
||||||
npm i # install at root to symlink packages
|
pnpm i # install at root to symlink packages
|
||||||
npx lerna bootstrap # install all dependencies in packages
|
pnpm start # start repl
|
||||||
cd repl && npm i # install repl dependencies
|
|
||||||
npm run start # start repl
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Those commands might look slightly different for your OS.
|
Those commands might look slightly different for your OS.
|
||||||
Please report any problems you've had with the setup instructions!
|
Please report any problems you've had with the setup instructions!
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
To make sure the code changes only where it should, we are using prettier to unify the code style.
|
To make sure the code changes only where it should, we are using prettier to unify the code style.
|
||||||
|
|
||||||
|
- You can format all files at once by running `pnpm prettier` from the project root
|
||||||
|
- Run `pnpm format-check` from the project root to check if all files are well formatted
|
||||||
|
|
||||||
If you use VSCode, you can
|
If you use VSCode, you can
|
||||||
|
|
||||||
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||||
@@ -93,21 +93,50 @@ If you use VSCode, you can
|
|||||||
3. Choose "Configure Default Formatter..."
|
3. Choose "Configure Default Formatter..."
|
||||||
4. Select prettier
|
4. Select prettier
|
||||||
|
|
||||||
|
## ESLint
|
||||||
|
|
||||||
|
To prevent unwanted runtime errors, this project uses [eslint](https://eslint.org/).
|
||||||
|
|
||||||
|
- You can check for lint errors by running `pnpm lint`
|
||||||
|
|
||||||
|
There are also eslint extensions / plugins for most editors.
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
- Run all tests with `pnpm test`
|
||||||
|
- Run all tests with UI using `pnpm test-ui`
|
||||||
|
|
||||||
|
## Running all CI Checks
|
||||||
|
|
||||||
|
When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests.
|
||||||
|
You can run the same check with `pnpm check`
|
||||||
|
|
||||||
## Package Workflow
|
## Package Workflow
|
||||||
|
|
||||||
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
|
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
|
||||||
When you run `npm i` on the root folder, [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces) will symlink all packages
|
When you run `pnpm i` on the root folder, [pnpm workspaces](https://pnpm.io/workspaces) will install all dependencies of all subpackages. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
|
||||||
in the `node_modules` folder. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
|
allowing to develop multiple packages at the same time.
|
||||||
which allows developing multiple packages at the same time
|
|
||||||
|
|
||||||
## Package Publishing
|
## Package Publishing
|
||||||
|
|
||||||
To publish all packages that have been changed since the last release, run:
|
To publish all packages that have been changed since the last release, run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
npm login
|
||||||
npx lerna publish
|
npx lerna publish
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
|
||||||
|
Important: Always publish with `pnpm`, as `npm` does not support overriding main files in `publishConfig`, which is done in all the packages.
|
||||||
|
|
||||||
|
### New Packages
|
||||||
|
|
||||||
|
To add a new package, you have to publish it manually the first time, using:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd packages/<package-name> && pnpm publish --access public
|
||||||
|
```
|
||||||
|
|
||||||
## Have Fun
|
## Have Fun
|
||||||
|
|
||||||
Remember to have fun, and that this project is driven by the passion of volunteers!
|
Remember to have fun, and that this project is driven by the passion of volunteers!
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
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 slowly stabilising, but please continue to tread carefully.
|
||||||
|
|
||||||
- Try it here: <https://strudel.tidalcycles.org/>
|
- Try it here: <https://strudel.tidalcycles.org/>
|
||||||
- Tutorial: <https://strudel.tidalcycles.org/tutorial/>
|
- Docs: <https://strudel.tidalcycles.org/learn/>
|
||||||
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
||||||
|
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
||||||
|
|
||||||
## Running Locally
|
## Running Locally
|
||||||
|
|
||||||
|
|||||||
+1806
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 150 KiB |
@@ -0,0 +1,17 @@
|
|||||||
|
// this barrel export is currently only used to find undocumented exports
|
||||||
|
export * from './packages/core/index.mjs';
|
||||||
|
export * from './packages/csound/index.mjs';
|
||||||
|
export * from './packages/embed/index.mjs';
|
||||||
|
export * from './packages/eval/index.mjs';
|
||||||
|
export * from './packages/midi/index.mjs';
|
||||||
|
export * from './packages/mini/index.mjs';
|
||||||
|
export * from './packages/osc/index.mjs';
|
||||||
|
export * from './packages/react/index.mjs';
|
||||||
|
export * from './packages/serial/index.mjs';
|
||||||
|
export * from './packages/soundfonts/index.mjs';
|
||||||
|
export * from './packages/tonal/index.mjs';
|
||||||
|
export * from './packages/tone/index.mjs';
|
||||||
|
export * from './packages/transpiler/index.mjs';
|
||||||
|
export * from './packages/webaudio/index.mjs';
|
||||||
|
export * from './packages/webdirt/index.mjs';
|
||||||
|
export * from './packages/xen/index.mjs';
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"source": {
|
"source": {
|
||||||
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
|
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
|
||||||
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser"
|
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
|
||||||
},
|
},
|
||||||
"plugins": ["plugins/markdown"],
|
"plugins": ["plugins/markdown"],
|
||||||
"opts": {
|
"opts": {
|
||||||
|
|||||||
+3
-1
@@ -2,5 +2,7 @@
|
|||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"version": "independent"
|
"version": "independent",
|
||||||
|
"npmClient": "pnpm",
|
||||||
|
"useWorkspaces": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# my-patterns
|
||||||
|
|
||||||
|
This directory can be used to save your own patterns, which then get
|
||||||
|
made into a pattern swatch.
|
||||||
|
|
||||||
|
Example: <https://felixroos.github.io/strudel/swatch/>
|
||||||
|
|
||||||
|
## deploy
|
||||||
|
|
||||||
|
### 1. fork the [strudel repo on github](https://github.com/tidalcycles/strudel.git)
|
||||||
|
|
||||||
|
### 2. clone your fork to your machine `git clone https://github.com/<your-username>/strudel.git strudel && cd strudel`
|
||||||
|
|
||||||
|
### 3. create a separate branch like `git branch patternuary && git checkout patternuary`
|
||||||
|
|
||||||
|
### 4. save one or more .txt files in the my-patterns folder
|
||||||
|
|
||||||
|
### 5. edit `website/public/CNAME` to contain `<your-username>.github.io/strudel`
|
||||||
|
|
||||||
|
### 6. edit `website/astro.config.mjs` to use site: `https://<your-username>.github.io` and base `/strudel`, like this
|
||||||
|
|
||||||
|
```js
|
||||||
|
const site = 'https://<your-username>.github.io';
|
||||||
|
const base = '/strudel';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. commit & push the changes
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git add . && git commit -m "site config" && git push --set-upstream origin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. deploy to github pages
|
||||||
|
|
||||||
|
- go to settings -> pages and select "Github Actions" as source
|
||||||
|
- go to settings -> environments -> github-pages and press the edit button next to `main` and type in `patternuary` (under "Deployment branches")
|
||||||
|
- go to Actions -> `Build and Deploy` and click `Run workflow` with branch `patternuary`
|
||||||
|
|
||||||
|
### 9. view your patterns at `<your-username>.github.io/strudel/swatch/`
|
||||||
|
|
||||||
|
Alternatively, github pages allows you to use a custom domain, like https://mycooldomain.org/swatch/. [See their documentation for details](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site).
|
||||||
|
|
||||||
|
### 10. optional: automatic deployment
|
||||||
|
|
||||||
|
If you want to automatically deploy your site on push, go to `deploy.yml` and change `workflow_dispatch` to `push`.
|
||||||
|
|
||||||
|
## running locally
|
||||||
|
|
||||||
|
- install dependencies with `npm run setup`
|
||||||
|
- run dev server with `npm run repl` and open `http://localhost:3000/strudel/swatch/`
|
||||||
|
|
||||||
|
## tests fail?
|
||||||
|
|
||||||
|
Your tests might fail if the code does not follow prettiers format.
|
||||||
|
In that case, run `npm run codeformat`. To disable that, remove `npm run format-check` from `test.yml`
|
||||||
|
|
||||||
|
## updating your fork
|
||||||
|
|
||||||
|
To update your fork, you can pull the main branch and merge it into your `patternuary` branch.
|
||||||
Generated
-21906
File diff suppressed because it is too large
Load Diff
+39
-17
@@ -1,25 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/monorepo",
|
"name": "@strudel.cycles/monorepo",
|
||||||
"version": "0.0.1",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Port of tidalcycles to javascript",
|
"description": "Port of tidalcycles to javascript",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest run --version",
|
"setup": "pnpm i",
|
||||||
"test-ui": "vitest --ui",
|
"pretest": "npm run jsdoc-json",
|
||||||
"bootstrap": "lerna bootstrap",
|
"prebuild": "npm run jsdoc-json",
|
||||||
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
|
"prestart": "npm run jsdoc-json",
|
||||||
"snapshot": "cd repl && npm run snapshot",
|
"test": "npm run pretest && vitest run --version",
|
||||||
"repl": "cd repl && npm run dev",
|
"test-ui": "npm run pretest && vitest --ui",
|
||||||
|
"test-coverage": "npm run pretest && vitest --coverage",
|
||||||
|
"snapshot": "npm run pretest && vitest run -u --silent",
|
||||||
|
"repl": "npm run prestart && cd website && npm run dev",
|
||||||
|
"start": "npm run prestart && cd website && npm run dev",
|
||||||
|
"dev": "npm run prestart && cd website && npm run dev",
|
||||||
|
"build": "npm run prebuild && cd website && npm run build",
|
||||||
|
"preview": "cd website && npm run preview",
|
||||||
"osc": "cd packages/osc && npm run server",
|
"osc": "cd packages/osc && npm run server",
|
||||||
"build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build",
|
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
|
||||||
"preview": "npx serve ./out",
|
|
||||||
"deploy": "gh-pages -d out",
|
|
||||||
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
||||||
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json"
|
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
|
||||||
|
"lint": "eslint . --ext mjs,js --quiet",
|
||||||
|
"codeformat": "prettier --write .",
|
||||||
|
"format-check": "prettier --check .",
|
||||||
|
"report-undocumented": "npm run jsdoc-json && node undocumented.mjs > undocumented.json",
|
||||||
|
"check": "npm run format-check && npm run lint && npm run test",
|
||||||
|
"iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
@@ -37,15 +45,29 @@
|
|||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"homepage": "https://strudel.tidalcycles.org",
|
||||||
|
"dependencies": {
|
||||||
|
"dependency-tree": "^9.0.0",
|
||||||
|
"vitest": "^0.25.7",
|
||||||
|
"@strudel.cycles/core": "workspace:*",
|
||||||
|
"@strudel.cycles/mini": "workspace:*",
|
||||||
|
"@strudel.cycles/tonal": "workspace:*",
|
||||||
|
"@strudel.cycles/transpiler": "workspace:*",
|
||||||
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
|
"@strudel.cycles/xen": "workspace:*"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitest/ui": "^0.21.1",
|
"@vitest/ui": "^0.25.7",
|
||||||
|
"c8": "^7.12.0",
|
||||||
|
"canvas": "^2.11.0",
|
||||||
|
"eslint": "^8.28.0",
|
||||||
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"gh-pages": "^4.0.0",
|
"gh-pages": "^4.0.0",
|
||||||
"happy-dom": "^6.0.4",
|
|
||||||
"jsdoc": "^3.6.10",
|
"jsdoc": "^3.6.10",
|
||||||
"jsdoc-json": "^2.0.2",
|
"jsdoc-json": "^2.0.2",
|
||||||
"jsdoc-to-markdown": "^7.1.1",
|
"jsdoc-to-markdown": "^7.1.1",
|
||||||
"lerna": "^4.0.0",
|
"lerna": "^4.0.0",
|
||||||
"vitest": "^0.21.1"
|
"prettier": "^2.8.1",
|
||||||
|
"rollup-plugin-visualizer": "^5.8.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Packages
|
||||||
|
|
||||||
|
Each folder represents one of the @strudel.cycles/* packages [published to npm](https://www.npmjs.com/org/strudel.cycles).
|
||||||
|
|
||||||
|
To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs.
|
||||||
|
|
||||||
|
This is a graphical view of all the packages: [full screen](https://raw.githubusercontent.com/tidalcycles/strudel/main/dependencies.svg)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Generated with
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx depcruise --include-only "^packages" -X "node_modules" --output-type dot packages | dot -T svg > dependencygraph.svg
|
||||||
|
```
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
examples
|
||||||
@@ -33,6 +33,7 @@ b: 3/2 - 7/4
|
|||||||
c: 7/4 - 2
|
c: 7/4 - 2
|
||||||
```
|
```
|
||||||
|
|
||||||
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-qmz6qr?file=/src/index.js).
|
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-forked-9ywhv7?file=/src/index.js).
|
||||||
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/package-examples/packages/core/examples/canvas.html)
|
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/canvas.html)
|
||||||
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/package-examples/packages/core/examples/metro.html)
|
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/vanilla.html)
|
||||||
|
- [open minimal vite example](./examples/vite-vanilla-repl/)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
||||||
|
const { createParams } = controls;
|
||||||
|
|
||||||
|
let clearColor = '#22222210';
|
||||||
|
|
||||||
|
Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } = {}) {
|
||||||
|
window.frame && cancelAnimationFrame(window.frame);
|
||||||
|
const ctx = getDrawContext();
|
||||||
|
const { clientWidth: ww, clientHeight: wh } = ctx.canvas;
|
||||||
|
let smearPart = smear === 0 ? '99' : Number((1 - smear) * 100).toFixed(0);
|
||||||
|
smearPart = smearPart.length === 1 ? `0${smearPart}` : smearPart;
|
||||||
|
clearColor = `#200010${smearPart}`;
|
||||||
|
const render = (t) => {
|
||||||
|
let frame;
|
||||||
|
/* if (sync) {
|
||||||
|
t = scheduler.now();
|
||||||
|
frame = this.queryArc(t, t);
|
||||||
|
} else { */
|
||||||
|
t = Math.round(t);
|
||||||
|
frame = this.slow(1000).queryArc(t, t);
|
||||||
|
// }
|
||||||
|
ctx.fillStyle = clearColor;
|
||||||
|
ctx.fillRect(0, 0, ww, wh);
|
||||||
|
frame.forEach((f) => {
|
||||||
|
let { x, y, w, h, s, r, angle = 0, fill = 'darkseagreen' } = f.value;
|
||||||
|
w *= ww;
|
||||||
|
h *= wh;
|
||||||
|
if (r !== undefined && angle !== undefined) {
|
||||||
|
const radians = angle * 2 * Math.PI;
|
||||||
|
const [cx, cy] = [(ww - w) / 2, (wh - h) / 2];
|
||||||
|
x = cx + Math.cos(radians) * r * cx;
|
||||||
|
y = cy + Math.sin(radians) * r * cy;
|
||||||
|
} else {
|
||||||
|
x *= ww - w;
|
||||||
|
y *= wh - h;
|
||||||
|
}
|
||||||
|
const val = { ...f.value, x, y, w, h };
|
||||||
|
ctx.fillStyle = fill;
|
||||||
|
if (s === 'rect') {
|
||||||
|
ctx.fillRect(x, y, w, h);
|
||||||
|
} else if (s === 'ellipse') {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(x + w / 2, y + h / 2, w / 2, h / 2, 0, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
callback && callback(ctx, val, f);
|
||||||
|
});
|
||||||
|
window.frame = requestAnimationFrame(render);
|
||||||
|
};
|
||||||
|
window.frame = requestAnimationFrame(render);
|
||||||
|
return silence;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const { x, y, w, h, angle, r, fill, smear } = createParams('x', 'y', 'w', 'h', 'angle', 'r', 'fill', 'smear');
|
||||||
|
|
||||||
|
export const rescale = register('rescale', function (f, pat) {
|
||||||
|
return pat.mul(x(f).w(f).y(f).h(f));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const moveXY = register('moveXY', function (dx, dy, pat) {
|
||||||
|
return pat.add(x(dx).y(dy));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zoomIn = register('zoomIn', function (f, pat) {
|
||||||
|
const d = pure(1).sub(f).div(2);
|
||||||
|
return pat.rescale(f).move(d, d);
|
||||||
|
});
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
export const colorMap = {
|
||||||
|
aliceblue: '#f0f8ff',
|
||||||
|
antiquewhite: '#faebd7',
|
||||||
|
aqua: '#00ffff',
|
||||||
|
aquamarine: '#7fffd4',
|
||||||
|
azure: '#f0ffff',
|
||||||
|
beige: '#f5f5dc',
|
||||||
|
bisque: '#ffe4c4',
|
||||||
|
black: '#000000',
|
||||||
|
blanchedalmond: '#ffebcd',
|
||||||
|
blue: '#0000ff',
|
||||||
|
blueviolet: '#8a2be2',
|
||||||
|
brown: '#a52a2a',
|
||||||
|
burlywood: '#deb887',
|
||||||
|
cadetblue: '#5f9ea0',
|
||||||
|
chartreuse: '#7fff00',
|
||||||
|
chocolate: '#d2691e',
|
||||||
|
coral: '#ff7f50',
|
||||||
|
cornflowerblue: '#6495ed',
|
||||||
|
cornsilk: '#fff8dc',
|
||||||
|
crimson: '#dc143c',
|
||||||
|
cyan: '#00ffff',
|
||||||
|
darkblue: '#00008b',
|
||||||
|
darkcyan: '#008b8b',
|
||||||
|
darkgoldenrod: '#b8860b',
|
||||||
|
darkgray: '#a9a9a9',
|
||||||
|
darkgreen: '#006400',
|
||||||
|
darkgrey: '#a9a9a9',
|
||||||
|
darkkhaki: '#bdb76b',
|
||||||
|
darkmagenta: '#8b008b',
|
||||||
|
darkolivegreen: '#556b2f',
|
||||||
|
darkorange: '#ff8c00',
|
||||||
|
darkorchid: '#9932cc',
|
||||||
|
darkred: '#8b0000',
|
||||||
|
darksalmon: '#e9967a',
|
||||||
|
darkseagreen: '#8fbc8f',
|
||||||
|
darkslateblue: '#483d8b',
|
||||||
|
darkslategray: '#2f4f4f',
|
||||||
|
darkslategrey: '#2f4f4f',
|
||||||
|
darkturquoise: '#00ced1',
|
||||||
|
darkviolet: '#9400d3',
|
||||||
|
deeppink: '#ff1493',
|
||||||
|
deepskyblue: '#00bfff',
|
||||||
|
dimgray: '#696969',
|
||||||
|
dimgrey: '#696969',
|
||||||
|
dodgerblue: '#1e90ff',
|
||||||
|
firebrick: '#b22222',
|
||||||
|
floralwhite: '#fffaf0',
|
||||||
|
forestgreen: '#228b22',
|
||||||
|
fuchsia: '#ff00ff',
|
||||||
|
gainsboro: '#dcdcdc',
|
||||||
|
ghostwhite: '#f8f8ff',
|
||||||
|
gold: '#ffd700',
|
||||||
|
goldenrod: '#daa520',
|
||||||
|
gray: '#808080',
|
||||||
|
green: '#008000',
|
||||||
|
greenyellow: '#adff2f',
|
||||||
|
grey: '#808080',
|
||||||
|
honeydew: '#f0fff0',
|
||||||
|
hotpink: '#ff69b4',
|
||||||
|
indianred: '#cd5c5c',
|
||||||
|
indigo: '#4b0082',
|
||||||
|
ivory: '#fffff0',
|
||||||
|
khaki: '#f0e68c',
|
||||||
|
lavender: '#e6e6fa',
|
||||||
|
lavenderblush: '#fff0f5',
|
||||||
|
lawngreen: '#7cfc00',
|
||||||
|
lemonchiffon: '#fffacd',
|
||||||
|
lightblue: '#add8e6',
|
||||||
|
lightcoral: '#f08080',
|
||||||
|
lightcyan: '#e0ffff',
|
||||||
|
lightgoldenrodyellow: '#fafad2',
|
||||||
|
lightgray: '#d3d3d3',
|
||||||
|
lightgreen: '#90ee90',
|
||||||
|
lightgrey: '#d3d3d3',
|
||||||
|
lightpink: '#ffb6c1',
|
||||||
|
lightsalmon: '#ffa07a',
|
||||||
|
lightseagreen: '#20b2aa',
|
||||||
|
lightskyblue: '#87cefa',
|
||||||
|
lightslategray: '#778899',
|
||||||
|
lightslategrey: '#778899',
|
||||||
|
lightsteelblue: '#b0c4de',
|
||||||
|
lightyellow: '#ffffe0',
|
||||||
|
lime: '#00ff00',
|
||||||
|
limegreen: '#32cd32',
|
||||||
|
linen: '#faf0e6',
|
||||||
|
magenta: '#ff00ff',
|
||||||
|
maroon: '#800000',
|
||||||
|
mediumaquamarine: '#66cdaa',
|
||||||
|
mediumblue: '#0000cd',
|
||||||
|
mediumorchid: '#ba55d3',
|
||||||
|
mediumpurple: '#9370db',
|
||||||
|
mediumseagreen: '#3cb371',
|
||||||
|
mediumslateblue: '#7b68ee',
|
||||||
|
mediumspringgreen: '#00fa9a',
|
||||||
|
mediumturquoise: '#48d1cc',
|
||||||
|
mediumvioletred: '#c71585',
|
||||||
|
midnightblue: '#191970',
|
||||||
|
mintcream: '#f5fffa',
|
||||||
|
mistyrose: '#ffe4e1',
|
||||||
|
moccasin: '#ffe4b5',
|
||||||
|
navajowhite: '#ffdead',
|
||||||
|
navy: '#000080',
|
||||||
|
oldlace: '#fdf5e6',
|
||||||
|
olive: '#808000',
|
||||||
|
olivedrab: '#6b8e23',
|
||||||
|
orange: '#ffa500',
|
||||||
|
orangered: '#ff4500',
|
||||||
|
orchid: '#da70d6',
|
||||||
|
palegoldenrod: '#eee8aa',
|
||||||
|
palegreen: '#98fb98',
|
||||||
|
paleturquoise: '#afeeee',
|
||||||
|
palevioletred: '#db7093',
|
||||||
|
papayawhip: '#ffefd5',
|
||||||
|
peachpuff: '#ffdab9',
|
||||||
|
peru: '#cd853f',
|
||||||
|
pink: '#ffc0cb',
|
||||||
|
plum: '#dda0dd',
|
||||||
|
powderblue: '#b0e0e6',
|
||||||
|
purple: '#800080',
|
||||||
|
red: '#ff0000',
|
||||||
|
rosybrown: '#bc8f8f',
|
||||||
|
royalblue: '#4169e1',
|
||||||
|
saddlebrown: '#8b4513',
|
||||||
|
salmon: '#fa8072',
|
||||||
|
sandybrown: '#f4a460',
|
||||||
|
seagreen: '#2e8b57',
|
||||||
|
seashell: '#fff5ee',
|
||||||
|
sienna: '#a0522d',
|
||||||
|
silver: '#c0c0c0',
|
||||||
|
skyblue: '#87ceeb',
|
||||||
|
slateblue: '#6a5acd',
|
||||||
|
slategray: '#708090',
|
||||||
|
slategrey: '#708090',
|
||||||
|
snow: '#fffafa',
|
||||||
|
springgreen: '#00ff7f',
|
||||||
|
steelblue: '#4682b4',
|
||||||
|
tan: '#d2b48c',
|
||||||
|
teal: '#008080',
|
||||||
|
thistle: '#d8bfd8',
|
||||||
|
tomato: '#ff6347',
|
||||||
|
turquoise: '#40e0d0',
|
||||||
|
violet: '#ee82ee',
|
||||||
|
wheat: '#f5deb3',
|
||||||
|
white: '#ffffff',
|
||||||
|
whitesmoke: '#f5f5f5',
|
||||||
|
yellow: '#ffff00',
|
||||||
|
yellowgreen: '#9acd32',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function convertColorToNumber(color) {
|
||||||
|
// Convert color to lowercase for easier matching
|
||||||
|
color = color.toLowerCase();
|
||||||
|
|
||||||
|
// If the color is a hex code, convert it to a number
|
||||||
|
if (color[0] === '#') {
|
||||||
|
return convertHexToNumber(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the color is a named color, return the corresponding number
|
||||||
|
if (colorMap[color] !== undefined) {
|
||||||
|
return convertHexToNumber(colorMap[color]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the color is not recognized, return null
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertHexToNumber(hex) {
|
||||||
|
// Remove the leading '#' from the hex code
|
||||||
|
hex = hex.slice(1);
|
||||||
|
|
||||||
|
// Convert the hex code to a number
|
||||||
|
return parseInt(hex, 16);
|
||||||
|
}
|
||||||
+193
-114
@@ -4,49 +4,51 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, sequence } from './pattern.mjs';
|
import { Pattern, sequence, registerControl } from './pattern.mjs';
|
||||||
|
|
||||||
const controls = {};
|
const controls = {};
|
||||||
const generic_params = [
|
const generic_params = [
|
||||||
/**
|
/**
|
||||||
* Select a sound / sample by name.
|
* Select a sound / sample by name.
|
||||||
*
|
*
|
||||||
* <details>
|
|
||||||
* <summary>show all sounds</summary>
|
|
||||||
*
|
|
||||||
* 808 (6) 808bd (25) 808cy (25) 808hc (5) 808ht (5) 808lc (5) 808lt (5) 808mc (5) 808mt (5) 808oh (5) 808sd (25) 909 (1) ab (12) ade (10) ades2 (9) ades3 (7) ades4 (6) alex (2) alphabet (26) amencutup (32) armora (7) arp (2) arpy (11) auto (11) baa (7) baa2 (7) bass (4) bass0 (3) bass1 (30) bass2 (5) bass3 (11) bassdm (24) bassfoo (3) battles (2) bd (24) bend (4) bev (2) bin (2) birds (10) birds3 (19) bleep (13) blip (2) blue (2) bottle (13) breaks125 (2) breaks152 (1) breaks157 (1) breaks165 (1) breath (1) bubble (8) can (14) casio (3) cb (1) cc (6) chin (4) circus (3) clak (2) click (4) clubkick (5) co (4) coins (1) control (2) cosmicg (15) cp (2) cr (6) crow (4) d (4) db (13) diphone (38) diphone2 (12) dist (16) dork2 (4) dorkbot (2) dr (42) dr2 (6) dr55 (4) dr_few (8) drum (6) drumtraks (13) e (8) east (9) electro1 (13) em2 (6) erk (1) f (1) feel (7) feelfx (8) fest (1) fire (1) flick (17) fm (17) foo (27) future (17) gab (10) gabba (4) gabbaloud (4) gabbalouder (4) glasstap (3) glitch (8) glitch2 (8) gretsch (24) gtr (3) h (7) hand (17) hardcore (12) hardkick (6) haw (6) hc (6) hh (13) hh27 (13) hit (6) hmm (1) ho (6) hoover (6) house (8) ht (16) if (5) ifdrums (3) incoming (8) industrial (32) insect (3) invaders (18) jazz (8) jungbass (20) jungle (13) juno (12) jvbass (13) kicklinn (1) koy (2) kurt (7) latibro (8) led (1) less (4) lighter (33) linnhats (6) lt (16) made (7) made2 (1) mash (2) mash2 (4) metal (10) miniyeah (4) monsterb (6) moog (7) mouth (15) mp3 (4) msg (9) mt (16) mute (28) newnotes (15) noise (1) noise2 (8) notes (15) numbers (9) oc (4) odx (15) off (1) outdoor (6) pad (3) padlong (1) pebbles (1) perc (6) peri (15) pluck (17) popkick (10) print (11) proc (2) procshort (8) psr (30) rave (8) rave2 (4) ravemono (2) realclaps (4) reverbkick (1) rm (2) rs (1) sax (22) sd (2) seawolf (3) sequential (8) sf (18) sheffield (1) short (5) sid (12) sine (6) sitar (8) sn (52) space (18) speakspell (12) speech (7) speechless (10) speedupdown (9) stab (23) stomp (10) subroc3d (11) sugar (2) sundance (6) tabla (26) tabla2 (46) tablex (3) tacscan (22) tech (13) techno (7) tink (5) tok (4) toys (13) trump (11) ul (10) ulgab (5) uxay (3) v (6) voodoo (5) wind (10) wobble (1) world (3) xmas (1) yeah (31)
|
|
||||||
*
|
|
||||||
* <a href="https://tidalcycles.org/docs/configuration/Audio%20Samples/default_library" target="_blank">more info</a>
|
|
||||||
*
|
|
||||||
* </details>
|
|
||||||
*
|
|
||||||
* @name s
|
* @name s
|
||||||
* @param {string | Pattern} sound The sound / pattern of sounds to pick
|
* @param {string | Pattern} sound The sound / pattern of sounds to pick
|
||||||
* @example
|
* @example
|
||||||
* s("bd hh").osc()
|
* s("bd hh")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['s', 's', 'sound'],
|
['s', 's', 'sound'],
|
||||||
/**
|
/**
|
||||||
* The note or sample number to choose for a synth or sampleset
|
* Selects the given index from the sample map.
|
||||||
* Note names currently not working yet, but will hopefully soon. Just stick to numbers for now
|
* Numbers too high will wrap around.
|
||||||
|
* `n` can also be used to play midi numbers, but it is recommended to use `note` instead.
|
||||||
*
|
*
|
||||||
* @name n
|
* @name n
|
||||||
* @param {string | number | Pattern} value note name, note number or sample number
|
* @param {number | Pattern} value sample index starting from 0
|
||||||
* @example
|
* @example
|
||||||
* s('superpiano').n("<0 1 2 3>").osc()
|
* s("bd sd,hh*3").n("<0 1>")
|
||||||
* @example
|
|
||||||
* s('superpiano').n("<c4 d4 e4 g4>").osc()
|
|
||||||
* @example
|
|
||||||
* n("0 1 2 3").s('east').osc()
|
|
||||||
*/
|
*/
|
||||||
// TODO: nOut does not work
|
|
||||||
// TODO: notes don't work as expected
|
|
||||||
// current "workaround" for notes:
|
|
||||||
// s('superpiano').n("<c0 d0 e0 g0>"._asNumber()).osc()
|
|
||||||
// -> .n or .osc (or .superdirt) would need to convert note strings to numbers
|
|
||||||
// also see https://github.com/tidalcycles/strudel/pull/63
|
// also see https://github.com/tidalcycles/strudel/pull/63
|
||||||
['f', 'n', 'The note or sample number to choose for a synth or sampleset'],
|
['f', 'n', 'The sample number to choose for a synth or sampleset'],
|
||||||
|
/**
|
||||||
|
* Plays the given note name or midi number. A note name consists of
|
||||||
|
*
|
||||||
|
* - a letter (a-g or A-G)
|
||||||
|
* - optional accidentals (b or #)
|
||||||
|
* - optional octave number (0-9). Defaults to 3
|
||||||
|
*
|
||||||
|
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
|
||||||
|
*
|
||||||
|
* You can also use midi numbers instead of note names, where 69 is mapped to A4 440Hz in 12EDO.
|
||||||
|
*
|
||||||
|
* @name note
|
||||||
|
* @example
|
||||||
|
* note("c a f e")
|
||||||
|
* @example
|
||||||
|
* note("c4 a4 f4 e4")
|
||||||
|
* @example
|
||||||
|
* note("60 69 65 64")
|
||||||
|
*/
|
||||||
['f', 'note', 'The note or pitch to play a sound or synth with'],
|
['f', 'note', 'The note or pitch to play a sound or synth with'],
|
||||||
//['s', 'toArg', 'for internal sound routing'],
|
//['s', 'toArg', 'for internal sound routing'],
|
||||||
// ["f", "from", "for internal sound routing"),
|
// ["f", "from", "for internal sound routing"),
|
||||||
@@ -56,18 +58,19 @@ const generic_params = [
|
|||||||
*
|
*
|
||||||
* @name accelerate
|
* @name accelerate
|
||||||
* @param {number | Pattern} amount acceleration.
|
* @param {number | Pattern} amount acceleration.
|
||||||
|
* @superdirtOnly
|
||||||
* @example
|
* @example
|
||||||
* s("sax").accelerate("<0 1 2 4 8 16>").slow(2).osc()
|
* s("sax").accelerate("<0 1 2 4 8 16>").slow(2).osc()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'accelerate', 'a pattern of numbers that speed up (or slow down) samples while they play.'],
|
['f', 'accelerate', 'a pattern of numbers that speed up (or slow down) samples while they play.'],
|
||||||
/**
|
/**
|
||||||
* Like {@link amp}, but exponential.
|
* Controls the gain by an exponential amount.
|
||||||
*
|
*
|
||||||
* @name gain
|
* @name gain
|
||||||
* @param {number | Pattern} amount gain.
|
* @param {number | Pattern} amount gain.
|
||||||
* @example
|
* @example
|
||||||
* s("bd*8").gain(".7*2 1 .7*2 1 .7 1").osc()
|
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -80,38 +83,64 @@ const generic_params = [
|
|||||||
*
|
*
|
||||||
* @name amp
|
* @name amp
|
||||||
* @param {number | Pattern} amount gain.
|
* @param {number | Pattern} amount gain.
|
||||||
|
* @superdirtOnly
|
||||||
* @example
|
* @example
|
||||||
* s("bd*8").amp(".1*2 .5 .1*2 .5 .1 .5").osc()
|
* s("bd*8").amp(".1*2 .5 .1*2 .5 .1 .5").osc()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'amp', 'like @gain@, but linear.'],
|
['f', 'amp', 'like @gain@, but linear.'],
|
||||||
// TODO: find out why 0 does not work, and it generally seems not right
|
/**
|
||||||
/*
|
* Amplitude envelope attack time: Specifies how long it takes for the sound to reach its peak value, relative to the onset.
|
||||||
* A pattern of numbers to specify the attack time of an envelope applied to each sample.
|
|
||||||
*
|
*
|
||||||
* @name attack
|
* @name attack
|
||||||
* @param {number | Pattern} attack time in seconds.
|
* @param {number | Pattern} attack time in seconds.
|
||||||
* @example
|
* @example
|
||||||
* n("c5 e5").s('superpiano').attack("<0 .1>").osc()
|
* note("c3 e3").attack("<0 .1 .5>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
['f', 'attack'],
|
||||||
'f',
|
|
||||||
'attack',
|
/**
|
||||||
'a pattern of numbers to specify the attack time (in seconds) of an envelope applied to each sample.',
|
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
|
||||||
],
|
*
|
||||||
// TODO: find out how this works?
|
* @name bank
|
||||||
/*
|
* @param {string | Pattern} bank the name of the bank
|
||||||
* Envelope decay time = the time it takes after the attack time to reach the sustain level.
|
* @example
|
||||||
|
* s("bd sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
['f', 'bank', 'selects sound bank to use'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level.
|
||||||
|
* Note that the decay is only audible if the sustain value is lower than 1.
|
||||||
*
|
*
|
||||||
* @name decay
|
* @name decay
|
||||||
* @param {number | Pattern} time decay time in seconds
|
* @param {number | Pattern} time decay time in seconds
|
||||||
* @example
|
* @example
|
||||||
* s("sax").cut(1).decay("<.1 .2 .3 .4>").sustain(0).osc()
|
* note("c3 e3").decay("<.1 .2 .3 .4>").sustain(0)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'decay', ''],
|
['f', 'decay', ''],
|
||||||
|
/**
|
||||||
|
* Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset.
|
||||||
|
*
|
||||||
|
* @name sustain
|
||||||
|
* @param {number | Pattern} gain sustain level between 0 and 1
|
||||||
|
* @example
|
||||||
|
* note("c3 e3").decay(.2).sustain("<0 .1 .4 .6 1>")
|
||||||
|
*
|
||||||
|
*/
|
||||||
['f', 'sustain', ''],
|
['f', 'sustain', ''],
|
||||||
|
/**
|
||||||
|
* Amplitude envelope release time: The time it takes after the offset to go from sustain level to zero.
|
||||||
|
*
|
||||||
|
* @name release
|
||||||
|
* @param {number | Pattern} time release time in seconds
|
||||||
|
* @example
|
||||||
|
* note("c3 e3 g3 c4").release("<0 .1 .4 .6 1>/2")
|
||||||
|
*
|
||||||
|
*/
|
||||||
[
|
[
|
||||||
'f',
|
'f',
|
||||||
'release',
|
'release',
|
||||||
@@ -124,33 +153,39 @@ const generic_params = [
|
|||||||
],
|
],
|
||||||
// TODO: in tidal, it seems to be normalized
|
// TODO: in tidal, it seems to be normalized
|
||||||
/**
|
/**
|
||||||
* Sets the center frequency of the band-pass filter.
|
* Sets the center frequency of the **b**and-**p**ass **f**ilter.
|
||||||
*
|
*
|
||||||
* @name bandf
|
* @name bpf
|
||||||
* @param {number | Pattern} frequency center frequency
|
* @param {number | Pattern} frequency center frequency
|
||||||
|
* @synonyms bandf
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd").bandf("<1000 2000 4000 8000>").osc()
|
* s("bd sd,hh*3").bpf("<1000 2000 4000 8000>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
['f', 'bpf', ''],
|
||||||
['f', 'bandf', 'A pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
|
['f', 'bandf', 'A pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
|
||||||
// TODO: in tidal, it seems to be normalized
|
// TODO: in tidal, it seems to be normalized
|
||||||
/**
|
/**
|
||||||
* Sets the q-factor of the band-pass filter
|
* Sets the **b**and-**p**ass **q**-factor (resonance)
|
||||||
*
|
*
|
||||||
* @name bandq
|
* @name bpq
|
||||||
* @param {number | Pattern} q q factor
|
* @param {number | Pattern} q q factor
|
||||||
|
* @synonyms bandq
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd").bandf(2000).bandq("<.2 .9>").osc()
|
* s("bd sd").bpf(500).bpq("<0 1 2 3>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
['f', 'bpq', ''],
|
||||||
['f', 'bandq', 'a pattern of anumbers from 0 to 1. Sets the q-factor of the band-pass filter.'],
|
['f', 'bandq', 'a pattern of anumbers from 0 to 1. Sets the q-factor of the band-pass filter.'],
|
||||||
/**
|
/**
|
||||||
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
||||||
*
|
*
|
||||||
|
* @memberof Pattern
|
||||||
* @name begin
|
* @name begin
|
||||||
* @param {number | Pattern} amount between 0 and 1, where 1 is the length of the sample
|
* @param {number | Pattern} amount between 0 and 1, where 1 is the length of the sample
|
||||||
* @example
|
* @example
|
||||||
* s("rave").begin("<0 .25 .5 .75>").osc()
|
* samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/Dirt-Samples/master/')
|
||||||
|
* s("rave").begin("<0 .25 .5 .75>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -159,12 +194,13 @@ const generic_params = [
|
|||||||
'a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.',
|
'a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.',
|
||||||
],
|
],
|
||||||
/**
|
/**
|
||||||
* The same as {@link begin}, but cuts off the end off each sample.
|
* The same as .begin, but cuts off the end off each sample.
|
||||||
*
|
*
|
||||||
|
* @memberof Pattern
|
||||||
* @name end
|
* @name end
|
||||||
* @param {number | Pattern} length 1 = whole sample, .5 = half sample, .25 = quarter sample etc..
|
* @param {number | Pattern} length 1 = whole sample, .5 = half sample, .25 = quarter sample etc..
|
||||||
* @example
|
* @example
|
||||||
* s("bd*2,ho*4").end("<.1 .2 .5 1>").osc()
|
* s("bd*2,oh*4").end("<.1 .2 .5 1>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -194,7 +230,7 @@ const generic_params = [
|
|||||||
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
|
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'legato', 'controls the amount of overlap between two adjacent sounds'],
|
// ['f', 'legato', 'controls the amount of overlap between two adjacent sounds'],
|
||||||
// ['f', 'clhatdecay', ''],
|
// ['f', 'clhatdecay', ''],
|
||||||
/**
|
/**
|
||||||
* bit crusher effect.
|
* bit crusher effect.
|
||||||
@@ -202,7 +238,7 @@ const generic_params = [
|
|||||||
* @name crush
|
* @name crush
|
||||||
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
|
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
|
||||||
* @example
|
* @example
|
||||||
* s("<bd sd>,hh*3,jvbass*2").fast(2).crush("<16 8 7 6 5 4 3 2>").osc()
|
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -211,12 +247,12 @@ const generic_params = [
|
|||||||
'bit crushing, a pattern of numbers from 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).',
|
'bit crushing, a pattern of numbers from 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).',
|
||||||
],
|
],
|
||||||
/**
|
/**
|
||||||
* fake-resampling for lowering the sample rate
|
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
|
||||||
*
|
*
|
||||||
* @name coarse
|
* @name coarse
|
||||||
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
|
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
|
||||||
* @example
|
* @example
|
||||||
* s("xmas").coarse("<1 4 8 16 32>").osc()
|
* s("bd sd,hh*4").coarse("<1 4 8 16 32>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -239,7 +275,7 @@ const generic_params = [
|
|||||||
* @name cut
|
* @name cut
|
||||||
* @param {number | Pattern} group cut group number
|
* @param {number | Pattern} group cut group number
|
||||||
* @example
|
* @example
|
||||||
* s("bd sax").cut(1).osc()
|
* s("rd*4").cut(1)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -248,59 +284,55 @@ const generic_params = [
|
|||||||
'In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.',
|
'In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.',
|
||||||
],
|
],
|
||||||
/**
|
/**
|
||||||
* Applies the cutoff frequency of the low-pass filter.
|
* Applies the cutoff frequency of the **l**ow-**p**ass **f**ilter.
|
||||||
*
|
*
|
||||||
* @name cutoff
|
* @name lpf
|
||||||
* @param {number | Pattern} frequency audible between 0 and 20000
|
* @param {number | Pattern} frequency audible between 0 and 20000
|
||||||
|
* @synonyms cutoff
|
||||||
* @example
|
* @example
|
||||||
* s("bd,hh*2,<~ sd>").fast(2).cutoff("<4000 2000 1000 500 200 100>").osc()
|
* s("bd sd,hh*3").lpf("<4000 2000 1000 500 200 100>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
// TODO: add lpf synonym
|
['f', 'lpf'],
|
||||||
['f', 'cutoff', 'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the low-pass filter.'],
|
['f', 'cutoff', 'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the low-pass filter.'],
|
||||||
/**
|
/**
|
||||||
* Applies the cutoff frequency of the high-pass filter.
|
* Applies the cutoff frequency of the **h**igh-**p**ass **f**ilter.
|
||||||
*
|
*
|
||||||
* @name hcutoff
|
* @name hpf
|
||||||
* @param {number | Pattern} frequency audible between 0 and 20000
|
* @param {number | Pattern} frequency audible between 0 and 20000
|
||||||
|
* @synonyms hcutoff
|
||||||
* @example
|
* @example
|
||||||
* s("bd,hh*2,<~ sd>").fast(2).hcutoff("<4000 2000 1000 500 200 100>").osc()
|
* s("bd sd,hh*4").hpf("<4000 2000 1000 500 200 100>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
// TODO: add hpf synonym
|
['f', 'hpf', ''],
|
||||||
[
|
['f', 'hcutoff', ''],
|
||||||
'f',
|
|
||||||
'hcutoff',
|
|
||||||
'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the high-pass filter. Also has alias @hpf@',
|
|
||||||
],
|
|
||||||
/**
|
/**
|
||||||
* Applies the cutoff frequency of the high-pass filter.
|
* Controls the **h**igh-**p**ass **q**-value.
|
||||||
*
|
*
|
||||||
* @name hresonance
|
* @name hpq
|
||||||
* @param {number | Pattern} q resonance factor between 0 and 1
|
* @param {number | Pattern} q resonance factor between 0 and 50
|
||||||
|
* @synonyms hresonance
|
||||||
* @example
|
* @example
|
||||||
* s("bd,hh*2,<~ sd>").fast(2).hcutoff(2000).hresonance("<0 .2 .4 .6>").osc()
|
* s("bd sd,hh*4").hpf(2000).hpq("<0 10 20 30>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
['f', 'hresonance', ''],
|
||||||
'f',
|
['f', 'hpq', ''],
|
||||||
'hresonance',
|
|
||||||
'a pattern of numbers from 0 to 1. Applies the resonance of the high-pass filter. Has alias @hpq@',
|
|
||||||
],
|
|
||||||
// TODO: add hpq synonym
|
|
||||||
/**
|
/**
|
||||||
* Applies the cutoff frequency of the low-pass filter.
|
* Controls the **l**ow-**p**ass **q**-value.
|
||||||
*
|
*
|
||||||
* @name resonance
|
* @name lpq
|
||||||
* @param {number | Pattern} q resonance factor between 0 and 1
|
* @param {number | Pattern} q resonance factor between 0 and 50
|
||||||
|
* @synonyms resonance
|
||||||
* @example
|
* @example
|
||||||
* s("bd,hh*2,<~ sd>").fast(2).cutoff(2000).resonance("<0 .2 .4 .6>").osc()
|
* s("bd sd,hh*4").lpf(2000).lpq("<0 10 20 30>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'resonance', 'a pattern of numbers from 0 to 1. Specifies the resonance of the low-pass filter.'],
|
['f', 'lpq'],
|
||||||
// TODO: add lpq synonym?
|
['f', 'resonance', ''],
|
||||||
/**
|
/**
|
||||||
* Set detune of oscillators. Works only with some synths, see <a target="_blank" href="https://tidalcycles.org/docs/patternlib/tutorials/synthesizers">tidal doc</a>
|
* DJ filter, below 0.5 is low pass filter, above is high pass filter.
|
||||||
*
|
*
|
||||||
* @name djf
|
* @name djf
|
||||||
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
|
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
|
||||||
@@ -311,17 +343,36 @@ const generic_params = [
|
|||||||
['f', 'djf', 'DJ filter, below 0.5 is low pass filter, above is high pass filter.'],
|
['f', 'djf', 'DJ filter, below 0.5 is low pass filter, above is high pass filter.'],
|
||||||
// ['f', 'cutoffegint', ''],
|
// ['f', 'cutoffegint', ''],
|
||||||
// TODO: does not seem to work
|
// TODO: does not seem to work
|
||||||
/*
|
/**
|
||||||
* Sets the level of the delay signal.
|
* Sets the level of the delay signal.
|
||||||
*
|
*
|
||||||
* @name delay
|
* @name delay
|
||||||
* @param {number | Pattern} level between 0 and 1
|
* @param {number | Pattern} level between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* s("bd").delay("<0 .5 .75 1>").osc()
|
* s("bd").delay("<0 .25 .5 1>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'delay', 'a pattern of numbers from 0 to 1. Sets the level of the delay signal.'],
|
['f', 'delay', 'a pattern of numbers from 0 to 1. Sets the level of the delay signal.'],
|
||||||
|
/**
|
||||||
|
* Sets the level of the signal that is fed back into the delay.
|
||||||
|
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
|
||||||
|
*
|
||||||
|
* @name delayfeedback
|
||||||
|
* @param {number | Pattern} feedback between 0 and 1
|
||||||
|
* @example
|
||||||
|
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>").slow(2)
|
||||||
|
*
|
||||||
|
*/
|
||||||
['f', 'delayfeedback', 'a pattern of numbers from 0 to 1. Sets the amount of delay feedback.'],
|
['f', 'delayfeedback', 'a pattern of numbers from 0 to 1. Sets the amount of delay feedback.'],
|
||||||
|
/**
|
||||||
|
* Sets the time of the delay effect.
|
||||||
|
*
|
||||||
|
* @name delaytime
|
||||||
|
* @param {number | Pattern} seconds between 0 and Infinity
|
||||||
|
* @example
|
||||||
|
* s("bd").delay(.25).delaytime("<.125 .25 .5 1>").slow(2)
|
||||||
|
*
|
||||||
|
*/
|
||||||
['f', 'delaytime', 'a pattern of numbers from 0 to 1. Sets the length of the delay.'],
|
['f', 'delaytime', 'a pattern of numbers from 0 to 1. Sets the length of the delay.'],
|
||||||
/* // TODO: test
|
/* // TODO: test
|
||||||
* Specifies whether delaytime is calculated relative to cps.
|
* Specifies whether delaytime is calculated relative to cps.
|
||||||
@@ -342,6 +393,7 @@ const generic_params = [
|
|||||||
*
|
*
|
||||||
* @name detune
|
* @name detune
|
||||||
* @param {number | Pattern} amount between 0 and 1
|
* @param {number | Pattern} amount between 0 and 1
|
||||||
|
* @superdirtOnly
|
||||||
* @example
|
* @example
|
||||||
* n("0 3 7").s('superzow').octave(3).detune("<0 .25 .5 1 2>").osc()
|
* n("0 3 7").s('superzow').octave(3).detune("<0 .25 .5 1 2>").osc()
|
||||||
*
|
*
|
||||||
@@ -354,6 +406,7 @@ const generic_params = [
|
|||||||
* @param {number | Pattern} dry 0 = wet, 1 = dry
|
* @param {number | Pattern} dry 0 = wet, 1 = dry
|
||||||
* @example
|
* @example
|
||||||
* n("[0,3,7](3,8)").s("superpiano").room(.7).dry("<0 .5 .75 1>").osc()
|
* n("[0,3,7](3,8)").s("superpiano").room(.7).dry("<0 .5 .75 1>").osc()
|
||||||
|
* @superdirtOnly
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -368,7 +421,7 @@ const generic_params = [
|
|||||||
* @name fadeTime
|
* @name fadeTime
|
||||||
* @param {number | Pattern} time between 0 and 1
|
* @param {number | Pattern} time between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* s("ho*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -390,7 +443,7 @@ const generic_params = [
|
|||||||
* @example
|
* @example
|
||||||
* freq("220 110 440 110").s("superzow").osc()
|
* freq("220 110 440 110").s("superzow").osc()
|
||||||
* @example
|
* @example
|
||||||
* freq("110".mulOut(".5 1.5 .6 [2 3]")).s("superzow").osc()
|
* freq("110".mul.out(".5 1.5 .6 [2 3]")).s("superzow").osc()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'freq', ''],
|
['f', 'freq', ''],
|
||||||
@@ -412,6 +465,7 @@ const generic_params = [
|
|||||||
* @param {number | Pattern} wet between 0 and 1
|
* @param {number | Pattern} wet between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* n("0,4,7").s("supersquare").leslie("<0 .4 .6 1>").osc()
|
* n("0,4,7").s("supersquare").leslie("<0 .4 .6 1>").osc()
|
||||||
|
* @superdirtOnly
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'leslie', ''],
|
['f', 'leslie', ''],
|
||||||
@@ -422,6 +476,7 @@ const generic_params = [
|
|||||||
* @param {number | Pattern} rate 6.7 for fast, 0.7 for slow
|
* @param {number | Pattern} rate 6.7 for fast, 0.7 for slow
|
||||||
* @example
|
* @example
|
||||||
* n("0,4,7").s("supersquare").leslie(1).lrate("<1 2 4 8>").osc()
|
* n("0,4,7").s("supersquare").leslie(1).lrate("<1 2 4 8>").osc()
|
||||||
|
* @superdirtOnly
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
// TODO: the rate seems to "lag" (in the example, 1 will be fast)
|
// TODO: the rate seems to "lag" (in the example, 1 will be fast)
|
||||||
@@ -433,6 +488,7 @@ const generic_params = [
|
|||||||
* @param {number | Pattern} meters somewhere between 0 and 1
|
* @param {number | Pattern} meters somewhere between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* n("0,4,7").s("supersquare").leslie(1).lrate(2).lsize("<.1 .5 1>").osc()
|
* n("0,4,7").s("supersquare").leslie(1).lrate(2).lsize("<.1 .5 1>").osc()
|
||||||
|
* @superdirtOnly
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'lsize', ''],
|
['f', 'lsize', ''],
|
||||||
@@ -468,17 +524,22 @@ const generic_params = [
|
|||||||
* @param {number | Pattern} octave octave number
|
* @param {number | Pattern} octave octave number
|
||||||
* @example
|
* @example
|
||||||
* n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc()
|
* n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc()
|
||||||
|
* @superDirtOnly
|
||||||
*/
|
*/
|
||||||
['i', 'octave', ''],
|
['i', 'octave', ''],
|
||||||
['f', 'offset', ''], // TODO: what is this? not found in tidal doc
|
['f', 'offset', ''], // TODO: what is this? not found in tidal doc
|
||||||
// ['f', 'ophatdecay', ''],
|
// ['f', 'ophatdecay', ''],
|
||||||
// TODO: example
|
// TODO: example
|
||||||
/**
|
/**
|
||||||
* a pattern of numbers. An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share hardware output bus offset and global effects, e.g. reverb and delay. The maximum number of orbits is specified in the superdirt startup, numbers higher than maximum will wrap around.
|
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
|
||||||
*
|
*
|
||||||
* @name orbit
|
* @name orbit
|
||||||
* @param {number | Pattern} number
|
* @param {number | Pattern} number
|
||||||
*
|
* @example
|
||||||
|
* stack(
|
||||||
|
* s("hh*3").delay(.5).delaytime(.25).orbit(1),
|
||||||
|
* s("~ sd").delay(.5).delaytime(.125).orbit(2)
|
||||||
|
* )
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
'i',
|
'i',
|
||||||
@@ -493,7 +554,7 @@ const generic_params = [
|
|||||||
* @name pan
|
* @name pan
|
||||||
* @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)
|
* @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)
|
||||||
* @example
|
* @example
|
||||||
* s("[bd hh]*2").pan("<.5 1 .5 0>").osc()
|
* s("[bd hh]*2").pan("<.5 1 .5 0>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -552,7 +613,7 @@ const generic_params = [
|
|||||||
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
||||||
['f', 'semitone', ''],
|
['f', 'semitone', ''],
|
||||||
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
|
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
|
||||||
['f', 'velocity', ''],
|
// ['f', 'velocity', ''],
|
||||||
['f', 'voice', ''], // TODO: synth param
|
['f', 'voice', ''], // TODO: synth param
|
||||||
/**
|
/**
|
||||||
* Sets the level of reverb.
|
* Sets the level of reverb.
|
||||||
@@ -560,27 +621,33 @@ const generic_params = [
|
|||||||
* @name room
|
* @name room
|
||||||
* @param {number | Pattern} level between 0 and 1
|
* @param {number | Pattern} level between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd").room("<0 .2 .4 .6 .8 1>").osc()
|
* s("bd sd").room("<0 .2 .4 .6 .8 1>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'room', 'a pattern of numbers from 0 to 1. Sets the level of reverb.'],
|
['f', 'room', 'a pattern of numbers from 0 to 1. Sets the level of reverb.'],
|
||||||
/**
|
/**
|
||||||
* Sets the room size of the reverb, see {@link room}.
|
* Sets the room size of the reverb, see {@link room}.
|
||||||
*
|
*
|
||||||
* @name size
|
* @name roomsize
|
||||||
* @param {number | Pattern} size between 0 and 1
|
* @synonyms size
|
||||||
|
* @param {number | Pattern} size between 0 and 10
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd").room(.8).size("<0 .2 .4 .6 .8 1>").osc()
|
* s("bd sd").room(.8).roomsize("<0 1 2 4 8>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
// TODO: find out why :
|
// TODO: find out why :
|
||||||
// s("bd sd").room(.8).size("<0 .2 .4 .6 .8 [1,0]>").osc()
|
// s("bd sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc()
|
||||||
// .. does not work. Is it because room is only one effect?
|
// .. does not work. Is it because room is only one effect?
|
||||||
[
|
[
|
||||||
'f',
|
'f',
|
||||||
'size',
|
'size',
|
||||||
'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.',
|
'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'f',
|
||||||
|
'roomsize',
|
||||||
|
'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.',
|
||||||
|
],
|
||||||
// ['f', 'sagogo', ''],
|
// ['f', 'sagogo', ''],
|
||||||
// ['f', 'sclap', ''],
|
// ['f', 'sclap', ''],
|
||||||
// ['f', 'sclaves', ''],
|
// ['f', 'sclaves', ''],
|
||||||
@@ -591,7 +658,7 @@ const generic_params = [
|
|||||||
* @name shape
|
* @name shape
|
||||||
* @param {number | Pattern} distortion between 0 and 1
|
* @param {number | Pattern} distortion between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd").shape("<0 .2 .4 .6 .8 1>").osc()
|
* s("bd sd,hh*4").shape("<0 .2 .4 .6 .8>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -605,9 +672,9 @@ const generic_params = [
|
|||||||
* @name speed
|
* @name speed
|
||||||
* @param {number | Pattern} speed -inf to inf, negative numbers play the sample backwards.
|
* @param {number | Pattern} speed -inf to inf, negative numbers play the sample backwards.
|
||||||
* @example
|
* @example
|
||||||
* s("bd").speed("<1 2 4 1 -2 -4>").osc()
|
* s("bd").speed("<1 2 4 1 -2 -4>")
|
||||||
* @example
|
* @example
|
||||||
* speed("1 1.5*2 [2 1.1]").s("sax").cut(1).osc()
|
* speed("1 1.5*2 [2 1.1]").s("piano").clip(1)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -622,6 +689,7 @@ const generic_params = [
|
|||||||
* @param {number | string | Pattern} unit see description above
|
* @param {number | string | Pattern} unit see description above
|
||||||
* @example
|
* @example
|
||||||
* speed("1 2 .5 3").s("bd").unit("c").osc()
|
* speed("1 2 .5 3").s("bd").unit("c").osc()
|
||||||
|
* @superdirtOnly
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -638,6 +706,7 @@ const generic_params = [
|
|||||||
* @param {number | Pattern} squiz Try passing multiples of 2 to it - 2, 4, 8 etc.
|
* @param {number | Pattern} squiz Try passing multiples of 2 to it - 2, 4, 8 etc.
|
||||||
* @example
|
* @example
|
||||||
* squiz("2 4/2 6 [8 16]").s("bd").osc()
|
* squiz("2 4/2 6 [8 16]").s("bd").osc()
|
||||||
|
* @superdirtOnly
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'squiz', ''],
|
['f', 'squiz', ''],
|
||||||
@@ -648,16 +717,16 @@ const generic_params = [
|
|||||||
// ['f', 'tomdecay', ''],
|
// ['f', 'tomdecay', ''],
|
||||||
// ['f', 'vcfegint', ''],
|
// ['f', 'vcfegint', ''],
|
||||||
// ['f', 'vcoegint', ''],
|
// ['f', 'vcoegint', ''],
|
||||||
|
// TODO: Use a rest (~) to override the effect <- vowel
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Formant filter to make things sound like vowels.
|
* Formant filter to make things sound like vowels.
|
||||||
*
|
*
|
||||||
* @name vowel
|
* @name vowel
|
||||||
* @param {string | Pattern} vowel You can use a e i o u. Use a rest (~) to override the effect
|
* @param {string | Pattern} vowel You can use a e i o u.
|
||||||
* @example
|
* @example
|
||||||
* vowel("a e i [o u]").slow(2)
|
* note("c2 <eb2 <g2 g1>>").s('sawtooth')
|
||||||
* .n("<[0,7]!4 [2,7]!4>")
|
* .vowel("<a e i <o u>>")
|
||||||
* .s('supersquare').osc()
|
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -744,34 +813,44 @@ const generic_params = [
|
|||||||
['f', 'uid', ''],
|
['f', 'uid', ''],
|
||||||
['f', 'val', ''],
|
['f', 'val', ''],
|
||||||
['f', 'cps', ''],
|
['f', 'cps', ''],
|
||||||
|
/**
|
||||||
|
* If set to 1, samples will be cut to the duration of their event.
|
||||||
|
* In tidal, this would be done with legato, which [is about to land in strudel too](https://github.com/tidalcycles/strudel/issues/111)
|
||||||
|
*
|
||||||
|
* @name clip
|
||||||
|
* @param {number | Pattern} active 1 or 0
|
||||||
|
* @example
|
||||||
|
* note("c a f e ~").s("piano").clip(1)
|
||||||
|
*
|
||||||
|
*/
|
||||||
['f', 'clip', ''],
|
['f', 'clip', ''],
|
||||||
];
|
];
|
||||||
|
|
||||||
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
|
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
|
||||||
|
|
||||||
const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
|
const makeControl = function (name) {
|
||||||
|
const func = (...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
|
||||||
const _setter = (func, name) =>
|
const setter = function (...pats) {
|
||||||
function (...pats) {
|
|
||||||
if (!pats.length) {
|
if (!pats.length) {
|
||||||
return this.fmap((value) => ({ [name]: value }));
|
return this.fmap((value) => ({ [name]: value }));
|
||||||
}
|
}
|
||||||
return this.set(func(...pats));
|
return this.set(func(...pats));
|
||||||
};
|
};
|
||||||
|
Pattern.prototype[name] = setter;
|
||||||
|
registerControl(name, func);
|
||||||
|
return func;
|
||||||
|
};
|
||||||
|
|
||||||
generic_params.forEach(([type, name, description]) => {
|
generic_params.forEach(([type, name, description]) => {
|
||||||
controls[name] = (...pats) => _name(name, ...pats);
|
controls[name] = makeControl(name);
|
||||||
Pattern.prototype[name] = _setter(controls[name], name);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// create custom param
|
// create custom param
|
||||||
controls.createParam = (name) => {
|
controls.createParam = (name) => {
|
||||||
const func = (...pats) => _name(name, ...pats);
|
return makeControl(name);
|
||||||
Pattern.prototype[name] = _setter(func, name);
|
|
||||||
return (...pats) => _name(name, ...pats);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
controls.createParams = (...names) =>
|
controls.createParams = (...names) =>
|
||||||
names.reduce((acc, name) => Object.assign(acc, { [name]: createParam(name) }), {});
|
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
|
||||||
|
|
||||||
export default controls;
|
export default controls;
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
cyclist.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import createClock from './zyklus.mjs';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
|
export class Cyclist {
|
||||||
|
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||||
|
this.started = false;
|
||||||
|
this.cps = 1; // TODO
|
||||||
|
this.phase = 0;
|
||||||
|
this.getTime = getTime;
|
||||||
|
this.onToggle = onToggle;
|
||||||
|
this.latency = latency;
|
||||||
|
const round = (x) => Math.round(x * 1000) / 1000;
|
||||||
|
this.clock = createClock(
|
||||||
|
getTime,
|
||||||
|
(phase, duration, tick) => {
|
||||||
|
if (tick === 0) {
|
||||||
|
this.origin = phase;
|
||||||
|
}
|
||||||
|
const begin = round(phase - this.origin);
|
||||||
|
this.phase = begin - latency;
|
||||||
|
const end = round(begin + duration);
|
||||||
|
const time = getTime();
|
||||||
|
try {
|
||||||
|
const haps = this.pattern.queryArc(begin, end); // get Haps
|
||||||
|
haps.forEach((hap) => {
|
||||||
|
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||||
|
const deadline = hap.whole.begin + this.origin - time + latency;
|
||||||
|
const duration = hap.duration * 1;
|
||||||
|
onTrigger?.(hap, deadline, duration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger(`[cyclist] error: ${e.message}`);
|
||||||
|
onError?.(e);
|
||||||
|
}
|
||||||
|
}, // called slightly before each cycle
|
||||||
|
interval, // duration of each cycle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
getPhase() {
|
||||||
|
return this.getTime() - this.origin - this.latency;
|
||||||
|
}
|
||||||
|
now() {
|
||||||
|
return this.getTime() - this.origin + this.clock.minLatency;
|
||||||
|
}
|
||||||
|
setStarted(v) {
|
||||||
|
this.started = v;
|
||||||
|
this.onToggle?.(v);
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
if (!this.pattern) {
|
||||||
|
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||||
|
}
|
||||||
|
logger('[cyclist] start');
|
||||||
|
this.clock.start();
|
||||||
|
this.setStarted(true);
|
||||||
|
}
|
||||||
|
pause() {
|
||||||
|
logger('[cyclist] pause');
|
||||||
|
this.clock.pause();
|
||||||
|
this.setStarted(false);
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
logger('[cyclist] stop');
|
||||||
|
this.clock.stop();
|
||||||
|
this.setStarted(false);
|
||||||
|
}
|
||||||
|
setPattern(pat, autostart = false) {
|
||||||
|
this.pattern = pat;
|
||||||
|
if (autostart && !this.started) {
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCps(cps = 1) {
|
||||||
|
this.cps = cps;
|
||||||
|
}
|
||||||
|
log(begin, end, haps) {
|
||||||
|
const onsets = haps.filter((h) => h.hasOnset());
|
||||||
|
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Tone } from './tone.mjs';
|
import { Pattern, getTime, State, TimeSpan } from './index.mjs';
|
||||||
import { Pattern } from '@strudel.cycles/core';
|
|
||||||
|
|
||||||
export const getDrawContext = (id = 'test-canvas') => {
|
export const getDrawContext = (id = 'test-canvas') => {
|
||||||
let canvas = document.querySelector('#' + id);
|
let canvas = document.querySelector('#' + id);
|
||||||
@@ -28,7 +27,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
|||||||
let cycle,
|
let cycle,
|
||||||
events = [];
|
events = [];
|
||||||
const animate = (time) => {
|
const animate = (time) => {
|
||||||
const t = Tone.getTransport().seconds;
|
const t = getTime();
|
||||||
if (from !== undefined && to !== undefined) {
|
if (from !== undefined && to !== undefined) {
|
||||||
const currentCycle = Math.floor(t);
|
const currentCycle = Math.floor(t);
|
||||||
if (cycle !== currentCycle) {
|
if (cycle !== currentCycle) {
|
||||||
@@ -50,9 +49,9 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
|||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanupDraw = () => {
|
export const cleanupDraw = (clearScreen = true) => {
|
||||||
const ctx = getDrawContext();
|
const ctx = getDrawContext();
|
||||||
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||||
if (window.strudelAnimation) {
|
if (window.strudelAnimation) {
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
}
|
}
|
||||||
@@ -60,3 +59,9 @@ export const cleanupDraw = () => {
|
|||||||
clearInterval(window.strudelScheduler);
|
clearInterval(window.strudelScheduler);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Pattern.prototype.onPaint = function (onPaint) {
|
||||||
|
// this is evil! TODO: add pattern.context
|
||||||
|
this.context = { onPaint };
|
||||||
|
return this;
|
||||||
|
};
|
||||||
@@ -21,6 +21,7 @@ import Fraction, { gcd } from './fraction.mjs';
|
|||||||
* @example
|
* @example
|
||||||
* const line = drawLine("0 [1 2 3]", 10); // |0--123|0--123
|
* const line = drawLine("0 [1 2 3]", 10); // |0--123|0--123
|
||||||
* console.log(line);
|
* console.log(line);
|
||||||
|
* silence;
|
||||||
*/
|
*/
|
||||||
function drawLine(pat, chars = 60) {
|
function drawLine(pat, chars = 60) {
|
||||||
let cycle = 0;
|
let cycle = 0;
|
||||||
|
|||||||
+156
-74
@@ -1,95 +1,177 @@
|
|||||||
/*
|
/*
|
||||||
euclid.mjs - <short description TODO>
|
euclid.mjs - Bjorklund/Euclidean/Diaspora rhythms
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs>
|
Copyright (C) 2023 Rohan Drape and strudel contributors
|
||||||
|
|
||||||
|
See <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs> for authors of this file.
|
||||||
|
|
||||||
|
The Bjorklund algorithm implementation is ported from the Haskell Music Theory Haskell module by Rohan Drape -
|
||||||
|
https://rohandrape.net/?t=hmt
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, timeCat } from './pattern.mjs';
|
import { Pattern, timeCat, register, silence } from './pattern.mjs';
|
||||||
import bjork from 'bjork';
|
import { rotate, flatten } from './util.mjs';
|
||||||
import { rotate } from './util.mjs';
|
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
|
|
||||||
const euclid = (pulses, steps, rotation = 0) => {
|
const splitAt = function (index, value) {
|
||||||
const b = bjork(steps, pulses);
|
return [value.slice(0, index), value.slice(index)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||||
|
|
||||||
|
const left = function (n, x) {
|
||||||
|
const [ons, offs] = n;
|
||||||
|
const [xs, ys] = x;
|
||||||
|
const [_xs, __xs] = splitAt(offs, xs);
|
||||||
|
return [
|
||||||
|
[offs, ons - offs],
|
||||||
|
[zipWith((a, b) => a.concat(b), _xs, ys), __xs],
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const right = function (n, x) {
|
||||||
|
const [ons, offs] = n;
|
||||||
|
const [xs, ys] = x;
|
||||||
|
const [_ys, __ys] = splitAt(ons, ys);
|
||||||
|
const result = [
|
||||||
|
[ons, offs - ons],
|
||||||
|
[zipWith((a, b) => a.concat(b), xs, _ys), __ys],
|
||||||
|
];
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _bjork = function (n, x) {
|
||||||
|
const [ons, offs] = n;
|
||||||
|
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bjork = function (ons, steps) {
|
||||||
|
const offs = steps - ons;
|
||||||
|
const x = Array(ons).fill([1]);
|
||||||
|
const y = Array(offs).fill([0]);
|
||||||
|
const result = _bjork([ons, offs], [x, y]);
|
||||||
|
return flatten(result[1][0]).concat(flatten(result[1][1]));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the structure of the pattern to form an euclidean rhythm.
|
||||||
|
* Euclidian rhythms are rhythms obtained using the greatest common
|
||||||
|
* divisor of two numbers. They were described in 2004 by Godfried
|
||||||
|
* Toussaint, a canadian computer scientist. Euclidian rhythms are
|
||||||
|
* really useful for computer/algorithmic music because they can
|
||||||
|
* describe a large number of rhythms with a couple of numbers.
|
||||||
|
*
|
||||||
|
* @memberof Pattern
|
||||||
|
* @name euclid
|
||||||
|
* @param {number} pulses the number of onsets / beats
|
||||||
|
* @param {number} steps the number of steps to fill
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* // The Cuban tresillo pattern.
|
||||||
|
* note("c3").euclid(3,8)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence.
|
||||||
|
* @memberof Pattern
|
||||||
|
* @name euclidRot
|
||||||
|
* @param {number} pulses the number of onsets / beats
|
||||||
|
* @param {number} steps the number of steps to fill
|
||||||
|
* @param {number} rotation offset in steps
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* // A Samba rhythm necklace from Brazil
|
||||||
|
* note("c3").euclidRot(3,16,14)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
|
||||||
|
* note("c3").euclid(2,5)
|
||||||
|
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
|
||||||
|
* note("c3").euclid(3,4)
|
||||||
|
* @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm.
|
||||||
|
* note("c3").euclidRot(3,5,2)
|
||||||
|
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
|
||||||
|
* note("c3").euclid(3,7)
|
||||||
|
* @example // The Cuban tresillo pattern.
|
||||||
|
* note("c3").euclid(3,8)
|
||||||
|
* @example // Another Ruchenitza Bulgarian folk-dance rhythm.
|
||||||
|
* note("c3").euclid(4,7)
|
||||||
|
* @example // The Aksak rhythm of Turkey.
|
||||||
|
* note("c3").euclid(4,9)
|
||||||
|
* @example // The metric pattern used by Frank Zappa in his piece titled Outside Now.
|
||||||
|
* note("c3").euclid(4,11)
|
||||||
|
* @example // Yields the York-Samai pattern, a popular Arab rhythm.
|
||||||
|
* note("c3").euclid(5,6)
|
||||||
|
* @example // The Nawakhat pattern, another popular Arab rhythm.
|
||||||
|
* note("c3").euclid(5,7)
|
||||||
|
* @example // The Cuban cinquillo pattern.
|
||||||
|
* note("c3").euclid(5,8)
|
||||||
|
* @example // A popular Arab rhythm called Agsag-Samai.
|
||||||
|
* note("c3").euclid(5,9)
|
||||||
|
* @example // The metric pattern used by Moussorgsky in Pictures at an Exhibition.
|
||||||
|
* note("c3").euclid(5,11)
|
||||||
|
* @example // The Venda clapping pattern of a South African children’s song.
|
||||||
|
* note("c3").euclid(5,12)
|
||||||
|
* @example // The Bossa-Nova rhythm necklace of Brazil.
|
||||||
|
* note("c3").euclid(5,16)
|
||||||
|
* @example // A typical rhythm played on the Bendir (frame drum).
|
||||||
|
* note("c3").euclid(7,8)
|
||||||
|
* @example // A common West African bell pattern.
|
||||||
|
* note("c3").euclid(7,12)
|
||||||
|
* @example // A Samba rhythm necklace from Brazil.
|
||||||
|
* note("c3").euclidRot(7,16,14)
|
||||||
|
* @example // A rhythm necklace used in the Central African Republic.
|
||||||
|
* note("c3").euclid(9,16)
|
||||||
|
* @example // A rhythm necklace of the Aka Pygmies of Central Africa.
|
||||||
|
* note("c3").euclidRot(11,24,14)
|
||||||
|
* @example // Another rhythm necklace of the Aka Pygmies of the upper Sangha.
|
||||||
|
* note("c3").euclidRot(13,24,5)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const _euclidRot = function (pulses, steps, rotation) {
|
||||||
|
const b = bjork(pulses, steps);
|
||||||
if (rotation) {
|
if (rotation) {
|
||||||
return rotate(b, -rotation);
|
return rotate(b, -rotation);
|
||||||
}
|
}
|
||||||
return b;
|
return b;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export const euclid = register('euclid', function (pulses, steps, pat) {
|
||||||
* Changes the structure of the pattern to form an euclidean rhythm.
|
return pat.struct(_euclidRot(pulses, steps, 0));
|
||||||
* Euclidian rhythms are rhythms obtained using the greatest common divisor of two numbers.
|
});
|
||||||
* They were described in 2004 by Godfried Toussaint, a canadian computer scientist.
|
|
||||||
* Euclidian rhythms are really useful for computer/algorithmic music because they can accurately
|
export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) {
|
||||||
* describe a large number of rhythms used in the most important music world traditions.
|
return pat.struct(_euclidRot(pulses, steps, rotation));
|
||||||
*
|
});
|
||||||
* @memberof Pattern
|
|
||||||
* @param {number} pulses the number of onsets / beats
|
|
||||||
* @param {number} steps the number of steps to fill
|
|
||||||
* @param {number} rotation (optional) offset in steps
|
|
||||||
* @returns Pattern
|
|
||||||
* @example // The Cuban tresillo pattern.
|
|
||||||
* "c3".euclid(3,8)
|
|
||||||
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
|
|
||||||
* "c3".euclid(2,5)
|
|
||||||
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
|
|
||||||
* "c3".euclid(3,4)
|
|
||||||
* @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm.
|
|
||||||
* "c3".euclid(3,5,2)
|
|
||||||
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
|
|
||||||
* "c3".euclid(3,7)
|
|
||||||
* @example // The Cuban tresillo pattern.
|
|
||||||
* "c3".euclid(3,8)
|
|
||||||
* @example // Another Ruchenitza Bulgarian folk-dance rhythm.
|
|
||||||
* "c3".euclid(4,7)
|
|
||||||
* @example // The Aksak rhythm of Turkey.
|
|
||||||
* "c3".euclid(4,9)
|
|
||||||
* @example // The metric pattern used by Frank Zappa in his piece titled Outside Now.
|
|
||||||
* "c3".euclid(4,11)
|
|
||||||
* @example // Yields the York-Samai pattern, a popular Arab rhythm.
|
|
||||||
* "c3".euclid(5,6)
|
|
||||||
* @example // The Nawakhat pattern, another popular Arab rhythm.
|
|
||||||
* "c3".euclid(5,7)
|
|
||||||
* @example // The Cuban cinquillo pattern.
|
|
||||||
* "c3".euclid(5,8)
|
|
||||||
* @example // A popular Arab rhythm called Agsag-Samai.
|
|
||||||
* "c3".euclid(5,9)
|
|
||||||
* @example // The metric pattern used by Moussorgsky in Pictures at an Exhibition.
|
|
||||||
* "c3".euclid(5,11)
|
|
||||||
* @example // The Venda clapping pattern of a South African children’s song.
|
|
||||||
* "c3".euclid(5,12)
|
|
||||||
* @example // The Bossa-Nova rhythm necklace of Brazil.
|
|
||||||
* "c3".euclid(5,16)
|
|
||||||
* @example // A typical rhythm played on the Bendir (frame drum).
|
|
||||||
* "c3".euclid(7,8)
|
|
||||||
* @example // A common West African bell pattern.
|
|
||||||
* "c3".euclid(7,12)
|
|
||||||
* @example // A Samba rhythm necklace from Brazil.
|
|
||||||
* "c3".euclid(7,16,14)
|
|
||||||
* @example // A rhythm necklace used in the Central African Republic.
|
|
||||||
* "c3".euclid(9,16)
|
|
||||||
* @example // A rhythm necklace of the Aka Pygmies of Central Africa.
|
|
||||||
* "c3".euclid(11,24,14)
|
|
||||||
* @example // Another rhythm necklace of the Aka Pygmies of the upper Sangha.
|
|
||||||
* "c3".euclid(13,24,5)
|
|
||||||
*/
|
|
||||||
Pattern.prototype.euclid = function (pulses, steps, rotation = 0) {
|
|
||||||
return this.struct(euclid(pulses, steps, rotation));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Similar to {@link Pattern#euclid}, but each pulse is held until the next pulse, so there will be no gaps.
|
* Similar to `euclid`, but each pulse is held until the next pulse,
|
||||||
|
* so there will be no gaps.
|
||||||
|
* @name euclidLegato
|
||||||
|
* @memberof Pattern
|
||||||
|
* @example
|
||||||
|
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.euclidLegato = function (pulses, steps, rotation = 0) {
|
|
||||||
const bin_pat = euclid(pulses, steps, rotation);
|
const _euclidLegato = function (pulses, steps, rotation, pat) {
|
||||||
const firstOne = bin_pat.indexOf(1);
|
if (pulses < 1) {
|
||||||
const gapless = rotate(bin_pat, firstOne)
|
return silence;
|
||||||
|
}
|
||||||
|
const bin_pat = _euclidRot(pulses, steps, rotation);
|
||||||
|
const gapless = bin_pat
|
||||||
.join('')
|
.join('')
|
||||||
.split('1')
|
.split('1')
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.map((s) => [s.length + 1, true]);
|
.map((s) => [s.length + 1, true]);
|
||||||
return this.struct(timeCat(...gapless)).late(Fraction(firstOne).div(steps));
|
return pat.struct(timeCat(...gapless));
|
||||||
};
|
};
|
||||||
|
|
||||||
export default euclid;
|
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {
|
||||||
|
return _euclidLegato(pulses, steps, 0, pat);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
||||||
|
return _euclidLegato(pulses, steps, rotation, pat);
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
evaluate.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isPattern } from './index.mjs';
|
||||||
|
|
||||||
|
let scoped = false;
|
||||||
|
export const evalScope = async (...args) => {
|
||||||
|
if (scoped) {
|
||||||
|
console.warn('evalScope was called more than once.');
|
||||||
|
}
|
||||||
|
scoped = true;
|
||||||
|
const results = await Promise.allSettled(args);
|
||||||
|
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||||
|
results.forEach((result, i) => {
|
||||||
|
if (result.status === 'rejected') {
|
||||||
|
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Object.assign(globalThis, ...modules);
|
||||||
|
// below is a fix for above commented out line
|
||||||
|
// same error as https://github.com/vitest-dev/vitest/issues/1807 when running this on astro server
|
||||||
|
modules.forEach((module) => {
|
||||||
|
Object.entries(module).forEach(([name, value]) => {
|
||||||
|
globalThis[name] = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function safeEval(str, options = {}) {
|
||||||
|
const { wrapExpression = true, wrapAsync = true } = options;
|
||||||
|
if (wrapExpression) {
|
||||||
|
str = `{${str}}`;
|
||||||
|
}
|
||||||
|
if (wrapAsync) {
|
||||||
|
str = `(async ()=>${str})()`;
|
||||||
|
}
|
||||||
|
const body = `"use strict";return (${str})`;
|
||||||
|
return Function(body)();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const evaluate = async (code, transpiler) => {
|
||||||
|
if (!scoped) {
|
||||||
|
await evalScope(); // at least scope Pattern.prototype.boostrap
|
||||||
|
}
|
||||||
|
if (transpiler) {
|
||||||
|
code = transpiler(code); // transform syntactically correct js code to semantically usable code
|
||||||
|
}
|
||||||
|
// if no transpiler is given, we expect a single instruction (!wrapExpression)
|
||||||
|
const options = { wrapExpression: !!transpiler };
|
||||||
|
let evaluated = await safeEval(code, options);
|
||||||
|
if (!isPattern(evaluated)) {
|
||||||
|
console.log('evaluated', evaluated);
|
||||||
|
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||||
|
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||||
|
}
|
||||||
|
return { mode: 'javascript', pattern: evaluated };
|
||||||
|
};
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="text"
|
id="text"
|
||||||
value="cat('a', 'b')"
|
value="seq('a', ['b', 'c'])"
|
||||||
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
|
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
/>
|
/>
|
||||||
<div id="output"></div>
|
<div id="output"></div>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.0.2');
|
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8');
|
||||||
Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
|
Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
|
||||||
const input = document.getElementById('text');
|
const input = document.getElementById('text');
|
||||||
const getEvents = () => {
|
const getEvents = () => {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="text"
|
id="text"
|
||||||
value="cat('orange', 'indigo')"
|
value="seq('tomato', 'indigo', ['white', 'steelblue']).fast(4)"
|
||||||
style="width: 100%; font-size: 2em; background: black; color: white; outline: none; position: absolute; top: 0"
|
style="width: 100%; font-size: 2em; background: black; color: white; outline: none; position: absolute; top: 0"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
/>
|
/>
|
||||||
<canvas id="canvas"></canvas>
|
<canvas id="canvas"></canvas>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.0.2');
|
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8');
|
||||||
// this adds all strudel functions to the global scope, to be used by eval
|
// this adds all strudel functions to the global scope, to be used by eval
|
||||||
Object.assign(window, strudel);
|
Object.assign(window, strudel);
|
||||||
// setup elements
|
// setup elements
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Buildless Vanilla Strudel REPL</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; background: #222">
|
||||||
|
<div style="display: grid; height: 100vh; grid-template-rows: 32px auto">
|
||||||
|
<button id="start" style="width: 100vw; height: 32px">evaluate</button>
|
||||||
|
<textarea
|
||||||
|
id="text"
|
||||||
|
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
|
||||||
|
spellcheck="false"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div id="output"></div>
|
||||||
|
<script type="module">
|
||||||
|
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.6.8';
|
||||||
|
import { mini } from 'https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0';
|
||||||
|
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.6.0';
|
||||||
|
import {
|
||||||
|
getAudioContext,
|
||||||
|
webaudioOutput,
|
||||||
|
initAudioOnFirstClick,
|
||||||
|
} from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0';
|
||||||
|
|
||||||
|
initAudioOnFirstClick();
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
const input = document.getElementById('text');
|
||||||
|
input.innerHTML = getTune();
|
||||||
|
|
||||||
|
evalScope(
|
||||||
|
controls,
|
||||||
|
import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8'),
|
||||||
|
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0'),
|
||||||
|
import('https://cdn.skypack.dev/@strudel.cycles/tonal@0.6.0'),
|
||||||
|
import('https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { evaluate } = repl({
|
||||||
|
defaultOutput: webaudioOutput,
|
||||||
|
getTime: () => ctx.currentTime,
|
||||||
|
transpiler,
|
||||||
|
});
|
||||||
|
document.getElementById('start').addEventListener('click', () => evaluate(input.value));
|
||||||
|
|
||||||
|
function getTune() {
|
||||||
|
return `await samples('github:tidalcycles/Dirt-Samples/master')
|
||||||
|
|
||||||
|
stack(
|
||||||
|
// amen
|
||||||
|
n("0 1 2 3 4 5 6 7")
|
||||||
|
.sometimes(x=>x.ply(2))
|
||||||
|
.rarely(x=>x.speed("2 | -2"))
|
||||||
|
.sometimesBy(.4, x=>x.delay(".5"))
|
||||||
|
.s("amencutup")
|
||||||
|
.slow(2)
|
||||||
|
.room(.5)
|
||||||
|
,
|
||||||
|
// bass
|
||||||
|
sine.add(saw.slow(4)).range(0,7).segment(8)
|
||||||
|
.superimpose(x=>x.add(.1))
|
||||||
|
.scale('G0 minor').note()
|
||||||
|
.s("sawtooth").decay(.1).sustain(0)
|
||||||
|
.gain(.4).cutoff(perlin.range(300,3000).slow(8)).resonance(10)
|
||||||
|
.degradeBy("0 0.1 .5 .1")
|
||||||
|
.rarely(add(note("12")))
|
||||||
|
,
|
||||||
|
// chord
|
||||||
|
note("Bb3,D4".superimpose(x=>x.add(.2)))
|
||||||
|
.s('sawtooth').cutoff(1000).struct("<~@3 [~ x]>")
|
||||||
|
.decay(.05).sustain(.0).delay(.8).delaytime(.125).room(.8)
|
||||||
|
,
|
||||||
|
// alien
|
||||||
|
s("breath").room(1).shape(.6).chop(16).rev().mask("<x ~@7>")
|
||||||
|
,
|
||||||
|
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
|
||||||
|
).reset("<x@7 x(5,8)>")`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
!dist
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
or view it [live on githack](https://rawcdn.githack.com/tidalcycles/strudel/5fb36acb046ead7cd6ad3cd10f532e7f585f536a/packages/core/examples/vite-vanilla-repl/dist/index.html)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{b as s,h as i,m,a as n,c as p,p as t}from"./index.4cbc0a10.js";export{s as SyntaxError,i as h,m as mini,n as minify,p as parse,t as patternifyAST};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{g as s,f as d,i as t,n as r,l as u,j as o,d as f,k as p,r as g,s as i,w as l,e as m}from"./index.4cbc0a10.js";export{s as getAudioContext,d as getCachedBuffer,t as getLoadedBuffer,r as getLoadedSamples,u as loadBuffer,o as loadGithubSamples,f as panic,p as resetLoadedSamples,g as reverseBuffer,i as samples,l as webaudioOutput,m as webaudioOutputTrigger};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite Vanilla Strudel REPL</title>
|
||||||
|
<script type="module" crossorigin src="/tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/assets/index.4cbc0a10.js"></script>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; background: #222">
|
||||||
|
<div style="display: grid; height: 100vh">
|
||||||
|
<textarea
|
||||||
|
id="text"
|
||||||
|
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
|
||||||
|
spellcheck="false"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="start"
|
||||||
|
style="
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 10px;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid white;
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
evaluate
|
||||||
|
</button>
|
||||||
|
<div id="output"></div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite Vanilla Strudel REPL</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; background: #222">
|
||||||
|
<div style="display: grid; height: 100vh">
|
||||||
|
<textarea
|
||||||
|
id="text"
|
||||||
|
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
|
||||||
|
spellcheck="false"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="start"
|
||||||
|
style="
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 10px;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid white;
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
evaluate
|
||||||
|
</button>
|
||||||
|
<div id="output"></div>
|
||||||
|
<script type="module" src="./main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { controls, repl, evalScope } from '@strudel.cycles/core';
|
||||||
|
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
||||||
|
import { transpiler } from '@strudel.cycles/transpiler';
|
||||||
|
import tune from './tune.mjs';
|
||||||
|
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
const input = document.getElementById('text');
|
||||||
|
input.innerHTML = tune;
|
||||||
|
initAudioOnFirstClick();
|
||||||
|
|
||||||
|
evalScope(
|
||||||
|
controls,
|
||||||
|
import('@strudel.cycles/core'),
|
||||||
|
import('@strudel.cycles/mini'),
|
||||||
|
import('@strudel.cycles/webaudio'),
|
||||||
|
import('@strudel.cycles/tonal'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { evaluate } = repl({
|
||||||
|
defaultOutput: webaudioOutput,
|
||||||
|
getTime: () => ctx.currentTime,
|
||||||
|
transpiler,
|
||||||
|
});
|
||||||
|
document.getElementById('start').addEventListener('click', () => {
|
||||||
|
ctx.resume();
|
||||||
|
console.log('eval', input.value);
|
||||||
|
evaluate(input.value);
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-vanilla-repl",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build-githack": "vite build --base /tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^3.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@strudel.cycles/core": "workspace:*",
|
||||||
|
"@strudel.cycles/mini": "workspace:*",
|
||||||
|
"@strudel.cycles/transpiler": "workspace:*",
|
||||||
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
|
"@strudel.cycles/tonal": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export default `await samples('github:tidalcycles/Dirt-Samples/master')
|
||||||
|
|
||||||
|
stack(
|
||||||
|
// amen
|
||||||
|
n("0 1 2 3 4 5 6 7")
|
||||||
|
.sometimes(x=>x.ply(2))
|
||||||
|
.rarely(x=>x.speed("2 | -2"))
|
||||||
|
.sometimesBy(.4, x=>x.delay(".5"))
|
||||||
|
.s("amencutup")
|
||||||
|
.slow(2)
|
||||||
|
.room(.5)
|
||||||
|
,
|
||||||
|
// bass
|
||||||
|
sine.add(saw.slow(4)).range(0,7).segment(8)
|
||||||
|
.superimpose(x=>x.add(.1))
|
||||||
|
.scale('G0 minor').note()
|
||||||
|
.s("sawtooth").decay(.1).sustain(0)
|
||||||
|
.gain(.4).cutoff(perlin.range(300,3000).slow(8)).resonance(10)
|
||||||
|
.degradeBy("0 0.1 .5 .1")
|
||||||
|
.rarely(add(note("12")))
|
||||||
|
,
|
||||||
|
// chord
|
||||||
|
note("Bb3,D4".superimpose(x=>x.add(.2)))
|
||||||
|
.s('sawtooth').cutoff(1000).struct("<~@3 [~ x]>")
|
||||||
|
.decay(.05).sustain(.0).delay(.8).delaytime(.125).room(.8)
|
||||||
|
,
|
||||||
|
// alien
|
||||||
|
s("breath").room(1).shape(.6).chop(16).rev().mask("<x ~@7>")
|
||||||
|
,
|
||||||
|
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
|
||||||
|
).reset("<x@7 x(5,8)>")`;
|
||||||
@@ -55,7 +55,8 @@ Fraction.prototype.min = function (other) {
|
|||||||
return this.lt(other) ? this : other;
|
return this.lt(other) ? this : other;
|
||||||
};
|
};
|
||||||
|
|
||||||
Fraction.prototype.show = function () {
|
Fraction.prototype.show = function (/* excludeWhole = false */) {
|
||||||
|
// return this.toFraction(excludeWhole);
|
||||||
return this.s * this.n + '/' + this.d;
|
return this.s * this.n + '/' + this.d;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ const fraction = (n) => {
|
|||||||
-> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
|
-> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
|
||||||
-> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
|
-> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
|
||||||
*/
|
*/
|
||||||
n = String(n);
|
// n = String(n); // this is actually faster but imprecise...
|
||||||
}
|
}
|
||||||
return Fraction(n);
|
return Fraction(n);
|
||||||
};
|
};
|
||||||
|
|||||||
+33
-6
@@ -79,15 +79,42 @@ export class Hap {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show(compact = false) {
|
||||||
return (
|
const value =
|
||||||
'(' + (this.whole == undefined ? '~' : this.whole.show()) + ', ' + this.part.show() + ', ' + this.value + ')'
|
typeof this.value === 'object'
|
||||||
);
|
? compact
|
||||||
|
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||||
|
: JSON.stringify(this.value)
|
||||||
|
: this.value;
|
||||||
|
var spans = '';
|
||||||
|
if (this.whole == undefined) {
|
||||||
|
spans = '~' + this.part.show;
|
||||||
|
} else {
|
||||||
|
var is_whole = this.whole.begin.equals(this.part.begin) && this.whole.end.equals(this.part.end);
|
||||||
|
if (!this.whole.begin.equals(this.part.begin)) {
|
||||||
|
spans = this.whole.begin.show() + ' ⇜ ';
|
||||||
|
}
|
||||||
|
if (!is_whole) {
|
||||||
|
spans += '(';
|
||||||
|
}
|
||||||
|
spans += this.part.show();
|
||||||
|
if (!is_whole) {
|
||||||
|
spans += ')';
|
||||||
|
}
|
||||||
|
if (!this.whole.end.equals(this.part.end)) {
|
||||||
|
spans += ' ⇝ ' + this.whole.end.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '[ ' + spans + ' | ' + value + ' ]';
|
||||||
}
|
}
|
||||||
|
|
||||||
showWhole() {
|
showWhole(compact = false) {
|
||||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
|
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
|
||||||
typeof this.value === 'object' ? JSON.stringify(this.value) : this.value
|
typeof this.value === 'object'
|
||||||
|
? compact
|
||||||
|
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||||
|
: JSON.stringify(this.value)
|
||||||
|
: this.value
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-6
@@ -4,10 +4,11 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './controls.mjs';
|
import controls from './controls.mjs';
|
||||||
export * from './euclid.mjs';
|
export * from './euclid.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
export { Fraction };
|
import { logger } from './logger.mjs';
|
||||||
|
export { Fraction, controls };
|
||||||
export * from './hap.mjs';
|
export * from './hap.mjs';
|
||||||
export * from './pattern.mjs';
|
export * from './pattern.mjs';
|
||||||
export * from './signal.mjs';
|
export * from './signal.mjs';
|
||||||
@@ -15,14 +16,20 @@ export * from './state.mjs';
|
|||||||
export * from './timespan.mjs';
|
export * from './timespan.mjs';
|
||||||
export * from './util.mjs';
|
export * from './util.mjs';
|
||||||
export * from './speak.mjs';
|
export * from './speak.mjs';
|
||||||
|
export * from './evaluate.mjs';
|
||||||
|
export * from './repl.mjs';
|
||||||
|
export * from './logger.mjs';
|
||||||
|
export * from './time.mjs';
|
||||||
|
export * from './draw.mjs';
|
||||||
|
export * from './animate.mjs';
|
||||||
|
export * from './pianoroll.mjs';
|
||||||
|
export * from './ui.mjs';
|
||||||
|
export { default as drawLine } from './drawLine.mjs';
|
||||||
export { default as gist } from './gist.js';
|
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; */
|
||||||
console.log(
|
logger('🌀 @strudel.cycles/core loaded 🌀');
|
||||||
'%c // 🌀 @strudel.cycles/core loaded 🌀', // keep "//" for runnable snapshot source..
|
|
||||||
'background-color: black;color:white;padding:4px;border-radius:15px',
|
|
||||||
);
|
|
||||||
if (globalThis._strudelLoaded) {
|
if (globalThis._strudelLoaded) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`@strudel.cycles/core was loaded more than once...
|
`@strudel.cycles/core was loaded more than once...
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export const logKey = 'strudel.log';
|
||||||
|
|
||||||
|
export function logger(message, type, data = {}) {
|
||||||
|
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
||||||
|
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(logKey, {
|
||||||
|
detail: {
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.key = logKey;
|
||||||
Generated
-1639
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/core",
|
"name": "@strudel.cycles/core",
|
||||||
"version": "0.2.0",
|
"version": "0.6.8",
|
||||||
"description": "Port of Tidal Cycles to JavaScript",
|
"description": "Port of Tidal Cycles to JavaScript",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest run"
|
"test": "vitest run",
|
||||||
|
"build": "vite build",
|
||||||
|
"prepublishOnly": "pnpm build"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -25,8 +31,11 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"homepage": "https://strudel.tidalcycles.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bjork": "^0.0.1",
|
|
||||||
"fraction.js": "^4.2.0"
|
"fraction.js": "^4.2.0"
|
||||||
},
|
},
|
||||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2"
|
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^3.2.2",
|
||||||
|
"vitest": "^0.25.7"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1841
-997
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,27 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern } from '@strudel.cycles/core';
|
import { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
||||||
|
|
||||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||||
const getValue = (e) => {
|
const getValue = (e) => {
|
||||||
let value = typeof e.value === 'object' ? e.value.note ?? e.value.n : e.value;
|
let { value } = e;
|
||||||
if (typeof value === 'string') {
|
if (typeof e.value !== 'object') {
|
||||||
value = toMidi(value);
|
value = { value };
|
||||||
|
}
|
||||||
|
let { note, n, freq, s } = value;
|
||||||
|
if (freq) {
|
||||||
|
return freqToMidi(freq);
|
||||||
|
}
|
||||||
|
note = note ?? n;
|
||||||
|
if (typeof note === 'string') {
|
||||||
|
return toMidi(note);
|
||||||
|
}
|
||||||
|
if (typeof note === 'number') {
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
if (s) {
|
||||||
|
return '_' + s;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
@@ -48,9 +62,6 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
from = 0;
|
from = 0;
|
||||||
to = timeframeProp;
|
to = timeframeProp;
|
||||||
}
|
}
|
||||||
if (!autorange && fold) {
|
|
||||||
console.warn('disabling autorange has no effect when fold is enabled');
|
|
||||||
}
|
|
||||||
const timeAxis = vertical ? h : w;
|
const timeAxis = vertical ? h : w;
|
||||||
const valueAxis = vertical ? w : h;
|
const valueAxis = vertical ? w : h;
|
||||||
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
|
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
|
||||||
@@ -138,10 +149,149 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
maxMidi = max;
|
maxMidi = max;
|
||||||
valueExtent = maxMidi - minMidi + 1;
|
valueExtent = maxMidi - minMidi + 1;
|
||||||
}
|
}
|
||||||
foldValues = values.sort((a, b) => a - b);
|
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
|
||||||
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// this function allows drawing a pianoroll without ties to Pattern.prototype
|
||||||
|
// it will probably replace the above in the future
|
||||||
|
export function pianoroll({
|
||||||
|
time,
|
||||||
|
haps,
|
||||||
|
cycles = 4,
|
||||||
|
playhead = 0.5,
|
||||||
|
flipTime = 0,
|
||||||
|
flipValues = 0,
|
||||||
|
hideNegative = false,
|
||||||
|
// inactive = '#C9E597',
|
||||||
|
// inactive = '#FFCA28',
|
||||||
|
inactive = '#7491D2',
|
||||||
|
active = '#FFCA28',
|
||||||
|
// background = '#2A3236',
|
||||||
|
background = 'transparent',
|
||||||
|
smear = 0,
|
||||||
|
playheadColor = 'white',
|
||||||
|
minMidi = 10,
|
||||||
|
maxMidi = 90,
|
||||||
|
autorange = 0,
|
||||||
|
timeframe: timeframeProp,
|
||||||
|
fold = 0,
|
||||||
|
vertical = 0,
|
||||||
|
ctx,
|
||||||
|
} = {}) {
|
||||||
|
const w = ctx.canvas.width;
|
||||||
|
const h = ctx.canvas.height;
|
||||||
|
let from = -cycles * playhead;
|
||||||
|
let to = cycles * (1 - playhead);
|
||||||
|
|
||||||
|
if (timeframeProp) {
|
||||||
|
console.warn('timeframe is deprecated! use from/to instead');
|
||||||
|
from = 0;
|
||||||
|
to = timeframeProp;
|
||||||
|
}
|
||||||
|
const timeAxis = vertical ? h : w;
|
||||||
|
const valueAxis = vertical ? w : h;
|
||||||
|
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
|
||||||
|
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
|
||||||
|
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
|
||||||
|
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
|
||||||
|
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
|
||||||
|
let foldValues = [];
|
||||||
|
flipTime && timeRange.reverse();
|
||||||
|
flipValues && valueRange.reverse();
|
||||||
|
|
||||||
|
// onQuery
|
||||||
|
const { min, max, values } = haps.reduce(
|
||||||
|
({ min, max, values }, e) => {
|
||||||
|
const v = getValue(e);
|
||||||
|
return {
|
||||||
|
min: v < min ? v : min,
|
||||||
|
max: v > max ? v : max,
|
||||||
|
values: values.includes(v) ? values : [...values, v],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ min: Infinity, max: -Infinity, values: [] },
|
||||||
|
);
|
||||||
|
if (autorange) {
|
||||||
|
minMidi = min;
|
||||||
|
maxMidi = max;
|
||||||
|
valueExtent = maxMidi - minMidi + 1;
|
||||||
|
}
|
||||||
|
// foldValues = values.sort((a, b) => a - b);
|
||||||
|
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
|
||||||
|
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
||||||
|
|
||||||
|
ctx.fillStyle = background;
|
||||||
|
ctx.globalAlpha = 1; // reset!
|
||||||
|
if (!smear) {
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
}
|
||||||
|
/* const inFrame = (event) =>
|
||||||
|
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
|
||||||
|
haps
|
||||||
|
// .filter(inFrame)
|
||||||
|
.forEach((event) => {
|
||||||
|
const isActive = event.whole.begin <= time && event.whole.end > time;
|
||||||
|
const color = event.value?.color || event.context?.color;
|
||||||
|
ctx.fillStyle = color || inactive;
|
||||||
|
ctx.strokeStyle = color || active;
|
||||||
|
ctx.globalAlpha = event.context.velocity ?? 1;
|
||||||
|
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
||||||
|
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||||
|
const value = getValue(event);
|
||||||
|
const valuePx = scale(
|
||||||
|
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
|
||||||
|
...valueRange,
|
||||||
|
);
|
||||||
|
let margin = 0;
|
||||||
|
const offset = scale(time / timeExtent, ...timeRange);
|
||||||
|
let coords;
|
||||||
|
if (vertical) {
|
||||||
|
coords = [
|
||||||
|
valuePx + 1 - (flipValues ? barThickness : 0), // x
|
||||||
|
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
|
||||||
|
barThickness - 2, // width
|
||||||
|
durationPx - 2, // height
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
coords = [
|
||||||
|
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
|
||||||
|
valuePx + 1 - (flipValues ? 0 : barThickness), // y
|
||||||
|
durationPx - 2, // widith
|
||||||
|
barThickness - 2, // height
|
||||||
|
];
|
||||||
|
}
|
||||||
|
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
||||||
|
});
|
||||||
|
ctx.globalAlpha = 1; // reset!
|
||||||
|
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||||
|
// draw playhead
|
||||||
|
ctx.strokeStyle = playheadColor;
|
||||||
|
ctx.beginPath();
|
||||||
|
if (vertical) {
|
||||||
|
ctx.moveTo(0, playheadPosition);
|
||||||
|
ctx.lineTo(valueAxis, playheadPosition);
|
||||||
|
} else {
|
||||||
|
ctx.moveTo(playheadPosition, 0);
|
||||||
|
ctx.lineTo(playheadPosition, valueAxis);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptions(drawTime, options = {}) {
|
||||||
|
let [lookbehind, lookahead] = drawTime;
|
||||||
|
lookbehind = Math.abs(lookbehind);
|
||||||
|
const cycles = lookahead + lookbehind;
|
||||||
|
const playhead = lookbehind / cycles;
|
||||||
|
return { fold: 1, ...options, cycles, playhead };
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern.prototype.punchcard = function (options) {
|
||||||
|
return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
|
||||||
|
};
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Cyclist } from './cyclist.mjs';
|
||||||
|
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
|
import { setTime } from './time.mjs';
|
||||||
|
|
||||||
|
export function repl({
|
||||||
|
interval,
|
||||||
|
defaultOutput,
|
||||||
|
onSchedulerError,
|
||||||
|
onEvalError,
|
||||||
|
beforeEval,
|
||||||
|
afterEval,
|
||||||
|
getTime,
|
||||||
|
transpiler,
|
||||||
|
onToggle,
|
||||||
|
editPattern,
|
||||||
|
}) {
|
||||||
|
const scheduler = new Cyclist({
|
||||||
|
interval,
|
||||||
|
onTrigger: async (hap, deadline, duration) => {
|
||||||
|
try {
|
||||||
|
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||||
|
await defaultOutput(hap, deadline, duration);
|
||||||
|
}
|
||||||
|
if (hap.context.onTrigger) {
|
||||||
|
const cps = 1;
|
||||||
|
// call signature of output / onTrigger is different...
|
||||||
|
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: onSchedulerError,
|
||||||
|
getTime,
|
||||||
|
onToggle,
|
||||||
|
});
|
||||||
|
setTime(() => scheduler.now()); // TODO: refactor?
|
||||||
|
const evaluate = async (code, autostart = true) => {
|
||||||
|
if (!code) {
|
||||||
|
throw new Error('no code to evaluate');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
beforeEval?.({ code });
|
||||||
|
let { pattern } = await _evaluate(code, transpiler);
|
||||||
|
|
||||||
|
logger(`[eval] code updated`);
|
||||||
|
pattern = editPattern?.(pattern) || pattern;
|
||||||
|
scheduler.setPattern(pattern, autostart);
|
||||||
|
afterEval?.({ code, pattern });
|
||||||
|
return pattern;
|
||||||
|
} catch (err) {
|
||||||
|
// console.warn(`[repl] eval error: ${err.message}`);
|
||||||
|
logger(`[eval] error: ${err.message}`, 'error');
|
||||||
|
onEvalError?.(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const stop = () => scheduler.stop();
|
||||||
|
const start = () => scheduler.start();
|
||||||
|
const pause = () => scheduler.pause();
|
||||||
|
return { scheduler, evaluate, start, stop, pause };
|
||||||
|
}
|
||||||
+257
-91
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Hap } from './hap.mjs';
|
import { Hap } from './hap.mjs';
|
||||||
import { Pattern, fastcat, reify, silence, stack } from './pattern.mjs';
|
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
import { id } from './util.mjs';
|
import { id } from './util.mjs';
|
||||||
|
|
||||||
@@ -20,20 +20,20 @@ export const signal = (func) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isaw = signal((t) => 1 - (t % 1));
|
export const isaw = signal((t) => 1 - (t % 1));
|
||||||
export const isaw2 = isaw._toBipolar();
|
export const isaw2 = isaw.toBipolar();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sawtooth signal between 0 and 1.
|
* A sawtooth signal between 0 and 1.
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4))
|
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
|
||||||
* @example
|
* @example
|
||||||
* saw.range(0,8).segment(8).scale('C major').slow(4)
|
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const saw = signal((t) => t % 1);
|
export const saw = signal((t) => t % 1);
|
||||||
export const saw2 = saw._toBipolar();
|
export const saw2 = saw.toBipolar();
|
||||||
|
|
||||||
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||||
|
|
||||||
@@ -42,17 +42,17 @@ 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')
|
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const sine = sine2._fromBipolar();
|
export const sine = sine2.fromBipolar();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cosine signal between 0 and 1.
|
* A cosine signal between 0 and 1.
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor')
|
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor').note()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const cosine = sine._early(Fraction(1).div(4));
|
export const cosine = sine._early(Fraction(1).div(4));
|
||||||
@@ -63,18 +63,18 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
|
|||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* square.segment(2).range(0,7).scale('C minor')
|
* square.segment(2).range(0,7).scale('C minor').note()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
export const square = signal((t) => Math.floor((t * 2) % 2));
|
||||||
export const square2 = square._toBipolar();
|
export const square2 = square.toBipolar();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A triangle signal between 0 and 1.
|
* A triangle signal between 0 and 1.
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* triangle.segment(2).range(0,7).scale('C minor')
|
* tri.segment(8).range(0,7).scale('C minor').note()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const tri = fastcat(isaw, saw);
|
export const tri = fastcat(isaw, saw);
|
||||||
@@ -111,19 +111,48 @@ const timeToRandsPrime = (seed, n) => {
|
|||||||
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A continuous pattern of random numbers, between 0 and 1
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A discrete pattern of numbers from 0 to n-1
|
||||||
|
* @example
|
||||||
|
* run(4).scale('C4 major').note()
|
||||||
|
* // "0 1 2 3".scale('C4 major').note()
|
||||||
|
*/
|
||||||
|
export const run = (n) => saw.range(0, n).floor().segment(n);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A continuous pattern of random numbers, between 0 and 1.
|
||||||
|
*
|
||||||
|
* @name rand
|
||||||
|
* @example
|
||||||
|
* // randomly change the cutoff
|
||||||
|
* s("bd sd,hh*4").cutoff(rand.range(500,2000))
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
export const rand = signal(timeToRand);
|
export const rand = signal(timeToRand);
|
||||||
/**
|
/**
|
||||||
* A continuous pattern of random numbers, between -1 and 1
|
* A continuous pattern of random numbers, between -1 and 1
|
||||||
*/
|
*/
|
||||||
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);
|
||||||
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
||||||
export const brand = _brandBy(0.5);
|
export const brand = _brandBy(0.5);
|
||||||
|
|
||||||
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A continuous pattern of random integers, between 0 and n-1.
|
||||||
|
*
|
||||||
|
* @name irand
|
||||||
|
* @param {number} n max value (exclusive)
|
||||||
|
* @example
|
||||||
|
* // randomly select scale notes from 0 - 7 (= C to C)
|
||||||
|
* irand(8).struct("x(3,8)").scale('C minor').note()
|
||||||
|
*
|
||||||
|
*/
|
||||||
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
||||||
|
|
||||||
export const __chooseWith = (pat, xs) => {
|
export const __chooseWith = (pat, xs) => {
|
||||||
@@ -156,8 +185,8 @@ export const chooseInWith = (pat, xs) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chooses randomly from the given list of values.
|
* Chooses randomly from the given list of elements.
|
||||||
* @param {...any} xs
|
* @param {...any} xs values / patterns to choose from.
|
||||||
* @returns {Pattern} - a continuous pattern.
|
* @returns {Pattern} - a continuous pattern.
|
||||||
*/
|
*/
|
||||||
export const choose = (...xs) => chooseWith(rand, xs);
|
export const choose = (...xs) => chooseWith(rand, xs);
|
||||||
@@ -180,9 +209,17 @@ Pattern.prototype.choose = function (...xs) {
|
|||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.choose2 = function (...xs) {
|
Pattern.prototype.choose2 = function (...xs) {
|
||||||
return chooseWith(this._fromBipolar(), xs);
|
return chooseWith(this.fromBipolar(), xs);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks one of the elements at random each cycle.
|
||||||
|
* @returns {Pattern}
|
||||||
|
* @example
|
||||||
|
* chooseCycles("bd", "hh", "sd").s().fast(4)
|
||||||
|
* @example
|
||||||
|
* "bd | hh | sd".s().fast(4)
|
||||||
|
*/
|
||||||
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
|
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
|
||||||
|
|
||||||
export const randcat = chooseCycles;
|
export const randcat = chooseCycles;
|
||||||
@@ -209,6 +246,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
|||||||
|
|
||||||
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
|
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
|
||||||
|
|
||||||
|
// this function expects pat to be a pattern of floats...
|
||||||
export const perlinWith = (pat) => {
|
export const perlinWith = (pat) => {
|
||||||
const pata = pat.fmap(Math.floor);
|
const pata = pat.fmap(Math.floor);
|
||||||
const patb = pat.fmap((t) => Math.floor(t) + 1);
|
const patb = pat.fmap((t) => Math.floor(t) + 1);
|
||||||
@@ -217,101 +255,229 @@ export const perlinWith = (pat) => {
|
|||||||
return pat.sub(pata).fmap(interp).appBoth(pata.fmap(timeToRand)).appBoth(patb.fmap(timeToRand));
|
return pat.sub(pata).fmap(interp).appBoth(pata.fmap(timeToRand)).appBoth(patb.fmap(timeToRand));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const perlin = perlinWith(time);
|
/**
|
||||||
|
* Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1.
|
||||||
|
*
|
||||||
|
* @name perlin
|
||||||
|
* @example
|
||||||
|
* // randomly change the cutoff
|
||||||
|
* s("bd sd,hh*4").cutoff(perlin.range(500,2000))
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
||||||
|
|
||||||
Pattern.prototype._degradeByWith = function (withPat, x) {
|
export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
|
||||||
return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x));
|
pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
|
||||||
};
|
);
|
||||||
|
|
||||||
Pattern.prototype._degradeBy = function (x) {
|
/**
|
||||||
return this._degradeByWith(rand, x);
|
* Randomly removes events from the pattern by a given amount.
|
||||||
};
|
* 0 = 0% chance of removal
|
||||||
|
* 1 = 100% chance of removal
|
||||||
|
*
|
||||||
|
* @name degradeBy
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {number} amount - a number between 0 and 1
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").degradeBy(0.2)
|
||||||
|
* @example
|
||||||
|
* s("[hh?0.2]*8")
|
||||||
|
*/
|
||||||
|
export const degradeBy = register('degradeBy', function (x, pat) {
|
||||||
|
return pat._degradeByWith(rand, x);
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype.degrade = function () {
|
/**
|
||||||
return this._degradeBy(0.5);
|
*
|
||||||
};
|
* Randomly removes 50% of events from the pattern. Shorthand for `.degradeBy(0.5)`
|
||||||
|
*
|
||||||
|
* @name degrade
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").degrade()
|
||||||
|
* @example
|
||||||
|
* s("[hh?]*8")
|
||||||
|
*/
|
||||||
|
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
||||||
|
|
||||||
Pattern.prototype._undegradeBy = function (x) {
|
/**
|
||||||
return this._degradeByWith(
|
* Inverse of {@link Pattern#degradeBy}: Randomly removes events from the pattern by a given amount.
|
||||||
|
* 0 = 100% chance of removal
|
||||||
|
* 1 = 0% chance of removal
|
||||||
|
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
|
||||||
|
*
|
||||||
|
* @name undegradeBy
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {number} amount - a number between 0 and 1
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").undegradeBy(0.2)
|
||||||
|
*/
|
||||||
|
export const undegradeBy = register('undegradeBy', function (x, pat) {
|
||||||
|
return pat._degradeByWith(
|
||||||
rand.fmap((r) => 1 - r),
|
rand.fmap((r) => 1 - r),
|
||||||
x,
|
x,
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
Pattern.prototype.undegrade = function () {
|
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
||||||
return this._undegradeBy(0.5);
|
|
||||||
};
|
|
||||||
|
|
||||||
Pattern.prototype._sometimesBy = function (x, func) {
|
/**
|
||||||
return stack(this._degradeBy(x), func(this._undegradeBy(1 - x)));
|
*
|
||||||
};
|
* Randomly applies the given function by the given probability.
|
||||||
|
* Similar to {@link Pattern#someCyclesBy}
|
||||||
|
*
|
||||||
|
* @name sometimesBy
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {number | Pattern} probability - a number between 0 and 1
|
||||||
|
* @param {function} function - the transformation to apply
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
|
||||||
Pattern.prototype.sometimesBy = function (patx, func) {
|
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
||||||
const pat = this;
|
|
||||||
return reify(patx)
|
return reify(patx)
|
||||||
.fmap((x) => pat._sometimesBy(x, func))
|
.fmap((x) => stack(pat._degradeBy(x), func(pat._undegradeBy(1 - x))))
|
||||||
.innerJoin();
|
.innerJoin();
|
||||||
};
|
});
|
||||||
|
|
||||||
Pattern.prototype._sometimesByPre = function (x, func) {
|
/**
|
||||||
return stack(this._degradeBy(x), func(this).undegradeBy(1 - x));
|
*
|
||||||
};
|
* Applies the given function with a 50% chance
|
||||||
|
*
|
||||||
|
* @name sometimes
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {function} function - the transformation to apply
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*4").sometimes(x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
export const sometimes = register('sometimes', function (func, pat) {
|
||||||
|
return pat._sometimesBy(0.5, func);
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype.sometimesByPre = function (patx, func) {
|
/**
|
||||||
const pat = this;
|
*
|
||||||
|
* Randomly applies the given function by the given probability on a cycle by cycle basis.
|
||||||
|
* Similar to {@link Pattern#sometimesBy}
|
||||||
|
*
|
||||||
|
* @name someCyclesBy
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {number | Pattern} probability - a number between 0 and 1
|
||||||
|
* @param {function} function - the transformation to apply
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
|
||||||
return reify(patx)
|
return reify(patx)
|
||||||
.fmap((x) => pat._sometimesByPre(x, func))
|
.fmap((x) =>
|
||||||
|
stack(
|
||||||
|
pat._degradeByWith(rand._segment(1), x),
|
||||||
|
func(pat._degradeByWith(rand.fmap((r) => 1 - r)._segment(1), 1 - x)),
|
||||||
|
),
|
||||||
|
)
|
||||||
.innerJoin();
|
.innerJoin();
|
||||||
};
|
});
|
||||||
|
|
||||||
Pattern.prototype.sometimes = function (func) {
|
/**
|
||||||
return this._sometimesBy(0.5, func);
|
*
|
||||||
};
|
* Shorthand for `.someCyclesBy(0.5, fn)`
|
||||||
|
*
|
||||||
|
* @name someCycles
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
export const someCycles = register('someCycles', function (func, pat) {
|
||||||
|
return pat._someCyclesBy(0.5, func);
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype.sometimesPre = function (func) {
|
/**
|
||||||
return this._sometimesByPre(0.5, func);
|
*
|
||||||
};
|
* Shorthand for `.sometimesBy(0.75, fn)`
|
||||||
|
*
|
||||||
|
* @name often
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").often(x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
export const often = register('often', function (func, pat) {
|
||||||
|
return pat.sometimesBy(0.75, func);
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype._someCyclesBy = function (x, func) {
|
/**
|
||||||
return stack(
|
*
|
||||||
this._degradeByWith(rand._segment(1), x),
|
* Shorthand for `.sometimesBy(0.25, fn)`
|
||||||
func(this._degradeByWith(rand.fmap((r) => 1 - r)._segment(1), 1 - x)),
|
*
|
||||||
);
|
* @name rarely
|
||||||
};
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").rarely(x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
export const rarely = register('rarely', function (func, pat) {
|
||||||
|
return pat.sometimesBy(0.25, func);
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype.someCyclesBy = function (patx, func) {
|
/**
|
||||||
const pat = this;
|
*
|
||||||
return reify(patx)
|
* Shorthand for `.sometimesBy(0.1, fn)`
|
||||||
.fmap((x) => pat._someCyclesBy(x, func))
|
*
|
||||||
.innerJoin();
|
* @name almostNever
|
||||||
};
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").almostNever(x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
export const almostNever = register('almostNever', function (func, pat) {
|
||||||
|
return pat.sometimesBy(0.1, func);
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype.someCycles = function (func) {
|
/**
|
||||||
return this._someCyclesBy(0.5, func);
|
*
|
||||||
};
|
* Shorthand for `.sometimesBy(0.9, fn)`
|
||||||
|
*
|
||||||
|
* @name almostAlways
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").almostAlways(x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
export const almostAlways = register('almostAlways', function (func, pat) {
|
||||||
|
return pat.sometimesBy(0.9, func);
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype.often = function (func) {
|
/**
|
||||||
return this.sometimesBy(0.75, func);
|
*
|
||||||
};
|
* Shorthand for `.sometimesBy(0, fn)` (never calls fn)
|
||||||
|
*
|
||||||
|
* @name never
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("hh*8").never(x=>x.speed("0.5"))
|
||||||
|
*/
|
||||||
|
export const never = register('never', function (_, pat) {
|
||||||
|
return pat;
|
||||||
|
});
|
||||||
|
|
||||||
Pattern.prototype.rarely = function (func) {
|
/**
|
||||||
return this.sometimesBy(0.25, func);
|
*
|
||||||
};
|
* Shorthand for `.sometimesBy(1, fn)` (always calls fn)
|
||||||
|
*
|
||||||
Pattern.prototype.almostNever = function (func) {
|
* @name always
|
||||||
return this.sometimesBy(0.1, func);
|
* @memberof Pattern
|
||||||
};
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
Pattern.prototype.almostAlways = function (func) {
|
* s("hh*8").always(x=>x.speed("0.5"))
|
||||||
return this.sometimesBy(0.9, func);
|
*/
|
||||||
};
|
export const always = register('always', function (func, pat) {
|
||||||
|
return func(pat);
|
||||||
Pattern.prototype.never = function (func) {
|
});
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
Pattern.prototype.always = function (func) {
|
|
||||||
return func(this);
|
|
||||||
};
|
|
||||||
|
|
||||||
Pattern.prototype.patternified.push('degradeBy', 'undegradeBy');
|
|
||||||
|
|||||||
+6
-13
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, patternify2 } from './index.mjs';
|
import { register } from './index.mjs';
|
||||||
|
|
||||||
let synth;
|
let synth;
|
||||||
try {
|
try {
|
||||||
@@ -16,7 +16,7 @@ try {
|
|||||||
let allVoices = synth?.getVoices();
|
let allVoices = synth?.getVoices();
|
||||||
// console.log('voices', allVoices);
|
// console.log('voices', allVoices);
|
||||||
|
|
||||||
function speak(words, lang, voice) {
|
function triggerSpeech(words, lang, voice) {
|
||||||
synth.cancel();
|
synth.cancel();
|
||||||
const utterance = new SpeechSynthesisUtterance(words);
|
const utterance = new SpeechSynthesisUtterance(words);
|
||||||
utterance.lang = lang;
|
utterance.lang = lang;
|
||||||
@@ -31,15 +31,8 @@ function speak(words, lang, voice) {
|
|||||||
speechSynthesis.speak(utterance);
|
speechSynthesis.speak(utterance);
|
||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype._speak = function (lang, voice) {
|
export const speak = register('speak', function (lang, voice, pat) {
|
||||||
return this._withHap((hap) => {
|
return pat.onTrigger((_, hap) => {
|
||||||
const onTrigger = (time, hap) => {
|
triggerSpeech(hap.value, lang, voice);
|
||||||
speak(hap.value, lang, voice);
|
|
||||||
};
|
|
||||||
return hap.setContext({ ...hap.context, onTrigger });
|
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
Pattern.prototype.speak = function (lang, voice) {
|
|
||||||
return patternify2(Pattern.prototype._speak)(reify(lang), reify(voice), this);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
pure,
|
pure,
|
||||||
stack,
|
stack,
|
||||||
fastcat,
|
fastcat,
|
||||||
|
firstOf,
|
||||||
slowcat,
|
slowcat,
|
||||||
cat,
|
cat,
|
||||||
sequence,
|
sequence,
|
||||||
@@ -42,10 +43,17 @@ import {
|
|||||||
id,
|
id,
|
||||||
ply,
|
ply,
|
||||||
rev,
|
rev,
|
||||||
|
time,
|
||||||
|
run,
|
||||||
|
hitch,
|
||||||
|
set,
|
||||||
} from '../index.mjs';
|
} from '../index.mjs';
|
||||||
|
|
||||||
import { steady } from '../signal.mjs';
|
import { steady } from '../signal.mjs';
|
||||||
|
|
||||||
|
import controls 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);
|
||||||
@@ -54,7 +62,7 @@ const third = Fraction(1, 3);
|
|||||||
const twothirds = Fraction(2, 3);
|
const twothirds = Fraction(2, 3);
|
||||||
|
|
||||||
const sameFirst = (a, b) => {
|
const sameFirst = (a, b) => {
|
||||||
return expect(a._sortHapsByPart().firstCycle()).toStrictEqual(b._sortHapsByPart().firstCycle());
|
return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle());
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('TimeSpan', () => {
|
describe('TimeSpan', () => {
|
||||||
@@ -136,6 +144,9 @@ describe('Pattern', () => {
|
|||||||
it('Can make a pattern', () => {
|
it('Can make a pattern', () => {
|
||||||
expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3);
|
expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3);
|
||||||
});
|
});
|
||||||
|
it('Supports zero-width queries', () => {
|
||||||
|
expect(pure('hello').queryArc(0, 0).length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('fmap()', () => {
|
describe('fmap()', () => {
|
||||||
it('Can add things', () => {
|
it('Can add things', () => {
|
||||||
@@ -146,35 +157,46 @@ describe('Pattern', () => {
|
|||||||
).toBe(7);
|
).toBe(7);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('out()', () => {
|
||||||
|
it('is an alias for set.out()', () => {
|
||||||
|
sameFirst(sequence(1, 2).out(5, 6, 7, 8), sequence(1, 2).set.out(5, 6, 7, 8));
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('add()', () => {
|
describe('add()', () => {
|
||||||
|
it('works as toplevel function', () => {
|
||||||
|
expect(add(pure(4), pure(5)).query(st(0, 1))[0].value).toBe(9);
|
||||||
|
});
|
||||||
|
it('works as toplevel function, with bare values for arguments', () => {
|
||||||
|
expect(add(4, 5).query(st(0, 1))[0].value).toBe(9);
|
||||||
|
});
|
||||||
it('can structure In()', () => {
|
it('can structure In()', () => {
|
||||||
expect(pure(3).add(pure(4)).query(st(0, 1))[0].value).toBe(7);
|
expect(pure(3).add(pure(4)).query(st(0, 1))[0].value).toBe(7);
|
||||||
expect(pure(3).addIn(pure(4)).query(st(0, 1))[0].value).toBe(7);
|
expect(pure(3).add.in(pure(4)).query(st(0, 1))[0].value).toBe(7);
|
||||||
});
|
});
|
||||||
it('can structure Out()', () => {
|
it('can structure Out()', () => {
|
||||||
sameFirst(sequence(1, 2).addOut(4), sequence(5, 6).struct(true));
|
sameFirst(sequence(1, 2).add.out(4), sequence(5, 6).struct(true));
|
||||||
});
|
});
|
||||||
it('can Mix() structure', () => {
|
it('can Mix() structure', () => {
|
||||||
expect(sequence(1, 2).addMix(silence, 5, silence).firstCycle()).toStrictEqual([
|
expect(sequence(1, 2).add.mix(silence, 5, silence).firstCycle()).toStrictEqual([
|
||||||
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 6),
|
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 6),
|
||||||
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 Trig() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).addTrig(20, 30).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trig(20, 30).early(2),
|
||||||
sequence(26, 27, 36, 37),
|
sequence(26, 27, 36, 37),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Trigzero() structure', () => {
|
it('can Trigzero() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).addTrigzero(20, 30).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trigzero(20, 30).early(2),
|
||||||
sequence(21, 22, 31, 32),
|
sequence(21, 22, 31, 32),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Squeeze() structure', () => {
|
it('can Squeeze() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
sequence(1, [2, 3]).addSqueeze(sequence(10, 20, 30)),
|
sequence(1, [2, 3]).add.squeeze(sequence(10, 20, 30)),
|
||||||
sequence(
|
sequence(
|
||||||
[11, 21, 31],
|
[11, 21, 31],
|
||||||
[
|
[
|
||||||
@@ -184,42 +206,45 @@ describe('Pattern', () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can SqueezeOut() structure', () => {
|
it('can squeezeout() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
sequence(1, [2, 3]).addSqueezeOut(10, 20, 30),
|
sequence(1, [2, 3]).add.squeezeout(10, 20, 30),
|
||||||
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
|
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('can add object patterns', () => {
|
||||||
|
sameFirst(n(sequence(1, [2, 3])).add(n(10)), n(sequence(11, [12, 13])));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('keep()', () => {
|
describe('keep()', () => {
|
||||||
it('can structure In()', () => {
|
it('can structure In()', () => {
|
||||||
expect(pure(3).keep(pure(4)).query(st(0, 1))[0].value).toBe(3);
|
expect(pure(3).keep(pure(4)).query(st(0, 1))[0].value).toBe(3);
|
||||||
expect(pure(3).keepIn(pure(4)).query(st(0, 1))[0].value).toBe(3);
|
expect(pure(3).keep.in(pure(4)).query(st(0, 1))[0].value).toBe(3);
|
||||||
});
|
});
|
||||||
it('can structure Out()', () => {
|
it('can structure Out()', () => {
|
||||||
sameFirst(sequence(1, 2).keepOut(4), sequence(1, 2).struct(true));
|
sameFirst(sequence(1, 2).keep.out(4), sequence(1, 2).struct(true));
|
||||||
});
|
});
|
||||||
it('can Mix() structure', () => {
|
it('can Mix() structure', () => {
|
||||||
expect(sequence(1, 2).keepMix(silence, 5, silence).firstCycle()).toStrictEqual([
|
expect(sequence(1, 2).keep.mix(silence, 5, silence).firstCycle()).toStrictEqual([
|
||||||
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
|
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
|
||||||
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 Trig() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepTrig(20, 30).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trig(20, 30).early(2),
|
||||||
sequence(6, 7, 6, 7),
|
sequence(6, 7, 6, 7),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Trigzero() structure', () => {
|
it('can Trigzero() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepTrigzero(20, 30).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trigzero(20, 30).early(2),
|
||||||
sequence(1, 2, 1, 2),
|
sequence(1, 2, 1, 2),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Squeeze() structure', () => {
|
it('can Squeeze() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
sequence(1, [2, 3]).keepSqueeze(sequence(10, 20, 30)),
|
sequence(1, [2, 3]).keep.squeeze(sequence(10, 20, 30)),
|
||||||
sequence(
|
sequence(
|
||||||
[1, 1, 1],
|
[1, 1, 1],
|
||||||
[
|
[
|
||||||
@@ -229,39 +254,39 @@ describe('Pattern', () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can SqueezeOut() structure', () => {
|
it('can squeezeout() structure', () => {
|
||||||
sameFirst(sequence(1, [2, 3]).keepSqueezeOut(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
|
sameFirst(sequence(1, [2, 3]).keep.squeezeout(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('keepif()', () => {
|
describe('keepif()', () => {
|
||||||
it('can structure In()', () => {
|
it('can structure In()', () => {
|
||||||
sameFirst(sequence(3, 4).keepif(true, false), sequence(3, silence));
|
sameFirst(sequence(3, 4).keepif(true, false), sequence(3, silence));
|
||||||
sameFirst(sequence(3, 4).keepifIn(true, false), sequence(3, silence));
|
sameFirst(sequence(3, 4).keepif.in(true, false), sequence(3, silence));
|
||||||
});
|
});
|
||||||
it('can structure Out()', () => {
|
it('can structure Out()', () => {
|
||||||
sameFirst(pure(1).keepifOut(true, false), sequence(1, silence));
|
sameFirst(pure(1).keepif.out(true, false), sequence(1, silence));
|
||||||
});
|
});
|
||||||
it('can Mix() structure', () => {
|
it('can Mix() structure', () => {
|
||||||
expect(sequence(1, 2).keepifMix(false, true, false).firstCycle()).toStrictEqual([
|
expect(sequence(1, 2).keepif.mix(false, true, false).firstCycle()).toStrictEqual([
|
||||||
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
|
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
|
||||||
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 Trig() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepifTrig(false, true).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trig(false, true).early(2),
|
||||||
sequence(silence, silence, 6, 7),
|
sequence(silence, silence, 6, 7),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Trigzero() structure', () => {
|
it('can Trigzero() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepifTrigzero(false, true).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trigzero(false, true).early(2),
|
||||||
sequence(silence, silence, 1, 2),
|
sequence(silence, silence, 1, 2),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Squeeze() structure', () => {
|
it('can Squeeze() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
sequence(1, [2, 3]).keepifSqueeze(sequence(true, true, false)),
|
sequence(1, [2, 3]).keepif.squeeze(sequence(true, true, false)),
|
||||||
sequence(
|
sequence(
|
||||||
[1, 1, silence],
|
[1, 1, silence],
|
||||||
[
|
[
|
||||||
@@ -271,8 +296,8 @@ describe('Pattern', () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can SqueezeOut() structure', () => {
|
it('can squeezeout() structure', () => {
|
||||||
sameFirst(sequence(1, [2, 3]).keepifSqueezeOut(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
|
sameFirst(sequence(1, [2, 3]).keepif.squeezeout(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('sub()', () => {
|
describe('sub()', () => {
|
||||||
@@ -312,13 +337,13 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
describe('setOut()', () => {
|
describe('setOut()', () => {
|
||||||
it('Can set things with structure from second pattern', () => {
|
it('Can set things with structure from second pattern', () => {
|
||||||
sameFirst(sequence(1, 2).setOut(4), pure(4).mask(true, true));
|
sameFirst(sequence(1, 2).set.out(4), pure(4).mask(true, true));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('setSqueeze()', () => {
|
describe('setSqueeze()', () => {
|
||||||
it('Can squeeze one pattern inside the haps of another', () => {
|
it('Can squeeze one pattern inside the haps of another', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
sequence(1, [2, 3]).setSqueeze(sequence('a', 'b', 'c')),
|
sequence(1, [2, 3]).set.squeeze(sequence('a', 'b', 'c')),
|
||||||
sequence(
|
sequence(
|
||||||
['a', 'b', 'c'],
|
['a', 'b', 'c'],
|
||||||
[
|
[
|
||||||
@@ -328,7 +353,7 @@ describe('Pattern', () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
sameFirst(
|
sameFirst(
|
||||||
sequence(1, [2, 3]).setSqueeze('a', 'b', 'c'),
|
sequence(1, [2, 3]).set.squeeze('a', 'b', 'c'),
|
||||||
sequence(
|
sequence(
|
||||||
['a', 'b', 'c'],
|
['a', 'b', 'c'],
|
||||||
[
|
[
|
||||||
@@ -371,6 +396,12 @@ describe('Pattern', () => {
|
|||||||
sequence(['a', 'b', 'c'], silence, ['a', 'b', 'c'], silence).firstCycle(),
|
sequence(['a', 'b', 'c'], silence, ['a', 'b', 'c'], silence).firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('copes with breaking up events across cycles', () => {
|
||||||
|
expect(pure('a').slow(2)._fastGap(2).setContext({}).query(st(0, 2))).toStrictEqual([
|
||||||
|
hap(ts(0, 1), ts(0, 0.5), 'a'),
|
||||||
|
hap(ts(0.5, 1.5), ts(1, 1.5), 'a'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('_compressSpan()', () => {
|
describe('_compressSpan()', () => {
|
||||||
it('Can squash cycles of a pattern into a given timespan', () => {
|
it('Can squash cycles of a pattern into a given timespan', () => {
|
||||||
@@ -390,8 +421,8 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('defaults to accepting sequences', () => {
|
it('defaults to accepting sequences', () => {
|
||||||
expect(sequence(1, 2, 3).fast(sequence(1.5, 2)).firstCycle()).toStrictEqual(
|
expect(sequence('a', 'b', 'c').fast(sequence(1.5, 2)).sortHapsByPart().firstCycle()).toStrictEqual(
|
||||||
sequence(1, 2, 3).fast(1.5, 2).firstCycle(),
|
sequence('a', 'b', 'c').fast(1.5, 2).sortHapsByPart().firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('works as a static function', () => {
|
it('works as a static function', () => {
|
||||||
@@ -424,6 +455,16 @@ describe('Pattern', () => {
|
|||||||
// mini('[c3 g3]/2 eb3') always plays [c3 eb3]
|
// mini('[c3 g3]/2 eb3') always plays [c3 eb3]
|
||||||
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
|
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
|
||||||
});
|
});
|
||||||
|
it('Supports zero-length queries', () => {
|
||||||
|
expect(steady('a')._slow(1).queryArc(0, 0)).toStrictEqual(steady('a').queryArc(0, 0));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('slow()', () => {
|
||||||
|
it('Supports zero-length queries', () => {
|
||||||
|
expect(steady('a').slow(1).setContext({}).queryArc(0, 0)).toStrictEqual(
|
||||||
|
steady('a').setContext({}).queryArc(0, 0),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('inside', () => {
|
describe('inside', () => {
|
||||||
it('can rev inside a cycle', () => {
|
it('can rev inside a cycle', () => {
|
||||||
@@ -439,7 +480,7 @@ describe('Pattern', () => {
|
|||||||
it('Filters true', () => {
|
it('Filters true', () => {
|
||||||
expect(
|
expect(
|
||||||
pure(true)
|
pure(true)
|
||||||
._filterValues((x) => x)
|
.filterValues((x) => x)
|
||||||
.firstCycle().length,
|
.firstCycle().length,
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
});
|
});
|
||||||
@@ -464,7 +505,7 @@ describe('Pattern', () => {
|
|||||||
pure(10)
|
pure(10)
|
||||||
.when(slowcat(true, false), (x) => x.add(3))
|
.when(slowcat(true, false), (x) => x.add(3))
|
||||||
.fast(4)
|
.fast(4)
|
||||||
._sortHapsByPart()
|
.sortHapsByPart()
|
||||||
.firstCycle(),
|
.firstCycle(),
|
||||||
).toStrictEqual(fastcat(13, 10, 13, 10).firstCycle());
|
).toStrictEqual(fastcat(13, 10, 13, 10).firstCycle());
|
||||||
});
|
});
|
||||||
@@ -551,26 +592,36 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('every()', () => {
|
describe('firstOf()', () => {
|
||||||
it('Can apply a function every 3rd time', () => {
|
it('Can apply a function every 3rd time', () => {
|
||||||
expect(
|
expect(
|
||||||
pure('a')
|
pure('a')
|
||||||
.every(3, (x) => x._fast(2))
|
.firstOf(3, (x) => x._fast(2))
|
||||||
._fast(3)
|
._fast(3)
|
||||||
.firstCycle(),
|
.firstCycle(),
|
||||||
).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle());
|
).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle());
|
||||||
});
|
});
|
||||||
it('works with currying', () => {
|
it('Works as a toplevel function', () => {
|
||||||
expect(pure('a').every(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
|
expect(firstOf(3, fast(2), pure('a'))._fast(3).firstCycle()).toStrictEqual(
|
||||||
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
||||||
);
|
);
|
||||||
expect(sequence(3, 4, 5).every(3, add(3)).fast(5).firstCycle()).toStrictEqual(
|
});
|
||||||
|
it('Works as a toplevel function, with a patterned first argument', () => {
|
||||||
|
expect(firstOf(pure(3), fast(2), pure('a'))._fast(3).firstCycle()).toStrictEqual(
|
||||||
|
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('works with currying', () => {
|
||||||
|
expect(pure('a').firstOf(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
|
||||||
|
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
||||||
|
);
|
||||||
|
expect(sequence(3, 4, 5).firstOf(3, add(3)).fast(5).firstCycle()).toStrictEqual(
|
||||||
sequence(6, 7, 8, 3, 4, 5, 3, 4, 5, 6, 7, 8, 3, 4, 5).firstCycle(),
|
sequence(6, 7, 8, 3, 4, 5, 3, 4, 5, 6, 7, 8, 3, 4, 5).firstCycle(),
|
||||||
);
|
);
|
||||||
expect(sequence(3, 4, 5).every(2, sub(1)).fast(5).firstCycle()).toStrictEqual(
|
expect(sequence(3, 4, 5).firstOf(2, sub(1)).fast(5).firstCycle()).toStrictEqual(
|
||||||
sequence(2, 3, 4, 3, 4, 5, 2, 3, 4, 3, 4, 5, 2, 3, 4).firstCycle(),
|
sequence(2, 3, 4, 3, 4, 5, 2, 3, 4, 3, 4, 5, 2, 3, 4).firstCycle(),
|
||||||
);
|
);
|
||||||
expect(sequence(3, 4, 5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle()).toStrictEqual(
|
expect(sequence(3, 4, 5).firstOf(3, add(3)).firstOf(2, sub(1)).fast(2).firstCycle()).toStrictEqual(
|
||||||
sequence(5, 6, 7, 3, 4, 5).firstCycle(),
|
sequence(5, 6, 7, 3, 4, 5).firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -666,7 +717,7 @@ describe('Pattern', () => {
|
|||||||
it('Can set the hap context', () => {
|
it('Can set the hap context', () => {
|
||||||
expect(
|
expect(
|
||||||
pure('a')
|
pure('a')
|
||||||
._setContext([
|
.setContext([
|
||||||
[
|
[
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[1, 2],
|
[1, 2],
|
||||||
@@ -687,13 +738,13 @@ describe('Pattern', () => {
|
|||||||
it('Can update the hap context', () => {
|
it('Can update the hap context', () => {
|
||||||
expect(
|
expect(
|
||||||
pure('a')
|
pure('a')
|
||||||
._setContext([
|
.setContext([
|
||||||
[
|
[
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[1, 2],
|
[1, 2],
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
._withContext((c) => [
|
.withContext((c) => [
|
||||||
...c,
|
...c,
|
||||||
[
|
[
|
||||||
[3, 4],
|
[3, 4],
|
||||||
@@ -717,7 +768,7 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
describe('apply', () => {
|
describe('apply', () => {
|
||||||
it('Can apply a function', () => {
|
it('Can apply a function', () => {
|
||||||
expect(sequence('a', 'b')._apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
|
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
|
||||||
}),
|
}),
|
||||||
it('Can apply a pattern of functions', () => {
|
it('Can apply a pattern of functions', () => {
|
||||||
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
|
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
|
||||||
@@ -758,18 +809,18 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
describe('jux', () => {
|
describe('jux', () => {
|
||||||
it('Can juxtapose', () => {
|
it('Can juxtapose', () => {
|
||||||
expect(pure({ a: 1 }).jux(fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
|
expect(pure({ a: 1 }).jux(fast(2)).sortHapsByPart().firstCycle()).toStrictEqual(
|
||||||
stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
|
stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
|
||||||
._sortHapsByPart()
|
.sortHapsByPart()
|
||||||
.firstCycle(),
|
.firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('juxBy', () => {
|
describe('juxBy', () => {
|
||||||
it('Can juxtapose by half', () => {
|
it('Can juxtapose by half', () => {
|
||||||
expect(pure({ a: 1 }).juxBy(0.5, fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
|
expect(pure({ a: 1 }).juxBy(0.5, fast(2)).sortHapsByPart().firstCycle()).toStrictEqual(
|
||||||
stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
|
stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
|
||||||
._sortHapsByPart()
|
.sortHapsByPart()
|
||||||
.firstCycle(),
|
.firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -779,7 +830,7 @@ describe('Pattern', () => {
|
|||||||
expect(
|
expect(
|
||||||
sequence('a', ['a', 'a'])
|
sequence('a', ['a', 'a'])
|
||||||
.fmap((a) => fastcat('b', 'c'))
|
.fmap((a) => fastcat('b', 'c'))
|
||||||
._squeezeJoin()
|
.squeezeJoin()
|
||||||
.firstCycle(),
|
.firstCycle(),
|
||||||
).toStrictEqual(
|
).toStrictEqual(
|
||||||
sequence(
|
sequence(
|
||||||
@@ -791,6 +842,14 @@ describe('Pattern', () => {
|
|||||||
).firstCycle(),
|
).firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('Squeezes to the correct cycle', () => {
|
||||||
|
expect(
|
||||||
|
pure(time.struct(true))
|
||||||
|
.squeezeJoin()
|
||||||
|
.queryArc(3, 4)
|
||||||
|
.map((x) => x.value),
|
||||||
|
).toStrictEqual([Fraction(3.5)]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('ply', () => {
|
describe('ply', () => {
|
||||||
it('Can ply(3)', () => {
|
it('Can ply(3)', () => {
|
||||||
@@ -823,7 +882,7 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('Can chop(2,3)', () => {
|
it('Can chop(2,3)', () => {
|
||||||
expect(pure({ sound: 'a' }).fast(2).chop(2, 3)._sortHapsByPart().firstCycle()).toStrictEqual(
|
expect(pure({ sound: 'a' }).fast(2).chop(2, 3).sortHapsByPart().firstCycle()).toStrictEqual(
|
||||||
sequence(
|
sequence(
|
||||||
[
|
[
|
||||||
{ sound: 'a', begin: 0, end: 0.5 },
|
{ sound: 'a', begin: 0, end: 0.5 },
|
||||||
@@ -835,11 +894,16 @@ describe('Pattern', () => {
|
|||||||
{ sound: 'a', begin: 2 / 3, end: 1 },
|
{ sound: 'a', begin: 2 / 3, end: 1 },
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
._sortHapsByPart()
|
.sortHapsByPart()
|
||||||
.firstCycle(),
|
.firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('range', () => {
|
||||||
|
it('Can be patterned', () => {
|
||||||
|
expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(sequence(0, 0.5).firstCycle());
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('range2', () => {
|
describe('range2', () => {
|
||||||
it('Can change the range of a bipolar pattern', () => {
|
it('Can change the range of a bipolar pattern', () => {
|
||||||
expect(sequence(-1, -0.5, 0, 0.5).range2(1000, 1100).firstCycle()).toStrictEqual(
|
expect(sequence(-1, -0.5, 0, 0.5).range2(1000, 1100).firstCycle()).toStrictEqual(
|
||||||
@@ -847,6 +911,18 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('run', () => {
|
||||||
|
it('Can run', () => {
|
||||||
|
expect(run(4).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('ribbon', () => {
|
||||||
|
it('Can ribbon', () => {
|
||||||
|
expect(cat(0, 1, 2, 3, 4, 5, 6, 7).ribbon(2, 4).fast(4).firstCycle()).toStrictEqual(
|
||||||
|
sequence(2, 3, 4, 5).firstCycle(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('linger', () => {
|
describe('linger', () => {
|
||||||
it('Can linger on the first quarter of a cycle', () => {
|
it('Can linger on the first quarter of a cycle', () => {
|
||||||
expect(sequence(0, 1, 2, 3, 4, 5, 6, 7).linger(0.25).firstCycle()).toStrictEqual(
|
expect(sequence(0, 1, 2, 3, 4, 5, 6, 7).linger(0.25).firstCycle()).toStrictEqual(
|
||||||
@@ -854,4 +930,55 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('alignments', () => {
|
||||||
|
it('Can combine controls', () => {
|
||||||
|
sameFirst(s('bd').set.in.n(3), s('bd').n(3));
|
||||||
|
sameFirst(s('bd').set.squeeze.n(3, 4), sequence(s('bd').n(3), s('bd').n(4)));
|
||||||
|
});
|
||||||
|
it('Can combine functions with alignmed controls', () => {
|
||||||
|
sameFirst(s('bd').apply(fast(2).set(n(3))), s('bd').fast(2).set.in.n(3));
|
||||||
|
sameFirst(s('bd').apply(fast(2).set.in.n(3)), s('bd').fast(2).set.in.n(3));
|
||||||
|
});
|
||||||
|
it('Can squeeze arguments', () => {
|
||||||
|
expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('defragmentHaps', () => {
|
||||||
|
it('Can merge two touching haps with same whole and value', () => {
|
||||||
|
expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1);
|
||||||
|
});
|
||||||
|
it('Doesnt merge two overlapping haps', () => {
|
||||||
|
expect(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', () => {
|
||||||
|
expect(stack(pure('a').mask(1, 0), pure('b').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
|
||||||
|
});
|
||||||
|
it('Doesnt merge two touching haps with different wholes', () => {
|
||||||
|
expect(stack(sequence('a', silence), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('press', () => {
|
||||||
|
it('Can syncopate events', () => {
|
||||||
|
sameFirst(sequence('a', 'b', 'c', 'd').press(), sequence(silence, 'a', silence, 'b', silence, 'c', silence, 'd'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('hurry', () => {
|
||||||
|
it('Can speed up patterns and sounds', () => {
|
||||||
|
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('composable functions', () => {
|
||||||
|
it('Can compose functions', () => {
|
||||||
|
sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
|
||||||
|
});
|
||||||
|
it('Can compose by method chaining operators with controls', () => {
|
||||||
|
sameFirst(s('bd').apply(set.n(3).fast(2)), s('bd').set.n(3).fast(2));
|
||||||
|
});
|
||||||
|
it('Can compose by method chaining operators and alignments with controls', () => {
|
||||||
|
sameFirst(s('bd').apply(set.in.n(3).fast(2)), s('bd').set.n(3).fast(2));
|
||||||
|
// sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
/*
|
/*
|
||||||
util.test.mjs - <short description TODO>
|
util.test.mjs - Tests for the core 'util' module
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/util.test.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/util.test.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { pure } from '../pattern.mjs';
|
import { pure } from '../pattern.mjs';
|
||||||
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency } from '../util.mjs';
|
import {
|
||||||
|
isNote,
|
||||||
|
tokenizeNote,
|
||||||
|
toMidi,
|
||||||
|
fromMidi,
|
||||||
|
freqToMidi,
|
||||||
|
_mod,
|
||||||
|
compose,
|
||||||
|
getFrequency,
|
||||||
|
getPlayableNoteValue,
|
||||||
|
parseNumeral,
|
||||||
|
parseFractional,
|
||||||
|
numeralArgs,
|
||||||
|
fractionalArgs,
|
||||||
|
} from '../util.mjs';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('isNote', () => {
|
describe('isNote', () => {
|
||||||
@@ -19,9 +33,12 @@ describe('isNote', () => {
|
|||||||
expect(isNote(note)).toBe(true);
|
expect(isNote(note)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('should recognize notes without octave', () => {
|
||||||
|
expect(isNote('C')).toBe(true);
|
||||||
|
expect(isNote('Bb')).toBe(true);
|
||||||
|
});
|
||||||
it('should not recognize invalid notes', () => {
|
it('should not recognize invalid notes', () => {
|
||||||
expect(isNote('H5')).toBe(false);
|
expect(isNote('H5')).toBe(false);
|
||||||
expect(isNote('C')).toBe(false);
|
|
||||||
expect(isNote('X')).toBe(false);
|
expect(isNote('X')).toBe(false);
|
||||||
expect(isNote(1)).toBe(false);
|
expect(isNote(1)).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -70,6 +87,10 @@ describe('toMidi', () => {
|
|||||||
expect(toMidi('C#3')).toEqual(49);
|
expect(toMidi('C#3')).toEqual(49);
|
||||||
expect(toMidi('C##3')).toEqual(50);
|
expect(toMidi('C##3')).toEqual(50);
|
||||||
});
|
});
|
||||||
|
it('should throw an error when given a non-note', () => {
|
||||||
|
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||||
|
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('fromMidi', () => {
|
describe('fromMidi', () => {
|
||||||
it('should turn midi into frequency', () => {
|
it('should turn midi into frequency', () => {
|
||||||
@@ -77,32 +98,52 @@ describe('fromMidi', () => {
|
|||||||
expect(fromMidi(57)).toEqual(220);
|
expect(fromMidi(57)).toEqual(220);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('freqToMidi', () => {
|
||||||
|
it('should turn frequency into midi', () => {
|
||||||
|
expect(freqToMidi(440)).toEqual(69);
|
||||||
|
expect(freqToMidi(220)).toEqual(57);
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('getFrequency', () => {
|
describe('getFrequency', () => {
|
||||||
it('should turn midi into frequency', () => {
|
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
it('should turn note into frequency', () => {
|
||||||
expect(getFrequency(happify('a4'))).toEqual(440);
|
expect(getFrequency(happify('a4'))).toEqual(440);
|
||||||
expect(getFrequency(happify('a3'))).toEqual(220);
|
expect(getFrequency(happify('a3'))).toEqual(220);
|
||||||
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440); // TODO: migrate when values are objects..
|
});
|
||||||
|
it('should turn midi into frequency', () => {
|
||||||
|
expect(getFrequency(happify(69, { type: 'midi' }))).toEqual(440);
|
||||||
|
expect(getFrequency(happify(57, { type: 'midi' }))).toEqual(220);
|
||||||
|
});
|
||||||
|
it('should return frequencies unchanged', () => {
|
||||||
|
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440);
|
||||||
expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432);
|
expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432);
|
||||||
});
|
});
|
||||||
|
it('should turn object with a "freq" property into frequency', () => {
|
||||||
|
expect(getFrequency(happify({ freq: 220 }))).toEqual(220);
|
||||||
|
expect(getFrequency(happify({ freq: 440 }))).toEqual(440);
|
||||||
|
});
|
||||||
|
it('should throw an error when given a non-note', () => {
|
||||||
|
expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`);
|
||||||
|
expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mod', () => {
|
describe('_mod', () => {
|
||||||
it('should work like regular modulo with positive numbers', () => {
|
it('should work like regular modulo with positive numbers', () => {
|
||||||
expect(mod(0, 3)).toEqual(0);
|
expect(_mod(0, 3)).toEqual(0);
|
||||||
expect(mod(1, 3)).toEqual(1);
|
expect(_mod(1, 3)).toEqual(1);
|
||||||
expect(mod(2, 3)).toEqual(2);
|
expect(_mod(2, 3)).toEqual(2);
|
||||||
expect(mod(3, 3)).toEqual(0);
|
expect(_mod(3, 3)).toEqual(0);
|
||||||
expect(mod(4, 3)).toEqual(1);
|
expect(_mod(4, 3)).toEqual(1);
|
||||||
expect(mod(4, 2)).toEqual(0);
|
expect(_mod(4, 2)).toEqual(0);
|
||||||
});
|
});
|
||||||
it('should work with negative numbers', () => {
|
it('should work with negative numbers', () => {
|
||||||
expect(mod(-1, 3)).toEqual(2);
|
expect(_mod(-1, 3)).toEqual(2);
|
||||||
expect(mod(-2, 3)).toEqual(1);
|
expect(_mod(-2, 3)).toEqual(1);
|
||||||
expect(mod(-3, 3)).toEqual(0);
|
expect(_mod(-3, 3)).toEqual(0);
|
||||||
expect(mod(-4, 3)).toEqual(2);
|
expect(_mod(-4, 3)).toEqual(2);
|
||||||
expect(mod(-5, 3)).toEqual(1);
|
expect(_mod(-5, 3)).toEqual(1);
|
||||||
expect(mod(-3, 2)).toEqual(1);
|
expect(_mod(-3, 2)).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,3 +159,77 @@ describe('compose', () => {
|
|||||||
expect(compose(addS('a'), addS('b'))('x')).toEqual('xab');
|
expect(compose(addS('a'), addS('b'))('x')).toEqual('xab');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getPlayableNoteValue', () => {
|
||||||
|
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||||
|
it('should return object "note" property', () => {
|
||||||
|
expect(getPlayableNoteValue(happify({ note: 'a4' }))).toEqual('a4');
|
||||||
|
});
|
||||||
|
it('should return object "n" property', () => {
|
||||||
|
expect(getPlayableNoteValue(happify({ n: 'a4' }))).toEqual('a4');
|
||||||
|
});
|
||||||
|
it('should return object "value" property', () => {
|
||||||
|
expect(getPlayableNoteValue(happify({ value: 'a4' }))).toEqual('a4');
|
||||||
|
});
|
||||||
|
it('should turn midi into frequency', () => {
|
||||||
|
expect(getPlayableNoteValue(happify(57, { type: 'midi' }))).toEqual(220);
|
||||||
|
});
|
||||||
|
it('should return frequency value', () => {
|
||||||
|
expect(getPlayableNoteValue(happify(220, { type: 'frequency' }))).toEqual(220);
|
||||||
|
});
|
||||||
|
it('should throw an error if value is not an object, number, or string', () => {
|
||||||
|
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`);
|
||||||
|
expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseNumeral', () => {
|
||||||
|
it('should parse numbers as is', () => {
|
||||||
|
expect(parseNumeral(4)).toBe(4);
|
||||||
|
expect(parseNumeral(0)).toBe(0);
|
||||||
|
expect(parseNumeral(20)).toBe(20);
|
||||||
|
expect(parseNumeral('20')).toBe(20);
|
||||||
|
expect(parseNumeral(1.5)).toBe(1.5);
|
||||||
|
});
|
||||||
|
it('should parse notes', () => {
|
||||||
|
expect(parseNumeral('c')).toBe(48);
|
||||||
|
expect(parseNumeral('c4')).toBe(60);
|
||||||
|
expect(parseNumeral('c#4')).toBe(61);
|
||||||
|
expect(parseNumeral('db4')).toBe(61);
|
||||||
|
});
|
||||||
|
it('should throw an error for unknown strings', () => {
|
||||||
|
expect(() => parseNumeral('xyz')).toThrowError('cannot parse as numeral: "xyz"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseFractional', () => {
|
||||||
|
it('should parse numbers as is', () => {
|
||||||
|
expect(parseFractional(4)).toBe(4);
|
||||||
|
expect(parseFractional(0)).toBe(0);
|
||||||
|
expect(parseFractional(20)).toBe(20);
|
||||||
|
expect(parseFractional('20')).toBe(20);
|
||||||
|
expect(parseFractional(1.5)).toBe(1.5);
|
||||||
|
});
|
||||||
|
it('should parse fractional shorthands values', () => {
|
||||||
|
expect(parseFractional('w')).toBe(1);
|
||||||
|
expect(parseFractional('h')).toBe(0.5);
|
||||||
|
expect(parseFractional('q')).toBe(0.25);
|
||||||
|
expect(parseFractional('e')).toBe(0.125);
|
||||||
|
});
|
||||||
|
it('should throw an error for unknown strings', () => {
|
||||||
|
expect(() => parseFractional('xyz')).toThrowError('cannot parse as fractional: "xyz"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('numeralArgs', () => {
|
||||||
|
it('should convert function arguments to numbers', () => {
|
||||||
|
const add = numeralArgs((a, b) => a + b);
|
||||||
|
expect(add('c4', 2)).toBe(62);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('fractionalArgs', () => {
|
||||||
|
it('should convert function arguments to numbers', () => {
|
||||||
|
const add = fractionalArgs((a, b) => a + b);
|
||||||
|
expect(add('q', 2)).toBe(2.25);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ 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';
|
||||||
|
const { n } = controls;
|
||||||
|
|
||||||
describe('Value', () => {
|
describe('Value', () => {
|
||||||
it('unionWith', () => {
|
it('unionWith', () => {
|
||||||
@@ -21,4 +23,8 @@ 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 }]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
let time;
|
||||||
|
export function getTime() {
|
||||||
|
if (!time) {
|
||||||
|
throw new Error('no time set! use setTime to define a time source');
|
||||||
|
}
|
||||||
|
return time();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTime(func) {
|
||||||
|
time = func;
|
||||||
|
}
|
||||||
@@ -18,6 +18,11 @@ export class TimeSpan {
|
|||||||
const end = this.end;
|
const end = this.end;
|
||||||
const end_sam = end.sam();
|
const end_sam = end.sam();
|
||||||
|
|
||||||
|
// Support zero-width timespans
|
||||||
|
if (begin.equals(end)) {
|
||||||
|
return [new TimeSpan(begin, end)];
|
||||||
|
}
|
||||||
|
|
||||||
while (end.gt(begin)) {
|
while (end.gt(begin)) {
|
||||||
// If begin and end are in the same cycle, we're done.
|
// If begin and end are in the same cycle, we're done.
|
||||||
if (begin.sam().equals(end_sam)) {
|
if (begin.sam().equals(end_sam)) {
|
||||||
@@ -105,7 +110,7 @@ export class TimeSpan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
return this.begin.show() + ' -> ' + this.end.show();
|
return this.begin.show() + ' → ' + this.end.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,14 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Tone } from './tone.mjs';
|
import { getTime } from './time.mjs';
|
||||||
|
|
||||||
export const hideHeader = () => {
|
|
||||||
document.getElementById('header').style = 'display:none';
|
|
||||||
};
|
|
||||||
|
|
||||||
function frame(callback) {
|
function frame(callback) {
|
||||||
if (window.strudelAnimation) {
|
if (window.strudelAnimation) {
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
}
|
}
|
||||||
const animate = (animationTime) => {
|
const animate = (animationTime) => {
|
||||||
const toneTime = Tone.getTransport().seconds;
|
callback(animationTime, getTime());
|
||||||
callback(animationTime, toneTime);
|
|
||||||
window.strudelAnimation = requestAnimationFrame(animate);
|
window.strudelAnimation = requestAnimationFrame(animate);
|
||||||
};
|
};
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
@@ -31,7 +26,7 @@ export const backgroundImage = function (src, animateOptions = {}) {
|
|||||||
({
|
({
|
||||||
style: () => (container.style = bg + ';' + value),
|
style: () => (container.style = bg + ';' + value),
|
||||||
className: () => (container.className = value + ' ' + initialClassName),
|
className: () => (container.className = value + ' ' + initialClassName),
|
||||||
}[option]());
|
})[option]();
|
||||||
};
|
};
|
||||||
const funcOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'function');
|
const funcOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'function');
|
||||||
const stringOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'string');
|
const stringOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'string');
|
||||||
@@ -51,6 +46,7 @@ export const cleanupUi = () => {
|
|||||||
const container = document.getElementById('code');
|
const container = document.getElementById('code');
|
||||||
if (container) {
|
if (container) {
|
||||||
container.style = '';
|
container.style = '';
|
||||||
container.className = 'grow relative'; // has to match App.tsx
|
// TODO: find a way to remove that duplication..
|
||||||
|
container.className = 'grow flex text-gray-100 relative overflow-auto cursor-text pb-0'; // has to match App.tsx
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
+94
-12
@@ -5,7 +5,8 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// returns true if the given string is a note
|
// returns true if the given string is a note
|
||||||
export const isNote = (name) => /^[a-gA-G][#b]*[0-9]$/.test(name);
|
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
||||||
|
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]?$/.test(name);
|
||||||
export const tokenizeNote = (note) => {
|
export const tokenizeNote = (note) => {
|
||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
return [];
|
||||||
@@ -19,7 +20,7 @@ export const tokenizeNote = (note) => {
|
|||||||
|
|
||||||
// turns the given note into its midi number representation
|
// turns the given note into its midi number representation
|
||||||
export const toMidi = (note) => {
|
export const toMidi = (note) => {
|
||||||
const [pc, acc, oct] = tokenizeNote(note);
|
const [pc, acc, oct = 3] = tokenizeNote(note);
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
throw new Error('not a note: "' + note + '"');
|
throw new Error('not a note: "' + note + '"');
|
||||||
}
|
}
|
||||||
@@ -31,6 +32,34 @@ export const fromMidi = (n) => {
|
|||||||
return Math.pow(2, (n - 69) / 12) * 440;
|
return Math.pow(2, (n - 69) / 12) * 440;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const freqToMidi = (freq) => {
|
||||||
|
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const valueToMidi = (value, fallbackValue) => {
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
throw new Error('valueToMidi: expected object value');
|
||||||
|
}
|
||||||
|
let { freq, note } = value;
|
||||||
|
if (typeof freq === 'number') {
|
||||||
|
return freqToMidi(freq);
|
||||||
|
}
|
||||||
|
if (typeof note === 'string') {
|
||||||
|
return toMidi(note);
|
||||||
|
}
|
||||||
|
if (typeof note === 'number') {
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
if (!fallbackValue) {
|
||||||
|
throw new Error('valueToMidi: expected freq or note to be set');
|
||||||
|
}
|
||||||
|
return fallbackValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||||
|
* @noAutocomplete
|
||||||
|
*/
|
||||||
export const getFreq = (noteOrMidi) => {
|
export const getFreq = (noteOrMidi) => {
|
||||||
if (typeof noteOrMidi === 'number') {
|
if (typeof noteOrMidi === 'number') {
|
||||||
return fromMidi(noteOrMidi);
|
return fromMidi(noteOrMidi);
|
||||||
@@ -38,20 +67,27 @@ export const getFreq = (noteOrMidi) => {
|
|||||||
return fromMidi(toMidi(noteOrMidi));
|
return fromMidi(toMidi(noteOrMidi));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||||
|
* @noAutocomplete
|
||||||
|
*/
|
||||||
export const midi2note = (n) => {
|
export const midi2note = (n) => {
|
||||||
const oct = Math.floor(n / 12) - 1;
|
const oct = Math.floor(n / 12) - 1;
|
||||||
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
|
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
|
||||||
return pc + oct;
|
return pc + oct;
|
||||||
};
|
};
|
||||||
|
|
||||||
// modulo that works with negative numbers e.g. mod(-1, 3) = 2
|
// 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)
|
||||||
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
|
export const _mod = (n, m) => ((n % m) + m) % m;
|
||||||
export const mod = (n, m) => ((n % m) + m) % m;
|
|
||||||
|
|
||||||
export const getPlayableNoteValue = (hap) => {
|
export const getPlayableNoteValue = (hap) => {
|
||||||
let { value: note, context } = hap;
|
let { value, context } = hap;
|
||||||
|
let note = value;
|
||||||
if (typeof note === 'object' && !Array.isArray(note)) {
|
if (typeof note === 'object' && !Array.isArray(note)) {
|
||||||
note = note.note || note.n || note.value;
|
note = note.note || note.n || note.value;
|
||||||
|
if (note === undefined) {
|
||||||
|
throw new Error(`cannot find a playable note for ${JSON.stringify(value)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||||
@@ -67,15 +103,18 @@ export const getPlayableNoteValue = (hap) => {
|
|||||||
export const getFrequency = (hap) => {
|
export const getFrequency = (hap) => {
|
||||||
let { value, context } = hap;
|
let { value, context } = hap;
|
||||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||||
if (typeof value === 'object' && value.freq) {
|
if (typeof value === 'object') {
|
||||||
return value.freq;
|
if (value.freq) {
|
||||||
|
return value.freq;
|
||||||
|
}
|
||||||
|
return getFreq(value.note || value.n || value.value);
|
||||||
}
|
}
|
||||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||||
value = fromMidi(hap.value);
|
value = fromMidi(hap.value);
|
||||||
} else if (typeof value === 'string' && isNote(value)) {
|
} else if (typeof value === 'string' && isNote(value)) {
|
||||||
value = fromMidi(toMidi(hap.value));
|
value = fromMidi(toMidi(hap.value));
|
||||||
} else if (typeof value !== 'number') {
|
} else if (typeof value !== 'number') {
|
||||||
throw new Error('not a note or frequency:' + value);
|
throw new Error('not a note or frequency: ' + value);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
@@ -100,13 +139,13 @@ export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
|
|||||||
export const flatten = (arr) => [].concat(...arr);
|
export const flatten = (arr) => [].concat(...arr);
|
||||||
|
|
||||||
export const id = (a) => a;
|
export const id = (a) => a;
|
||||||
export const constant = (a, b) => a;
|
export const constant = curry((a, b) => a);
|
||||||
|
|
||||||
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
||||||
|
|
||||||
export function curry(func, overload) {
|
export function curry(func, overload, arity = func.length) {
|
||||||
const fn = function curried(...args) {
|
const fn = function curried(...args) {
|
||||||
if (args.length >= func.length) {
|
if (args.length >= arity) {
|
||||||
return func.apply(this, args);
|
return func.apply(this, args);
|
||||||
} else {
|
} else {
|
||||||
const partial = function (...args2) {
|
const partial = function (...args2) {
|
||||||
@@ -124,3 +163,46 @@ export function curry(func, overload) {
|
|||||||
}
|
}
|
||||||
return fn;
|
return fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseNumeral(numOrString) {
|
||||||
|
const asNumber = Number(numOrString);
|
||||||
|
if (!isNaN(asNumber)) {
|
||||||
|
return asNumber;
|
||||||
|
}
|
||||||
|
if (isNote(numOrString)) {
|
||||||
|
return toMidi(numOrString);
|
||||||
|
}
|
||||||
|
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapArgs(fn, mapFn) {
|
||||||
|
return (...args) => fn(...args.map(mapFn));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function numeralArgs(fn) {
|
||||||
|
return mapArgs(fn, parseNumeral);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseFractional(numOrString) {
|
||||||
|
const asNumber = Number(numOrString);
|
||||||
|
if (!isNaN(asNumber)) {
|
||||||
|
return asNumber;
|
||||||
|
}
|
||||||
|
const specialValue = {
|
||||||
|
pi: Math.PI,
|
||||||
|
w: 1,
|
||||||
|
h: 0.5,
|
||||||
|
q: 0.25,
|
||||||
|
e: 0.125,
|
||||||
|
s: 0.0625,
|
||||||
|
t: 1 / 3,
|
||||||
|
f: 0.2,
|
||||||
|
x: 1 / 6,
|
||||||
|
}[numOrString];
|
||||||
|
if (typeof specialValue !== 'undefined') {
|
||||||
|
return specialValue;
|
||||||
|
}
|
||||||
|
throw new Error(`cannot parse as fractional: "${numOrString}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import { curry } from './util.mjs';
|
import { curry } from './util.mjs';
|
||||||
|
|
||||||
export function unionWithObj(a, b, func) {
|
export function unionWithObj(a, b, func) {
|
||||||
|
if (typeof b?.value === 'number') {
|
||||||
|
// https://github.com/tidalcycles/strudel/issues/262
|
||||||
|
const numKeys = Object.keys(a).filter((k) => typeof a[k] === 'number');
|
||||||
|
const numerals = Object.fromEntries(numKeys.map((k) => [k, b.value]));
|
||||||
|
b = Object.assign(b, numerals);
|
||||||
|
delete b.value;
|
||||||
|
}
|
||||||
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
|
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
|
||||||
return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])])));
|
return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])])));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { dependencies } from './package.json';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'index.mjs'),
|
||||||
|
formats: ['es', 'cjs'],
|
||||||
|
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [...Object.keys(dependencies)],
|
||||||
|
},
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// will move to https://github.com/felixroos/zyklus
|
||||||
|
// TODO: started flag
|
||||||
|
|
||||||
|
function createClock(
|
||||||
|
getTime,
|
||||||
|
callback, // called slightly before each cycle
|
||||||
|
duration = 0.05, // duration of each cycle
|
||||||
|
interval = 0.1, // interval between callbacks
|
||||||
|
overlap = 0.1, // overlap between callbacks
|
||||||
|
) {
|
||||||
|
let tick = 0; // counts callbacks
|
||||||
|
let phase = 0; // next callback time
|
||||||
|
let precision = 10 ** 4; // used to round phase
|
||||||
|
let minLatency = 0.01;
|
||||||
|
const setDuration = (setter) => (duration = setter(duration));
|
||||||
|
overlap = overlap || interval / 2;
|
||||||
|
const onTick = () => {
|
||||||
|
const t = getTime();
|
||||||
|
const lookahead = t + interval + overlap; // the time window for this tick
|
||||||
|
if (phase === 0) {
|
||||||
|
phase = t + minLatency;
|
||||||
|
}
|
||||||
|
// callback as long as we're inside the lookahead
|
||||||
|
while (phase < lookahead) {
|
||||||
|
phase = Math.round(phase * precision) / precision;
|
||||||
|
phase >= t && callback(phase, duration, tick);
|
||||||
|
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
|
||||||
|
phase += duration; // increment phase by duration
|
||||||
|
tick++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let intervalID;
|
||||||
|
const start = () => {
|
||||||
|
clear(); // just in case start was called more than once
|
||||||
|
onTick();
|
||||||
|
intervalID = setInterval(onTick, interval * 1000);
|
||||||
|
};
|
||||||
|
const clear = () => intervalID !== undefined && clearInterval(intervalID);
|
||||||
|
const pause = () => clear();
|
||||||
|
const stop = () => {
|
||||||
|
tick = 0;
|
||||||
|
phase = 0;
|
||||||
|
clear();
|
||||||
|
};
|
||||||
|
const getPhase = () => phase;
|
||||||
|
// setCallback
|
||||||
|
return { setDuration, start, stop, pause, duration, getPhase, minLatency };
|
||||||
|
}
|
||||||
|
export default createClock;
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { getFrequency, logger, register } from '@strudel.cycles/core';
|
||||||
|
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||||
|
import csd from './project.csd?raw';
|
||||||
|
// import livecodeOrc from './livecode.orc?raw';
|
||||||
|
import presetsOrc from './presets.orc?raw';
|
||||||
|
|
||||||
|
let csoundLoader, _csound;
|
||||||
|
|
||||||
|
// initializes csound + can be used to reevaluate given instrument code
|
||||||
|
export async function loadCSound(code = '') {
|
||||||
|
await init();
|
||||||
|
if (code) {
|
||||||
|
code = `${code}`;
|
||||||
|
// ^ ^
|
||||||
|
// wrapping in backticks makes sure it works when calling as templated function
|
||||||
|
await _csound?.evalCode(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const loadcsound = loadCSound;
|
||||||
|
export const loadCsound = loadCSound;
|
||||||
|
|
||||||
|
export const csound = register('csound', (instrument, pat) => {
|
||||||
|
instrument = instrument || 'triangle';
|
||||||
|
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||||
|
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
||||||
|
return pat.onTrigger((time, hap) => {
|
||||||
|
if (!_csound) {
|
||||||
|
logger('[csound] not loaded yet', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof hap.value !== 'object') {
|
||||||
|
throw new Error('csound only support objects as hap values');
|
||||||
|
}
|
||||||
|
let { gain = 0.8 } = hap.value;
|
||||||
|
gain *= 0.2;
|
||||||
|
|
||||||
|
const freq = Math.round(getFrequency(hap));
|
||||||
|
const controls = Object.entries({ ...hap.value, freq })
|
||||||
|
.flat()
|
||||||
|
.join('/');
|
||||||
|
// TODO: find out how to send a precise ctx based time
|
||||||
|
// http://www.csounds.com/manual/html/i.html
|
||||||
|
const params = [
|
||||||
|
`"${instrument}"`, // p1: instrument name
|
||||||
|
time - getAudioContext().currentTime, //.toFixed(precision), // p2: starting time in arbitrary unit called beats
|
||||||
|
hap.duration + 0, // p3: duration in beats
|
||||||
|
// instrument specific params:
|
||||||
|
freq, //.toFixed(precision), // p4: frequency
|
||||||
|
gain, // p5: gain
|
||||||
|
`"${controls}"`, // p6 controls as string (like superdirt osc message)
|
||||||
|
];
|
||||||
|
const msg = `i ${params.join(' ')}`;
|
||||||
|
_csound.inputMessage(msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function eventLogger(type, args) {
|
||||||
|
const [msg] = args;
|
||||||
|
if (
|
||||||
|
type === 'message' &&
|
||||||
|
(['[commit: HEAD]'].includes(msg) ||
|
||||||
|
msg.startsWith('--Csound version') ||
|
||||||
|
msg.startsWith('libsndfile') ||
|
||||||
|
msg.startsWith('sr =') ||
|
||||||
|
msg.startsWith('0dBFS') ||
|
||||||
|
msg.startsWith('audio buffered') ||
|
||||||
|
msg.startsWith('writing') ||
|
||||||
|
msg.startsWith('SECTION 1:'))
|
||||||
|
) {
|
||||||
|
// ignore
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let logType = 'info';
|
||||||
|
if (msg.startsWith('error:')) {
|
||||||
|
logType = 'error';
|
||||||
|
}
|
||||||
|
logger(`[csound] ${msg || ''}`, logType);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
if (window.__csound__) {
|
||||||
|
// Allows using some other csound instance.
|
||||||
|
// In that case, the external Csound is responsible
|
||||||
|
// for compiling an orchestra and starting to perform.
|
||||||
|
logger('[load] Using external Csound', 'warning');
|
||||||
|
_csound = window.__csound__;
|
||||||
|
return _csound;
|
||||||
|
} else {
|
||||||
|
const { Csound } = await import('@csound/browser');
|
||||||
|
_csound = await Csound({ audioContext: getAudioContext() });
|
||||||
|
_csound.removeAllListeners('message');
|
||||||
|
['message'].forEach((k) => _csound.on(k, (...args) => eventLogger(k, args)));
|
||||||
|
await _csound.setOption('-m0d'); // see -m flag https://csound.com/docs/manual/CommandFlags.html
|
||||||
|
await _csound.setOption('--sample-accurate');
|
||||||
|
await _csound.setOption('-odac');
|
||||||
|
await _csound.compileCsdText(csd);
|
||||||
|
// await _csound.compileOrc(livecodeOrc);
|
||||||
|
await _csound.compileOrc(presetsOrc);
|
||||||
|
await _csound.start();
|
||||||
|
return _csound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
csoundLoader = csoundLoader || load();
|
||||||
|
return csoundLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
let orcCache = {};
|
||||||
|
export async function loadOrc(url) {
|
||||||
|
await init();
|
||||||
|
if (typeof url !== 'string') {
|
||||||
|
throw new Error('loadOrc: expected url string');
|
||||||
|
}
|
||||||
|
if (url.startsWith('github:')) {
|
||||||
|
const [_, path] = url.split('github:');
|
||||||
|
url = `https://raw.githubusercontent.com/${path}`;
|
||||||
|
}
|
||||||
|
if (!orcCache[url]) {
|
||||||
|
orcCache[url] = fetch(url)
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((code) => _csound.compileOrc(code));
|
||||||
|
}
|
||||||
|
await orcCache[url];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends notes to Csound for rendering with MIDI semantics. The hap value is
|
||||||
|
* translated to these Csound pfields:
|
||||||
|
*
|
||||||
|
* p1 -- Csound instrument either as a number (1-based, can be a fraction),
|
||||||
|
* or as a string name.
|
||||||
|
* p2 -- time in beats (usually seconds) from start of performance.
|
||||||
|
* p3 -- duration in beats (usually seconds).
|
||||||
|
* p4 -- MIDI key number (as a real number, not an integer but in [0, 127].
|
||||||
|
* p5 -- MIDI velocity (as a real number, not an integer but in [0, 127].
|
||||||
|
* p6 -- Strudel controls, as a string.
|
||||||
|
*/
|
||||||
|
export const csoundm = register('csoundm', (instrument, pat) => {
|
||||||
|
let p1 = instrument;
|
||||||
|
if (typeof instrument === 'string') {
|
||||||
|
p1 = `"{instrument}"`;
|
||||||
|
}
|
||||||
|
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||||
|
return pat.onTrigger((tidal_time, hap) => {
|
||||||
|
if (!_csound) {
|
||||||
|
logger('[csound] not loaded yet', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof hap.value !== 'object') {
|
||||||
|
throw new Error('csound only support objects as hap values');
|
||||||
|
}
|
||||||
|
// Time in seconds counting from now.
|
||||||
|
const p2 = tidal_time - getAudioContext().currentTime;
|
||||||
|
const p3 = hap.duration.valueOf() + 0;
|
||||||
|
const frequency = getFrequency(hap);
|
||||||
|
// Translate frequency to MIDI key number _without_ rounding.
|
||||||
|
const C4 = 261.62558;
|
||||||
|
let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0;
|
||||||
|
const p4 = octave * 12.0 - 36.0;
|
||||||
|
// We prefer floating point precision, but over the MIDI range [0, 127].
|
||||||
|
const p5 = 127 * (hap.context?.velocity ?? 0.9);
|
||||||
|
// The Strudel controls as a string.
|
||||||
|
const p6 = Object.entries({ ...hap.value, frequency })
|
||||||
|
.flat()
|
||||||
|
.join('/');
|
||||||
|
const i_statement = `i ${p1} ${p2} ${p3} ${p4} ${p5} "${p6}"`;
|
||||||
|
console.log('[csoundm]:', i_statement);
|
||||||
|
_csound.inputMessage(i_statement);
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "@strudel.cycles/csound",
|
||||||
|
"version": "0.6.2",
|
||||||
|
"description": "csound bindings for strudel",
|
||||||
|
"main": "index.mjs",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"tidalcycles",
|
||||||
|
"strudel",
|
||||||
|
"pattern",
|
||||||
|
"livecoding",
|
||||||
|
"algorave"
|
||||||
|
],
|
||||||
|
"author": "Felix Roos <flix91@gmail.com>",
|
||||||
|
"contributors": [
|
||||||
|
"Alex McLean <alex@slab.org>"
|
||||||
|
],
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"@csound/browser": "6.18.5",
|
||||||
|
"@strudel.cycles/core": "workspace:*",
|
||||||
|
"@strudel.cycles/webaudio": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^3.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
; returns value of given key in given "string map"
|
||||||
|
; keymap("freq", "note/c3/freq/220/gain/0.5")
|
||||||
|
; yields "220"
|
||||||
|
opcode keymap, S, SS
|
||||||
|
Skey, Smap xin
|
||||||
|
idelimiter = strindex(Smap, strcat(Skey, "/"))
|
||||||
|
ifrom = idelimiter + strlen(Skey) + 1
|
||||||
|
Svalue = strsub(Smap, ifrom, strlen(Smap))
|
||||||
|
Svalue = strsub(Svalue, 0, strindex(Svalue, "/"))
|
||||||
|
xout Svalue
|
||||||
|
endop
|
||||||
|
|
||||||
|
; TODO add incredibly dope synths
|
||||||
|
instr organ
|
||||||
|
iduration = p3
|
||||||
|
ifreq = p4
|
||||||
|
igain = p5
|
||||||
|
ioct = octcps(ifreq)
|
||||||
|
|
||||||
|
asig = vco2(igain, ifreq, 12, .5) ; my edit
|
||||||
|
kpwm = oscili(.1, 5)
|
||||||
|
asig = vco2(igain, ifreq, 4, .5 + kpwm)
|
||||||
|
asig += vco2(igain/4, ifreq * 2)
|
||||||
|
|
||||||
|
; filter
|
||||||
|
; idepth = 2
|
||||||
|
; acut = transegr:a(0, .005, 0, idepth, .06, -4.2, 0.001, .01, -4.2, 0) ; filter envelope
|
||||||
|
; asig = zdf_2pole(asig, cpsoct(ioct + acut), 0.5)
|
||||||
|
|
||||||
|
; amp envelope
|
||||||
|
iattack = .001
|
||||||
|
irelease = .05
|
||||||
|
asig *= linsegr:a(0, iattack, 1, iduration, 1, irelease, 0)
|
||||||
|
|
||||||
|
out(asig, asig)
|
||||||
|
|
||||||
|
endin
|
||||||
|
|
||||||
|
instr triangle
|
||||||
|
iduration = p3
|
||||||
|
ifreq = p4
|
||||||
|
igain = p5
|
||||||
|
ioct = octcps(ifreq)
|
||||||
|
|
||||||
|
asig = vco2(igain, ifreq, 12, .5)
|
||||||
|
|
||||||
|
; amp envelope
|
||||||
|
iattack = .001
|
||||||
|
irelease = .05
|
||||||
|
asig *= linsegr:a(0, iattack, 1, iduration, 1, irelease, 0)
|
||||||
|
|
||||||
|
out(asig, asig)
|
||||||
|
endin
|
||||||
|
|
||||||
|
instr pad
|
||||||
|
iduration = p3
|
||||||
|
ifreq = p4
|
||||||
|
igain = p5
|
||||||
|
ioct = octcps(ifreq)
|
||||||
|
|
||||||
|
asig = vco2(igain, ifreq, 0)
|
||||||
|
|
||||||
|
; amp envelope
|
||||||
|
iattack = .5
|
||||||
|
irelease = .1
|
||||||
|
asig *= linsegr:a(0, iattack, 1, iduration, 1, irelease, 0)
|
||||||
|
|
||||||
|
idepth = 2
|
||||||
|
acut = transegr:a(0, .005, 0, idepth, .06, -4.2, 0.001, .01, -4.2, 0)
|
||||||
|
asig = zdf_2pole(asig, 1000, 2)
|
||||||
|
|
||||||
|
out(asig, asig)
|
||||||
|
endin
|
||||||
|
|
||||||
|
|
||||||
|
gisine ftgen 0, 0, 4096, 10, 1
|
||||||
|
|
||||||
|
instr bow
|
||||||
|
kpres = 2
|
||||||
|
krat = 0.16
|
||||||
|
kvibf = 6.12723
|
||||||
|
|
||||||
|
kvib linseg 0, 0.5, 0, 1, 1, p3-0.5, 1
|
||||||
|
kvamp = kvib * 0.01
|
||||||
|
asig wgbow .7, p4, kpres, krat, kvibf, kvamp, gisine
|
||||||
|
asig = asig*p5
|
||||||
|
outs asig, asig
|
||||||
|
endin
|
||||||
|
|
||||||
|
|
||||||
|
instr Meta
|
||||||
|
Smap = strget(p6)
|
||||||
|
Sinstrument = keymap("s", Smap)
|
||||||
|
schedule(Sinstrument, 0, p3, p4, p5)
|
||||||
|
; TODO find a way to pipe Sinstrument through effects
|
||||||
|
endin
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<CsoundSynthesizer>
|
||||||
|
<CsInstruments>
|
||||||
|
|
||||||
|
sr=48000
|
||||||
|
ksmps=64
|
||||||
|
nchnls=2
|
||||||
|
0dbfs=1
|
||||||
|
|
||||||
|
</CsInstruments>
|
||||||
|
</CsoundSynthesizer>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { dependencies } from './package.json';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'index.mjs'),
|
||||||
|
formats: ['es', 'cjs'],
|
||||||
|
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [...Object.keys(dependencies)],
|
||||||
|
},
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|
||||||
+18
-11
@@ -10,18 +10,25 @@ Either install with `npm i @strudel.cycles/embed` or just use a cdn to import th
|
|||||||
<script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
|
<script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
|
||||||
<strudel-repl>
|
<strudel-repl>
|
||||||
<!--
|
<!--
|
||||||
"a4 [a3 c3] a3 c3".color('#F9D649')
|
note(`[[e5 [b4 c5] d5 [c5 b4]]
|
||||||
.sub("<7 12 5 12>".slow(2))
|
[a4 [a4 c5] e5 [d5 c5]]
|
||||||
.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928"))
|
[b4 [~ c5] d5 e5]
|
||||||
.off(1/8,x=>x.add(12).color('#215CB6'))
|
[c5 a4 a4 ~]
|
||||||
.slow(2)
|
[[~ d5] [~ f5] a5 [g5 f5]]
|
||||||
.legato(sine.range(0.3, 2).slow(28))
|
[e5 [~ c5] e5 [d5 c5]]
|
||||||
.wave("sawtooth square".fast(2))
|
[b4 [b4 c5] d5 e5]
|
||||||
.filter('lowpass', cosine.range(500,4000).slow(16))
|
[c5 a4 a4 ~]],
|
||||||
.out()
|
[[e2 e3]*4]
|
||||||
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
|
[[a2 a3]*4]
|
||||||
|
[[g#2 g#3]*2 [e2 e3]*2]
|
||||||
|
[a2 a3 a2 a3 a2 a3 b1 c2]
|
||||||
|
[[d2 d3]*4]
|
||||||
|
[[c2 c3]*4]
|
||||||
|
[[b1 b2]*2 [e2 e3]*2]
|
||||||
|
[[a1 a2]*4]`).slow(16)
|
||||||
-->
|
-->
|
||||||
</strudel-repl>
|
</strudel-repl>
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that the Code is placed inside HTML comments to prevent the browser from treating it as HTML.
|
- Note that the Code is placed inside HTML comments to prevent the browser from treating it as HTML.
|
||||||
|
- [Play with this example on stackblitz](https://stackblitz.com/edit/js-75cvww?file=index.html)
|
||||||
|
|||||||
+16
-10
@@ -2,15 +2,21 @@
|
|||||||
<!-- <script src="./embed.js"></script> -->
|
<!-- <script src="./embed.js"></script> -->
|
||||||
<strudel-repl>
|
<strudel-repl>
|
||||||
<!--
|
<!--
|
||||||
"a4 [a3 c3] a3 c3".color('#F9D649')
|
note(`[[e5 [b4 c5] d5 [c5 b4]]
|
||||||
.sub("<7 12 5 12>".slow(2))
|
[a4 [a4 c5] e5 [d5 c5]]
|
||||||
.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928"))
|
[b4 [~ c5] d5 e5]
|
||||||
.off(1/8,x=>x.add(12).color('#215CB6'))
|
[c5 a4 a4 ~]
|
||||||
.slow(2)
|
[[~ d5] [~ f5] a5 [g5 f5]]
|
||||||
.legato(sine.range(0.3, 2).slow(28))
|
[e5 [~ c5] e5 [d5 c5]]
|
||||||
.wave("sawtooth square".fast(2))
|
[b4 [b4 c5] d5 e5]
|
||||||
.filter('lowpass', cosine.range(500,4000).slow(16))
|
[c5 a4 a4 ~]],
|
||||||
.out()
|
[[e2 e3]*4]
|
||||||
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
|
[[a2 a3]*4]
|
||||||
|
[[g#2 g#3]*2 [e2 e3]*2]
|
||||||
|
[a2 a3 a2 a3 a2 a3 b1 c2]
|
||||||
|
[[d2 d3]*4]
|
||||||
|
[[c2 c3]*4]
|
||||||
|
[[b1 b2]*2 [e2 e3]*2]
|
||||||
|
[[a1 a2]*4]`).slow(16)
|
||||||
-->
|
-->
|
||||||
</strudel-repl>
|
</strudel-repl>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/embed",
|
"name": "@strudel.cycles/embed",
|
||||||
"version": "0.1.1",
|
"version": "0.2.0",
|
||||||
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
||||||
"main": "embed.js",
|
"main": "embed.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
This package contains the strudel code transformer and evaluator.
|
This package contains the strudel code transformer and evaluator.
|
||||||
It allows creating strudel patterns from input code that is optimized for minimal keystrokes and human readability.
|
It allows creating strudel patterns from input code that is optimized for minimal keystrokes and human readability.
|
||||||
|
|
||||||
|
## Deprecation Note
|
||||||
|
|
||||||
|
This package will not be developed further. Consider using `@strudel.cycles/transpiler` as a replacement.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -11,10 +15,9 @@ npm i @strudel.cycles/eval --save
|
|||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
<!-- TODO: -extend +evalScope -->
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { evaluate, extend } from '@strudel.cycles/eval';
|
import { evalScope } from '@strudel.cycles/core';
|
||||||
|
import { evaluate } from '@strudel.cycles/eval';
|
||||||
|
|
||||||
evalScope(
|
evalScope(
|
||||||
import('@strudel.cycles/core'),
|
import('@strudel.cycles/core'),
|
||||||
|
|||||||
@@ -4,46 +4,9 @@ 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 { evaluate as _evaluate } from '@strudel.cycles/core';
|
||||||
import shapeshifter from './shapeshifter.mjs';
|
import shapeshifter from './shapeshifter.mjs';
|
||||||
import * as strudel from '@strudel.cycles/core';
|
|
||||||
|
|
||||||
const { isPattern, Pattern } = strudel;
|
|
||||||
|
|
||||||
export const extend = (...args) => {
|
|
||||||
console.warn('@strudel.cycles/eval extend is deprecated, please use evalScope instead');
|
|
||||||
Object.assign(globalThis, ...args);
|
|
||||||
};
|
|
||||||
|
|
||||||
let scoped = false;
|
|
||||||
export const evalScope = async (...args) => {
|
|
||||||
if (scoped) {
|
|
||||||
console.warn('@strudel.cycles/eval evalScope was called more than once.');
|
|
||||||
}
|
|
||||||
scoped = true;
|
|
||||||
const results = await Promise.allSettled(args);
|
|
||||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
|
||||||
results.forEach((result, i) => {
|
|
||||||
if (result.status === 'rejected') {
|
|
||||||
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap());
|
|
||||||
};
|
|
||||||
|
|
||||||
function safeEval(str) {
|
|
||||||
return Function('"use strict";return (' + str + ')')();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const evaluate = async (code) => {
|
export const evaluate = async (code) => {
|
||||||
if (!scoped) {
|
return _evaluate(code, shapeshifter);
|
||||||
await evalScope(); // at least scope Pattern.prototype.boostrap
|
|
||||||
}
|
|
||||||
const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code
|
|
||||||
let evaluated = await safeEval(shapeshifted);
|
|
||||||
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 };
|
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
-297
@@ -1,297 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@strudel.cycles/eval",
|
|
||||||
"version": "0.2.0",
|
|
||||||
"lockfileVersion": 2,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "@strudel.cycles/eval",
|
|
||||||
"version": "0.1.1",
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"dependencies": {
|
|
||||||
"estraverse": "^5.3.0",
|
|
||||||
"shift-ast": "^6.1.0",
|
|
||||||
"shift-codegen": "^7.0.3",
|
|
||||||
"shift-parser": "^7.0.3",
|
|
||||||
"shift-spec": "^2018.0.2",
|
|
||||||
"shift-traverser": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/estraverse": {
|
|
||||||
"version": "5.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
|
||||||
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/esutils": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
|
||||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/multimap": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/multimap/-/multimap-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw=="
|
|
||||||
},
|
|
||||||
"node_modules/object-assign": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
|
||||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shift-ast": {
|
|
||||||
"version": "6.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.1.0.tgz",
|
|
||||||
"integrity": "sha512-Vj4XUIJIFPIh6VcBGJ1hjH/kM88XGer94Pr7Rvxa+idEylDsrwtLw268HoxGo5xReL6T3DdRl/9/Pr1XihZ/8Q=="
|
|
||||||
},
|
|
||||||
"node_modules/shift-codegen": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-codegen/-/shift-codegen-7.0.3.tgz",
|
|
||||||
"integrity": "sha512-dfCVVdBF0qZ6pkajQ3bjxRdNEltyxEITVe7tBJkQt2eCI3znUkSxq0VSe/tTWq1LKHeAS4HuOiqYEuHMFkSq9w==",
|
|
||||||
"dependencies": {
|
|
||||||
"esutils": "^2.0.2",
|
|
||||||
"object-assign": "^4.1.0",
|
|
||||||
"shift-reducer": "6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shift-parser": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-parser/-/shift-parser-7.0.3.tgz",
|
|
||||||
"integrity": "sha512-uYX2ORyZfKZrUc4iKKkO9KOhzUSxCrSBk7QK6ZmShId+BOo1gh1IwecVy97ynyOTpmhPWUttjC8BzsnQl65Zew==",
|
|
||||||
"dependencies": {
|
|
||||||
"multimap": "^1.0.2",
|
|
||||||
"shift-ast": "6.0.0",
|
|
||||||
"shift-reducer": "6.0.0",
|
|
||||||
"shift-regexp-acceptor": "2.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shift-parser/node_modules/shift-ast": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw=="
|
|
||||||
},
|
|
||||||
"node_modules/shift-reducer": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-reducer/-/shift-reducer-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-2rJraRP8drIOjvaE/sALa+0tGJmMVUzlmS3wIJerJbaYuCjpFAiF0WjkTOFVtz1144Nm/ECmqeG+7yRhuMVsMg==",
|
|
||||||
"dependencies": {
|
|
||||||
"shift-ast": "6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shift-reducer/node_modules/shift-ast": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw=="
|
|
||||||
},
|
|
||||||
"node_modules/shift-regexp-acceptor": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-regexp-acceptor/-/shift-regexp-acceptor-2.0.3.tgz",
|
|
||||||
"integrity": "sha512-sxL7e5JNUFxm+gutFRXktX2D6KVgDAHNuDsk5XHB9Z+N5yXooZG6pdZ1GEbo3Jz6lF7ETYLBC4WAjIFm2RKTmA==",
|
|
||||||
"dependencies": {
|
|
||||||
"unicode-match-property-ecmascript": "1.0.4",
|
|
||||||
"unicode-match-property-value-ecmascript": "1.0.2",
|
|
||||||
"unicode-property-aliases-ecmascript": "1.0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shift-spec": {
|
|
||||||
"version": "2018.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-spec/-/shift-spec-2018.0.2.tgz",
|
|
||||||
"integrity": "sha512-5CP/cKDEim4rZ6ViCSipTLY2U7HJr8q/kpDuCBmebFqbx/0DeozWO+9ienHmYjgGLDfHrqj+LBAN67FRK2vE6w=="
|
|
||||||
},
|
|
||||||
"node_modules/shift-traverser": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-traverser/-/shift-traverser-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-DMY3512wJbdC+IC+nhLH3/Stgr2BbxbNcg7qyZ6+e5qNnNs8TBQJWdMsRgHlX1JXwF4C0ONKS8VUxsPT0Tf7aw==",
|
|
||||||
"dependencies": {
|
|
||||||
"estraverse": "4.2.0",
|
|
||||||
"shift-spec": "2018.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shift-traverser/node_modules/estraverse": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
|
|
||||||
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shift-traverser/node_modules/shift-spec": {
|
|
||||||
"version": "2018.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-spec/-/shift-spec-2018.0.0.tgz",
|
|
||||||
"integrity": "sha512-/aiPOkj7dbe+CV2VZhIMTHQToZmgniofpRG7Yr7x2/0sO6CSVC++py1Wzf+s+rWSTDHKcLvziVAxjRRV4i4EoQ=="
|
|
||||||
},
|
|
||||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unicode-match-property-ecmascript": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==",
|
|
||||||
"dependencies": {
|
|
||||||
"unicode-canonical-property-names-ecmascript": "^1.0.4",
|
|
||||||
"unicode-property-aliases-ecmascript": "^1.0.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unicode-match-property-value-ecmascript": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unicode-property-aliases-ecmascript": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"estraverse": {
|
|
||||||
"version": "5.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
|
||||||
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="
|
|
||||||
},
|
|
||||||
"esutils": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
|
||||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
|
|
||||||
},
|
|
||||||
"multimap": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/multimap/-/multimap-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw=="
|
|
||||||
},
|
|
||||||
"object-assign": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
|
||||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
|
||||||
},
|
|
||||||
"shift-ast": {
|
|
||||||
"version": "6.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.1.0.tgz",
|
|
||||||
"integrity": "sha512-Vj4XUIJIFPIh6VcBGJ1hjH/kM88XGer94Pr7Rvxa+idEylDsrwtLw268HoxGo5xReL6T3DdRl/9/Pr1XihZ/8Q=="
|
|
||||||
},
|
|
||||||
"shift-codegen": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-codegen/-/shift-codegen-7.0.3.tgz",
|
|
||||||
"integrity": "sha512-dfCVVdBF0qZ6pkajQ3bjxRdNEltyxEITVe7tBJkQt2eCI3znUkSxq0VSe/tTWq1LKHeAS4HuOiqYEuHMFkSq9w==",
|
|
||||||
"requires": {
|
|
||||||
"esutils": "^2.0.2",
|
|
||||||
"object-assign": "^4.1.0",
|
|
||||||
"shift-reducer": "6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"shift-parser": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-parser/-/shift-parser-7.0.3.tgz",
|
|
||||||
"integrity": "sha512-uYX2ORyZfKZrUc4iKKkO9KOhzUSxCrSBk7QK6ZmShId+BOo1gh1IwecVy97ynyOTpmhPWUttjC8BzsnQl65Zew==",
|
|
||||||
"requires": {
|
|
||||||
"multimap": "^1.0.2",
|
|
||||||
"shift-ast": "6.0.0",
|
|
||||||
"shift-reducer": "6.0.0",
|
|
||||||
"shift-regexp-acceptor": "2.0.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"shift-ast": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"shift-reducer": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-reducer/-/shift-reducer-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-2rJraRP8drIOjvaE/sALa+0tGJmMVUzlmS3wIJerJbaYuCjpFAiF0WjkTOFVtz1144Nm/ECmqeG+7yRhuMVsMg==",
|
|
||||||
"requires": {
|
|
||||||
"shift-ast": "6.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"shift-ast": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"shift-regexp-acceptor": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-regexp-acceptor/-/shift-regexp-acceptor-2.0.3.tgz",
|
|
||||||
"integrity": "sha512-sxL7e5JNUFxm+gutFRXktX2D6KVgDAHNuDsk5XHB9Z+N5yXooZG6pdZ1GEbo3Jz6lF7ETYLBC4WAjIFm2RKTmA==",
|
|
||||||
"requires": {
|
|
||||||
"unicode-match-property-ecmascript": "1.0.4",
|
|
||||||
"unicode-match-property-value-ecmascript": "1.0.2",
|
|
||||||
"unicode-property-aliases-ecmascript": "1.0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"shift-spec": {
|
|
||||||
"version": "2018.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-spec/-/shift-spec-2018.0.2.tgz",
|
|
||||||
"integrity": "sha512-5CP/cKDEim4rZ6ViCSipTLY2U7HJr8q/kpDuCBmebFqbx/0DeozWO+9ienHmYjgGLDfHrqj+LBAN67FRK2vE6w=="
|
|
||||||
},
|
|
||||||
"shift-traverser": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-traverser/-/shift-traverser-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-DMY3512wJbdC+IC+nhLH3/Stgr2BbxbNcg7qyZ6+e5qNnNs8TBQJWdMsRgHlX1JXwF4C0ONKS8VUxsPT0Tf7aw==",
|
|
||||||
"requires": {
|
|
||||||
"estraverse": "4.2.0",
|
|
||||||
"shift-spec": "2018.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"estraverse": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
|
|
||||||
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM="
|
|
||||||
},
|
|
||||||
"shift-spec": {
|
|
||||||
"version": "2018.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shift-spec/-/shift-spec-2018.0.0.tgz",
|
|
||||||
"integrity": "sha512-/aiPOkj7dbe+CV2VZhIMTHQToZmgniofpRG7Yr7x2/0sO6CSVC++py1Wzf+s+rWSTDHKcLvziVAxjRRV4i4EoQ=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"unicode-canonical-property-names-ecmascript": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ=="
|
|
||||||
},
|
|
||||||
"unicode-match-property-ecmascript": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==",
|
|
||||||
"requires": {
|
|
||||||
"unicode-canonical-property-names-ecmascript": "^1.0.4",
|
|
||||||
"unicode-property-aliases-ecmascript": "^1.0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"unicode-match-property-value-ecmascript": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ=="
|
|
||||||
},
|
|
||||||
"unicode-property-aliases-ecmascript": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/eval",
|
"name": "@strudel.cycles/eval",
|
||||||
"version": "0.2.0",
|
"version": "0.6.2",
|
||||||
"description": "Code evaluator for strudel",
|
"description": "Code evaluator for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"test": "vitest run",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"directories": {
|
"directories": {
|
||||||
"test": "test"
|
"test": "test"
|
||||||
},
|
},
|
||||||
"scripts": {
|
|
||||||
"test": "vitest run"
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
@@ -28,12 +34,17 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.2.0",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"estraverse": "^5.3.0",
|
"estraverse": "^5.3.0",
|
||||||
"shift-ast": "^6.1.0",
|
"shift-ast": "^7.0.0",
|
||||||
"shift-codegen": "^7.0.3",
|
"shift-codegen": "^8.1.0",
|
||||||
"shift-parser": "^7.0.3",
|
"shift-parser": "^8.0.0",
|
||||||
"shift-spec": "^2018.0.2",
|
"shift-spec": "^2019.0.0",
|
||||||
"shift-traverser": "^1.0.0"
|
"shift-traverser": "^1.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@strudel.cycles/mini": "workspace:*",
|
||||||
|
"vite": "^3.2.2",
|
||||||
|
"vitest": "^0.25.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
|
|||||||
const addLocations = true;
|
const addLocations = true;
|
||||||
export const addMiniLocations = true;
|
export const addMiniLocations = true;
|
||||||
export const minifyStrings = true;
|
export const minifyStrings = true;
|
||||||
export const wrappedAsync = true;
|
export const wrappedAsync = false; // this is now handled by core evaluate by default
|
||||||
|
export const shouldAddReturn = true;
|
||||||
|
|
||||||
export default (_code) => {
|
export default (_code) => {
|
||||||
const { code, addReturn } = wrapAsync(_code);
|
const { code, addReturn } = wrapAsync(_code);
|
||||||
@@ -125,7 +126,7 @@ export default (_code) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
// add return to last statement (because it's wrapped in an async function artificially)
|
// add return to last statement (because it's wrapped in an async function artificially)
|
||||||
if (wrappedAsync) {
|
if (shouldAddReturn) {
|
||||||
addReturn(shifted);
|
addReturn(shifted);
|
||||||
}
|
}
|
||||||
const generated = undisguiseImports(codegen(shifted));
|
const generated = undisguiseImports(codegen(shifted));
|
||||||
@@ -153,7 +154,7 @@ ${code}
|
|||||||
})()`;
|
})()`;
|
||||||
}
|
}
|
||||||
const addReturn = (ast) => {
|
const addReturn = (ast) => {
|
||||||
const body = ast.statements[0].expression.callee.body; // actual code ast inside async function body
|
const body = wrappedAsync ? ast.statements[0].expression.callee.body : ast;
|
||||||
body.statements = body.statements
|
body.statements = body.statements
|
||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
.concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]);
|
.concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]);
|
||||||
@@ -224,9 +225,8 @@ function isPatternArg(parents) {
|
|||||||
function hasModifierCall(parent) {
|
function hasModifierCall(parent) {
|
||||||
// TODO: modifiers are more than composables, for example every is not composable but should be seen as modifier..
|
// TODO: modifiers are more than composables, for example every is not composable but should be seen as modifier..
|
||||||
// need all prototypes of Pattern
|
// need all prototypes of Pattern
|
||||||
return (
|
return parent?.type === 'StaticMemberExpression';
|
||||||
parent?.type === 'StaticMemberExpression' && Object.keys(Pattern.prototype.composable).includes(parent.property)
|
// && Object.keys(Pattern.prototype.composable).includes(parent.property)
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const factories = Object.keys(Pattern.prototype.factories).concat(['mini']);
|
const factories = Object.keys(Pattern.prototype.factories).concat(['mini']);
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { expect, describe, it } from 'vitest';
|
import { expect, describe, it } from 'vitest';
|
||||||
|
|
||||||
import { evaluate, evalScope } from '../evaluate.mjs';
|
import { evaluate } from '../evaluate.mjs';
|
||||||
import { mini } from '@strudel.cycles/mini';
|
import { mini } from '@strudel.cycles/mini';
|
||||||
import * as strudel from '@strudel.cycles/core';
|
import * as strudel from '@strudel.cycles/core';
|
||||||
const { fastcat } = strudel;
|
const { fastcat, evalScope } = strudel;
|
||||||
|
|
||||||
describe('evaluate', async () => {
|
describe('evaluate', async () => {
|
||||||
await evalScope({ mini }, strudel);
|
await evalScope({ mini }, strudel);
|
||||||
const ev = async (code) => (await evaluate(code)).pattern._firstCycleValues;
|
const ev = async (code) => (await evaluate(code)).pattern.firstCycleValues;
|
||||||
it('Should evaluate strudel functions', async () => {
|
it('Should evaluate strudel functions', async () => {
|
||||||
expect(await ev('pure("c3")')).toEqual(['c3']);
|
expect(await ev('pure("c3")')).toEqual(['c3']);
|
||||||
expect(await ev('cat("c3")')).toEqual(['c3']);
|
expect(await ev('cat("c3")')).toEqual(['c3']);
|
||||||
|
|||||||
@@ -5,15 +5,21 @@ 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 shapeshifter from '../shapeshifter.mjs';
|
import shapeshifter, { wrappedAsync } from '../shapeshifter.mjs';
|
||||||
|
|
||||||
describe('shapeshifter', () => {
|
describe('shapeshifter', () => {
|
||||||
it('Should shift simple double quote string', () => {
|
it('Should shift simple double quote string', () => {
|
||||||
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
|
if (wrappedAsync) {
|
||||||
});
|
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
|
||||||
it('Should handle dynamic imports', () => {
|
} else {
|
||||||
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
|
expect(shapeshifter('"c3"')).toEqual('return mini("c3").withMiniLocation([1,0,0],[1,4,4])');
|
||||||
'(async()=>{const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])})()',
|
}
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
if (wrappedAsync) {
|
||||||
|
it('Should handle dynamic imports', () => {
|
||||||
|
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
|
||||||
|
'const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { dependencies } from './package.json';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'index.mjs'),
|
||||||
|
formats: ['es', 'cjs'],
|
||||||
|
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [...Object.keys(dependencies)],
|
||||||
|
},
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -7,7 +7,3 @@ This package adds midi functionality to strudel Patterns.
|
|||||||
```sh
|
```sh
|
||||||
npm i @strudel.cycles/midi --save
|
npm i @strudel.cycles/midi --save
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dev Notes
|
|
||||||
|
|
||||||
- is this package really necessary? currently, /tone also depends on webmidi through @tonejs/piano. Either move piano out of /tone or merge /midi into /tone...
|
|
||||||
|
|||||||
+84
-48
@@ -4,15 +4,23 @@ 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 { isNote } from 'tone';
|
import * as _WebMidi from 'webmidi';
|
||||||
import _WebMidi from 'webmidi';
|
import { Pattern, isPattern, isNote, getPlayableNoteValue, logger } from '@strudel.cycles/core';
|
||||||
import { Pattern, isPattern } from '@strudel.cycles/core';
|
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||||
import { Tone } from '@strudel.cycles/tone';
|
|
||||||
|
|
||||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
export const WebMidi = _WebMidi;
|
export const { WebMidi } = _WebMidi;
|
||||||
|
|
||||||
export function enableWebMidi() {
|
function supportsMidi() {
|
||||||
|
return typeof navigator.requestMIDIAccess === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enableWebMidi(options = {}) {
|
||||||
|
const { onReady, onConnected, onDisconnected } = options;
|
||||||
|
|
||||||
|
if (!supportsMidi()) {
|
||||||
|
throw new Error('Your Browser does not support WebMIDI.');
|
||||||
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (WebMidi.enabled) {
|
if (WebMidi.enabled) {
|
||||||
// if already enabled, just resolve WebMidi
|
// if already enabled, just resolve WebMidi
|
||||||
@@ -23,6 +31,14 @@ export function enableWebMidi() {
|
|||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
|
WebMidi.addListener('connected', (e) => {
|
||||||
|
onConnected?.(WebMidi);
|
||||||
|
});
|
||||||
|
// Reacting when a device becomes unavailable
|
||||||
|
WebMidi.addListener('disconnected', (e) => {
|
||||||
|
onDisconnected?.(WebMidi, e);
|
||||||
|
});
|
||||||
|
onReady?.(WebMidi);
|
||||||
resolve(WebMidi);
|
resolve(WebMidi);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -30,55 +46,75 @@ export function enableWebMidi() {
|
|||||||
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
|
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
|
||||||
const outputByName = (name) => WebMidi.getOutputByName(name);
|
const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||||
|
|
||||||
|
let midiReady;
|
||||||
|
|
||||||
|
// output?: string | number, outputs: typeof WebMidi.outputs
|
||||||
|
function getDevice(output, outputs) {
|
||||||
|
if (!outputs.length) {
|
||||||
|
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||||
|
}
|
||||||
|
if (typeof output === 'number') {
|
||||||
|
return outputs[output];
|
||||||
|
}
|
||||||
|
if (typeof output === 'string') {
|
||||||
|
return outputByName(output);
|
||||||
|
}
|
||||||
|
return outputs[0];
|
||||||
|
}
|
||||||
|
|
||||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||||
Pattern.prototype.midi = function (output, channel = 1) {
|
Pattern.prototype.midi = function (output, channel = 1) {
|
||||||
if (isPattern(output?.constructor?.name)) {
|
if (!supportsMidi()) {
|
||||||
|
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||||
|
}
|
||||||
|
/* await */ enableWebMidi({
|
||||||
|
onConnected: ({ outputs }) =>
|
||||||
|
logger(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
||||||
|
onDisconnected: ({ outputs }) =>
|
||||||
|
logger(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
||||||
|
onReady: ({ outputs }) => {
|
||||||
|
const device = getDevice(output, outputs);
|
||||||
|
const otherOutputs = outputs
|
||||||
|
.filter((o) => o.name !== device.name)
|
||||||
|
.map((o) => `'${o.name}'`)
|
||||||
|
.join(' | ');
|
||||||
|
midiReady = true;
|
||||||
|
logger(`Midi connected! Using "${device.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (isPattern(output)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
||||||
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
||||||
}')`,
|
}')`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this._withHap((hap) => {
|
return this.onTrigger((time, hap) => {
|
||||||
// const onTrigger = (time: number, hap: any) => {
|
let note = getPlayableNoteValue(hap);
|
||||||
const onTrigger = (time, hap) => {
|
const velocity = hap.context?.velocity ?? 0.9;
|
||||||
let note = hap.value;
|
if (!isNote(note)) {
|
||||||
const velocity = hap.context?.velocity ?? 0.9;
|
throw new Error('not a note: ' + note);
|
||||||
if (!isNote(note)) {
|
}
|
||||||
throw new Error('not a note: ' + note);
|
if (!midiReady) {
|
||||||
}
|
return;
|
||||||
if (!WebMidi.enabled) {
|
}
|
||||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
const device = getDevice(output, WebMidi.outputs);
|
||||||
}
|
if (!device) {
|
||||||
if (!WebMidi.outputs.length) {
|
throw new Error(
|
||||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||||
}
|
.map((o) => `'${o.name}'`)
|
||||||
let device;
|
.join(' | ')}`,
|
||||||
if (typeof output === 'number') {
|
);
|
||||||
device = WebMidi.outputs[output];
|
}
|
||||||
} else if (typeof output === 'string') {
|
// console.log('midi', value, output);
|
||||||
device = outputByName(output);
|
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
|
||||||
} else {
|
time = time * 1000 + timingOffset;
|
||||||
device = WebMidi.outputs[0];
|
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
||||||
}
|
// await enableWebMidi()
|
||||||
if (!device) {
|
device.playNote(note, channel, {
|
||||||
throw new Error(
|
time,
|
||||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
duration: hap.duration.valueOf() * 1000 - 5,
|
||||||
.map((o) => `'${o.name}'`)
|
attack: velocity,
|
||||||
.join(' | ')}`,
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
// console.log('midi', value, output);
|
|
||||||
const timingOffset = WebMidi.time - Tone.getContext().currentTime * 1000;
|
|
||||||
time = time * 1000 + timingOffset;
|
|
||||||
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
|
||||||
// await enableWebMidi()
|
|
||||||
device.playNote(note, channel, {
|
|
||||||
time,
|
|
||||||
duration: hap.duration.valueOf() * 1000 - 5,
|
|
||||||
velocity,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return hap.setContext({ ...hap.context, onTrigger });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
-130
@@ -1,130 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@strudel.cycles/midi",
|
|
||||||
"version": "0.2.0",
|
|
||||||
"lockfileVersion": 2,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "@strudel.cycles/midi",
|
|
||||||
"version": "0.1.1",
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"dependencies": {
|
|
||||||
"tone": "^14.7.77",
|
|
||||||
"webmidi": "^2.5.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/runtime": {
|
|
||||||
"version": "7.17.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz",
|
|
||||||
"integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==",
|
|
||||||
"dependencies": {
|
|
||||||
"regenerator-runtime": "^0.13.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/automation-events": {
|
|
||||||
"version": "4.0.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/automation-events/-/automation-events-4.0.14.tgz",
|
|
||||||
"integrity": "sha512-CB2Me0yW8sz7gSGwMiSfgfs1Oqlgs53k+eVESN6axvRyMAD3zlSp2nqndD2TQAtW3yOtSEJWNGsw0r48+f1wtw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.17.2",
|
|
||||||
"tslib": "^2.3.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.20.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/regenerator-runtime": {
|
|
||||||
"version": "0.13.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
|
||||||
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
|
|
||||||
},
|
|
||||||
"node_modules/standardized-audio-context": {
|
|
||||||
"version": "25.3.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.21.tgz",
|
|
||||||
"integrity": "sha512-CZEnayJJjNefeU+z1QGDhaid1LAAYxWYa2ipNk75ropwec9rq6fmclyhXb1wGtDsZ402irX3HLt1U/PwP9+1fA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.17.2",
|
|
||||||
"automation-events": "^4.0.14",
|
|
||||||
"tslib": "^2.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tone": {
|
|
||||||
"version": "14.7.77",
|
|
||||||
"resolved": "https://registry.npmjs.org/tone/-/tone-14.7.77.tgz",
|
|
||||||
"integrity": "sha512-tCfK73IkLHyzoKUvGq47gyDyxiKLFvKiVCOobynGgBB9Dl0NkxTM2p+eRJXyCYrjJwy9Y0XCMqD3uOYsYt2Fdg==",
|
|
||||||
"dependencies": {
|
|
||||||
"standardized-audio-context": "^25.1.8",
|
|
||||||
"tslib": "^2.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tslib": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
|
||||||
},
|
|
||||||
"node_modules/webmidi": {
|
|
||||||
"version": "2.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/webmidi/-/webmidi-2.5.3.tgz",
|
|
||||||
"integrity": "sha512-PyMGvKcDGpvbQUfnmBORQJciyG3VAZ4aHlGy1iRZ3uEs4kG4HCvI7KRthUpM1vuHDPL98lidRIUaoRomkJtWtg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">0.6.x"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": {
|
|
||||||
"version": "7.17.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz",
|
|
||||||
"integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==",
|
|
||||||
"requires": {
|
|
||||||
"regenerator-runtime": "^0.13.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"automation-events": {
|
|
||||||
"version": "4.0.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/automation-events/-/automation-events-4.0.14.tgz",
|
|
||||||
"integrity": "sha512-CB2Me0yW8sz7gSGwMiSfgfs1Oqlgs53k+eVESN6axvRyMAD3zlSp2nqndD2TQAtW3yOtSEJWNGsw0r48+f1wtw==",
|
|
||||||
"requires": {
|
|
||||||
"@babel/runtime": "^7.17.2",
|
|
||||||
"tslib": "^2.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"regenerator-runtime": {
|
|
||||||
"version": "0.13.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
|
||||||
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
|
|
||||||
},
|
|
||||||
"standardized-audio-context": {
|
|
||||||
"version": "25.3.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.21.tgz",
|
|
||||||
"integrity": "sha512-CZEnayJJjNefeU+z1QGDhaid1LAAYxWYa2ipNk75ropwec9rq6fmclyhXb1wGtDsZ402irX3HLt1U/PwP9+1fA==",
|
|
||||||
"requires": {
|
|
||||||
"@babel/runtime": "^7.17.2",
|
|
||||||
"automation-events": "^4.0.14",
|
|
||||||
"tslib": "^2.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tone": {
|
|
||||||
"version": "14.7.77",
|
|
||||||
"resolved": "https://registry.npmjs.org/tone/-/tone-14.7.77.tgz",
|
|
||||||
"integrity": "sha512-tCfK73IkLHyzoKUvGq47gyDyxiKLFvKiVCOobynGgBB9Dl0NkxTM2p+eRJXyCYrjJwy9Y0XCMqD3uOYsYt2Fdg==",
|
|
||||||
"requires": {
|
|
||||||
"standardized-audio-context": "^25.1.8",
|
|
||||||
"tslib": "^2.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tslib": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
|
||||||
},
|
|
||||||
"webmidi": {
|
|
||||||
"version": "2.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/webmidi/-/webmidi-2.5.3.tgz",
|
|
||||||
"integrity": "sha512-PyMGvKcDGpvbQUfnmBORQJciyG3VAZ4aHlGy1iRZ3uEs4kG4HCvI7KRthUpM1vuHDPL98lidRIUaoRomkJtWtg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/midi",
|
"name": "@strudel.cycles/midi",
|
||||||
"version": "0.2.0",
|
"version": "0.6.0",
|
||||||
"description": "Midi API for strudel",
|
"description": "Midi API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
@@ -21,8 +29,11 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/tone": "^0.2.0",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"tone": "^14.7.77",
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
"webmidi": "^2.5.2"
|
"webmidi": "^3.0.21"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { dependencies } from './package.json';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'index.mjs'),
|
||||||
|
formats: ['es', 'cjs'],
|
||||||
|
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [...Object.keys(dependencies)],
|
||||||
|
},
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|
||||||
+10
-1
@@ -32,4 +32,13 @@ yields:
|
|||||||
|
|
||||||
## Mini Notation API
|
## Mini Notation API
|
||||||
|
|
||||||
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
|
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/learn/mini-notation)
|
||||||
|
|
||||||
|
## Building the Parser
|
||||||
|
|
||||||
|
The parser [krill-parser.js] is generated from [krill.pegjs](./krill.pegjs) using [peggy](https://peggyjs.org/).
|
||||||
|
To generate the parser, run
|
||||||
|
|
||||||
|
```js
|
||||||
|
npm build:parser
|
||||||
|
```
|
||||||
|
|||||||
+449
-361
File diff suppressed because it is too large
Load Diff
+80
-56
@@ -5,13 +5,20 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Some terminology:
|
// Some terminology:
|
||||||
// a sequence = a serie of elements placed between quotes
|
// mini(notation) = a series of elements placed between quotes
|
||||||
// a stack = a serie of vertically aligned slices sharing the same overall length
|
// a stack = a series of vertically aligned slices sharing the same overall length
|
||||||
// a slice = a serie of horizontally aligned elements
|
// a sequence = a series of horizontally aligned elements
|
||||||
// a choose = a serie of elements, one of which is chosen at random
|
// a choose = a series of elements, one of which is chosen at random
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
|
var AtomStub = function(source)
|
||||||
|
{
|
||||||
|
this.type_ = "atom";
|
||||||
|
this.source_ = source;
|
||||||
|
this.location_ = location();
|
||||||
|
}
|
||||||
|
|
||||||
var PatternStub = function(source, alignment)
|
var PatternStub = function(source, alignment)
|
||||||
{
|
{
|
||||||
this.type_ = "pattern";
|
this.type_ = "pattern";
|
||||||
@@ -90,82 +97,99 @@ quote = '"' / "'"
|
|||||||
|
|
||||||
// single step definition (e.g bd)
|
// single step definition (e.g bd)
|
||||||
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
|
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
|
||||||
step = ws chars:step_char+ ws { return chars.join("") }
|
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
|
||||||
|
|
||||||
// define a sub cycle e.g. [1 2, 3 [4]]
|
// define a sub cycle e.g. [1 2, 3 [4]]
|
||||||
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s}
|
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s }
|
||||||
|
|
||||||
// define a timeline e.g <1 3 [3 5]>. We simply defer to a stack and change the alignement
|
// define a polymeter e.g. {1 2, 3 4 5}
|
||||||
timeline = ws "<" ws sc:single_cycle ws ">" ws
|
polymeter = ws "{" ws s:polymeter_stack ws "}" stepsPerCycle:polymeter_steps? ws
|
||||||
{ sc.arguments_.alignment = "t"; return sc;}
|
{ s.arguments_.stepsPerCycle = stepsPerCycle ; return s; }
|
||||||
|
|
||||||
|
polymeter_steps = "%"a:slice
|
||||||
|
{ return a }
|
||||||
|
|
||||||
|
// define a step-per-cycle timeline e.g <1 3 [3 5]>. We simply defer to a sequence and
|
||||||
|
// change the alignment to slowcat
|
||||||
|
slow_sequence = ws "<" ws s:sequence ws ">" ws
|
||||||
|
{ s.arguments_.alignment = 'slowcat'; return s; }
|
||||||
|
|
||||||
// a slice is either a single step or a sub cycle
|
// a slice is either a single step or a sub cycle
|
||||||
slice = step / sub_cycle / timeline
|
slice = step / sub_cycle / polymeter / slow_sequence
|
||||||
|
|
||||||
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
||||||
// at this point, we assume we can represent them as regular sequence operators
|
// at this point, we assume we can represent them as regular sequence operators
|
||||||
slice_modifier = slice_weight / slice_bjorklund / slice_slow / slice_fast / slice_fixed_step / slice_replicate / slice_degrade
|
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade
|
||||||
|
|
||||||
slice_weight = "@" a:number
|
op_weight = "@" a:number
|
||||||
{ return { weight: a} }
|
{ return x => x.options_['weight'] = a }
|
||||||
|
|
||||||
slice_replicate = "!"a:number
|
op_replicate = "!"a:number
|
||||||
{ return { replicate: a } }
|
{ return x => x.options_['reps'] = a }
|
||||||
|
|
||||||
slice_bjorklund = "(" ws p:number ws comma ws s:number ws comma? ws r:number? ws ")"
|
op_bjorklund = "(" ws p:slice_with_ops ws comma ws s:slice_with_ops ws comma? ws r:slice_with_ops? ws ")"
|
||||||
{ return { operator : { type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r || 0 } } } }
|
{ return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }
|
||||||
|
|
||||||
slice_slow = "/"a:number
|
op_slow = "/"a:slice
|
||||||
{ return { operator : { type_: "stretch", arguments_ :{ amount:a } } } }
|
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) }
|
||||||
|
|
||||||
slice_fast = "*"a:number
|
op_fast = "*"a:slice
|
||||||
{ return { operator : { type_: "stretch", arguments_ :{ amount:"1/"+a } } } }
|
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
|
||||||
|
|
||||||
slice_fixed_step = "%"a:number
|
op_degrade = "?"a:number?
|
||||||
{ return { operator : { type_: "fixed-step", arguments_ :{ amount:a } } } }
|
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
|
||||||
|
|
||||||
slice_degrade = "?"a:number?
|
|
||||||
{ return { operator : { type_: "degradeBy", arguments_ :{ amount:(a? a : 0.5) } } } }
|
|
||||||
|
|
||||||
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
||||||
slice_with_modifier = s:slice o:slice_modifier?
|
slice_with_ops = s:slice ops:slice_op*
|
||||||
{ return new ElementStub(s, o);}
|
{ const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||||
|
for (const op of ops) {
|
||||||
|
op(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// a single cycle is a combination of one or more successive slices (as an array). If we
|
// a sequence is a combination of one or more successive slices (as an array)
|
||||||
// have only one element, we skip the array and return the element itself
|
sequence = s:(slice_with_ops)+
|
||||||
single_cycle = s:(slice_with_modifier)+
|
{ return new PatternStub(s, 'fastcat'); }
|
||||||
{ return new PatternStub(s,"h"); }
|
|
||||||
|
|
||||||
// a stack is a serie of vertically aligned single cycles, separated by a comma
|
// a stack is a series of vertically aligned sequence, separated by a comma
|
||||||
stack_tail = tail:(comma @single_cycle)+
|
stack_tail = tail:(comma @sequence)+
|
||||||
{ return { alignment: 'v', list: tail }; }
|
{ return { alignment: 'stack', list: tail }; }
|
||||||
|
|
||||||
// a choose is a serie of pipe-separated single cycles, one of which is chosen
|
// a choose is a series of pipe-separated sequence, one of which is
|
||||||
// at random each time through the pattern
|
// chosen at random, each cycle
|
||||||
choose_tail = tail:(pipe @single_cycle)+
|
choose_tail = tail:(pipe @sequence)+
|
||||||
{ return { alignment: 'r', list: tail }; }
|
{ return { alignment: 'rand', list: tail }; }
|
||||||
|
|
||||||
// if the stack contains only one element, we don't create a stack but return the
|
// if the stack contains only one element, we don't create a stack but return the
|
||||||
// underlying element
|
// underlying element
|
||||||
stack_or_choose = head:single_cycle tail:(stack_tail / choose_tail)?
|
stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
|
||||||
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
|
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
|
||||||
|
|
||||||
// a sequence is a quoted stack
|
polymeter_stack = head:sequence tail:stack_tail?
|
||||||
sequence = ws quote sc:stack_or_choose quote
|
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
|
||||||
|
|
||||||
|
|
||||||
|
// Mini-notation innards ends
|
||||||
|
// ---------->8---------->8---------->8---------->8---------->8----------
|
||||||
|
// Experimental haskellish parser begins
|
||||||
|
|
||||||
|
// mini-notation = a quoted stack
|
||||||
|
mini = ws quote sc:stack_or_choose quote
|
||||||
{ return sc; }
|
{ return sc; }
|
||||||
|
|
||||||
// ------------------ operators ---------------------------
|
// ------------------ operators ---------------------------
|
||||||
|
|
||||||
operator = scale / slow / fast / target / bjorklund / struct / rotR / rotL
|
operator = scale / slow / fast / target / bjorklund / struct / rotR / rotL
|
||||||
|
|
||||||
struct = "struct" ws s:sequence_or_operator
|
struct = "struct" ws s:mini_or_operator
|
||||||
{ return { name: "struct", args: { sequence:s }}}
|
{ return { name: "struct", args: { mini:s }}}
|
||||||
|
|
||||||
target = "target" ws quote s:step quote
|
target = "target" ws quote s:step quote
|
||||||
{ return { name: "target", args : { name:s}}}
|
{ return { name: "target", args : { name:s}}}
|
||||||
|
|
||||||
bjorklund = "euclid" ws p:int ws s:int ws r:int?
|
bjorklund = "euclid" ws p:int ws s:int ws r:int?
|
||||||
{ return { name: "bjorklund", args :{ pulse: parseInt(p), step:parseInt(s) }}}
|
{ return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}}
|
||||||
|
|
||||||
slow = "slow" ws a:number
|
slow = "slow" ws a:number
|
||||||
{ return { name: "stretch", args :{ amount: a}}}
|
{ return { name: "stretch", args :{ amount: a}}}
|
||||||
@@ -189,27 +213,27 @@ comment = '//' p:([^\n]*)
|
|||||||
group_operator = cat
|
group_operator = cat
|
||||||
|
|
||||||
// cat is another form of timeline
|
// cat is another form of timeline
|
||||||
cat = "cat" ws "[" ws s:sequence_or_operator ss:(comma v:sequence_or_operator { return v})* ws "]"
|
cat = "cat" ws "[" ws s:mini_or_operator ss:(comma v:mini_or_operator { return v})* ws "]"
|
||||||
{ ss.unshift(s); return new PatternStub(ss,"t"); }
|
{ ss.unshift(s); return new PatternStub(ss, 'slowcat'); }
|
||||||
|
|
||||||
// ------------------ high level sequence ---------------------------
|
// ------------------ high level mini ---------------------------
|
||||||
|
|
||||||
sequence_or_group =
|
mini_or_group =
|
||||||
group_operator /
|
group_operator /
|
||||||
sequence
|
mini
|
||||||
|
|
||||||
sequence_or_operator =
|
mini_or_operator =
|
||||||
sg:sequence_or_group ws (comment)*
|
sg:mini_or_group ws (comment)*
|
||||||
{return sg}
|
{return sg}
|
||||||
/ o:operator ws "$" ws soc:sequence_or_operator
|
/ o:operator ws "$" ws soc:mini_or_operator
|
||||||
{ return new OperatorStub(o.name,o.args,soc)}
|
{ return new OperatorStub(o.name,o.args,soc)}
|
||||||
|
|
||||||
sequ_or_operator_or_comment =
|
sequ_or_operator_or_comment =
|
||||||
sc: sequence_or_operator
|
sc: mini_or_operator
|
||||||
{ return sc }
|
{ return sc }
|
||||||
/ comment
|
/ comment
|
||||||
|
|
||||||
sequence_definition = s:sequ_or_operator_or_comment
|
mini_definition = s:sequ_or_operator_or_comment
|
||||||
|
|
||||||
// ---------------------- statements ----------------------------
|
// ---------------------- statements ----------------------------
|
||||||
|
|
||||||
@@ -227,4 +251,4 @@ hush = "hush"
|
|||||||
|
|
||||||
// ---------------------- statements ----------------------------
|
// ---------------------- statements ----------------------------
|
||||||
|
|
||||||
statement = sequence_definition / command
|
statement = mini_definition / command
|
||||||
|
|||||||
+128
-102
@@ -6,128 +6,158 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import * as krill from './krill-parser.js';
|
import * as krill from './krill-parser.js';
|
||||||
import * as strudel from '@strudel.cycles/core';
|
import * as strudel from '@strudel.cycles/core';
|
||||||
import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs';
|
|
||||||
|
|
||||||
const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel;
|
/* var _seedState = 0;
|
||||||
|
|
||||||
var _seedState = 0;
|
|
||||||
const randOffset = 0.0002;
|
const randOffset = 0.0002;
|
||||||
|
|
||||||
function _nextSeed() {
|
function _nextSeed() {
|
||||||
return _seedState++;
|
return _seedState++;
|
||||||
}
|
} */
|
||||||
|
|
||||||
const applyOptions = (parent) => (pat, i) => {
|
const applyOptions = (parent, code) => (pat, i) => {
|
||||||
const ast = parent.source_[i];
|
const ast = parent.source_[i];
|
||||||
const options = ast.options_;
|
const options = ast.options_;
|
||||||
const operator = options?.operator;
|
const ops = options?.ops;
|
||||||
if (operator) {
|
if (ops) {
|
||||||
switch (operator.type_) {
|
for (const op of ops) {
|
||||||
case 'stretch':
|
switch (op.type_) {
|
||||||
const speed = Fraction(operator.arguments_.amount).inverse();
|
case 'stretch': {
|
||||||
return reify(pat).fast(speed);
|
const legalTypes = ['fast', 'slow'];
|
||||||
case 'bjorklund':
|
const { type, amount } = op.arguments_;
|
||||||
return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation);
|
if (!legalTypes.includes(type)) {
|
||||||
case 'degradeBy':
|
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
|
||||||
return reify(pat)._degradeByWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), operator.arguments_.amount);
|
}
|
||||||
// TODO: case 'fixed-step': "%"
|
pat = strudel.reify(pat)[type](patternifyAST(amount, code));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'bjorklund': {
|
||||||
|
if (op.arguments_.rotation) {
|
||||||
|
pat = pat.euclidRot(
|
||||||
|
patternifyAST(op.arguments_.pulse, code),
|
||||||
|
patternifyAST(op.arguments_.step, code),
|
||||||
|
patternifyAST(op.arguments_.rotation, code),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
pat = pat.euclid(patternifyAST(op.arguments_.pulse, code), patternifyAST(op.arguments_.step, code));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'degradeBy': {
|
||||||
|
// TODO: find out what is right here
|
||||||
|
// example:
|
||||||
|
/*
|
||||||
|
stack(
|
||||||
|
s("hh*8").degrade(),
|
||||||
|
s("[ht*8]?")
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
// above example will only be in sync when _degradeBy is used...
|
||||||
|
// it also seems that the nextSeed will create undeterministic behaviour
|
||||||
|
// as it uses a global _seedState. This is probably the reason for
|
||||||
|
// https://github.com/tidalcycles/strudel/issues/245
|
||||||
|
|
||||||
|
// this is how it was:
|
||||||
|
/*
|
||||||
|
return strudel.reify(pat)._degradeByWith(
|
||||||
|
strudel.rand.early(randOffset * _nextSeed()).segment(1),
|
||||||
|
op.arguments_.amount ?? 0.5,
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
console.warn(`operator "${op.type_}" not implemented`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.warn(`operator "${operator.type_}" not implemented`);
|
|
||||||
}
|
|
||||||
if (options?.weight) {
|
|
||||||
// weight is handled by parent
|
|
||||||
return pat;
|
|
||||||
}
|
|
||||||
// TODO: bjorklund e.g. "c3(5,8)"
|
|
||||||
const unimplemented = Object.keys(options || {}).filter((key) => key !== 'operator');
|
|
||||||
if (unimplemented.length) {
|
|
||||||
console.warn(
|
|
||||||
`option${unimplemented.length > 1 ? 's' : ''} ${unimplemented.map((o) => `"${o}"`).join(', ')} not implemented`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return pat;
|
return pat;
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveReplications(ast) {
|
function resolveReplications(ast) {
|
||||||
// the general idea here: x!3 = [x*3]@3
|
ast.source_ = strudel.flatten(
|
||||||
// could this be made easier?!
|
ast.source_.map((child) => {
|
||||||
ast.source_ = ast.source_.map((child) => {
|
const { reps } = child.options_ || {};
|
||||||
const { replicate, ...options } = child.options_ || {};
|
if (!reps) {
|
||||||
if (replicate) {
|
return [child];
|
||||||
return {
|
}
|
||||||
...child,
|
delete child.options_.reps;
|
||||||
options_: { ...options, weight: replicate },
|
return Array(reps).fill(child);
|
||||||
source_: {
|
}),
|
||||||
type_: 'pattern',
|
);
|
||||||
arguments_: {
|
|
||||||
alignment: 'h',
|
|
||||||
},
|
|
||||||
source_: [
|
|
||||||
{
|
|
||||||
type_: 'element',
|
|
||||||
source_: child.source_,
|
|
||||||
location_: child.location_,
|
|
||||||
options_: {
|
|
||||||
operator: {
|
|
||||||
type_: 'stretch',
|
|
||||||
arguments_: { amount: Fraction(replicate).inverse().toString() },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function patternifyAST(ast) {
|
export function patternifyAST(ast, code) {
|
||||||
switch (ast.type_) {
|
switch (ast.type_) {
|
||||||
case 'pattern':
|
case 'pattern': {
|
||||||
resolveReplications(ast);
|
resolveReplications(ast);
|
||||||
const children = ast.source_.map(patternifyAST).map(applyOptions(ast));
|
const children = ast.source_.map((child) => patternifyAST(child, code)).map(applyOptions(ast, code));
|
||||||
const alignment = ast.arguments_.alignment;
|
const alignment = ast.arguments_.alignment;
|
||||||
if (alignment === 'v') {
|
if (alignment === 'stack') {
|
||||||
return stack(...children);
|
return strudel.stack(...children);
|
||||||
}
|
}
|
||||||
if (alignment === 'r') {
|
if (alignment === 'polymeter') {
|
||||||
return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
|
// polymeter
|
||||||
|
const stepsPerCycle = ast.arguments_.stepsPerCycle
|
||||||
|
? patternifyAST(ast.arguments_.stepsPerCycle, code).fmap((x) => strudel.Fraction(x))
|
||||||
|
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
|
||||||
|
|
||||||
|
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
|
||||||
|
return strudel.stack(...aligned);
|
||||||
|
}
|
||||||
|
if (alignment === 'rand') {
|
||||||
|
// https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422
|
||||||
|
// return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
|
||||||
|
return strudel.chooseCycles(...children);
|
||||||
}
|
}
|
||||||
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
||||||
if (!weightedChildren && alignment === 't') {
|
if (!weightedChildren && alignment === 'slowcat') {
|
||||||
return slowcat(...children);
|
return strudel.slowcat(...children);
|
||||||
}
|
}
|
||||||
if (weightedChildren) {
|
if (weightedChildren) {
|
||||||
const pat = timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
|
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
|
||||||
if (alignment === 't') {
|
const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
|
||||||
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
|
if (alignment === 'slowcat') {
|
||||||
return pat._slow(weightSum); // timecat + slow
|
return pat._slow(weightSum); // timecat + slow
|
||||||
}
|
}
|
||||||
|
pat.__weight = weightSum;
|
||||||
return pat;
|
return pat;
|
||||||
}
|
}
|
||||||
return sequence(...children);
|
const pat = strudel.sequence(...children);
|
||||||
case 'element':
|
pat.__weight = children.length;
|
||||||
|
return pat;
|
||||||
|
}
|
||||||
|
case 'element': {
|
||||||
|
return patternifyAST(ast.source_, code);
|
||||||
|
}
|
||||||
|
case 'atom': {
|
||||||
if (ast.source_ === '~') {
|
if (ast.source_ === '~') {
|
||||||
return silence;
|
return strudel.silence;
|
||||||
}
|
}
|
||||||
if (typeof ast.source_ !== 'object') {
|
if (!ast.location_) {
|
||||||
if (!addMiniLocations) {
|
console.warn('no location for', ast);
|
||||||
return ast.source_;
|
return ast.source_;
|
||||||
}
|
|
||||||
if (!ast.location_) {
|
|
||||||
console.warn('no location for', ast);
|
|
||||||
return ast.source_;
|
|
||||||
}
|
|
||||||
const { start, end } = ast.location_;
|
|
||||||
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
|
|
||||||
// the following line expects the shapeshifter append .withMiniLocation
|
|
||||||
// because location_ is only relative to the mini string, but we need it relative to whole code
|
|
||||||
return pure(value).withLocation([start.line, start.column, start.offset], [end.line, end.column, end.offset]);
|
|
||||||
}
|
}
|
||||||
return patternifyAST(ast.source_);
|
const { start, end } = ast.location_;
|
||||||
|
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
|
||||||
|
// the following line expects the shapeshifter append .withMiniLocation
|
||||||
|
// because location_ is only relative to the mini string, but we need it relative to whole code
|
||||||
|
// make sure whitespaces are not part of the highlight:
|
||||||
|
const actual = code?.split('').slice(start.offset, end.offset).join('');
|
||||||
|
const [offsetStart = 0, offsetEnd = 0] = actual
|
||||||
|
? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length)
|
||||||
|
: [];
|
||||||
|
return strudel
|
||||||
|
.pure(value)
|
||||||
|
.withLocation(
|
||||||
|
[start.line, start.column + offsetStart, start.offset + offsetStart],
|
||||||
|
[start.line, end.column - offsetEnd, end.offset - offsetEnd],
|
||||||
|
);
|
||||||
|
}
|
||||||
case 'stretch':
|
case 'stretch':
|
||||||
return patternifyAST(ast.source_).slow(ast.arguments_.amount);
|
return patternifyAST(ast.source_, code).slow(patternifyAST(ast.arguments_.amount, code));
|
||||||
/* case 'scale':
|
/* case 'scale':
|
||||||
let [tonic, scale] = Scale.tokenize(ast.arguments_.scale);
|
let [tonic, scale] = Scale.tokenize(ast.arguments_.scale);
|
||||||
const intervals = Scale.get(scale).intervals;
|
const intervals = Scale.get(scale).intervals;
|
||||||
@@ -149,37 +179,33 @@ export function patternifyAST(ast) {
|
|||||||
}); */
|
}); */
|
||||||
/* case 'struct':
|
/* case 'struct':
|
||||||
// TODO:
|
// TODO:
|
||||||
return silence; */
|
return strudel.silence; */
|
||||||
default:
|
default:
|
||||||
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
|
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
|
||||||
return silence;
|
return strudel.silence;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mini notation only (wraps in "")
|
// mini notation only (wraps in "")
|
||||||
export const mini = (...strings) => {
|
export const mini = (...strings) => {
|
||||||
const pats = strings.map((str) => {
|
const pats = strings.map((str) => {
|
||||||
const ast = krill.parse(`"${str}"`);
|
const code = `"${str}"`;
|
||||||
return patternifyAST(ast);
|
const ast = krill.parse(code);
|
||||||
|
return patternifyAST(ast, code);
|
||||||
});
|
});
|
||||||
return sequence(...pats);
|
return strudel.sequence(...pats);
|
||||||
};
|
};
|
||||||
|
|
||||||
// includes haskell style (raw krill parsing)
|
// includes haskell style (raw krill parsing)
|
||||||
export const h = (string) => {
|
export const h = (string) => {
|
||||||
const ast = krill.parse(string);
|
const ast = krill.parse(string);
|
||||||
// console.log('ast', ast);
|
// console.log('ast', ast);
|
||||||
return patternifyAST(ast);
|
return patternifyAST(ast, string);
|
||||||
};
|
};
|
||||||
|
|
||||||
// shorthand for mini
|
|
||||||
Pattern.prototype.define('mini', mini, { composable: true });
|
|
||||||
Pattern.prototype.define('m', mini, { composable: true });
|
|
||||||
Pattern.prototype.define('h', h, { composable: true });
|
|
||||||
|
|
||||||
export function minify(thing) {
|
export function minify(thing) {
|
||||||
if (typeof thing === 'string') {
|
if (typeof thing === 'string') {
|
||||||
return mini(thing);
|
return mini(thing);
|
||||||
}
|
}
|
||||||
return reify(thing);
|
return strudel.reify(thing);
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
-1031
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/mini",
|
"name": "@strudel.cycles/mini",
|
||||||
"version": "0.2.0",
|
"version": "0.6.0",
|
||||||
"description": "Mini notation for strudel",
|
"description": "Mini notation for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs"
|
"build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs",
|
||||||
|
"build": "vite build",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -26,11 +32,11 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.2.0",
|
"@strudel.cycles/core": "workspace:*"
|
||||||
"@strudel.cycles/eval": "^0.2.0",
|
|
||||||
"@strudel.cycles/tone": "^0.2.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"peggy": "^2.0.1"
|
"peggy": "^2.0.1",
|
||||||
|
"vite": "^3.2.2",
|
||||||
|
"vitest": "^0.25.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import '@strudel.cycles/core/euclid.mjs';
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('mini', () => {
|
describe('mini', () => {
|
||||||
const minV = (v) => mini(v)._firstCycleValues;
|
const minV = (v) => mini(v).sortHapsByPart().firstCycleValues;
|
||||||
const minS = (v) => mini(v)._showFirstCycle;
|
const minS = (v) => mini(v).sortHapsByPart().showFirstCycle;
|
||||||
it('supports single elements', () => {
|
it('supports single elements', () => {
|
||||||
expect(minV('a')).toEqual(['a']);
|
expect(minV('a')).toEqual(['a']);
|
||||||
});
|
});
|
||||||
@@ -21,6 +21,21 @@ describe('mini', () => {
|
|||||||
expect(minS('a b')).toEqual(['a: 0 - 1/2', 'b: 1/2 - 1']);
|
expect(minS('a b')).toEqual(['a: 0 - 1/2', 'b: 1/2 - 1']);
|
||||||
expect(minS('a b c')).toEqual(['a: 0 - 1/3', 'b: 1/3 - 2/3', 'c: 2/3 - 1']);
|
expect(minS('a b c')).toEqual(['a: 0 - 1/3', 'b: 1/3 - 2/3', 'c: 2/3 - 1']);
|
||||||
});
|
});
|
||||||
|
it('supports fast', () => {
|
||||||
|
expect(minS('a*3 b')).toEqual(minS('[a a a] b'));
|
||||||
|
});
|
||||||
|
it('supports patterned fast', () => {
|
||||||
|
expect(minS('[a*<3 5>]*2')).toEqual(minS('[a a a] [a a a a a]'));
|
||||||
|
});
|
||||||
|
it('supports slow', () => {
|
||||||
|
expect(minS('[a a a]/3 b')).toEqual(minS('a b'));
|
||||||
|
});
|
||||||
|
it('supports patterned slow', () => {
|
||||||
|
expect(minS('[a a a a a a a a]/[2 4]')).toEqual(minS('[a a] a'));
|
||||||
|
});
|
||||||
|
it('supports patterned fast', () => {
|
||||||
|
expect(minS('[a*<3 5>]*2')).toEqual(minS('[a a a] [a a a a a]'));
|
||||||
|
});
|
||||||
it('supports slowcat', () => {
|
it('supports slowcat', () => {
|
||||||
expect(minV('<a b>')).toEqual(['a']);
|
expect(minV('<a b>')).toEqual(['a']);
|
||||||
});
|
});
|
||||||
@@ -36,6 +51,16 @@ describe('mini', () => {
|
|||||||
expect(minS('c3 [d3 e3]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 1']);
|
expect(minS('c3 [d3 e3]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 1']);
|
||||||
expect(minS('c3 [d3 [e3 f3]]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 7/8', 'f3: 7/8 - 1']);
|
expect(minS('c3 [d3 [e3 f3]]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 7/8', 'f3: 7/8 - 1']);
|
||||||
});
|
});
|
||||||
|
it('supports curly brackets', () => {
|
||||||
|
expect(minS('{a b, c d e}*3')).toEqual(minS('[a b a b a b, c d e c d e]'));
|
||||||
|
expect(minS('{a b, c [d e] f}*3')).toEqual(minS('[a b a b a b, c [d e] f c [d e] f]'));
|
||||||
|
expect(minS('{a b c, d e}*2')).toEqual(minS('[a b c a b c, d e d e d e]'));
|
||||||
|
});
|
||||||
|
it('supports curly brackets with explicit step-per-cycle', () => {
|
||||||
|
expect(minS('{a b, c d e}%3')).toEqual(minS('[a b a, c d e]'));
|
||||||
|
expect(minS('{a b, c d e}%5')).toEqual(minS('[a b a b a, c d e c d]'));
|
||||||
|
expect(minS('{a b, c d e}%6')).toEqual(minS('[a b a b a b, c d e c d e]'));
|
||||||
|
});
|
||||||
it('supports commas', () => {
|
it('supports commas', () => {
|
||||||
expect(minS('c3,e3,g3')).toEqual(['c3: 0 - 1', 'e3: 0 - 1', 'g3: 0 - 1']);
|
expect(minS('c3,e3,g3')).toEqual(['c3: 0 - 1', 'e3: 0 - 1', 'g3: 0 - 1']);
|
||||||
expect(minS('[c3,e3,g3] f3')).toEqual(['c3: 0 - 1/2', 'e3: 0 - 1/2', 'g3: 0 - 1/2', 'f3: 1/2 - 1']);
|
expect(minS('[c3,e3,g3] f3')).toEqual(['c3: 0 - 1/2', 'e3: 0 - 1/2', 'g3: 0 - 1/2', 'f3: 1/2 - 1']);
|
||||||
@@ -46,10 +71,48 @@ describe('mini', () => {
|
|||||||
});
|
});
|
||||||
it('supports replication', () => {
|
it('supports replication', () => {
|
||||||
expect(minS('a!3 b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
|
expect(minS('a!3 b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
|
||||||
|
expect(minS('[<a b c>]!3 d')).toEqual(minS('<a b c> <a b c> <a b c> d'));
|
||||||
});
|
});
|
||||||
it('supports euclidean rhythms', () => {
|
it('supports euclidean rhythms', () => {
|
||||||
expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
|
expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
|
||||||
});
|
});
|
||||||
|
it('supports patterning euclidean rhythms', () => {
|
||||||
|
expect(minS('[a(<3 5>, <8 16>)]*2')).toEqual(minS('a(3,8) a(5,16)'));
|
||||||
|
});
|
||||||
|
it("reproduces Toussaint's example euclidean algorithms", () => {
|
||||||
|
const checkEuclid = function (spec, target) {
|
||||||
|
expect(minS(`x(${spec[0]},${spec[1]})`)).toEqual(minS(target));
|
||||||
|
};
|
||||||
|
checkEuclid([1, 2], 'x ~');
|
||||||
|
checkEuclid([1, 3], 'x ~ ~');
|
||||||
|
checkEuclid([1, 4], 'x ~ ~ ~');
|
||||||
|
checkEuclid([4, 12], 'x ~ ~ x ~ ~ x ~ ~ x ~ ~');
|
||||||
|
checkEuclid([2, 5], 'x ~ x ~ ~');
|
||||||
|
// checkEuclid([3, 4], "x ~ x x"); // Toussaint is wrong..
|
||||||
|
checkEuclid([3, 4], 'x x x ~'); // correction
|
||||||
|
checkEuclid([3, 5], 'x ~ x ~ x');
|
||||||
|
checkEuclid([3, 7], 'x ~ x ~ x ~ ~');
|
||||||
|
checkEuclid([3, 8], 'x ~ ~ x ~ ~ x ~');
|
||||||
|
checkEuclid([4, 7], 'x ~ x ~ x ~ x');
|
||||||
|
checkEuclid([4, 9], 'x ~ x ~ x ~ x ~ ~');
|
||||||
|
checkEuclid([4, 11], 'x ~ ~ x ~ ~ x ~ ~ x ~');
|
||||||
|
// checkEuclid([5, 6], "x ~ x x x x"); // Toussaint is wrong..
|
||||||
|
checkEuclid([5, 6], 'x x x x x ~'); // correction
|
||||||
|
checkEuclid([5, 7], 'x ~ x x ~ x x');
|
||||||
|
checkEuclid([5, 8], 'x ~ x x ~ x x ~');
|
||||||
|
checkEuclid([5, 9], 'x ~ x ~ x ~ x ~ x');
|
||||||
|
checkEuclid([5, 11], 'x ~ x ~ x ~ x ~ x ~ ~');
|
||||||
|
checkEuclid([5, 12], 'x ~ ~ x ~ x ~ ~ x ~ x ~');
|
||||||
|
// checkEuclid([5, 16], "x ~ ~ x ~ ~ x ~ ~ x ~ ~ x ~ ~ ~ ~"); // Toussaint is wrong..
|
||||||
|
checkEuclid([5, 16], 'x ~ ~ x ~ ~ x ~ ~ x ~ ~ x ~ ~ ~'); // correction
|
||||||
|
// checkEuclid([7, 8], "x ~ x x x x x x"); // Toussaint is wrong..
|
||||||
|
checkEuclid([7, 8], 'x x x x x x x ~'); // Correction
|
||||||
|
checkEuclid([7, 12], 'x ~ x x ~ x ~ x x ~ x ~');
|
||||||
|
checkEuclid([7, 16], 'x ~ ~ x ~ x ~ x ~ ~ x ~ x ~ x ~');
|
||||||
|
checkEuclid([9, 16], 'x ~ x x ~ x ~ x ~ x x ~ x ~ x ~');
|
||||||
|
checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~');
|
||||||
|
checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~');
|
||||||
|
});
|
||||||
it('supports the ? operator', () => {
|
it('supports the ? operator', () => {
|
||||||
expect(
|
expect(
|
||||||
mini('a?')
|
mini('a?')
|
||||||
@@ -77,7 +140,7 @@ describe('mini', () => {
|
|||||||
expect(haps.length < 230).toBe(true);
|
expect(haps.length < 230).toBe(true);
|
||||||
// 'Had too many cycles remaining after degradeBy 0.8');
|
// 'Had too many cycles remaining after degradeBy 0.8');
|
||||||
});
|
});
|
||||||
it('supports the random choice operator ("|") with nesting', () => {
|
/*it('supports the random choice operator ("|") with nesting', () => {
|
||||||
const numCycles = 900;
|
const numCycles = 900;
|
||||||
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
|
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
|
||||||
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
|
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
|
||||||
@@ -103,5 +166,5 @@ describe('mini', () => {
|
|||||||
// PRNG, this test should succeed
|
// PRNG, this test should succeed
|
||||||
expect(chisq <= 15.086).toBe(true);
|
expect(chisq <= 15.086).toBe(true);
|
||||||
// assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
|
// assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
|
||||||
});
|
});*/
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { dependencies } from './package.json';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'index.mjs'),
|
||||||
|
formats: ['es', 'cjs'],
|
||||||
|
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [...Object.keys(dependencies)],
|
||||||
|
},
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -34,4 +34,6 @@ Now open the REPL and type:
|
|||||||
s("<bd sd> hh").osc()
|
s("<bd sd> hh").osc()
|
||||||
```
|
```
|
||||||
|
|
||||||
or just [click here](http://localhost:3000/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
|
or just [click here](https://strudel.tidalcycles.org/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
|
||||||
|
|
||||||
|
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.tidalcycles.org/learn/input-output/#superdirt-api)
|
||||||
|
|||||||
+48
-22
@@ -5,39 +5,65 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import OSC from 'osc-js';
|
import OSC from 'osc-js';
|
||||||
import { Pattern } from '@strudel.cycles/core';
|
import { logger, parseNumeral, Pattern } from '@strudel.cycles/core';
|
||||||
|
|
||||||
|
let connection; // Promise<OSC>
|
||||||
|
function connect() {
|
||||||
|
if (!connection) {
|
||||||
|
// make sure this runs only once
|
||||||
|
connection = new Promise((resolve, reject) => {
|
||||||
|
const osc = new OSC();
|
||||||
|
osc.open();
|
||||||
|
osc.on('open', () => {
|
||||||
|
const url = osc.options?.plugin?.socket?.url;
|
||||||
|
logger(`[osc] connected${url ? ` to ${url}` : ''}`);
|
||||||
|
resolve(osc);
|
||||||
|
});
|
||||||
|
osc.on('close', () => {
|
||||||
|
connection = undefined; // allows new connection afterwards
|
||||||
|
console.log('[osc] disconnected');
|
||||||
|
reject('OSC connection closed');
|
||||||
|
});
|
||||||
|
osc.on('error', (err) => reject(err));
|
||||||
|
}).catch((err) => {
|
||||||
|
connection = undefined;
|
||||||
|
throw new Error('Could not connect to OSC server. Is it running?');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
const comm = new OSC();
|
|
||||||
comm.open();
|
|
||||||
const latency = 0.1;
|
const latency = 0.1;
|
||||||
let startedAt = -1;
|
let startedAt = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
|
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
|
||||||
|
* For more info, read [MIDI & OSC in the docs](https://strudel.tidalcycles.org/learn/input-output)
|
||||||
*
|
*
|
||||||
* @name osc
|
* @name osc
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.osc = function () {
|
Pattern.prototype.osc = async function () {
|
||||||
return this._withHap((hap) => {
|
const osc = await connect();
|
||||||
const onTrigger = (time, hap, currentTime, cps) => {
|
return this.onTrigger((time, hap, currentTime, cps = 1) => {
|
||||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||||
const delta = hap.duration.valueOf();
|
const delta = hap.duration.valueOf();
|
||||||
// time should be audio time of onset
|
// time should be audio time of onset
|
||||||
// currentTime should be current time of audio context (slightly before time)
|
// currentTime should be current time of audio context (slightly before time)
|
||||||
if (startedAt < 0) {
|
if (startedAt < 0) {
|
||||||
startedAt = Date.now() - currentTime * 1000;
|
startedAt = Date.now() - currentTime * 1000;
|
||||||
}
|
}
|
||||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||||
const keyvals = Object.entries(controls).flat();
|
// make sure n and note are numbers
|
||||||
const ts = Math.floor(startedAt + (time + latency) * 1000);
|
controls.n && (controls.n = parseNumeral(controls.n));
|
||||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
controls.note && (controls.note = parseNumeral(controls.note));
|
||||||
const bundle = new OSC.Bundle([message], ts);
|
const keyvals = Object.entries(controls).flat();
|
||||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
const ts = Math.floor(startedAt + (time + latency) * 1000);
|
||||||
comm.send(bundle);
|
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||||
};
|
const bundle = new OSC.Bundle([message], ts);
|
||||||
return hap.setContext({ ...hap.context, onTrigger });
|
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||||
|
osc.send(bundle);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
-60
@@ -1,60 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@strudel.cycles/osc",
|
|
||||||
"version": "0.1.1",
|
|
||||||
"lockfileVersion": 2,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "@strudel.cycles/osc",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"dependencies": {
|
|
||||||
"osc-js": "^2.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/osc-js": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-9i7J4u1hH+glooGMh+ki1ni0JGqKmylT8r0nXKugHbRK63rR+kl4O+5tGW6+/EszjbCju3KV+eXQQzFDdGrmhg==",
|
|
||||||
"dependencies": {
|
|
||||||
"ws": "^8.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ws": {
|
|
||||||
"version": "8.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
|
||||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": "^5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"osc-js": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-9i7J4u1hH+glooGMh+ki1ni0JGqKmylT8r0nXKugHbRK63rR+kl4O+5tGW6+/EszjbCju3KV+eXQQzFDdGrmhg==",
|
|
||||||
"requires": {
|
|
||||||
"ws": "^8.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ws": {
|
|
||||||
"version": "8.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
|
||||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
|
||||||
"requires": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/osc",
|
"name": "@strudel.cycles/osc",
|
||||||
"version": "0.1.1",
|
"version": "0.6.0",
|
||||||
"description": "OSC messaging for strudel",
|
"description": "OSC messaging for strudel",
|
||||||
"main": "osc.mjs",
|
"main": "osc.mjs",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"No tests present.\" && exit 0",
|
|
||||||
"server": "node server.js",
|
"server": "node server.js",
|
||||||
"tidal-sniffer": "node tidal-sniffer.js",
|
"tidal-sniffer": "node tidal-sniffer.js",
|
||||||
"client": "npx serve -p 4321"
|
"client": "npx serve -p 4321",
|
||||||
|
"build-bin": "npx pkg server.js --targets node16-macos-x64,node16-win-x64,node16-linux-x64 --out-path bin",
|
||||||
|
"build": "vite build",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -30,6 +36,11 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"osc-js": "^2.3.2"
|
"@strudel.cycles/core": "workspace:*",
|
||||||
|
"osc-js": "^2.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"pkg": "^5.7.0",
|
||||||
|
"vite": "^3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ const config = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
|
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
|
||||||
|
|
||||||
osc.open(); // start a WebSocket server on port 8080
|
osc.open(); // start a WebSocket server on port 8080
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { dependencies } from './package.json';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'osc.mjs'),
|
||||||
|
formats: ['es', 'cjs'],
|
||||||
|
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [...Object.keys(dependencies)],
|
||||||
|
},
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -11,8 +11,6 @@ node_modules
|
|||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
!dist
|
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user