mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-14 14:53:45 -04:00
Compare commits
642 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de9a52366f | |||
| 57265b9e33 | |||
| b7ef2d97e2 | |||
| 4fb364195c | |||
| 578bede46b | |||
| 38a51324b2 | |||
| 0e7c9a4001 | |||
| 32fee6313b | |||
| c6a74c040e | |||
| a97384cec1 | |||
| f052adb93a | |||
| c2560e0cf8 | |||
| cd6d1fb2d2 | |||
| c354ee32e3 | |||
| 5c5df74860 | |||
| 887fb6a84c | |||
| 80ea9e8de4 | |||
| 3879b01ddd | |||
| 4490681fa3 | |||
| b98e24fabf | |||
| 2deafe214a | |||
| d0258756e5 | |||
| 4fecad16e4 | |||
| a82eeb44c0 | |||
| 7ec5ab0c20 | |||
| 872e10fc95 | |||
| e3b7d0f84d | |||
| e0c424eb92 | |||
| 536427310d | |||
| a7eb0bb04a | |||
| af6a5b85f5 | |||
| b96e8093a0 | |||
| 989dc0aabe | |||
| 735f892a08 | |||
| e959c275fd | |||
| 7dbcc90450 | |||
| 7f86eac8f1 | |||
| 40de717cc9 | |||
| df211c994d | |||
| ee314e9b03 | |||
| 9049a8b964 | |||
| 88c029202c | |||
| 9142fb753a | |||
| 4688c0bf10 | |||
| 7e55258bbf | |||
| 828e475921 | |||
| 9d5f919023 | |||
| b8932390f7 | |||
| 445881992e | |||
| 7f1918ae8c | |||
| 78adaaedef | |||
| 4a65113bff | |||
| 843d9d52c3 | |||
| 7a74189771 | |||
| 6de04f7781 | |||
| ba27f2ba28 | |||
| f09b89ed63 | |||
| bac8594c5f | |||
| 3582ae9163 | |||
| 7a95bafa54 | |||
| 1383af6208 | |||
| bea9a3c2dc | |||
| 78ccec5d7b | |||
| 534e3f7f81 | |||
| 6a427d6f89 | |||
| b4297580a7 | |||
| 26b0e2ae43 | |||
| e75ac7b3c0 | |||
| 74442b0d76 | |||
| 3431803980 | |||
| c5fb247de6 | |||
| 18640d59da | |||
| 48753b2539 | |||
| 606dcc8cc3 | |||
| 9e4c548c69 | |||
| 75a974643b | |||
| e2711ba911 | |||
| 7605198774 | |||
| fc9525e7d8 | |||
| e597b6473d | |||
| 16c8dcf788 | |||
| 37842d4fbd | |||
| 133a1d2e8f | |||
| 15c15a9c36 | |||
| d233d50de3 | |||
| a9d8e7ca82 | |||
| 4001cc9531 | |||
| 4319df0cb6 | |||
| 5be7898522 | |||
| 009760c31a | |||
| 77349526e4 | |||
| 3ba195c2d9 | |||
| 60f5032b12 | |||
| fdc201a799 | |||
| ec8616a515 | |||
| 349d11e76d | |||
| 62e3774ff7 | |||
| abaeb52b7c | |||
| 60e600c381 | |||
| 97f84ee87f | |||
| c5738aeed9 | |||
| 192adccb10 | |||
| defcacb59b | |||
| e4ea786956 | |||
| c9d98bf4d7 | |||
| 8dd08c360f | |||
| d6c4bb0e4e | |||
| c54916cdfd | |||
| e29cf6fcc1 | |||
| 5b3cd166b0 | |||
| a7246c6518 | |||
| b48fd78cf0 | |||
| 66c895ee3c | |||
| 37c450dd83 | |||
| 4c744da3e2 | |||
| 388e7b3c2f | |||
| 6ec5377381 | |||
| 24ec47e4ff | |||
| 4a122523d9 | |||
| 1241911ec0 | |||
| 0f54bb1938 | |||
| a3b55aa5cc | |||
| 4fa02a483b | |||
| a786c642f1 | |||
| 1bade7bee8 | |||
| d399df8798 | |||
| 749b11a5ea | |||
| 328af5f560 | |||
| 876f97e9a2 | |||
| 26d7a32f11 | |||
| 3057773009 | |||
| 6ca89cb907 | |||
| 88b5b9b21c | |||
| 6bcf8b91cc | |||
| 7cf7ebc555 | |||
| ee0aaca2b0 | |||
| b1b084452c | |||
| 213b1a2dae | |||
| b991cfc493 | |||
| 90cdea3656 | |||
| 1463f27a0f | |||
| afb8ab44cf | |||
| 282f36c47c | |||
| c3f27d1ad9 | |||
| ad230a8dc7 | |||
| c29c4c0a8b | |||
| 284f5615cc | |||
| c625096f59 | |||
| c3cd5d08c9 | |||
| c87c3c6672 | |||
| 8fc7688585 | |||
| 94bcfb11de | |||
| ebeaac3c5f | |||
| 942ffc038d | |||
| 9de344e48d | |||
| 5efea045d2 | |||
| 6bd3274a4b | |||
| a179313db2 | |||
| 289376840e | |||
| 0e3d84e5ca | |||
| cb643f8fd9 | |||
| 1faa81099c | |||
| 2dc079b950 | |||
| f6cf740507 | |||
| 338c866d83 | |||
| 038e6c312b | |||
| 6f6def34f7 | |||
| 88651149d3 | |||
| e301090704 | |||
| d0f8a612ad | |||
| a7728e3d81 | |||
| 4dadc18a3a | |||
| 43efe6921d | |||
| 2e31c8695a | |||
| 988bd8ccdc | |||
| 965794712e | |||
| 68ea086e2a | |||
| 7370f41fa0 | |||
| 5cbe30095b | |||
| f20c5a1bcb | |||
| 02979926fd | |||
| b08337af45 | |||
| 980c1e4812 | |||
| f43dd8db1b | |||
| badda165fe | |||
| f7d17875c0 | |||
| 0c124bcca3 | |||
| 3eb63f1730 | |||
| abb0b3b4c0 | |||
| 42662748d3 | |||
| b5b866a894 | |||
| 01cccc6462 | |||
| 9287b8ea42 | |||
| 5fabc5c7f9 | |||
| 61dc6f980d | |||
| 84328cfb90 | |||
| 37a35022f5 | |||
| 3afe540111 | |||
| 35687b1ee0 | |||
| 195e6b61f9 | |||
| dabc0331cb | |||
| 824abb0406 | |||
| f6789aaace | |||
| 10bce85561 | |||
| cd0f1b6c5c | |||
| 834bb7d777 | |||
| 5c1d7baf72 | |||
| 8bdbc9ec4c | |||
| 0c6a0720af | |||
| c6095fd92a | |||
| 7d0fb0de83 | |||
| 75281afe05 | |||
| 738e714f45 | |||
| 898bfaeecf | |||
| 7f44d9cdd2 | |||
| 8d9be21c8a | |||
| 82cb4c8c11 | |||
| 2778e4d697 | |||
| 30fe2dd502 | |||
| 2483bf812f | |||
| 7a999de602 | |||
| 1929c27225 | |||
| 0cf5c9c7d6 | |||
| c20b691683 | |||
| 5c2101de1a | |||
| 2ec2e81511 | |||
| ac3d8b832c | |||
| ad174957e8 | |||
| 9b3fa83516 | |||
| 52b8d9d361 | |||
| d231ded7a2 | |||
| 5f57951e3d | |||
| b1a4788442 | |||
| 6377378a3d | |||
| 45b91cdea5 | |||
| d5647c6760 | |||
| f419559bde | |||
| 4cfaa39419 | |||
| d6b1762adf | |||
| 45c3293385 | |||
| 92d574605e | |||
| cec7b90631 | |||
| aa3cb35f09 | |||
| 7373138e3d | |||
| 2d8176f213 | |||
| 8589d72aee | |||
| 626b50087a | |||
| 1e62554dfb | |||
| bcf800482b | |||
| 08f94d2122 | |||
| 559cfd11db | |||
| ce820c2314 | |||
| 8583ed0e5c | |||
| f2c16a06a1 | |||
| 584d469a73 | |||
| 4298e582fb | |||
| a3fdfbbc16 | |||
| b8929a0d18 | |||
| 0fa5832b63 | |||
| bbb29ebd7c | |||
| 0fd9ac6da3 | |||
| e53098b179 | |||
| 78dcab2778 | |||
| e05f74cef1 | |||
| 3d8724c90d | |||
| ffeda19ce5 | |||
| 70fe8f14e3 | |||
| a639d9f49f | |||
| e5251422fe | |||
| d8f80c058d | |||
| 4cf3655c08 | |||
| 58fc786462 | |||
| 81b142704c | |||
| b0cbeda796 | |||
| 0b3a8a5f65 | |||
| d7d8d2a3e6 | |||
| d5c0309885 | |||
| 36fa9d81fb | |||
| 35f5a5d11a | |||
| d1fe50e271 | |||
| 2d08b31f9a | |||
| 27643f4a48 | |||
| 110e179e9e | |||
| fe6d99d4f1 | |||
| 06fbcbe24b | |||
| f40b57ee48 | |||
| 9438c69bed | |||
| 70d6f3000a | |||
| d47fd0cf18 | |||
| 40117fbe54 | |||
| 7466d8a1de | |||
| ce09443e48 | |||
| 1718dbe94e | |||
| 69894db206 | |||
| 6e4c873248 | |||
| 64693ffd26 | |||
| 66f8ca72c1 | |||
| c82f7bd8fe | |||
| 34176ab5f8 | |||
| 7f12ce9b45 | |||
| 9e1b9f7f5c | |||
| 23e949de1c | |||
| 00bd60a855 | |||
| dacd9afac0 | |||
| 3fc5bb31d0 | |||
| 78770888a5 | |||
| f5b092acf2 | |||
| ba9562f000 | |||
| aded178ab7 | |||
| 0b5d905120 | |||
| 08abec8fd5 | |||
| 63c23736ad | |||
| f7bd373ce6 | |||
| 8e717d2ea1 | |||
| 5f271ed127 | |||
| d686b65dbe | |||
| c7e882e001 | |||
| f07859996e | |||
| fbc73bc78a | |||
| bd8ad1ed1b | |||
| dc2ff83fa5 | |||
| 13545d147b | |||
| 1b85aa713b | |||
| 90a58858ca | |||
| 8149b85a55 | |||
| e79b6c514a | |||
| aaa894d74b | |||
| b55721f16f | |||
| b39d948d71 | |||
| 0fee279e3d | |||
| c7d8a9cb82 | |||
| b50075f102 | |||
| fb3ca9f960 | |||
| 058b971870 | |||
| 610ea414c1 | |||
| cc6997baec | |||
| d126c28b85 | |||
| c1649b9dc0 | |||
| b0a8d12112 | |||
| b5160d38cb | |||
| 360e9b5936 | |||
| 85a6cd828e | |||
| e698f19a64 | |||
| 83c1b580dd | |||
| 9273e1f8ed | |||
| 5287825279 | |||
| 5567959afd | |||
| eaab36b051 | |||
| 9a4f3af580 | |||
| b83664dfce | |||
| 349f7ae965 | |||
| 6f41716ab5 | |||
| abeb62dea8 | |||
| 7cfed80443 | |||
| 36aa7d9b25 | |||
| 2a449ad3c0 | |||
| a71c5957cc | |||
| eef6c7d55e | |||
| 3872322da1 | |||
| f1e850a62a | |||
| 8d9278419d | |||
| 0862827a51 | |||
| f37a220a05 | |||
| aa51c2b6dd | |||
| 92da70a7ae | |||
| a55862fc25 | |||
| 09b14a5702 | |||
| 260b831815 | |||
| 665cf6ac65 | |||
| 5e90021698 | |||
| 16aeb7e12c | |||
| b4cbec173b | |||
| 8c9e06c329 | |||
| e4a35fdd72 | |||
| 370d82e66c | |||
| 876e851517 | |||
| 190fe37a9f | |||
| 623661f8d2 | |||
| f31bc3f8c5 | |||
| 12fc48544c | |||
| 3b4e44a9fd | |||
| 7850fb727e | |||
| 9b3122837e | |||
| d4c59cc73b | |||
| fe36bf54b2 | |||
| 4d8308a763 | |||
| 11f26c1e2c | |||
| e10104d401 | |||
| 5e09c3c318 | |||
| 576b88221d | |||
| 7dc2a0091b | |||
| 89a4a85589 | |||
| a3baf07a5c | |||
| 8e6909478c | |||
| 2d2d0cabb3 | |||
| 6f04a1b8fa | |||
| a9ad3a56ad | |||
| 3069b14e9c | |||
| ae868c6372 | |||
| c287ff554c | |||
| 3ccbd35fad | |||
| 51cc69208f | |||
| 1fec2f7a46 | |||
| ffffdd2d7d | |||
| f8c3e3d5aa | |||
| eccce6fa84 | |||
| 44be6cee2c | |||
| 799afd511a | |||
| 37c4541468 | |||
| 119a66acd0 | |||
| 105193a452 | |||
| 02d793e9e0 | |||
| 90647a8ba8 | |||
| 06950a6bd8 | |||
| 0c2147d9c5 | |||
| 51e8174689 | |||
| 185318a70c | |||
| a88759ba94 | |||
| 96ac054392 | |||
| bd7f4dd73f | |||
| 2e22152089 | |||
| 98a89aea11 | |||
| 372e33c066 | |||
| 52641db590 | |||
| f9ad5f0d01 | |||
| 3db1c7a521 | |||
| b6d1f96930 | |||
| 61a9b01062 | |||
| f0faf5b42e | |||
| 11733d5e05 | |||
| c4a38d9008 | |||
| 0edd7e14b7 | |||
| 8b7bb7b6ae | |||
| 39d5955e58 | |||
| 536327f403 | |||
| 8679dc63be | |||
| 1e9979ae18 | |||
| d2dffe3186 | |||
| 9971867e2f | |||
| 8c93e578a0 | |||
| 1ba5d2e1ca | |||
| ed792fc0d4 | |||
| 4e575c44b3 | |||
| fc06181217 | |||
| 0d6fcf78d8 | |||
| 82225f0b81 | |||
| 2177e13b49 | |||
| b63f4eb503 | |||
| cd7bc09f9c | |||
| ccf775e976 | |||
| 811346e83d | |||
| d3240a75d0 | |||
| d93e396ce5 | |||
| d74cc234c4 | |||
| 0fda83d8de | |||
| a1ec8545fb | |||
| 087f574355 | |||
| 1de784b817 | |||
| db79c05d9a | |||
| 1fdb1f0da6 | |||
| e2bfaa6233 | |||
| 6089849d35 | |||
| 6e02bf59e9 | |||
| 12228c56d9 | |||
| 3b631cb6af | |||
| b27a58df7e | |||
| 308f139193 | |||
| 692fc4e11d | |||
| b52f65c4d7 | |||
| 299275e368 | |||
| bb71c519db | |||
| 9ec7109dc8 | |||
| 5b67fccb1b | |||
| 9807c4b7a1 | |||
| 9039600f6d | |||
| b5b0156e08 | |||
| 352e647d37 | |||
| e6b66f2525 | |||
| f5075906e2 | |||
| a6f57bced8 | |||
| d35bf9591c | |||
| d96f3f9dd4 | |||
| 1f4c2f8c5a | |||
| 95719654f3 | |||
| 4b921c47f5 | |||
| f069f53fae | |||
| 1494cc38fc | |||
| a035412956 | |||
| 08662ea831 | |||
| d97103268b | |||
| a959e2e34d | |||
| 602bfbdd79 | |||
| 0785bdd37a | |||
| 8b62b6e1b1 | |||
| d1c8ebd15b | |||
| 13088e4001 | |||
| 5c6fa1c82b | |||
| fd81867e7b | |||
| 78293e64dd | |||
| 930fc10aec | |||
| 0b2a4fda44 | |||
| efdce6d4cd | |||
| c0b0ea4493 | |||
| 0fcec0a0ca | |||
| c0701d8a35 | |||
| ae51092044 | |||
| b7fcccb8db | |||
| b21a52f5f3 | |||
| 2bdcfe13e6 | |||
| 12a97ec429 | |||
| 99298ce9f2 | |||
| a0ed25d373 | |||
| dc6c29c668 | |||
| 3a85f28b5c | |||
| 95c44b1ce2 | |||
| b88e0ecb78 | |||
| c26f6b72d9 | |||
| 6cd3cc96a3 | |||
| c138cf0b76 | |||
| 8edb086178 | |||
| 0f74b5a4b1 | |||
| 6de2feff05 | |||
| a9811ee955 | |||
| 98b6c11a82 | |||
| 3df2929d12 | |||
| 30d1f423cb | |||
| e0c5566f35 | |||
| c5a2ae47d7 | |||
| 17654b885b | |||
| acaf0ca0af | |||
| 6a201be1fd | |||
| 8617b9164e | |||
| 991e5f1b3c | |||
| 55c533c947 | |||
| d17543d5d9 | |||
| 73169563c4 | |||
| d351cb9f7f | |||
| 251be60630 | |||
| 18d7222804 | |||
| ff1c37d361 | |||
| 3abe6db3de | |||
| b9ca9d3b06 | |||
| 9bbc04843d | |||
| ddf61a6438 | |||
| ba35a81e9b | |||
| 13133583ca | |||
| e91b981740 | |||
| 1458ee64ec | |||
| 4e2a5e1663 | |||
| c98b0e9687 | |||
| 4e25990de9 | |||
| 4b51f1a3fc | |||
| 5490b01004 | |||
| 43176e1f1d | |||
| fffd3c07ca | |||
| 952bd38e70 | |||
| 8589f5cc89 | |||
| ec594d52f8 | |||
| 848cbf9c60 | |||
| e02be7dd9a | |||
| 33d35e47a6 | |||
| 18d62b32c2 | |||
| 58ed51b49c | |||
| 9f9fe529e4 | |||
| aad721890a | |||
| e63a5d10c8 | |||
| 15681262de | |||
| b5d3aa81f9 | |||
| a688061616 | |||
| f788d347ec | |||
| a33ce67dd4 | |||
| 8a22c5627a | |||
| 7b529a30f4 | |||
| 4910b34f26 | |||
| 74ad521fc9 | |||
| 6a73b43e3a | |||
| b59b26460d | |||
| aa324ae25d | |||
| ff412f6396 | |||
| ac148b2f32 | |||
| 6f5d096e6d | |||
| ff1e6714c6 | |||
| 41d82eaadf | |||
| be6b011204 | |||
| 65e48c05f0 | |||
| cee08ea67d | |||
| 35ef26c013 | |||
| b08a0b8102 | |||
| 220342154b | |||
| ef7182cd76 | |||
| 6059c69995 | |||
| 892d8ba152 | |||
| da11069f75 | |||
| bf72908dc9 | |||
| 81ab5c78e2 | |||
| de503c7d63 | |||
| 487ccb90a1 | |||
| 4bfeaa47bf | |||
| 9a4d8036a6 | |||
| a50bd6375d | |||
| 14508e91a9 | |||
| 2d5064bd5b | |||
| fdc4819e39 | |||
| efddbb616b | |||
| db4d0f26a8 | |||
| 06cffbc31b | |||
| 3cfb0f4c76 | |||
| 436ba067bc | |||
| f8724bf47e | |||
| 1f7e293204 | |||
| 86d2652258 | |||
| f5c0cc2e2d | |||
| 4463d4a417 | |||
| df35a5c9b1 | |||
| dda93b3c13 | |||
| 5de40b6bf5 | |||
| dcbe799d25 | |||
| a5641aef03 | |||
| 668f97a4c8 | |||
| e9d577496b | |||
| 66fba947ac | |||
| 18c7e02609 | |||
| 7e1351507a | |||
| 032acbd27c | |||
| 8b51f0fbd2 | |||
| ead5942ef0 | |||
| cfe369d9fe | |||
| 2750a3c130 | |||
| a7f6d6ecac | |||
| 3680282450 | |||
| 3b3435c999 | |||
| e2ae6278c2 | |||
| 37d6723e3c | |||
| 961bbf6f55 | |||
| fc71d349ed | |||
| 97c8ab0f71 | |||
| 28534ea547 | |||
| 1a75b79a33 | |||
| 75dfb0af35 | |||
| 073fa09648 | |||
| 5506d18bc5 | |||
| 57157fde9c |
+2
-1
@@ -17,4 +17,5 @@ vite.config.js
|
||||
**/*.ts
|
||||
**/*.json
|
||||
**/dev-dist
|
||||
**/dist
|
||||
**/dist
|
||||
/src-tauri/target/**/*
|
||||
@@ -0,0 +1,62 @@
|
||||
name: Tauri Builder
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-latest, ubuntu-latest, windows-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust_target: x86_64-unknown-linux-gnu
|
||||
- os: macos-latest
|
||||
rust_target: x86_64-apple-darwin
|
||||
- os: macos-latest
|
||||
rust_target: aarch64-apple-darwin
|
||||
- os: windows-latest
|
||||
rust_target: x86_64-pc-windows-msvc
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8.6.2
|
||||
|
||||
- name: Node.js setup
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: latest
|
||||
# node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install Rust (Stable)
|
||||
run:
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install app dependencies from lockfile and build web
|
||||
run: pnpm install
|
||||
|
||||
- name: Build the app
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
# tauri-action replaces \_\_VERSION\_\_ with the app version
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: "Strudel v__VERSION__"
|
||||
releaseBody: |
|
||||
See the assets to download this version and install.
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
+5
-1
@@ -39,4 +39,8 @@ server/samples/old
|
||||
repl/stats.html
|
||||
coverage
|
||||
public/icons/apple-splash-*
|
||||
dev-dist
|
||||
dev-dist
|
||||
Dirt-Samples
|
||||
tidal-drum-machines
|
||||
webaudiofontdata
|
||||
src-tauri/target
|
||||
+9
-2
@@ -31,7 +31,7 @@ Use one of the Communication Channels listed above.
|
||||
|
||||
## Improve the Docs
|
||||
|
||||
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/learn/getting-started),
|
||||
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/workshop/getting-started/),
|
||||
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
|
||||
|
||||
## Propose a Feature
|
||||
@@ -123,7 +123,14 @@ To publish all packages that have been changed since the last release, run:
|
||||
|
||||
```sh
|
||||
npm login
|
||||
npx lerna publish
|
||||
|
||||
# this will increment all the versions in package.json files of non private packages to selected versions
|
||||
npx lerna version --no-private
|
||||
|
||||
# publish all packages inside /packages using pnpm! don't use lerna to publish!!
|
||||
pnpm --filter "./packages/**" publish --dry-run
|
||||
|
||||
# the last command was only a dry-run, make sure everything looks ok, if yes, run the same command without flag
|
||||
```
|
||||
|
||||
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
|
||||
|
||||
@@ -14,8 +14,8 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
|
||||
After cloning the project, you can run the REPL locally:
|
||||
|
||||
```bash
|
||||
npm run setup
|
||||
npm run repl
|
||||
pnpm run setup
|
||||
pnpm run repl
|
||||
```
|
||||
|
||||
## Using Strudel In Your Project
|
||||
@@ -24,8 +24,8 @@ There are multiple npm packages you can use to use strudel, or only parts of it,
|
||||
|
||||
- [`core`](./packages/core/): tidal pattern engine
|
||||
- [`mini`](./packages/mini): mini notation parser + core binding
|
||||
- [`eval`](./packages/eval): user code evaluator. syntax sugar + highlighting
|
||||
- [`tone`](./packages/tone): bindings for Tone.js instruments and effects
|
||||
- [`transpiler`](./packages/transpiler): user code transpiler
|
||||
- [`webaudio`](./packages/webaudio): webaudio output
|
||||
- [`osc`](./packages/osc): bindings to communicate via OSC
|
||||
- [`midi`](./packages/midi): webmidi bindings
|
||||
- [`serial`](./packages/serial): webserial bindings
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
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/desktopbridge/index.mjs';
|
||||
export * from './packages/midi/index.mjs';
|
||||
export * from './packages/mini/index.mjs';
|
||||
export * from './packages/osc/index.mjs';
|
||||
@@ -10,8 +10,6 @@ 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';
|
||||
|
||||
+11
-13
@@ -18,7 +18,6 @@
|
||||
"build": "npm run prebuild && cd website && npm run build",
|
||||
"preview": "cd website && npm run preview",
|
||||
"osc": "cd packages/osc && npm run server",
|
||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
|
||||
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
||||
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
|
||||
"lint": "eslint . --ext mjs,js --quiet",
|
||||
@@ -53,22 +52,21 @@
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"@strudel.cycles/xen": "workspace:*",
|
||||
"acorn": "^8.8.1",
|
||||
"dependency-tree": "^9.0.0",
|
||||
"vitest": "^0.25.7"
|
||||
"dependency-tree": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/ui": "^0.25.7",
|
||||
"c8": "^7.12.0",
|
||||
"canvas": "^2.11.0",
|
||||
"eslint": "^8.28.0",
|
||||
"@tauri-apps/cli": "^1.4.0",
|
||||
"@vitest/ui": "^0.28.0",
|
||||
"canvas": "^2.11.2",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"events": "^3.3.0",
|
||||
"gh-pages": "^4.0.0",
|
||||
"jsdoc": "^3.6.10",
|
||||
"jsdoc": "^4.0.2",
|
||||
"jsdoc-json": "^2.0.2",
|
||||
"jsdoc-to-markdown": "^7.1.1",
|
||||
"lerna": "^4.0.0",
|
||||
"prettier": "^2.8.1",
|
||||
"rollup-plugin-visualizer": "^5.8.1"
|
||||
"jsdoc-to-markdown": "^8.0.0",
|
||||
"lerna": "^6.6.1",
|
||||
"prettier": "^2.8.8",
|
||||
"rollup-plugin-visualizer": "^5.8.1",
|
||||
"vitest": "^0.33.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# @strudel/codemirror
|
||||
|
||||
This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it.
|
||||
@@ -0,0 +1,113 @@
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from '@codemirror/view';
|
||||
import { Drawer, repl } from '@strudel.cycles/core';
|
||||
import { flashField, flash } from './flash.mjs';
|
||||
import { highlightExtension, highlightMiniLocations } from './highlight.mjs';
|
||||
import { oneDark } from './themes/one-dark';
|
||||
|
||||
// https://codemirror.net/docs/guide/
|
||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
|
||||
let state = EditorState.create({
|
||||
doc: initialCode,
|
||||
extensions: [
|
||||
theme,
|
||||
javascript(),
|
||||
lineNumbers(),
|
||||
highlightExtension,
|
||||
highlightActiveLineGutter(),
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
keymap.of(defaultKeymap),
|
||||
flashField,
|
||||
EditorView.updateListener.of((v) => onChange(v)),
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Ctrl-Enter',
|
||||
run: () => onEvaluate(),
|
||||
},
|
||||
{
|
||||
key: 'Ctrl-.',
|
||||
run: () => onStop(),
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
return new EditorView({
|
||||
state,
|
||||
parent: root,
|
||||
});
|
||||
}
|
||||
|
||||
export class StrudelMirror {
|
||||
constructor(options) {
|
||||
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
|
||||
this.code = initialCode;
|
||||
|
||||
this.drawer = new Drawer((haps, time) => {
|
||||
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
|
||||
this.highlight(currentFrame, time);
|
||||
onDraw?.(haps, time, currentFrame);
|
||||
}, drawTime);
|
||||
|
||||
const prebaked = prebake();
|
||||
prebaked.then(async () => {
|
||||
if (!onDraw) {
|
||||
return;
|
||||
}
|
||||
const { scheduler, evaluate } = await this.repl;
|
||||
// draw first frame instantly
|
||||
prebaked.then(async () => {
|
||||
await evaluate(this.code, false);
|
||||
this.drawer.invalidate(scheduler);
|
||||
onDraw?.(this.drawer.visibleHaps, 0, []);
|
||||
});
|
||||
});
|
||||
|
||||
this.repl = repl({
|
||||
...replOptions,
|
||||
onToggle: async (started) => {
|
||||
replOptions?.onToggle?.(started);
|
||||
const { scheduler } = await this.repl;
|
||||
if (started) {
|
||||
this.drawer.start(scheduler);
|
||||
} else {
|
||||
this.drawer.stop();
|
||||
}
|
||||
},
|
||||
beforeEval: async () => {
|
||||
await prebaked;
|
||||
},
|
||||
afterEval: (options) => {
|
||||
replOptions?.afterEval?.(options);
|
||||
this.drawer.invalidate();
|
||||
},
|
||||
});
|
||||
this.editor = initEditor({
|
||||
root,
|
||||
initialCode,
|
||||
onChange: (v) => {
|
||||
this.code = v.state.doc.toString();
|
||||
},
|
||||
onEvaluate: () => this.evaluate(),
|
||||
onStop: () => this.stop(),
|
||||
});
|
||||
}
|
||||
async evaluate() {
|
||||
const { evaluate } = await this.repl;
|
||||
this.flash();
|
||||
await evaluate(this.code);
|
||||
}
|
||||
async stop() {
|
||||
const { scheduler } = await this.repl;
|
||||
scheduler.stop();
|
||||
}
|
||||
flash(ms) {
|
||||
flash(this.editor, ms);
|
||||
}
|
||||
highlight(haps, time) {
|
||||
highlightMiniLocations(this.editor.view, time, haps);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { StateEffect, StateField } from '@codemirror/state';
|
||||
import { Decoration, EditorView } from '@codemirror/view';
|
||||
|
||||
export const setFlash = StateEffect.define();
|
||||
export const flashField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(flash, tr) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setFlash)) {
|
||||
if (e.value && tr.newDoc.length > 0) {
|
||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||
} else {
|
||||
flash = Decoration.set([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flash;
|
||||
} catch (err) {
|
||||
console.warn('flash error', err);
|
||||
return flash;
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
export const flash = (view, ms = 200) => {
|
||||
view.dispatch({ effects: setFlash.of(true) });
|
||||
setTimeout(() => {
|
||||
view.dispatch({ effects: setFlash.of(false) });
|
||||
}, ms);
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
||||
import { Decoration, EditorView } from '@codemirror/view';
|
||||
|
||||
export const setMiniLocations = StateEffect.define();
|
||||
export const showMiniLocations = StateEffect.define();
|
||||
export const updateMiniLocations = (view, locations) => {
|
||||
view.dispatch({ effects: setMiniLocations.of(locations) });
|
||||
};
|
||||
export const highlightMiniLocations = (view, atTime, haps) => {
|
||||
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
|
||||
};
|
||||
|
||||
const miniLocations = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(locations, tr) {
|
||||
if (tr.docChanged) {
|
||||
locations = locations.map(tr.changes);
|
||||
}
|
||||
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setMiniLocations)) {
|
||||
// this is called on eval, with the mini locations obtained from the transpiler
|
||||
// codemirror will automatically remap the marks when the document is edited
|
||||
// create a mark for each mini location, adding the range to the spec to find it later
|
||||
const marks = e.value
|
||||
.filter(([from]) => from < tr.newDoc.length)
|
||||
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
|
||||
.map(
|
||||
(range) =>
|
||||
Decoration.mark({
|
||||
id: range.join(':'),
|
||||
// this green is only to verify that the decoration moves when the document is edited
|
||||
// it will be removed later, so the mark is not visible by default
|
||||
attributes: { style: `background-color: #00CA2880` },
|
||||
}).range(...range), // -> Decoration
|
||||
);
|
||||
|
||||
locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<Decoration>
|
||||
}
|
||||
}
|
||||
|
||||
return locations;
|
||||
},
|
||||
});
|
||||
|
||||
const visibleMiniLocations = StateField.define({
|
||||
create() {
|
||||
return { atTime: 0, haps: new Map() };
|
||||
},
|
||||
update(visible, tr) {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(showMiniLocations)) {
|
||||
// this is called every frame to show the locations that are currently active
|
||||
// we can NOT create new marks because the context.locations haven't changed since eval time
|
||||
// this is why we need to find a way to update the existing decorations, showing the ones that have an active range
|
||||
const haps = new Map();
|
||||
for (let hap of e.value.haps) {
|
||||
for (let { start, end } of hap.context.locations) {
|
||||
let id = `${start}:${end}`;
|
||||
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
|
||||
haps.set(id, hap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visible = { atTime: e.value.atTime, haps };
|
||||
}
|
||||
}
|
||||
|
||||
return visible;
|
||||
},
|
||||
});
|
||||
|
||||
// // Derive the set of decorations from the miniLocations and visibleLocations
|
||||
const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
|
||||
const iterator = state.field(miniLocations).iter();
|
||||
const { haps } = state.field(visibleMiniLocations);
|
||||
const builder = new RangeSetBuilder();
|
||||
|
||||
while (iterator.value) {
|
||||
const {
|
||||
from,
|
||||
to,
|
||||
value: {
|
||||
spec: { id },
|
||||
},
|
||||
} = iterator;
|
||||
|
||||
if (haps.has(id)) {
|
||||
const hap = haps.get(id);
|
||||
const color = hap.context.color ?? 'var(--foreground)';
|
||||
// Get explicit channels for color values
|
||||
/*
|
||||
const swatch = document.createElement('div');
|
||||
swatch.style.color = color;
|
||||
document.body.appendChild(swatch);
|
||||
let channels = getComputedStyle(swatch)
|
||||
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
|
||||
.slice(1)
|
||||
.map((c) => parseFloat(c || 1));
|
||||
document.body.removeChild(swatch);
|
||||
|
||||
// Get percentage of event
|
||||
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
|
||||
channels[3] *= percent;
|
||||
*/
|
||||
|
||||
builder.add(
|
||||
from,
|
||||
to,
|
||||
Decoration.mark({
|
||||
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
|
||||
attributes: { style: `outline: solid 2px ${color}` },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
iterator.next();
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
});
|
||||
|
||||
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './codemirror.mjs';
|
||||
export * from './highlight.mjs';
|
||||
export * from './flash.mjs';
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webdirt",
|
||||
"version": "0.6.0",
|
||||
"description": "WebDirt integration for Strudel",
|
||||
"name": "@strudel/codemirror",
|
||||
"version": "0.9.0",
|
||||
"description": "Codemirror Extensions for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs"
|
||||
@@ -12,6 +11,7 @@
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
@@ -24,18 +24,24 @@
|
||||
"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": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"WebDirt": "github:dktr0/WebDirt"
|
||||
"@codemirror/commands": "^6.2.4",
|
||||
"@codemirror/lang-javascript": "^6.1.7",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/view": "^6.10.0",
|
||||
"@lezer/highlight": "^1.1.4",
|
||||
"@strudel.cycles/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^3.2.2",
|
||||
"vitest": "^0.25.7"
|
||||
"vite": "^4.3.3"
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
|
||||
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
|
||||
|
||||
const chalky = '#e5c07b',
|
||||
coral = '#e06c75',
|
||||
cyan = '#56b6c2',
|
||||
invalid = '#ffffff',
|
||||
ivory = '#abb2bf',
|
||||
stone = '#7d8799', // Brightened compared to original to increase contrast
|
||||
malibu = '#61afef',
|
||||
sage = '#98c379',
|
||||
whiskey = '#d19a66',
|
||||
violet = '#c678dd',
|
||||
darkBackground = '#21252b',
|
||||
highlightBackground = '#2c313a',
|
||||
background = '#282c34',
|
||||
tooltipBackground = '#353a42',
|
||||
selection = '#3E4451',
|
||||
cursor = '#528bff';
|
||||
|
||||
/// The colors used in the theme, as CSS color strings.
|
||||
export const color = {
|
||||
chalky,
|
||||
coral,
|
||||
cyan,
|
||||
invalid,
|
||||
ivory,
|
||||
stone,
|
||||
malibu,
|
||||
sage,
|
||||
whiskey,
|
||||
violet,
|
||||
darkBackground,
|
||||
highlightBackground,
|
||||
background,
|
||||
tooltipBackground,
|
||||
selection,
|
||||
cursor,
|
||||
};
|
||||
|
||||
/// The editor theme styles for One Dark.
|
||||
export const oneDarkTheme = EditorView.theme(
|
||||
{
|
||||
'&': {
|
||||
color: ivory,
|
||||
backgroundColor: background,
|
||||
},
|
||||
|
||||
'.cm-content': {
|
||||
caretColor: cursor,
|
||||
},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
|
||||
{ backgroundColor: selection },
|
||||
|
||||
'.cm-panels': { backgroundColor: darkBackground, color: ivory },
|
||||
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
||||
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: '#72a1ff59',
|
||||
outline: '1px solid #457dff',
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: '#6199ff2f',
|
||||
},
|
||||
|
||||
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
|
||||
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: '#bad0f847',
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: background,
|
||||
color: stone,
|
||||
border: 'none',
|
||||
},
|
||||
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: highlightBackground,
|
||||
},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#ddd',
|
||||
},
|
||||
|
||||
'.cm-tooltip': {
|
||||
border: 'none',
|
||||
backgroundColor: tooltipBackground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent',
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: tooltipBackground,
|
||||
borderBottomColor: tooltipBackground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
backgroundColor: highlightBackground,
|
||||
color: ivory,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ dark: true },
|
||||
);
|
||||
|
||||
/// The highlighting style for code in the One Dark theme.
|
||||
export const oneDarkHighlightStyle = HighlightStyle.define([
|
||||
{ tag: t.keyword, color: violet },
|
||||
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
|
||||
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
|
||||
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
|
||||
{ tag: [t.definition(t.name), t.separator], color: ivory },
|
||||
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
|
||||
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
|
||||
{ tag: [t.meta, t.comment], color: stone },
|
||||
{ tag: t.strong, fontWeight: 'bold' },
|
||||
{ tag: t.emphasis, fontStyle: 'italic' },
|
||||
{ tag: t.strikethrough, textDecoration: 'line-through' },
|
||||
{ tag: t.link, color: stone, textDecoration: 'underline' },
|
||||
{ tag: t.heading, fontWeight: 'bold', color: coral },
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
|
||||
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
|
||||
{ tag: t.invalid, color: invalid },
|
||||
]);
|
||||
|
||||
/// Extension to enable the One Dark theme (both the editor theme and
|
||||
/// the highlight style).
|
||||
export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
|
||||
@@ -1,4 +1,5 @@
|
||||
import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
||||
import { Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
||||
import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
|
||||
const { createParams } = controls;
|
||||
|
||||
let clearColor = '#22222210';
|
||||
|
||||
+680
-307
File diff suppressed because it is too large
Load Diff
+23
-17
@@ -10,44 +10,49 @@ 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.cps = 1;
|
||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||
this.lastBegin = 0; // query begin of last tick
|
||||
this.lastEnd = 0; // query end of last tick
|
||||
this.getTime = getTime; // get absolute time
|
||||
this.onToggle = onToggle;
|
||||
this.latency = latency;
|
||||
this.latency = latency; // fixed trigger time offset
|
||||
const round = (x) => Math.round(x * 1000) / 1000;
|
||||
this.clock = createClock(
|
||||
getTime,
|
||||
// called slightly before each cycle
|
||||
(phase, duration, tick) => {
|
||||
if (tick === 0) {
|
||||
this.origin = phase;
|
||||
}
|
||||
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
|
||||
const time = getTime();
|
||||
const begin = this.lastEnd;
|
||||
this.lastBegin = begin;
|
||||
const end = round(begin + duration * this.cps);
|
||||
this.lastEnd = end;
|
||||
const haps = this.pattern.queryArc(begin, end);
|
||||
const tickdeadline = phase - time; // time left till phase begins
|
||||
this.lastTick = time + tickdeadline;
|
||||
|
||||
haps.forEach((hap) => {
|
||||
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||
const deadline = hap.whole.begin + this.origin - time + latency;
|
||||
const duration = hap.duration * 1;
|
||||
onTrigger?.(hap, deadline, duration);
|
||||
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
|
||||
const duration = hap.duration / this.cps;
|
||||
onTrigger?.(hap, deadline, duration, this.cps);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger(`[cyclist] error: ${e.message}`);
|
||||
onError?.(e);
|
||||
}
|
||||
}, // 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;
|
||||
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
|
||||
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
|
||||
}
|
||||
setStarted(v) {
|
||||
this.started = v;
|
||||
@@ -69,6 +74,7 @@ export class Cyclist {
|
||||
stop() {
|
||||
logger('[cyclist] stop');
|
||||
this.clock.stop();
|
||||
this.lastEnd = 0;
|
||||
this.setStarted(false);
|
||||
}
|
||||
setPattern(pat, autostart = false) {
|
||||
|
||||
+112
-6
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
draw.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/draw.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@@ -9,17 +9,29 @@ import { Pattern, getTime, State, TimeSpan } from './index.mjs';
|
||||
export const getDrawContext = (id = 'test-canvas') => {
|
||||
let canvas = document.querySelector('#' + id);
|
||||
if (!canvas) {
|
||||
const scale = 2; // 2 = crisp on retina screens
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.id = id;
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0;z-index:5';
|
||||
canvas.width = window.innerWidth * scale;
|
||||
canvas.height = window.innerHeight * scale;
|
||||
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
|
||||
document.body.prepend(canvas);
|
||||
let timeout;
|
||||
window.addEventListener('resize', () => {
|
||||
timeout && clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
canvas.width = window.innerWidth * scale;
|
||||
canvas.height = window.innerHeight * scale;
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
return canvas.getContext('2d');
|
||||
};
|
||||
|
||||
Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
||||
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
|
||||
if (typeof window === 'undefined') {
|
||||
return this;
|
||||
}
|
||||
if (window.strudelAnimation) {
|
||||
cancelAnimationFrame(window.strudelAnimation);
|
||||
}
|
||||
@@ -51,7 +63,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
||||
|
||||
export const cleanupDraw = (clearScreen = true) => {
|
||||
const ctx = getDrawContext();
|
||||
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
|
||||
if (window.strudelAnimation) {
|
||||
cancelAnimationFrame(window.strudelAnimation);
|
||||
}
|
||||
@@ -65,3 +77,97 @@ Pattern.prototype.onPaint = function (onPaint) {
|
||||
this.context = { onPaint };
|
||||
return this;
|
||||
};
|
||||
|
||||
// const round = (x) => Math.round(x * 1000) / 1000;
|
||||
|
||||
// encapsulates starting and stopping animation frames
|
||||
export class Framer {
|
||||
constructor(onFrame, onError) {
|
||||
this.onFrame = onFrame;
|
||||
this.onError = onError;
|
||||
}
|
||||
start() {
|
||||
const self = this;
|
||||
let frame = requestAnimationFrame(function updateHighlights(time) {
|
||||
try {
|
||||
self.onFrame(time);
|
||||
} catch (err) {
|
||||
self.onError(err);
|
||||
}
|
||||
frame = requestAnimationFrame(updateHighlights);
|
||||
});
|
||||
self.cancel = () => {
|
||||
cancelAnimationFrame(frame);
|
||||
};
|
||||
}
|
||||
stop() {
|
||||
if (this.cancel) {
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// syncs animation frames to a cyclist scheduler
|
||||
// see vite-vanilla-repl-cm6 for an example
|
||||
export class Drawer {
|
||||
constructor(onDraw, drawTime) {
|
||||
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
|
||||
lookbehind = Math.abs(lookbehind);
|
||||
this.visibleHaps = [];
|
||||
this.lastFrame = null;
|
||||
this.drawTime = drawTime;
|
||||
this.framer = new Framer(
|
||||
() => {
|
||||
if (!this.scheduler) {
|
||||
console.warn('Drawer: no scheduler');
|
||||
return;
|
||||
}
|
||||
// calculate current frame time (think right side of screen for pianoroll)
|
||||
const phase = this.scheduler.now() + lookahead;
|
||||
// first frame just captures the phase
|
||||
if (this.lastFrame === null) {
|
||||
this.lastFrame = phase;
|
||||
return;
|
||||
}
|
||||
// query haps from last frame till now. take last 100ms max
|
||||
const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase);
|
||||
this.lastFrame = phase;
|
||||
this.visibleHaps = (this.visibleHaps || [])
|
||||
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
|
||||
.filter((h) => h.whole.end >= phase - lookbehind - lookahead)
|
||||
// add new haps with onset (think right edge bars scrolling in)
|
||||
.concat(haps.filter((h) => h.hasOnset()));
|
||||
const time = phase - lookahead;
|
||||
onDraw(this.visibleHaps, time, this);
|
||||
},
|
||||
(err) => {
|
||||
console.warn('draw error', err);
|
||||
},
|
||||
);
|
||||
}
|
||||
invalidate(scheduler = this.scheduler) {
|
||||
if (!scheduler) {
|
||||
return;
|
||||
}
|
||||
this.scheduler = scheduler;
|
||||
const t = scheduler.now();
|
||||
let [_, lookahead] = this.drawTime;
|
||||
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
|
||||
// remove all future haps
|
||||
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
|
||||
// query future haps
|
||||
const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query..
|
||||
// append future haps
|
||||
this.visibleHaps = this.visibleHaps.concat(futureHaps);
|
||||
}
|
||||
start(scheduler) {
|
||||
this.scheduler = scheduler;
|
||||
this.invalidate();
|
||||
this.framer.start();
|
||||
}
|
||||
stop() {
|
||||
if (this.framer) {
|
||||
this.framer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
/*
|
||||
evaluate.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/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) => {
|
||||
@@ -42,11 +37,12 @@ function safeEval(str, options = {}) {
|
||||
}
|
||||
|
||||
export const evaluate = async (code, transpiler) => {
|
||||
if (!scoped) {
|
||||
await evalScope(); // at least scope Pattern.prototype.boostrap
|
||||
}
|
||||
let meta = {};
|
||||
if (transpiler) {
|
||||
code = transpiler(code); // transform syntactically correct js code to semantically usable code
|
||||
// transform syntactically correct js code to semantically usable code
|
||||
const transpiled = transpiler(code);
|
||||
code = transpiled.output;
|
||||
meta = transpiled;
|
||||
}
|
||||
// if no transpiler is given, we expect a single instruction (!wrapExpression)
|
||||
const options = { wrapExpression: !!transpiler };
|
||||
@@ -56,5 +52,5 @@ export const evaluate = async (code, transpiler) => {
|
||||
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 };
|
||||
return { mode: 'javascript', pattern: evaluated, meta };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,8 @@
|
||||
# vite-vanilla-repl-cm6
|
||||
|
||||
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
|
||||
|
||||
```sh
|
||||
npm i
|
||||
npm run dev
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite Vanilla Strudel REPL</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav>
|
||||
<button id="play">eval</button>
|
||||
<button id="stop">stop</button>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<div id="editor"></div>
|
||||
<div id="output"></div>
|
||||
</div>
|
||||
<canvas id="roll"></canvas>
|
||||
</main>
|
||||
<script type="module" src="./main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { StrudelMirror } from '@strudel/codemirror';
|
||||
import { funk42 } from './tunes';
|
||||
import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
|
||||
import './style.css';
|
||||
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
||||
import { transpiler } from '@strudel.cycles/transpiler';
|
||||
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
|
||||
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
|
||||
|
||||
// init canvas
|
||||
const canvas = document.getElementById('roll');
|
||||
canvas.width = canvas.width * 2;
|
||||
canvas.height = canvas.height * 2;
|
||||
const drawContext = canvas.getContext('2d');
|
||||
const drawTime = [-2, 2]; // time window of drawn haps
|
||||
|
||||
const editor = new StrudelMirror({
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime: () => getAudioContext().currentTime,
|
||||
transpiler,
|
||||
root: document.getElementById('editor'),
|
||||
initialCode: funk42,
|
||||
drawTime,
|
||||
onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }),
|
||||
prebake: async () => {
|
||||
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
|
||||
const loadModules = evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
);
|
||||
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
|
||||
},
|
||||
});
|
||||
|
||||
document.getElementById('play').addEventListener('click', () => editor.evaluate());
|
||||
document.getElementById('stop').addEventListener('click', () => editor.stop());
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "vite-vanilla-repl-cm6",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strudel/codemirror": "workspace:*",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/mini": "workspace:*",
|
||||
"@strudel.cycles/soundfonts": "workspace:*",
|
||||
"@strudel.cycles/tonal": "workspace:*",
|
||||
"@strudel.cycles/transpiler": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
background: #282c34;
|
||||
}
|
||||
|
||||
main {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
flex-grow: 1;
|
||||
max-height: 100%;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#editor {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#roll {
|
||||
height: 300px;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
|
||||
await samples('github:felixroos/samples/main')
|
||||
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||
|
||||
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
|
||||
// make it 22 edo
|
||||
.fmap(v => Math.pow(2,v/22))
|
||||
// mess with the base frequency
|
||||
.mul("<300 [300@3 200]>/8").freq()
|
||||
.layer(
|
||||
// chords
|
||||
x=>x.div(freq(2)).s("flute").euclidLegato("<3 2>",8)
|
||||
.shape(.4).lpf(sine.range(800,4000).slow(8)),
|
||||
// adlibs
|
||||
x=>x.arp("{0 3 2 [1 3]}%1.5")
|
||||
.s('xylo').mul(freq(2))
|
||||
.delay(.5).delayfeedback(.4).juxBy(.5, rev)
|
||||
.hpf(sine.range(200,3000).slow(8)),
|
||||
// bass
|
||||
x=>x.arp("[0 [2 1?]](5,8)").s('sawtooth').div(freq(4))
|
||||
.lpf(sine.range(400,2000).slow(8)).lpq(8).shape(.4)
|
||||
.off(1/8, x=>x.mul(freq(2)).degradeBy(.5)).gain(.3)
|
||||
).clip(1).release(.2)
|
||||
.stack(
|
||||
// drums
|
||||
s("bd sd:<2 1>, [~ hh]*2, [~ rim]").bank('RolandTR909')
|
||||
.off(1/8, x=>x.speed(2).gain(.4)).sometimes(ply(2)).gain(.8)
|
||||
.mask("<0@4 1@12>/4")
|
||||
.reset("<x@15 [x(3,8) x*[4 8]]>")
|
||||
// wait for it...
|
||||
).fast(2/3)
|
||||
//.crush(6) // remove "//" if you dare`;
|
||||
|
||||
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
|
||||
|
||||
await samples('github:felixroos/samples/main')
|
||||
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||
|
||||
addVoicings('hip', {
|
||||
m11: ['2M 3m 4P 7m'],
|
||||
'^7#11': ['3M 4A 5P 7M'],
|
||||
}, ['C4', 'C6'])
|
||||
|
||||
stack(
|
||||
stack(
|
||||
"<Bbm11 A^7#11>/2".voicings('hip').note()
|
||||
.s("gm_epiano1:2")
|
||||
.arp("[<[0 1 2 3] [3 2 1 0]> ~@5]/2")
|
||||
.release(2).late(.25).lpf(2000),
|
||||
"<Bb1 A1>/2".note().s('gm_acoustic_bass'),
|
||||
n("<0 2 3>(3,8)".off(1/8, add(4)))
|
||||
.scale("<Bb4:minor A4:lydian>/2")
|
||||
.s('gm_electric_guitar_jazz')
|
||||
.decay(sine.range(.05, .2).slow(32)).sustain(0)
|
||||
.delay(.5).lpf(sine.range(100,5000).slow(64))
|
||||
.gain(.7).room(.5).pan(sine.range(0,1).slow(11))
|
||||
).add(perlin.range(0,.25).note()),
|
||||
stack(
|
||||
s("bd:1(3,8) rim").bank('RolandTR707').slow(2).room("<0 <.1 .6>>")
|
||||
.when("<0@7 1>",x=>x.echoWith(3, .0625, (x,i) => x.speed(1+i*.24))),
|
||||
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
|
||||
)
|
||||
)
|
||||
.late("[0 .05]*2").late(12)
|
||||
|
||||
`;
|
||||
|
||||
export const funk42 = `// froos - how to funk in 42 lines of code
|
||||
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
|
||||
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
|
||||
|
||||
await samples('github:felixroos/samples/main')
|
||||
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||
|
||||
setcps(.5)
|
||||
|
||||
let drums = stack(
|
||||
s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"),
|
||||
s("hh*4").begin(.2).release(.02).end(.25).release(.02)
|
||||
.gain(.3).bank('RolandTR707').late(.02).room(.5),
|
||||
//s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02)
|
||||
).fast(2)
|
||||
|
||||
let wurli = note(\`<
|
||||
[[a2,g3,[b3 c4],e4] ~ [g3,c4,e4](3,8)@4 ~@2]!3
|
||||
[[e2,e3,a3,b3,e4]@3 [e2,e3,ab3,b3,e4]@5]>\`)
|
||||
.s("gm_epiano1:5").decay(.2).sustain("<[1 0@7]!3 1>")
|
||||
.gain("<[.8@2 .4@14]!3 .7>").room(.3)
|
||||
|
||||
let organ = note("<[~@3 [a3,d4,f#4]@2 [[a3,c4,e4]@2 ~] ~@2]!3 ~>".add(12))
|
||||
.s("gm_percussive_organ:2").gain(.6).lpf(1800).pan(.2).room(.3);
|
||||
|
||||
let clav = note(\`<
|
||||
[~@3 a2 [g3,[b3 c4],e4]@2 ~ a2 [g3,b3,e4] ~@2 [g3,c4,e4] ~@4]!3
|
||||
[~@3 e3 [[a3 b3],c3,e3]@2 ~ e2 [e3,a3]@3 [b3,e3] ~@2 [b3,e3]@2]>\`)
|
||||
.s("gm_clavinet:1").decay("<.25!3 [.25 .4]>").sustain(0)
|
||||
.gain(.7).pan(.8).room(.2);
|
||||
|
||||
let bass = note(\`<
|
||||
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
|
||||
[a1 [~ [g2 a2]] [e3 d3] [c3 [g3 a3]]]
|
||||
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
|
||||
[e2@6 e1@5 e1 [[d2 e3] g1]@4]
|
||||
>\`).s("gm_electric_bass_pick:1").release(.1)
|
||||
|
||||
stack(
|
||||
drums
|
||||
,wurli
|
||||
,organ
|
||||
,clav
|
||||
,bass
|
||||
)`;
|
||||
@@ -6,5 +6,3 @@ This folder demonstrates how to set up a strudel repl using vite and vanilla JS.
|
||||
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)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^3.2.0"
|
||||
"vite": "^4.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
|
||||
@@ -32,7 +32,11 @@ export class Hap {
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return this.whole.end.sub(this.whole.begin);
|
||||
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
|
||||
}
|
||||
|
||||
get endClipped() {
|
||||
return this.whole.begin.add(this.duration);
|
||||
}
|
||||
|
||||
wholeOrPart() {
|
||||
|
||||
@@ -18,11 +18,13 @@ export * from './util.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * from './evaluate.mjs';
|
||||
export * from './repl.mjs';
|
||||
export * from './cyclist.mjs';
|
||||
export * from './logger.mjs';
|
||||
export * from './time.mjs';
|
||||
export * from './draw.mjs';
|
||||
export * from './animate.mjs';
|
||||
export * from './pianoroll.mjs';
|
||||
export * from './spiral.mjs';
|
||||
export * from './ui.mjs';
|
||||
export { default as drawLine } from './drawLine.mjs';
|
||||
export { default as gist } from './gist.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.6.8",
|
||||
"version": "0.9.0",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||
"devDependencies": {
|
||||
"vite": "^3.2.2",
|
||||
"vitest": "^0.25.7"
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.33.0"
|
||||
}
|
||||
}
|
||||
|
||||
+162
-322
@@ -21,141 +21,6 @@ let stringParser;
|
||||
// intended to use with mini to automatically interpret all strings as mini notation
|
||||
export const setStringParser = (parser) => (stringParser = parser);
|
||||
|
||||
const alignments = ['in', 'out', 'mix', 'squeeze', 'squeezeout', 'trig', 'trigzero'];
|
||||
|
||||
const methodRegistry = [];
|
||||
const getterRegistry = [];
|
||||
const controlRegistry = [];
|
||||
const controlSubscribers = [];
|
||||
const composifiedRegistry = [];
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Magic for supporting higher order composition of method chains
|
||||
|
||||
// Dresses the given (unary) function with methods for composition chaining, so e.g.
|
||||
// `fast(2).iter(4)` composes to pattern functions into a new one.
|
||||
function composify(func) {
|
||||
if (!func.__composified) {
|
||||
for (const [name, method] of methodRegistry) {
|
||||
func[name] = method;
|
||||
}
|
||||
for (const [name, getter] of getterRegistry) {
|
||||
Object.defineProperty(func, name, getter);
|
||||
}
|
||||
func.__composified = true;
|
||||
composifiedRegistry.push(func);
|
||||
} else {
|
||||
console.log('Warning: attempt at composifying a function more than once');
|
||||
}
|
||||
return func;
|
||||
}
|
||||
|
||||
export function registerMethod(name, addAlignments = false, addControls = false) {
|
||||
if (addAlignments || addControls) {
|
||||
// This method needs to make its 'this' object available to chained alignments and/or
|
||||
// control parameters, so it has to be implemented as a getter
|
||||
const getter = {
|
||||
get: function () {
|
||||
const func = this;
|
||||
const wrapped = function (...args) {
|
||||
const composed = (pat) => func(pat)[name](...args);
|
||||
return composify(composed);
|
||||
};
|
||||
|
||||
if (addAlignments) {
|
||||
for (const alignment of alignments) {
|
||||
wrapped[alignment] = function (...args) {
|
||||
const composed = (pat) => func(pat)[name][alignment](...args);
|
||||
return composify(composed);
|
||||
};
|
||||
for (const [controlname, controlfunc] of controlRegistry) {
|
||||
wrapped[alignment][controlname] = function (...args) {
|
||||
const composed = (pat) => func(pat)[name][alignment](controlfunc(...args));
|
||||
return composify(composed);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (addControls) {
|
||||
for (const [controlname, controlfunc] of controlRegistry) {
|
||||
wrapped[controlname] = function (...args) {
|
||||
const composed = (pat) => func(pat)[name](controlfunc(...args));
|
||||
return composify(composed);
|
||||
};
|
||||
}
|
||||
}
|
||||
return wrapped;
|
||||
},
|
||||
};
|
||||
|
||||
getterRegistry.push([name, getter]);
|
||||
|
||||
// Add to functions already 'composified'
|
||||
for (const composified of composifiedRegistry) {
|
||||
Object.defineProperty(composified, name, getter);
|
||||
}
|
||||
} else {
|
||||
// No chained alignments/controls needed, so we can just add as a plain method,
|
||||
// probably more efficient this way?
|
||||
const method = function (...args) {
|
||||
const func = this;
|
||||
const composed = (pat) => func(pat)[name](...args);
|
||||
return composify(composed);
|
||||
};
|
||||
|
||||
methodRegistry.push([name, method]);
|
||||
|
||||
// Add to functions already 'composified'
|
||||
for (const composified of composifiedRegistry) {
|
||||
composified[name] = method;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerControl(controlname, controlfunc) {
|
||||
registerMethod(controlname);
|
||||
controlRegistry.push([controlname, controlfunc]);
|
||||
for (const subscriber of controlSubscribers) {
|
||||
subscriber(controlname, controlfunc);
|
||||
}
|
||||
}
|
||||
|
||||
export function withControls(func) {
|
||||
for (const [controlname, controlfunc] of controlRegistry) {
|
||||
func(controlname, controlfunc);
|
||||
}
|
||||
controlSubscribers.push(func);
|
||||
}
|
||||
|
||||
export function addToPrototype(name, func) {
|
||||
Pattern.prototype[name] = func;
|
||||
registerMethod(name);
|
||||
}
|
||||
|
||||
export function curryPattern(func, arity = func.length) {
|
||||
const fn = function curried(...args) {
|
||||
if (args.length >= arity) {
|
||||
return func.apply(this, args);
|
||||
}
|
||||
|
||||
const partial = function (...args2) {
|
||||
return curried.apply(this, args.concat(args2));
|
||||
};
|
||||
if (args.length == arity - 1) {
|
||||
return composify(partial);
|
||||
}
|
||||
|
||||
return partial;
|
||||
};
|
||||
if (arity == 1) {
|
||||
composify(fn);
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// The core Pattern class
|
||||
|
||||
/** @class Class representing a pattern. */
|
||||
export class Pattern {
|
||||
/**
|
||||
@@ -607,15 +472,15 @@ export class Pattern {
|
||||
/**
|
||||
* Returns a new pattern with the given location information added to the
|
||||
* context of every hap.
|
||||
* @param {Number} start
|
||||
* @param {Number} end
|
||||
* @param {Number} start start offset
|
||||
* @param {Number} end end offset
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
withLocation(start, end) {
|
||||
withLoc(start, end) {
|
||||
const location = {
|
||||
start: { line: start[0], column: start[1], offset: start[2] },
|
||||
end: { line: end[0], column: end[1], offset: end[2] },
|
||||
start,
|
||||
end,
|
||||
};
|
||||
return this.withContext((context) => {
|
||||
const locations = (context.locations || []).concat([location]);
|
||||
@@ -623,32 +488,6 @@ export class Pattern {
|
||||
});
|
||||
}
|
||||
|
||||
withMiniLocation(start, end) {
|
||||
const offset = {
|
||||
start: { line: start[0], column: start[1], offset: start[2] },
|
||||
end: { line: end[0], column: end[1], offset: end[2] },
|
||||
};
|
||||
return this.withContext((context) => {
|
||||
let locations = context.locations || [];
|
||||
locations = locations.map(({ start, end }) => {
|
||||
const colOffset = start.line === 1 ? offset.start.column : 0;
|
||||
return {
|
||||
start: {
|
||||
...start,
|
||||
line: start.line - 1 + (offset.start.line - 1) + 1,
|
||||
column: start.column - 1 + colOffset,
|
||||
},
|
||||
end: {
|
||||
...end,
|
||||
line: end.line - 1 + (offset.start.line - 1) + 1,
|
||||
column: end.column - 1 + colOffset,
|
||||
},
|
||||
};
|
||||
});
|
||||
return { ...context, locations };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new Pattern, which only returns haps that meet the given test.
|
||||
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
|
||||
@@ -778,9 +617,7 @@ export class Pattern {
|
||||
* @noAutocomplete
|
||||
*/
|
||||
get firstCycleValues() {
|
||||
return this.sortHapsByPart()
|
||||
.firstCycle()
|
||||
.map((hap) => hap.value);
|
||||
return this.firstCycle().map((hap) => hap.value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -830,7 +667,7 @@ export class Pattern {
|
||||
const otherPat = reify(other);
|
||||
return this.fmap((a) => otherPat.fmap((b) => func(a)(b))).squeezeJoin();
|
||||
}
|
||||
_opSqueezeout(other, func) {
|
||||
_opSqueezeOut(other, func) {
|
||||
const thisPat = this;
|
||||
const otherPat = reify(other);
|
||||
return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a))).squeezeJoin();
|
||||
@@ -946,13 +783,14 @@ export class Pattern {
|
||||
hap.setContext({
|
||||
...hap.context,
|
||||
onTrigger: (...args) => {
|
||||
if (!dominant && hap.context.onTrigger) {
|
||||
hap.context.onTrigger(...args);
|
||||
}
|
||||
// run previously set trigger, if it exists
|
||||
hap.context.onTrigger?.(...args);
|
||||
onTrigger(...args);
|
||||
},
|
||||
// we need this to know later if the default trigger should still fire
|
||||
dominantTrigger: dominant,
|
||||
// if dominantTrigger is set to true, the default output (webaudio) will be disabled
|
||||
// when using multiple triggers, you cannot flip this flag to false again!
|
||||
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
|
||||
dominantTrigger: hap.context.dominantTrigger || dominant,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -997,11 +835,11 @@ function groupHapsBy(eq, haps) {
|
||||
const congruent = (a, b) => a.spanEquals(b);
|
||||
// Pattern<Hap<T>> -> Pattern<Hap<T[]>>
|
||||
// returned pattern contains arrays of congruent haps
|
||||
addToPrototype('collect', function () {
|
||||
Pattern.prototype.collect = function () {
|
||||
return this.withHaps((haps) =>
|
||||
groupHapsBy(congruent, haps).map((_haps) => new Hap(_haps[0].whole, _haps[0].part, _haps, {})),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects indices in in stacked notes.
|
||||
@@ -1009,12 +847,12 @@ addToPrototype('collect', function () {
|
||||
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
|
||||
* .arpWith(haps => haps[2])
|
||||
* */
|
||||
addToPrototype('arpWith', function (func) {
|
||||
Pattern.prototype.arpWith = function (func) {
|
||||
return this.collect()
|
||||
.fmap((v) => reify(func(v)))
|
||||
.innerJoin()
|
||||
.withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value)));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects indices in in stacked notes.
|
||||
@@ -1022,28 +860,27 @@ addToPrototype('arpWith', function (func) {
|
||||
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
|
||||
* .arp("0 [0,2] 1 [0,2]").slow(2)
|
||||
* */
|
||||
addToPrototype('arp', function (pat) {
|
||||
Pattern.prototype.arp = function (pat) {
|
||||
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
/*
|
||||
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
|
||||
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
|
||||
* @name weave
|
||||
* @memberof Pattern
|
||||
* @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd"))
|
||||
* @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd"))
|
||||
*/
|
||||
|
||||
addToPrototype('weave', function (t, ...pats) {
|
||||
return this.weaveWith(t, ...pats.map((x) => set.out(x)));
|
||||
});
|
||||
|
||||
/**
|
||||
*/
|
||||
/*
|
||||
* Like 'weave', but accepts functions rather than patterns, which are applied to the pattern.
|
||||
* @name weaveWith
|
||||
* @memberof Pattern
|
||||
*/
|
||||
|
||||
addToPrototype('weaveWith', function (t, ...funcs) {
|
||||
const pat = this;
|
||||
@@ -1054,6 +891,7 @@ addToPrototype('weaveWith', function (t, ...funcs) {
|
||||
}
|
||||
return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t);
|
||||
});
|
||||
*/
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// compose matrix functions
|
||||
@@ -1151,15 +989,15 @@ function _composeOp(a, b, func) {
|
||||
func: [(a, b) => b(a)],
|
||||
};
|
||||
|
||||
const hows = alignments.map((x) => x.charAt(0).toUpperCase() + x.slice(1));
|
||||
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Trig', 'Trigzero'];
|
||||
|
||||
// generate methods to do what and how
|
||||
for (const [what, [op, preprocess]] of Object.entries(composers)) {
|
||||
// make plain version, e.g. pat._add(value) adds that plain value
|
||||
// to all the values in pat
|
||||
addToPrototype('_' + what, function (value) {
|
||||
Pattern.prototype['_' + what] = function (value) {
|
||||
return this.fmap((x) => op(x, value));
|
||||
});
|
||||
};
|
||||
|
||||
// make patternified monster version
|
||||
Object.defineProperty(Pattern.prototype, what, {
|
||||
@@ -1173,7 +1011,7 @@ function _composeOp(a, b, func) {
|
||||
|
||||
// add methods to that function for each behaviour
|
||||
for (const how of hows) {
|
||||
const howfunc = function (...other) {
|
||||
wrapper[how.toLowerCase()] = function (...other) {
|
||||
var howpat = pat;
|
||||
other = sequence(other);
|
||||
if (preprocess) {
|
||||
@@ -1191,41 +1029,19 @@ function _composeOp(a, b, func) {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
for (const [controlname, controlfunc] of controlRegistry) {
|
||||
howfunc[controlname] = (...args) => howfunc(controlfunc(...args));
|
||||
}
|
||||
wrapper[how.toLowerCase()] = howfunc;
|
||||
}
|
||||
wrapper.squeezein = wrapper.squeeze;
|
||||
|
||||
for (const [controlname, controlfunc] of controlRegistry) {
|
||||
wrapper[controlname] = (...args) => wrapper.in(controlfunc(...args));
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
});
|
||||
|
||||
registerMethod(what, true, true);
|
||||
}
|
||||
|
||||
// Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
|
||||
for (const howLower of alignments) {
|
||||
// Using a 'get'ted function so that all the controls are added
|
||||
Object.defineProperty(Pattern.prototype, howLower, {
|
||||
get: function () {
|
||||
const pat = this;
|
||||
const howfunc = function (...args) {
|
||||
return pat.set[howLower](args);
|
||||
};
|
||||
for (const [controlname, controlfunc] of controlRegistry) {
|
||||
howfunc[controlname] = (...args) => howfunc(controlfunc(...args));
|
||||
}
|
||||
return howfunc;
|
||||
},
|
||||
});
|
||||
registerMethod(howLower, false, true);
|
||||
// Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
|
||||
for (const how of hows) {
|
||||
Pattern.prototype[how.toLowerCase()] = function (...args) {
|
||||
return this.set[how.toLowerCase()](args);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// binary composers
|
||||
@@ -1237,36 +1053,36 @@ function _composeOp(a, b, func) {
|
||||
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
|
||||
* .slow(4)
|
||||
*/
|
||||
addToPrototype('struct', function (...args) {
|
||||
Pattern.prototype.struct = function (...args) {
|
||||
return this.keepif.out(...args);
|
||||
});
|
||||
addToPrototype('structAll', function (...args) {
|
||||
};
|
||||
Pattern.prototype.structAll = function (...args) {
|
||||
return this.keep.out(...args);
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Returns silence when mask is 0 or "~"
|
||||
*
|
||||
* @example
|
||||
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>").slow(2)
|
||||
*/
|
||||
addToPrototype('mask', function (...args) {
|
||||
Pattern.prototype.mask = function (...args) {
|
||||
return this.keepif.in(...args);
|
||||
});
|
||||
addToPrototype('maskAll', function (...args) {
|
||||
};
|
||||
Pattern.prototype.maskAll = function (...args) {
|
||||
return this.keep.in(...args);
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Resets the pattern to the start of the cycle for each onset of the reset pattern.
|
||||
*
|
||||
* @example
|
||||
* s("<bd lt> sd, hh*4").reset("<x@3 x(3,8)>")
|
||||
*/
|
||||
addToPrototype('reset', function (...args) {
|
||||
Pattern.prototype.reset = function (...args) {
|
||||
return this.keepif.trig(...args);
|
||||
});
|
||||
addToPrototype('resetAll', function (...args) {
|
||||
};
|
||||
Pattern.prototype.resetAll = function (...args) {
|
||||
return this.keep.trig(...args);
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Restarts the pattern for each onset of the restart pattern.
|
||||
* While reset will only reset the current cycle, restart will start from cycle 0.
|
||||
@@ -1274,18 +1090,20 @@ function _composeOp(a, b, func) {
|
||||
* @example
|
||||
* s("<bd lt> sd, hh*4").restart("<x@3 x(3,8)>")
|
||||
*/
|
||||
addToPrototype('restart', function (...args) {
|
||||
Pattern.prototype.restart = function (...args) {
|
||||
return this.keepif.trigzero(...args);
|
||||
});
|
||||
addToPrototype('restartAll', function (...args) {
|
||||
};
|
||||
Pattern.prototype.restartAll = function (...args) {
|
||||
return this.keep.trigzero(...args);
|
||||
});
|
||||
};
|
||||
})();
|
||||
|
||||
// aliases
|
||||
export const polyrhythm = stack;
|
||||
export const pr = stack;
|
||||
|
||||
export const pm = polymeter;
|
||||
|
||||
// methods that create patterns, which are added to patternified Pattern methods
|
||||
// TODO: remove? this is only used in old transpiler (shapeshifter)
|
||||
Pattern.prototype.factories = {
|
||||
@@ -1443,6 +1261,20 @@ export function timeCat(...timepats) {
|
||||
return stack(...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to arrange multiple patterns together over multiple cycles.
|
||||
* Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* arrange([4, "<c a f e>(3,8)"],[2, "<g a>(5,8)"]).note()
|
||||
*/
|
||||
export function arrange(...sections) {
|
||||
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
|
||||
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
|
||||
return timeCat(...sections).slow(total);
|
||||
}
|
||||
|
||||
export function fastcat(...pats) {
|
||||
return slowcat(...pats)._fast(pats.length);
|
||||
}
|
||||
@@ -1519,73 +1351,36 @@ export function polymeter(...args) {
|
||||
return polymeterSteps(0, ...args);
|
||||
}
|
||||
|
||||
// alias
|
||||
export function pm(...args) {
|
||||
polymeter(...args);
|
||||
}
|
||||
|
||||
export const mask = curryPattern((a, b) => reify(b).mask(a));
|
||||
export const struct = curryPattern((a, b) => reify(b).struct(a));
|
||||
export const superimpose = curryPattern((a, b) => reify(b).superimpose(...a));
|
||||
|
||||
const methodToFunction = function (name, addAlignments = false) {
|
||||
const func = curryPattern((a, b) => reify(b)[name](a));
|
||||
|
||||
withControls((controlname, controlfunc) => {
|
||||
func[controlname] = function (...pats) {
|
||||
return func(controlfunc(...pats));
|
||||
};
|
||||
});
|
||||
|
||||
if (addAlignments) {
|
||||
for (const alignment of alignments) {
|
||||
func[alignment] = curryPattern((a, b) => reify(b)[name][alignment](a));
|
||||
withControls((controlname, controlfunc) => {
|
||||
func[alignment][controlname] = function (...pats) {
|
||||
return func[alignment](controlfunc(...pats));
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return func;
|
||||
};
|
||||
export const mask = curry((a, b) => reify(b).mask(a));
|
||||
export const struct = curry((a, b) => reify(b).struct(a));
|
||||
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
|
||||
|
||||
// operators
|
||||
export const set = methodToFunction('set', true);
|
||||
export const keep = methodToFunction('keep', true);
|
||||
export const keepif = methodToFunction('keepif', true);
|
||||
export const add = methodToFunction('add', true);
|
||||
export const sub = methodToFunction('sub', true);
|
||||
export const mul = methodToFunction('mul', true);
|
||||
export const div = methodToFunction('div', true);
|
||||
export const mod = methodToFunction('mod', true);
|
||||
export const pow = methodToFunction('pow', true);
|
||||
export const band = methodToFunction('band', true);
|
||||
export const bor = methodToFunction('bor', true);
|
||||
export const bxor = methodToFunction('bxor', true);
|
||||
export const blshift = methodToFunction('blshift', true);
|
||||
export const brshift = methodToFunction('brshift', true);
|
||||
export const lt = methodToFunction('lt', true);
|
||||
export const gt = methodToFunction('gt', true);
|
||||
export const lte = methodToFunction('lte', true);
|
||||
export const gte = methodToFunction('gte', true);
|
||||
export const eq = methodToFunction('eq', true);
|
||||
export const eqt = methodToFunction('eqt', true);
|
||||
export const ne = methodToFunction('ne', true);
|
||||
export const net = methodToFunction('net', true);
|
||||
export const and = methodToFunction('and', true);
|
||||
export const or = methodToFunction('or', true);
|
||||
export const func = methodToFunction('func', true);
|
||||
|
||||
// alignments
|
||||
// export const in = methodToFunction('in'); // reserved word :(
|
||||
export const out = methodToFunction('out');
|
||||
export const mix = methodToFunction('mix');
|
||||
export const squeeze = methodToFunction('squeeze');
|
||||
export const squeezeout = methodToFunction('squeezeout');
|
||||
export const trig = methodToFunction('trig');
|
||||
export const trigzero = methodToFunction('trigzero');
|
||||
export const set = curry((a, b) => reify(b).set(a));
|
||||
export const keep = curry((a, b) => reify(b).keep(a));
|
||||
export const keepif = curry((a, b) => reify(b).keepif(a));
|
||||
export const add = curry((a, b) => reify(b).add(a));
|
||||
export const sub = curry((a, b) => reify(b).sub(a));
|
||||
export const mul = curry((a, b) => reify(b).mul(a));
|
||||
export const div = curry((a, b) => reify(b).div(a));
|
||||
export const mod = curry((a, b) => reify(b).mod(a));
|
||||
export const pow = curry((a, b) => reify(b).pow(a));
|
||||
export const band = curry((a, b) => reify(b).band(a));
|
||||
export const bor = curry((a, b) => reify(b).bor(a));
|
||||
export const bxor = curry((a, b) => reify(b).bxor(a));
|
||||
export const blshift = curry((a, b) => reify(b).blshift(a));
|
||||
export const brshift = curry((a, b) => reify(b).brshift(a));
|
||||
export const lt = curry((a, b) => reify(b).lt(a));
|
||||
export const gt = curry((a, b) => reify(b).gt(a));
|
||||
export const lte = curry((a, b) => reify(b).lte(a));
|
||||
export const gte = curry((a, b) => reify(b).gte(a));
|
||||
export const eq = curry((a, b) => reify(b).eq(a));
|
||||
export const eqt = curry((a, b) => reify(b).eqt(a));
|
||||
export const ne = curry((a, b) => reify(b).ne(a));
|
||||
export const net = curry((a, b) => reify(b).net(a));
|
||||
export const and = curry((a, b) => reify(b).and(a));
|
||||
export const or = curry((a, b) => reify(b).or(a));
|
||||
export const func = curry((a, b) => reify(b).func(a));
|
||||
|
||||
/**
|
||||
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
|
||||
@@ -1604,10 +1399,7 @@ export function register(name, func, patternify = true) {
|
||||
return result;
|
||||
}
|
||||
const arity = func.length;
|
||||
|
||||
registerMethod(name);
|
||||
|
||||
var pfunc;
|
||||
var pfunc; // the patternified function
|
||||
|
||||
if (patternify) {
|
||||
pfunc = function (...args) {
|
||||
@@ -1626,14 +1418,8 @@ export function register(name, func, patternify = true) {
|
||||
.map((_, i) => args[i] ?? undefined);
|
||||
return func(...args, pat);
|
||||
};
|
||||
mapFn = curryPattern(mapFn, arity - 1);
|
||||
|
||||
const app = function (acc, p, i) {
|
||||
return acc.appLeft(p);
|
||||
};
|
||||
const start = left.fmap(mapFn);
|
||||
|
||||
return right.reduce(app, start).innerJoin();
|
||||
mapFn = curry(mapFn, null, arity - 1);
|
||||
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
|
||||
};
|
||||
} else {
|
||||
pfunc = function (...args) {
|
||||
@@ -1664,7 +1450,7 @@ export function register(name, func, patternify = true) {
|
||||
|
||||
// toplevel functions get curried as well as patternified
|
||||
// because pfunc uses spread args, we need to state the arity explicitly!
|
||||
return curryPattern(pfunc, arity);
|
||||
return curry(pfunc, null, arity);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
@@ -1771,6 +1557,24 @@ export const range2 = register('range2', function (min, max, pat) {
|
||||
return pat.fromBipolar()._range(min, max);
|
||||
});
|
||||
|
||||
/**
|
||||
* Allows dividing numbers via list notation using ":".
|
||||
* Returns a new pattern with just numbers.
|
||||
* @name ratio
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* ratio("1, 5:4, 3:2").mul(110).freq().s("piano").slow(2)
|
||||
*/
|
||||
export const ratio = register('ratio', (pat) =>
|
||||
pat.fmap((v) => {
|
||||
if (!Array.isArray(v)) {
|
||||
return v;
|
||||
}
|
||||
return v.slice(1).reduce((acc, n) => acc / n, v[0]);
|
||||
}),
|
||||
);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Structural and temporal transformations
|
||||
|
||||
@@ -1866,6 +1670,9 @@ export const ply = register('ply', function (factor, pat) {
|
||||
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
|
||||
*/
|
||||
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
|
||||
if (factor === 0) {
|
||||
return silence;
|
||||
}
|
||||
factor = Fraction(factor);
|
||||
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
||||
return fastQuery.withHapTime((t) => t.div(factor));
|
||||
@@ -1892,6 +1699,9 @@ export const hurry = register('hurry', function (r, pat) {
|
||||
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
|
||||
*/
|
||||
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
|
||||
if (factor === 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat._fast(Fraction(1).div(factor));
|
||||
});
|
||||
|
||||
@@ -2218,7 +2028,7 @@ export const jux = register('jux', function (func, pat) {
|
||||
* @example
|
||||
* "<0 [2 4]>"
|
||||
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
|
||||
* .scale('C minor').note().legato(.2)
|
||||
* .scale('C minor').note().clip(.2)
|
||||
*/
|
||||
export const { echoWith, echowith, stutWith, stutwith } = register(
|
||||
['echoWith', 'echowith', 'stutWith', 'stutwith'],
|
||||
@@ -2368,6 +2178,7 @@ export const velocity = register('velocity', function (velocity, pat) {
|
||||
/**
|
||||
*
|
||||
* Multiplies the hap duration with the given factor.
|
||||
* With samples, `clip` might be a better function to use ([more info](https://github.com/tidalcycles/strudel/pull/598))
|
||||
* @name legato
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
@@ -2432,14 +2243,19 @@ const _loopAt = function (factor, pat, cps = 1) {
|
||||
|
||||
/**
|
||||
* Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers.
|
||||
* Instead of a number, it also accepts a list of numbers from 0 to 1 to slice at specific points.
|
||||
* @name slice
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
|
||||
* @example
|
||||
* await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
* s("breaks125/2").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
|
||||
*/
|
||||
const slice = register(
|
||||
|
||||
export const slice = register(
|
||||
'slice',
|
||||
function (npat, ipat, opat) {
|
||||
return npat.innerBind((n) =>
|
||||
@@ -2447,9 +2263,9 @@ const slice = register(
|
||||
opat.outerBind((o) => {
|
||||
// If it's not an object, assume it's a string and make it a 's' control parameter
|
||||
o = o instanceof Object ? o : { s: o };
|
||||
// Remember we must stay pure and avoid editing the object directly
|
||||
const toAdd = { begin: i / n, end: (i + 1) / n, _slices: n };
|
||||
return pure({ ...toAdd, ...o });
|
||||
const begin = Array.isArray(n) ? n[i] : i / n;
|
||||
const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n;
|
||||
return pure({ begin, end, _slices: n, ...o });
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -2460,13 +2276,14 @@ const slice = register(
|
||||
/**
|
||||
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
|
||||
* @name splice
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
|
||||
* s("breaks165")
|
||||
* .splice(8, "0 1 [2 3 0]@2 3 0@2 7")
|
||||
* .hurry(0.65)
|
||||
*/
|
||||
const splice = register(
|
||||
|
||||
export const splice = register(
|
||||
'splice',
|
||||
function (npat, ipat, opat) {
|
||||
const sliced = slice(npat, ipat, opat);
|
||||
@@ -2483,10 +2300,33 @@ const splice = register(
|
||||
false, // turns off auto-patternification
|
||||
);
|
||||
|
||||
const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
|
||||
// this function will be redefined in repl.mjs to use the correct cps value.
|
||||
// It is still here to work in cases where repl.mjs is not used
|
||||
|
||||
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
|
||||
return _loopAt(factor, pat, 1);
|
||||
});
|
||||
|
||||
// the fit function will be redefined in repl.mjs to use the correct cps value.
|
||||
// It is still here to work in cases where repl.mjs is not used
|
||||
/**
|
||||
* Makes the sample fit its event duration. Good for rhythmical loops like drum breaks.
|
||||
* Similar to loopAt.
|
||||
* @name fit
|
||||
* @example
|
||||
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
||||
* s("rhodes/4").fit()
|
||||
*/
|
||||
export const fit = register('fit', (pat) =>
|
||||
pat.withHap((hap) =>
|
||||
hap.withValue((v) => ({
|
||||
...v,
|
||||
speed: 1 / hap.whole.duration,
|
||||
unit: 'c',
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Makes the sample fit the given number of cycles and cps value, by
|
||||
* changing the speed. Please note that at some point cps will be
|
||||
@@ -2500,6 +2340,6 @@ const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat)
|
||||
* s("rhodes").loopAtCps(4,1.5).cps(1.5)
|
||||
*/
|
||||
// TODO - global cps clock
|
||||
const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
|
||||
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
|
||||
return _loopAt(factor, pat, cps);
|
||||
});
|
||||
|
||||
+103
-156
@@ -1,10 +1,10 @@
|
||||
/*
|
||||
pianoroll.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/pianoroll.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pianoroll.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
||||
import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
||||
|
||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||
const getValue = (e) => {
|
||||
@@ -18,7 +18,7 @@ const getValue = (e) => {
|
||||
}
|
||||
note = note ?? n;
|
||||
if (typeof note === 'string') {
|
||||
return toMidi(note);
|
||||
return noteToMidi(note);
|
||||
}
|
||||
if (typeof note === 'number') {
|
||||
return note;
|
||||
@@ -29,129 +29,26 @@ const getValue = (e) => {
|
||||
return value;
|
||||
};
|
||||
|
||||
Pattern.prototype.pianoroll = function ({
|
||||
cycles = 4,
|
||||
playhead = 0.5,
|
||||
overscan = 1,
|
||||
flipTime = 0,
|
||||
flipValues = 0,
|
||||
hideNegative = false,
|
||||
// inactive = '#C9E597',
|
||||
// inactive = '#FFCA28',
|
||||
inactive = '#7491D2',
|
||||
active = '#FFCA28',
|
||||
// background = '#2A3236',
|
||||
background = 'transparent',
|
||||
smear = 0,
|
||||
playheadColor = 'white',
|
||||
minMidi = 10,
|
||||
maxMidi = 90,
|
||||
autorange = 0,
|
||||
timeframe: timeframeProp,
|
||||
fold = 0,
|
||||
vertical = 0,
|
||||
} = {}) {
|
||||
const ctx = getDrawContext();
|
||||
const w = ctx.canvas.width;
|
||||
const h = ctx.canvas.height;
|
||||
Pattern.prototype.pianoroll = function (options = {}) {
|
||||
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
|
||||
|
||||
let from = -cycles * playhead;
|
||||
let to = cycles * (1 - playhead);
|
||||
|
||||
if (timeframeProp) {
|
||||
console.warn('timeframe is deprecated! use from/to instead');
|
||||
from = 0;
|
||||
to = timeframeProp;
|
||||
}
|
||||
const timeAxis = vertical ? h : w;
|
||||
const valueAxis = vertical ? w : h;
|
||||
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
|
||||
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
|
||||
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
|
||||
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
|
||||
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
|
||||
let foldValues = [];
|
||||
flipTime && timeRange.reverse();
|
||||
flipValues && valueRange.reverse();
|
||||
|
||||
this.draw(
|
||||
(ctx, events, t) => {
|
||||
ctx.fillStyle = background;
|
||||
ctx.globalAlpha = 1; // reset!
|
||||
if (!smear) {
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
(ctx, haps, t) => {
|
||||
const inFrame = (event) =>
|
||||
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
|
||||
events.filter(inFrame).forEach((event) => {
|
||||
const isActive = event.whole.begin <= t && event.whole.end > t;
|
||||
ctx.fillStyle = event.context?.color || inactive;
|
||||
ctx.strokeStyle = event.context?.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(t / timeExtent, ...timeRange);
|
||||
let coords;
|
||||
if (vertical) {
|
||||
coords = [
|
||||
valuePx + 1 - (flipValues ? barThickness : 0), // x
|
||||
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
|
||||
barThickness - 2, // width
|
||||
durationPx - 2, // height
|
||||
];
|
||||
} else {
|
||||
coords = [
|
||||
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
|
||||
valuePx + 1 - (flipValues ? 0 : barThickness), // y
|
||||
durationPx - 2, // widith
|
||||
barThickness - 2, // height
|
||||
];
|
||||
}
|
||||
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
||||
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
|
||||
pianoroll({
|
||||
...options,
|
||||
time: t,
|
||||
ctx,
|
||||
haps: haps.filter(inFrame),
|
||||
});
|
||||
ctx.globalAlpha = 1; // reset!
|
||||
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||
// draw playhead
|
||||
ctx.strokeStyle = playheadColor;
|
||||
ctx.beginPath();
|
||||
if (vertical) {
|
||||
ctx.moveTo(0, playheadPosition);
|
||||
ctx.lineTo(valueAxis, playheadPosition);
|
||||
} else {
|
||||
ctx.moveTo(playheadPosition, 0);
|
||||
ctx.lineTo(playheadPosition, valueAxis);
|
||||
}
|
||||
ctx.stroke();
|
||||
},
|
||||
{
|
||||
from: from - overscan,
|
||||
to: to + overscan,
|
||||
onQuery: (events) => {
|
||||
const { min, max, values } = events.reduce(
|
||||
({ min, max, values }, e) => {
|
||||
const v = getValue(e);
|
||||
return {
|
||||
min: v < min ? v : min,
|
||||
max: v > max ? v : max,
|
||||
values: values.includes(v) ? values : [...values, v],
|
||||
};
|
||||
},
|
||||
{ min: Infinity, max: -Infinity, values: [] },
|
||||
);
|
||||
if (autorange) {
|
||||
minMidi = min;
|
||||
maxMidi = max;
|
||||
valueExtent = maxMidi - minMidi + 1;
|
||||
}
|
||||
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
|
||||
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
||||
},
|
||||
},
|
||||
);
|
||||
return this;
|
||||
@@ -181,6 +78,14 @@ export function pianoroll({
|
||||
timeframe: timeframeProp,
|
||||
fold = 0,
|
||||
vertical = 0,
|
||||
labels = false,
|
||||
fill = 1,
|
||||
fillActive = false,
|
||||
strokeActive = true,
|
||||
stroke,
|
||||
hideInactive = 0,
|
||||
colorizeInactive = 1,
|
||||
fontFamily,
|
||||
ctx,
|
||||
} = {}) {
|
||||
const w = ctx.canvas.width;
|
||||
@@ -224,50 +129,75 @@ export function pianoroll({
|
||||
// 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);
|
||||
});
|
||||
haps.forEach((event) => {
|
||||
const isActive = event.whole.begin <= time && event.endClipped > time;
|
||||
let strokeCurrent = stroke ?? (strokeActive && isActive);
|
||||
let fillCurrent = (!isActive && fill) || (isActive && fillActive);
|
||||
if (hideInactive && !isActive) {
|
||||
return;
|
||||
}
|
||||
let color = event.value?.color || event.context?.color;
|
||||
active = color || active;
|
||||
inactive = colorizeInactive ? color || inactive : inactive;
|
||||
color = isActive ? active : inactive;
|
||||
ctx.fillStyle = fillCurrent ? color : 'transparent';
|
||||
ctx.strokeStyle = color;
|
||||
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
|
||||
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
|
||||
const timePx = scale(timeProgress, ...timeRange);
|
||||
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||
const value = getValue(event);
|
||||
const valueProgress = fold
|
||||
? foldValues.indexOf(value) / foldValues.length
|
||||
: (Number(value) - minMidi) / valueExtent;
|
||||
const valuePx = scale(valueProgress, ...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
|
||||
];
|
||||
}
|
||||
/* const xFactor = Math.sin(performance.now() / 500) + 1;
|
||||
coords[0] *= xFactor; */
|
||||
|
||||
if (strokeCurrent) {
|
||||
ctx.strokeRect(...coords);
|
||||
}
|
||||
if (fillCurrent) {
|
||||
ctx.fillRect(...coords);
|
||||
}
|
||||
//ctx.ellipse(...ellipseFromRect(...coords))
|
||||
if (labels) {
|
||||
const defaultLabel = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
|
||||
const { label: inactiveLabel, activeLabel } = event.value;
|
||||
const customLabel = isActive ? activeLabel || inactiveLabel : inactiveLabel;
|
||||
const label = customLabel ?? defaultLabel;
|
||||
let measure = vertical ? durationPx : barThickness * 0.75;
|
||||
ctx.font = `${measure}px ${fontFamily || 'monospace'}`;
|
||||
// font color
|
||||
ctx.fillStyle = /* isActive && */ !fillCurrent ? color : 'black';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(label, ...coords);
|
||||
}
|
||||
});
|
||||
ctx.globalAlpha = 1; // reset!
|
||||
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||
// draw playhead
|
||||
@@ -284,7 +214,7 @@ export function pianoroll({
|
||||
return this;
|
||||
}
|
||||
|
||||
function getOptions(drawTime, options = {}) {
|
||||
export function getDrawOptions(drawTime, options = {}) {
|
||||
let [lookbehind, lookahead] = drawTime;
|
||||
lookbehind = Math.abs(lookbehind);
|
||||
const cycles = lookahead + lookbehind;
|
||||
@@ -293,5 +223,22 @@ function getOptions(drawTime, options = {}) {
|
||||
}
|
||||
|
||||
Pattern.prototype.punchcard = function (options) {
|
||||
return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
|
||||
return this.onPaint((ctx, time, haps, drawTime, paintOptions = {}) =>
|
||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) }),
|
||||
);
|
||||
};
|
||||
|
||||
Pattern.prototype.wordfall = function (options) {
|
||||
return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options });
|
||||
};
|
||||
|
||||
/* Pattern.prototype.pianoroll = function (options) {
|
||||
return this.onPaint((ctx, time, haps, drawTime) =>
|
||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),
|
||||
);
|
||||
}; */
|
||||
|
||||
export function drawPianoroll(options) {
|
||||
const { drawTime, ...rest } = options;
|
||||
pianoroll({ ...getDrawOptions(drawTime), ...rest });
|
||||
}
|
||||
|
||||
+55
-21
@@ -2,6 +2,8 @@ import { Cyclist } from './cyclist.mjs';
|
||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { setTime } from './time.mjs';
|
||||
import { evalScope } from './evaluate.mjs';
|
||||
import { register } from './pattern.mjs';
|
||||
|
||||
export function repl({
|
||||
interval,
|
||||
@@ -17,37 +19,26 @@ export function repl({
|
||||
}) {
|
||||
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');
|
||||
}
|
||||
},
|
||||
onTrigger: getTrigger({ defaultOutput, getTime }),
|
||||
onError: onSchedulerError,
|
||||
getTime,
|
||||
onToggle,
|
||||
});
|
||||
const setPattern = (pattern, autostart = true) => {
|
||||
pattern = editPattern?.(pattern) || pattern;
|
||||
scheduler.setPattern(pattern, autostart);
|
||||
};
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
const evaluate = async (code, autostart = true) => {
|
||||
if (!code) {
|
||||
throw new Error('no code to evaluate');
|
||||
}
|
||||
try {
|
||||
beforeEval?.({ code });
|
||||
let { pattern } = await _evaluate(code, transpiler);
|
||||
|
||||
await beforeEval?.({ code });
|
||||
let { pattern, meta } = await _evaluate(code, transpiler);
|
||||
logger(`[eval] code updated`);
|
||||
pattern = editPattern?.(pattern) || pattern;
|
||||
scheduler.setPattern(pattern, autostart);
|
||||
afterEval?.({ code, pattern });
|
||||
setPattern(pattern, autostart);
|
||||
afterEval?.({ code, pattern, meta });
|
||||
return pattern;
|
||||
} catch (err) {
|
||||
// console.warn(`[repl] eval error: ${err.message}`);
|
||||
@@ -58,5 +49,48 @@ export function repl({
|
||||
const stop = () => scheduler.stop();
|
||||
const start = () => scheduler.start();
|
||||
const pause = () => scheduler.pause();
|
||||
return { scheduler, evaluate, start, stop, pause };
|
||||
const setCps = (cps) => scheduler.setCps(cps);
|
||||
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
||||
|
||||
// the following functions use the cps value, which is why they are defined here..
|
||||
const loopAt = register('loopAt', (cycles, pat) => {
|
||||
return pat.loopAtCps(cycles, scheduler.cps);
|
||||
});
|
||||
|
||||
const fit = register('fit', (pat) =>
|
||||
pat.withHap((hap) =>
|
||||
hap.withValue((v) => ({
|
||||
...v,
|
||||
speed: scheduler.cps / hap.whole.duration, // overwrite speed completely?
|
||||
unit: 'c',
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
evalScope({
|
||||
loopAt,
|
||||
fit,
|
||||
setCps,
|
||||
setcps: setCps,
|
||||
setCpm,
|
||||
setcpm: setCpm,
|
||||
});
|
||||
|
||||
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
|
||||
}
|
||||
|
||||
export const getTrigger =
|
||||
({ getTime, defaultOutput }) =>
|
||||
async (hap, deadline, duration, cps) => {
|
||||
try {
|
||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||
await defaultOutput(hap, deadline, duration, cps);
|
||||
}
|
||||
if (hap.context.onTrigger) {
|
||||
// call signature of output / onTrigger is different...
|
||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
||||
}
|
||||
} catch (err) {
|
||||
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ export const isaw2 = isaw.toBipolar();
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
|
||||
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
|
||||
* @example
|
||||
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
||||
*
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Pattern } from './index.mjs';
|
||||
|
||||
// polar coords -> xy
|
||||
function fromPolar(angle, radius, cx, cy) {
|
||||
const radians = ((angle - 90) * Math.PI) / 180;
|
||||
return [cx + Math.cos(radians) * radius, cy + Math.sin(radians) * radius];
|
||||
}
|
||||
|
||||
const xyOnSpiral = (angle, margin, cx, cy, rotate = 0) => fromPolar((angle + rotate) * 360, margin * angle, cx, cy); // TODO: logSpiral
|
||||
|
||||
// draw spiral / segment of spiral
|
||||
function spiralSegment(options) {
|
||||
let {
|
||||
ctx,
|
||||
from = 0,
|
||||
to = 3,
|
||||
margin = 50,
|
||||
cx = 100,
|
||||
cy = 100,
|
||||
rotate = 0,
|
||||
thickness = margin / 2,
|
||||
color = '#0000ff30',
|
||||
cap = 'round',
|
||||
stretch = 1,
|
||||
fromOpacity = 1,
|
||||
toOpacity = 1,
|
||||
} = options;
|
||||
from *= stretch;
|
||||
to *= stretch;
|
||||
rotate *= stretch;
|
||||
ctx.lineWidth = thickness;
|
||||
ctx.lineCap = cap;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.globalAlpha = fromOpacity;
|
||||
|
||||
ctx.beginPath();
|
||||
let [sx, sy] = xyOnSpiral(from, margin, cx, cy, rotate);
|
||||
ctx.moveTo(sx, sy);
|
||||
|
||||
const increment = 1 / 60;
|
||||
let angle = from;
|
||||
while (angle <= to) {
|
||||
const [x, y] = xyOnSpiral(angle, margin, cx, cy, rotate);
|
||||
//ctx.lineWidth = angle*thickness;
|
||||
ctx.globalAlpha = ((angle - from) / (to - from)) * toOpacity;
|
||||
ctx.lineTo(x, y);
|
||||
angle += increment;
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
Pattern.prototype.spiral = function (options = {}) {
|
||||
const {
|
||||
stretch = 1,
|
||||
size = 80,
|
||||
thickness = size / 2,
|
||||
cap = 'butt', // round butt squar,
|
||||
inset = 3, // start angl,
|
||||
playheadColor = '#ffffff90',
|
||||
playheadLength = 0.02,
|
||||
playheadThickness = thickness,
|
||||
padding = 0,
|
||||
steady = 1,
|
||||
inactiveColor = '#ffffff20',
|
||||
colorizeInactive = 0,
|
||||
fade = true,
|
||||
// logSpiral = true,
|
||||
} = options;
|
||||
|
||||
function spiral({ ctx, time, haps, drawTime }) {
|
||||
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
|
||||
ctx.clearRect(0, 0, w * 2, h * 2);
|
||||
const [cx, cy] = [w / 2, h / 2];
|
||||
const settings = {
|
||||
margin: size / stretch,
|
||||
cx,
|
||||
cy,
|
||||
stretch,
|
||||
cap,
|
||||
thickness,
|
||||
};
|
||||
|
||||
const playhead = {
|
||||
...settings,
|
||||
thickness: playheadThickness,
|
||||
from: inset - playheadLength,
|
||||
to: inset,
|
||||
color: playheadColor,
|
||||
};
|
||||
|
||||
const [min] = drawTime;
|
||||
const rotate = steady * time;
|
||||
haps.forEach((hap) => {
|
||||
const isActive = hap.whole.begin <= time && hap.endClipped > time;
|
||||
const from = hap.whole.begin - time + inset;
|
||||
const to = hap.endClipped - time + inset - padding;
|
||||
const { color } = hap.context;
|
||||
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
|
||||
spiralSegment({
|
||||
ctx,
|
||||
...settings,
|
||||
from,
|
||||
to,
|
||||
rotate,
|
||||
color: colorizeInactive || isActive ? color : inactiveColor,
|
||||
fromOpacity: opacity,
|
||||
toOpacity: opacity,
|
||||
});
|
||||
});
|
||||
spiralSegment({
|
||||
ctx,
|
||||
...playhead,
|
||||
rotate,
|
||||
});
|
||||
}
|
||||
|
||||
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
controls.test.mjs - <short description TODO>
|
||||
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/controls.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import controls from '../controls.mjs';
|
||||
import { mini } from '../../mini/mini.mjs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('controls', () => {
|
||||
it('should support controls', () => {
|
||||
expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
|
||||
});
|
||||
it('should support compound controls', () => {
|
||||
expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
|
||||
expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
|
||||
{ s: 'bd', n: 3 },
|
||||
{ s: 'sd', n: 4, gain: 1.4 },
|
||||
]);
|
||||
});
|
||||
it('should support ignore extra elements in compound controls', () => {
|
||||
expect(controls.s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
|
||||
{ s: 'bd', n: 3, gain: 0.4 },
|
||||
{ s: 'sd', n: 4, gain: 0.5 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -45,9 +45,6 @@ import {
|
||||
rev,
|
||||
time,
|
||||
run,
|
||||
hitch,
|
||||
set,
|
||||
begin,
|
||||
} from '../index.mjs';
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
@@ -207,7 +204,7 @@ describe('Pattern', () => {
|
||||
),
|
||||
);
|
||||
});
|
||||
it('can squeezeout() structure', () => {
|
||||
it('can SqueezeOut() structure', () => {
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).add.squeezeout(10, 20, 30),
|
||||
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
|
||||
@@ -255,7 +252,7 @@ describe('Pattern', () => {
|
||||
),
|
||||
);
|
||||
});
|
||||
it('can squeezeout() structure', () => {
|
||||
it('can SqueezeOut() structure', () => {
|
||||
sameFirst(sequence(1, [2, 3]).keep.squeezeout(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
|
||||
});
|
||||
});
|
||||
@@ -297,7 +294,7 @@ describe('Pattern', () => {
|
||||
),
|
||||
);
|
||||
});
|
||||
it('can squeezeout() structure', () => {
|
||||
it('can SqueezeOut() structure', () => {
|
||||
sameFirst(sequence(1, [2, 3]).keepif.squeezeout(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
|
||||
});
|
||||
});
|
||||
@@ -932,14 +929,6 @@ 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());
|
||||
});
|
||||
@@ -970,7 +959,7 @@ describe('Pattern', () => {
|
||||
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
|
||||
});
|
||||
});
|
||||
describe('composable functions', () => {
|
||||
/*describe('composable functions', () => {
|
||||
it('Can compose functions', () => {
|
||||
sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
|
||||
});
|
||||
@@ -987,6 +976,7 @@ describe('Pattern', () => {
|
||||
sameFirst(n(0, 1).weave(2, s('bd', silence), s(silence, 'sd')), sequence(s('bd').n(0), s('sd').n(1)));
|
||||
});
|
||||
});
|
||||
*/
|
||||
describe('slice', () => {
|
||||
it('Can slice a sample', () => {
|
||||
sameFirst(
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*test for issue 302 support alternative solmization types */
|
||||
import { sol2note } from '../util.mjs';
|
||||
import { test } from 'vitest';
|
||||
import assert from 'assert';
|
||||
|
||||
test('solmization - letters', () => {
|
||||
const result = sol2note(60, 'letters');
|
||||
const expected = 'C4';
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
|
||||
test('solmization - solfeggio', () => {
|
||||
const result = sol2note(60, 'solfeggio');
|
||||
const expected = 'Do4';
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
|
||||
test('solmization - indian', () => {
|
||||
const result = sol2note(60, 'indian');
|
||||
const expected = 'Sa4';
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
|
||||
test('solmization - german', () => {
|
||||
const result = sol2note(60, 'german');
|
||||
const expected = 'C4';
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
|
||||
test('solmization - byzantine', () => {
|
||||
const result = sol2note(60, 'byzantine');
|
||||
const expected = 'Ni4';
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
|
||||
test('solmization - japanese', () => {
|
||||
const result = sol2note(60, 'japanese');
|
||||
const expected = 'I4';
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
@@ -8,8 +8,8 @@ import { pure } from '../pattern.mjs';
|
||||
import {
|
||||
isNote,
|
||||
tokenizeNote,
|
||||
toMidi,
|
||||
fromMidi,
|
||||
noteToMidi,
|
||||
midiToFreq,
|
||||
freqToMidi,
|
||||
_mod,
|
||||
compose,
|
||||
@@ -75,27 +75,27 @@ describe('isNote', () => {
|
||||
expect(tokenizeNote(123)).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
describe('toMidi', () => {
|
||||
describe('noteToMidi', () => {
|
||||
it('should turn notes into midi', () => {
|
||||
expect(toMidi('A4')).toEqual(69);
|
||||
expect(toMidi('C4')).toEqual(60);
|
||||
expect(toMidi('Db4')).toEqual(61);
|
||||
expect(toMidi('C3')).toEqual(48);
|
||||
expect(toMidi('Cb3')).toEqual(47);
|
||||
expect(toMidi('Cbb3')).toEqual(46);
|
||||
expect(toMidi('C#3')).toEqual(49);
|
||||
expect(toMidi('C#3')).toEqual(49);
|
||||
expect(toMidi('C##3')).toEqual(50);
|
||||
expect(noteToMidi('A4')).toEqual(69);
|
||||
expect(noteToMidi('C4')).toEqual(60);
|
||||
expect(noteToMidi('Db4')).toEqual(61);
|
||||
expect(noteToMidi('C3')).toEqual(48);
|
||||
expect(noteToMidi('Cb3')).toEqual(47);
|
||||
expect(noteToMidi('Cbb3')).toEqual(46);
|
||||
expect(noteToMidi('C#3')).toEqual(49);
|
||||
expect(noteToMidi('C#3')).toEqual(49);
|
||||
expect(noteToMidi('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"`);
|
||||
expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||
expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||
});
|
||||
});
|
||||
describe('fromMidi', () => {
|
||||
describe('midiToFreq', () => {
|
||||
it('should turn midi into frequency', () => {
|
||||
expect(fromMidi(69)).toEqual(440);
|
||||
expect(fromMidi(57)).toEqual(220);
|
||||
expect(midiToFreq(69)).toEqual(440);
|
||||
expect(midiToFreq(57)).toEqual(220);
|
||||
});
|
||||
});
|
||||
describe('freqToMidi', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
ui.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/ui.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/ui.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/>.
|
||||
*/
|
||||
|
||||
|
||||
+79
-17
@@ -6,29 +6,32 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
// returns true if the given string is a note
|
||||
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
||||
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]?$/.test(name);
|
||||
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
|
||||
export const tokenizeNote = (note) => {
|
||||
if (typeof note !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
|
||||
if (!pc) {
|
||||
return [];
|
||||
}
|
||||
return [pc, acc, oct ? Number(oct) : undefined];
|
||||
};
|
||||
|
||||
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
||||
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
||||
|
||||
// turns the given note into its midi number representation
|
||||
export const toMidi = (note) => {
|
||||
const [pc, acc, oct = 3] = tokenizeNote(note);
|
||||
export const noteToMidi = (note, defaultOctave = 3) => {
|
||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
||||
if (!pc) {
|
||||
throw new Error('not a note: "' + note + '"');
|
||||
}
|
||||
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
|
||||
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
|
||||
const chroma = chromas[pc.toLowerCase()];
|
||||
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||
};
|
||||
export const fromMidi = (n) => {
|
||||
export const midiToFreq = (n) => {
|
||||
return Math.pow(2, (n - 69) / 12) * 440;
|
||||
};
|
||||
|
||||
@@ -45,7 +48,7 @@ export const valueToMidi = (value, fallbackValue) => {
|
||||
return freqToMidi(freq);
|
||||
}
|
||||
if (typeof note === 'string') {
|
||||
return toMidi(note);
|
||||
return noteToMidi(note);
|
||||
}
|
||||
if (typeof note === 'number') {
|
||||
return note;
|
||||
@@ -62,18 +65,19 @@ export const valueToMidi = (value, fallbackValue) => {
|
||||
*/
|
||||
export const getFreq = (noteOrMidi) => {
|
||||
if (typeof noteOrMidi === 'number') {
|
||||
return fromMidi(noteOrMidi);
|
||||
return midiToFreq(noteOrMidi);
|
||||
}
|
||||
return fromMidi(toMidi(noteOrMidi));
|
||||
return midiToFreq(noteToMidi(noteOrMidi));
|
||||
};
|
||||
|
||||
const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
||||
/**
|
||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||
* @deprecated only used in workshop (first-notes)
|
||||
* @noAutocomplete
|
||||
*/
|
||||
export const midi2note = (n) => {
|
||||
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 = pcs[n % 12];
|
||||
return pc + oct;
|
||||
};
|
||||
|
||||
@@ -91,7 +95,7 @@ export const getPlayableNoteValue = (hap) => {
|
||||
}
|
||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||
note = fromMidi(hap.value);
|
||||
note = midiToFreq(hap.value);
|
||||
} else if (typeof note === 'number' && context.type === 'frequency') {
|
||||
note = hap.value; // legacy workaround.. will be removed in the future
|
||||
} else if (typeof note !== 'string' || !isNote(note)) {
|
||||
@@ -110,9 +114,9 @@ export const getFrequency = (hap) => {
|
||||
return getFreq(value.note || value.n || value.value);
|
||||
}
|
||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||
value = fromMidi(hap.value);
|
||||
value = midiToFreq(hap.value);
|
||||
} else if (typeof value === 'string' && isNote(value)) {
|
||||
value = fromMidi(toMidi(hap.value));
|
||||
value = midiToFreq(noteToMidi(hap.value));
|
||||
} else if (typeof value !== 'number') {
|
||||
throw new Error('not a note or frequency: ' + value);
|
||||
}
|
||||
@@ -139,7 +143,7 @@ export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
|
||||
export const flatten = (arr) => [].concat(...arr);
|
||||
|
||||
export const id = (a) => a;
|
||||
export const constant = curry((a, b) => a);
|
||||
export const constant = (a, b) => a;
|
||||
|
||||
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
||||
|
||||
@@ -170,7 +174,7 @@ export function parseNumeral(numOrString) {
|
||||
return asNumber;
|
||||
}
|
||||
if (isNote(numOrString)) {
|
||||
return toMidi(numOrString);
|
||||
return noteToMidi(numOrString);
|
||||
}
|
||||
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
||||
}
|
||||
@@ -212,3 +216,61 @@ export const splitAt = function (index, value) {
|
||||
};
|
||||
|
||||
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||
|
||||
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
|
||||
/* solmization, not used yet */
|
||||
const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/
|
||||
const indian = [
|
||||
'Sa',
|
||||
'Re',
|
||||
'Ga',
|
||||
'Ma',
|
||||
'Pa',
|
||||
'Dha',
|
||||
'Ni',
|
||||
]; /*indian musical notes, seems like they do not use flats or sharps*/
|
||||
const german = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Hb', 'H']; /*german & dutch musical notes*/
|
||||
const byzantine = [
|
||||
'Ni',
|
||||
'Pab',
|
||||
'Pa',
|
||||
'Voub',
|
||||
'Vou',
|
||||
'Ga',
|
||||
'Dib',
|
||||
'Di',
|
||||
'Keb',
|
||||
'Ke',
|
||||
'Zob',
|
||||
'Zo',
|
||||
]; /*byzantine musical notes*/
|
||||
const japanese = [
|
||||
'I',
|
||||
'Ro',
|
||||
'Ha',
|
||||
'Ni',
|
||||
'Ho',
|
||||
'He',
|
||||
'To',
|
||||
]; /*traditional japanese musical notes, seems like they do not use falts or sharps*/
|
||||
|
||||
const english = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
||||
|
||||
export const sol2note = (n, notation = 'letters') => {
|
||||
const pc =
|
||||
notation === 'solfeggio'
|
||||
? solfeggio /*check if its is any of the following*/
|
||||
: notation === 'indian'
|
||||
? indian
|
||||
: notation === 'german'
|
||||
? german
|
||||
: notation === 'byzantine'
|
||||
? byzantine
|
||||
: notation === 'japanese'
|
||||
? japanese
|
||||
: english; /*if not use standard version*/
|
||||
const note = pc[n % 12]; /*calculating the midi value to the note*/
|
||||
const oct = Math.floor(n / 12) - 1;
|
||||
return note + oct;
|
||||
};
|
||||
|
||||
@@ -44,6 +44,6 @@ function createClock(
|
||||
};
|
||||
const getPhase = () => phase;
|
||||
// setCallback
|
||||
return { setDuration, start, stop, pause, duration, getPhase, minLatency };
|
||||
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
|
||||
}
|
||||
export default createClock;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/csound",
|
||||
"version": "0.6.2",
|
||||
"version": "0.9.0",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -32,11 +32,11 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@csound/browser": "6.18.5",
|
||||
"@csound/browser": "6.18.7",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^3.2.2"
|
||||
"vite": "^4.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# @strudel/desktopbridge
|
||||
|
||||
This package contains utilities used to communicate with the Tauri backend
|
||||
@@ -1,12 +1,9 @@
|
||||
/*
|
||||
evaluate.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.mjs>
|
||||
index.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/desktopbridge/index.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 { evaluate as _evaluate } from '@strudel.cycles/core';
|
||||
import shapeshifter from './shapeshifter.mjs';
|
||||
|
||||
export const evaluate = async (code) => {
|
||||
return _evaluate(code, shapeshifter);
|
||||
};
|
||||
export * from './midibridge.mjs';
|
||||
export * from './utils.mjs';
|
||||
export * from './oscbridge.mjs';
|
||||
@@ -0,0 +1,11 @@
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { logger } from '../core/logger.mjs';
|
||||
|
||||
// listen for log events from the Tauri backend and log in the UI
|
||||
await listen('log-event', (e) => {
|
||||
if (e.payload == null) {
|
||||
return;
|
||||
}
|
||||
const { message, message_type } = e.payload;
|
||||
logger(message, message_type);
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Invoke } from './utils.mjs';
|
||||
import { Pattern, noteToMidi } from '@strudel.cycles/core';
|
||||
|
||||
const ON_MESSAGE = 0x90;
|
||||
const OFF_MESSAGE = 0x80;
|
||||
const CC_MESSAGE = 0xb0;
|
||||
|
||||
Pattern.prototype.midi = function (output) {
|
||||
return this.onTrigger((time, hap, currentTime) => {
|
||||
const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
|
||||
const offset = (time - currentTime) * 1000;
|
||||
const velocity = Math.floor((hap.context?.velocity ?? 0.9) * 100); // TODO: refactor velocity
|
||||
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
|
||||
const roundedOffset = Math.round(offset);
|
||||
const midichan = (hap.value.midichan ?? 1) - 1;
|
||||
const requestedport = output ?? 'IAC';
|
||||
const messagesfromjs = [];
|
||||
if (note != null) {
|
||||
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
||||
messagesfromjs.push({
|
||||
requestedport,
|
||||
message: [ON_MESSAGE + midichan, midiNumber, velocity],
|
||||
offset: roundedOffset,
|
||||
});
|
||||
messagesfromjs.push({
|
||||
requestedport,
|
||||
message: [OFF_MESSAGE + midichan, midiNumber, velocity],
|
||||
offset: roundedOffset + duration,
|
||||
});
|
||||
}
|
||||
if (ccv && ccn) {
|
||||
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
||||
throw new Error('expected ccv to be a number between 0 and 1');
|
||||
}
|
||||
if (!['string', 'number'].includes(typeof ccn)) {
|
||||
throw new Error('expected ccn to be a number or a string');
|
||||
}
|
||||
const scaled = Math.round(ccv * 127);
|
||||
messagesfromjs.push({
|
||||
requestedport,
|
||||
message: [CC_MESSAGE + midichan, ccn, scaled],
|
||||
offset: roundedOffset,
|
||||
});
|
||||
}
|
||||
// invoke is temporarily blocking, run in an async process
|
||||
if (messagesfromjs.length) {
|
||||
setTimeout(() => {
|
||||
Invoke('sendmidi', { messagesfromjs });
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { parseNumeral, Pattern } from '@strudel.cycles/core';
|
||||
import { Invoke } from './utils.mjs';
|
||||
|
||||
Pattern.prototype.osc = function () {
|
||||
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
|
||||
hap.ensureObjectValue();
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
// make sure n and note are numbers
|
||||
controls.n && (controls.n = parseNumeral(controls.n));
|
||||
controls.note && (controls.note = parseNumeral(controls.note));
|
||||
|
||||
const params = [];
|
||||
|
||||
const timestamp = Math.round(Date.now() + (time - currentTime) * 1000);
|
||||
|
||||
Object.keys(controls).forEach((key) => {
|
||||
const val = controls[key];
|
||||
const value = typeof val === 'number' ? val.toString() : val;
|
||||
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
params.push({
|
||||
name: key,
|
||||
value,
|
||||
valueisnumber: typeof val === 'number',
|
||||
});
|
||||
});
|
||||
|
||||
const messagesfromjs = [];
|
||||
if (params.length) {
|
||||
messagesfromjs.push({ target: '/dirt/play', timestamp, params });
|
||||
}
|
||||
|
||||
if (messagesfromjs.length) {
|
||||
setTimeout(() => {
|
||||
Invoke('sendosc', { messagesfromjs });
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@strudel/desktopbridge",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "tools/shims for communicating between the JS and Tauri (Rust) sides of the Studel desktop app",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Jade Rowland <jaderowlanddev@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@tauri-apps/api": "^1.4.0"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
|
||||
export const Invoke = invoke;
|
||||
export const isTauri = () => window.__TAURI_IPC__ != null;
|
||||
@@ -1,3 +0,0 @@
|
||||
shift-parser
|
||||
shift-reducer
|
||||
!shift-traverser
|
||||
@@ -1,50 +0,0 @@
|
||||
# @strudel.cycles/eval
|
||||
|
||||
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.
|
||||
|
||||
## Deprecation Note
|
||||
|
||||
This package will not be developed further. Consider using `@strudel.cycles/transpiler` as a replacement.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm i @strudel.cycles/eval --save
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import { evalScope } from '@strudel.cycles/core';
|
||||
import { evaluate } from '@strudel.cycles/eval';
|
||||
|
||||
evalScope(
|
||||
import('@strudel.cycles/core'),
|
||||
// import other strudel packages here
|
||||
); // add strudel to eval scope
|
||||
|
||||
async function run(code) {
|
||||
const { pattern } = await evaluate(code);
|
||||
const events = pattern.firstCycle();
|
||||
console.log(events.map((e) => e.show()).join('\n'));
|
||||
}
|
||||
|
||||
run('sequence([a3, [b3, c4]])');
|
||||
```
|
||||
|
||||
yields:
|
||||
|
||||
```js
|
||||
(0/1 -> 1/2, 0/1 -> 1/2, a3)
|
||||
(1/2 -> 3/4, 1/2 -> 3/4, b3)
|
||||
(3/4 -> 1/1, 3/4 -> 1/1, c4)
|
||||
```
|
||||
|
||||
[play with @strudel.cycles/eval on codesandbox](https://codesandbox.io/s/strudel-eval-example-ndz1d8?file=/src/index.js)
|
||||
|
||||
## Dev Notes
|
||||
|
||||
shift-traverser is currently monkey patched because its package.json uses estraverse@^4.2.0,
|
||||
which does not support the spread operator (Error: Unknown node type SpreadProperty.).
|
||||
By monkey patched, I mean I copied the source of shift-traverser to a subfolder and installed the dependencies (shift-spec + estraverse@^5.3.0)
|
||||
@@ -1 +0,0 @@
|
||||
export * from './evaluate.mjs';
|
||||
@@ -1,296 +0,0 @@
|
||||
/*
|
||||
shapeshifter.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/shapeshifter.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 { parseScriptWithLocation } from './shift-parser/index.js'; // npm module does not work in the browser
|
||||
import traverser from './shift-traverser/index.js'; // npm module does not work in the browser */
|
||||
import { parseScriptWithLocation } from 'shift-parser';
|
||||
import traverser from './shift-traverser/index.js';
|
||||
const { replace } = traverser;
|
||||
import {
|
||||
LiteralStringExpression,
|
||||
IdentifierExpression,
|
||||
CallExpression,
|
||||
StaticMemberExpression,
|
||||
ReturnStatement,
|
||||
ArrayExpression,
|
||||
LiteralNumericExpression,
|
||||
} from 'shift-ast';
|
||||
import shiftCodegen from 'shift-codegen';
|
||||
const codegen = shiftCodegen.default || shiftCodegen; // parcel module resolution fuckup
|
||||
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
const { Pattern } = strudel;
|
||||
|
||||
const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
|
||||
|
||||
const addLocations = true;
|
||||
export const addMiniLocations = true;
|
||||
export const minifyStrings = true;
|
||||
export const wrappedAsync = false; // this is now handled by core evaluate by default
|
||||
export const shouldAddReturn = true;
|
||||
|
||||
export default (_code) => {
|
||||
const { code, addReturn } = wrapAsync(_code);
|
||||
const ast = parseScriptWithLocation(disguiseImports(code));
|
||||
const artificialNodes = [];
|
||||
const parents = [];
|
||||
const shifted = replace(ast.tree, {
|
||||
enter(node, parent) {
|
||||
parents.push(parent);
|
||||
const isSynthetic = parents.some((p) => artificialNodes.includes(p));
|
||||
if (isSynthetic) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// replace template string `xxx` with mini(`xxx`)
|
||||
if (minifyStrings && isBackTickString(node)) {
|
||||
return minifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||
}
|
||||
// allows to use top level strings, which are normally directives... but we don't need directives
|
||||
if (minifyStrings && node.directives?.length === 1 && !node.statements?.length) {
|
||||
const str = new LiteralStringExpression({ value: node.directives[0].rawValue });
|
||||
const wrapped = minifyWithLocation(str, node.directives[0], ast.locations, artificialNodes);
|
||||
return { ...node, directives: [], statements: [wrapped] };
|
||||
}
|
||||
|
||||
// replace double quote string "xxx" with mini('xxx')
|
||||
if (minifyStrings && isStringWithDoubleQuotes(node, ast.locations, code)) {
|
||||
return minifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||
}
|
||||
|
||||
// operator overloading => still not done
|
||||
const operators = {
|
||||
'*': 'fast',
|
||||
'/': 'slow',
|
||||
'&': 'stack',
|
||||
'&&': 'append',
|
||||
};
|
||||
if (
|
||||
node.type === 'BinaryExpression' &&
|
||||
operators[node.operator] &&
|
||||
['LiteralNumericExpression', 'LiteralStringExpression', 'IdentifierExpression'].includes(node.right?.type) &&
|
||||
canBeOverloaded(node.left)
|
||||
) {
|
||||
let arg = node.left;
|
||||
if (node.left.type === 'IdentifierExpression') {
|
||||
arg = wrapFunction('reify', node.left);
|
||||
}
|
||||
return new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
property: operators[node.operator],
|
||||
object: wrapFunction('reify', arg),
|
||||
}),
|
||||
arguments: [node.right],
|
||||
});
|
||||
}
|
||||
|
||||
const isMarkable = isPatternArg(parents) || hasModifierCall(parent);
|
||||
// add to location to pure(x) calls
|
||||
if (node.type === 'CallExpression' && node.callee.name === 'pure') {
|
||||
const literal = node.arguments[0];
|
||||
// const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
|
||||
return reifyWithLocation(literal, node.arguments[0], ast.locations, artificialNodes);
|
||||
}
|
||||
// replace pseudo note variables
|
||||
if (node.type === 'IdentifierExpression') {
|
||||
if (isNote(node.name)) {
|
||||
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
|
||||
if (addLocations && isMarkable) {
|
||||
return reifyWithLocation(new LiteralStringExpression({ value }), node, ast.locations, artificialNodes);
|
||||
}
|
||||
return new LiteralStringExpression({ value });
|
||||
}
|
||||
if (node.name === 'r') {
|
||||
return new IdentifierExpression({ name: 'silence' });
|
||||
}
|
||||
}
|
||||
if (
|
||||
addLocations &&
|
||||
['LiteralStringExpression' /* , 'LiteralNumericExpression' */].includes(node.type) &&
|
||||
isMarkable
|
||||
) {
|
||||
// TODO: to make LiteralNumericExpression work, we need to make sure we're not inside timeCat...
|
||||
return reifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||
}
|
||||
if (addMiniLocations) {
|
||||
return addMiniNotationLocations(node, ast.locations, artificialNodes);
|
||||
}
|
||||
return node;
|
||||
},
|
||||
leave() {
|
||||
parents.pop();
|
||||
},
|
||||
});
|
||||
// add return to last statement (because it's wrapped in an async function artificially)
|
||||
if (shouldAddReturn) {
|
||||
addReturn(shifted);
|
||||
}
|
||||
const generated = undisguiseImports(codegen(shifted));
|
||||
return generated;
|
||||
};
|
||||
|
||||
// renames all import statements to "_mport" as Shift doesn't support dynamic import.
|
||||
// there shouldn't be any side-effects from this as this change does not affect
|
||||
// the syntax & will be undone by the equivalent replace in "undisguiseImports".
|
||||
function disguiseImports(code) {
|
||||
return code.replaceAll('import', '_mport'); // Must be the same length!
|
||||
}
|
||||
|
||||
// Rename the renamed import statements back to "import"
|
||||
function undisguiseImports(code) {
|
||||
return code.replaceAll('_mport', 'import');
|
||||
}
|
||||
|
||||
function wrapAsync(code) {
|
||||
// wrap code in async to make await work on top level => this will create 1 line offset to locations
|
||||
// this is why line offset is -1 in getLocationObject calls below
|
||||
if (wrappedAsync) {
|
||||
code = `(async () => {
|
||||
${code}
|
||||
})()`;
|
||||
}
|
||||
const addReturn = (ast) => {
|
||||
const body = wrappedAsync ? ast.statements[0].expression.callee.body : ast;
|
||||
body.statements = body.statements
|
||||
.slice(0, -1)
|
||||
.concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]);
|
||||
};
|
||||
return {
|
||||
code,
|
||||
addReturn,
|
||||
};
|
||||
}
|
||||
|
||||
function addMiniNotationLocations(node, locations, artificialNodes) {
|
||||
const miniFunctions = ['mini', 'm'];
|
||||
// const isAlreadyWrapped = parent?.type === 'CallExpression' && parent.callee.name === 'withLocationOffset';
|
||||
if (node.type === 'CallExpression' && miniFunctions.includes(node.callee.name)) {
|
||||
// mini('c3')
|
||||
if (node.arguments.length > 1) {
|
||||
// TODO: transform mini(...args) to cat(...args.map(mini)) ?
|
||||
console.warn('multi arg mini locations not supported yet...');
|
||||
return node;
|
||||
}
|
||||
const str = node.arguments[0];
|
||||
return minifyWithLocation(str, str, locations, artificialNodes);
|
||||
}
|
||||
if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property)) {
|
||||
// 'c3'.mini or 'c3'.m
|
||||
return minifyWithLocation(node.object, node, locations, artificialNodes);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function wrapFunction(name, ...args) {
|
||||
return new CallExpression({
|
||||
callee: new IdentifierExpression({ name }),
|
||||
arguments: args,
|
||||
});
|
||||
}
|
||||
|
||||
function isBackTickString(node) {
|
||||
return node.type === 'TemplateExpression' && node.elements.length === 1;
|
||||
}
|
||||
|
||||
function isStringWithDoubleQuotes(node, locations, code) {
|
||||
if (node.type !== 'LiteralStringExpression') {
|
||||
return false;
|
||||
}
|
||||
const loc = locations.get(node);
|
||||
const snippet = code.slice(loc.start.offset, loc.end.offset);
|
||||
return snippet[0] === '"'; // we can trust the end is also ", as the parsing did not fail
|
||||
}
|
||||
|
||||
// returns true if the given parents belong to a pattern argument node
|
||||
// this is used to check if a node should receive a location for highlighting
|
||||
function isPatternArg(parents) {
|
||||
if (!parents.length) {
|
||||
return false;
|
||||
}
|
||||
const ancestors = parents.slice(0, -1);
|
||||
const parent = parents[parents.length - 1];
|
||||
if (isPatternFactory(parent)) {
|
||||
return true;
|
||||
}
|
||||
if (parent?.type === 'ArrayExpression') {
|
||||
return isPatternArg(ancestors);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasModifierCall(parent) {
|
||||
// TODO: modifiers are more than composables, for example every is not composable but should be seen as modifier..
|
||||
// need all prototypes of Pattern
|
||||
return parent?.type === 'StaticMemberExpression';
|
||||
// && Object.keys(Pattern.prototype.composable).includes(parent.property)
|
||||
}
|
||||
const factories = Object.keys(Pattern.prototype.factories).concat(['mini']);
|
||||
|
||||
function isPatternFactory(node) {
|
||||
return node?.type === 'CallExpression' && factories.includes(node.callee.name);
|
||||
}
|
||||
|
||||
function canBeOverloaded(node) {
|
||||
return (node.type === 'IdentifierExpression' && isNote(node.name)) || isPatternFactory(node);
|
||||
// TODO: support sequence(c3).transpose(3).x.y.z
|
||||
}
|
||||
|
||||
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
|
||||
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
|
||||
function reifyWithLocation(literalNode, node, locations, artificialNodes) {
|
||||
const args = getLocationArguments(node, locations);
|
||||
const withLocation = new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
object: wrapFunction('reify', literalNode),
|
||||
property: 'withLocation',
|
||||
}),
|
||||
arguments: args,
|
||||
});
|
||||
artificialNodes.push(withLocation);
|
||||
return withLocation;
|
||||
}
|
||||
|
||||
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
|
||||
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
|
||||
function minifyWithLocation(literalNode, node, locations, artificialNodes) {
|
||||
const args = getLocationArguments(node, locations);
|
||||
const wrapped = wrapFunction('mini', literalNode);
|
||||
if (!addMiniLocations) {
|
||||
artificialNodes.push(wrapped);
|
||||
return wrapped;
|
||||
}
|
||||
const withLocation = new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
object: wrapped,
|
||||
property: 'withMiniLocation',
|
||||
}),
|
||||
arguments: args,
|
||||
});
|
||||
artificialNodes.push(withLocation);
|
||||
return withLocation;
|
||||
}
|
||||
|
||||
function getLocationArguments(node, locations) {
|
||||
const loc = locations.get(node);
|
||||
const lineOffset = wrappedAsync ? -1 : 0;
|
||||
return [
|
||||
new ArrayExpression({
|
||||
elements: [
|
||||
new LiteralNumericExpression({ value: loc.start.line + lineOffset }), // the minus 1 assumes the code has been wrapped in async iife
|
||||
new LiteralNumericExpression({ value: loc.start.column }),
|
||||
new LiteralNumericExpression({ value: loc.start.offset }),
|
||||
],
|
||||
}),
|
||||
new ArrayExpression({
|
||||
elements: [
|
||||
new LiteralNumericExpression({ value: loc.end.line + lineOffset }), // the minus 1 assumes the code has been wrapped in async iife
|
||||
new LiteralNumericExpression({ value: loc.end.column }),
|
||||
new LiteralNumericExpression({ value: loc.end.offset }),
|
||||
],
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
index.js - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/shift-traverser/index.js>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
Copyright (C) 2014 Yusuke Suzuki <utatane.tea@gmail.com>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import _Spec from 'shift-spec';
|
||||
const Spec = _Spec.default || _Spec; // parcel module resolution fuckup
|
||||
// import { version } from '../package.json'
|
||||
|
||||
// Loading uncached estraverse for changing estraverse.Syntax.
|
||||
import _estraverse from 'estraverse';
|
||||
|
||||
const estraverse = _estraverse.cloneEnvironment();
|
||||
|
||||
// Adjust estraverse members.
|
||||
|
||||
Object.keys(estraverse.Syntax)
|
||||
.filter((key) => key !== 'Property')
|
||||
.forEach((key) => {
|
||||
delete estraverse.Syntax[key];
|
||||
delete estraverse.VisitorKeys[key];
|
||||
});
|
||||
|
||||
Object.assign(
|
||||
estraverse.Syntax,
|
||||
Object.keys(Spec).reduce((result, key) => {
|
||||
result[key] = key;
|
||||
return result;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
Object.assign(
|
||||
estraverse.VisitorKeys,
|
||||
Object.keys(Spec).reduce((result, key) => {
|
||||
result[key] = Spec[key].fields.map((field) => field.name);
|
||||
return result;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
// estraverse.version = version;
|
||||
export default estraverse;
|
||||
|
||||
/* vim: set sw=4 ts=4 et tw=80 : */
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
evaluate.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/test/evaluate.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { expect, describe, it } from 'vitest';
|
||||
|
||||
import { evaluate } from '../evaluate.mjs';
|
||||
import { mini } from '@strudel.cycles/mini';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
const { fastcat, evalScope } = strudel;
|
||||
|
||||
describe('evaluate', async () => {
|
||||
await evalScope({ mini }, strudel);
|
||||
const ev = async (code) => (await evaluate(code)).pattern.firstCycleValues;
|
||||
it('Should evaluate strudel functions', async () => {
|
||||
expect(await ev('pure("c3")')).toEqual(['c3']);
|
||||
expect(await ev('cat("c3")')).toEqual(['c3']);
|
||||
expect(await ev('fastcat("c3", "d3")')).toEqual(['c3', 'd3']);
|
||||
expect(await ev('slowcat("c3", "d3")')).toEqual(['c3']);
|
||||
});
|
||||
it('Scope should be extendable', async () => {
|
||||
await evalScope({ myFunction: (...x) => fastcat(...x) });
|
||||
expect(await ev('myFunction("c3", "d3")')).toEqual(['c3', 'd3']);
|
||||
});
|
||||
it('Should evaluate simple double quoted mini notation', async () => {
|
||||
expect(await ev('"c3"')).toEqual(['c3']);
|
||||
expect(await ev('"c3 d3"')).toEqual(['c3', 'd3']);
|
||||
expect(await ev('"<c3 d3>"')).toEqual(['c3']);
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
shapeshifter.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/test/shapeshifter.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import shapeshifter, { wrappedAsync } from '../shapeshifter.mjs';
|
||||
|
||||
describe('shapeshifter', () => {
|
||||
it('Should shift simple double quote string', () => {
|
||||
if (wrappedAsync) {
|
||||
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
|
||||
} else {
|
||||
expect(shapeshifter('"c3"')).toEqual('return mini("c3").withMiniLocation([1,0,0],[1,4,4])');
|
||||
}
|
||||
});
|
||||
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])',
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
+58
-54
@@ -6,9 +6,8 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import * as _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
|
||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||
import { toMidi } from '@strudel.cycles/core';
|
||||
|
||||
import { noteToMidi } from '@strudel.cycles/core';
|
||||
import { Note } from 'webmidi';
|
||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||
export const { WebMidi } = _WebMidi;
|
||||
|
||||
@@ -16,12 +15,28 @@ function supportsMidi() {
|
||||
return typeof navigator.requestMIDIAccess === 'function';
|
||||
}
|
||||
|
||||
export function enableWebMidi(options = {}) {
|
||||
const { onReady, onConnected, onDisconnected } = options;
|
||||
function getMidiDeviceNamesString(outputs) {
|
||||
return outputs.map((o) => `'${o.name}'`).join(' | ');
|
||||
}
|
||||
|
||||
export function enableWebMidi(options = {}) {
|
||||
const { onReady, onConnected, onDisconnected, onEnabled } = options;
|
||||
if (WebMidi.enabled) {
|
||||
return;
|
||||
}
|
||||
if (!supportsMidi()) {
|
||||
throw new Error('Your Browser does not support WebMIDI.');
|
||||
}
|
||||
WebMidi.addListener('connected', () => {
|
||||
onConnected?.(WebMidi);
|
||||
});
|
||||
WebMidi.addListener('enabled', () => {
|
||||
onEnabled?.(WebMidi);
|
||||
});
|
||||
// Reacting when a device becomes unavailable
|
||||
WebMidi.addListener('disconnected', (e) => {
|
||||
onDisconnected?.(WebMidi, e);
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
if (WebMidi.enabled) {
|
||||
// if already enabled, just resolve WebMidi
|
||||
@@ -32,13 +47,6 @@ export function enableWebMidi(options = {}) {
|
||||
if (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);
|
||||
});
|
||||
@@ -47,8 +55,6 @@ export function enableWebMidi(options = {}) {
|
||||
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
|
||||
const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||
|
||||
let midiReady;
|
||||
|
||||
// output?: string | number, outputs: typeof WebMidi.outputs
|
||||
function getDevice(output, outputs) {
|
||||
if (!outputs.length) {
|
||||
@@ -60,29 +66,19 @@ function getDevice(output, outputs) {
|
||||
if (typeof output === 'string') {
|
||||
return outputByName(output);
|
||||
}
|
||||
return outputs[0];
|
||||
// attempt to default to first IAC device if none is specified
|
||||
const IACOutput = outputs.find((output) => output.name.includes('IAC'));
|
||||
const device = IACOutput ?? outputs[0];
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${getMidiDeviceNamesString(WebMidi.outputs)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return IACOutput ?? outputs[0];
|
||||
}
|
||||
|
||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||
Pattern.prototype.midi = function (output) {
|
||||
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(
|
||||
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
||||
@@ -90,35 +86,43 @@ Pattern.prototype.midi = function (output) {
|
||||
}')`,
|
||||
);
|
||||
}
|
||||
return this.onTrigger((time, hap) => {
|
||||
if (!midiReady) {
|
||||
|
||||
enableWebMidi({
|
||||
onEnabled: ({ outputs }) => {
|
||||
const device = getDevice(output, outputs);
|
||||
const otherOutputs = outputs.filter((o) => o.name !== device.name);
|
||||
logger(
|
||||
`Midi enabled! Using "${device.name}". ${
|
||||
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
|
||||
}`,
|
||||
);
|
||||
},
|
||||
onDisconnected: ({ outputs }) =>
|
||||
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
||||
});
|
||||
|
||||
return this.onTrigger((time, hap, currentTime, cps) => {
|
||||
if (!WebMidi.enabled) {
|
||||
return;
|
||||
}
|
||||
const device = getDevice(output, WebMidi.outputs);
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||
.map((o) => `'${o.name}'`)
|
||||
.join(' | ')}`,
|
||||
);
|
||||
}
|
||||
hap.ensureObjectValue();
|
||||
|
||||
// calculate time
|
||||
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
|
||||
time = time * 1000 + timingOffset;
|
||||
const offset = (time - currentTime) * 1000;
|
||||
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
|
||||
const timeOffsetString = `+${offset}`;
|
||||
|
||||
// destructure value
|
||||
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
|
||||
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
|
||||
const duration = hap.duration.valueOf() * 1000 - 5;
|
||||
|
||||
if (note) {
|
||||
const midiNumber = toMidi(note);
|
||||
device.playNote(midiNumber, midichan, {
|
||||
time,
|
||||
duration,
|
||||
attack: velocity,
|
||||
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
|
||||
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
|
||||
if (note != null) {
|
||||
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
||||
const midiNote = new Note(midiNumber, { attack: velocity, duration });
|
||||
device.playNote(midiNote, midichan, {
|
||||
time: timeOffsetString,
|
||||
});
|
||||
}
|
||||
if (ccv && ccn) {
|
||||
@@ -129,7 +133,7 @@ Pattern.prototype.midi = function (output) {
|
||||
throw new Error('expected ccn to be a number or a string');
|
||||
}
|
||||
const scaled = Math.round(ccv * 127);
|
||||
device.sendControlChange(ccn, scaled, midichan, { time });
|
||||
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.6.0",
|
||||
"version": "0.9.0",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -31,9 +31,9 @@
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"webmidi": "^3.0.21"
|
||||
"webmidi": "^3.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^3.2.2"
|
||||
"vite": "^4.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
+171
-134
@@ -1,4 +1,4 @@
|
||||
// Generated by Peggy 2.0.1.
|
||||
// Generated by Peggy 3.0.2.
|
||||
//
|
||||
// https://peggyjs.org/
|
||||
|
||||
@@ -43,16 +43,19 @@ peg$SyntaxError.prototype.format = function(sources) {
|
||||
}
|
||||
}
|
||||
var s = this.location.start;
|
||||
var loc = this.location.source + ":" + s.line + ":" + s.column;
|
||||
var offset_s = (this.location.source && (typeof this.location.source.offset === "function"))
|
||||
? this.location.source.offset(s)
|
||||
: s;
|
||||
var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
|
||||
if (src) {
|
||||
var e = this.location.end;
|
||||
var filler = peg$padEnd("", s.line.toString().length, ' ');
|
||||
var filler = peg$padEnd("", offset_s.line.toString().length, ' ');
|
||||
var line = src[s.line - 1];
|
||||
var last = s.line === e.line ? e.column : line.length + 1;
|
||||
var hatLen = (last - s.column) || 1;
|
||||
str += "\n --> " + loc + "\n"
|
||||
+ filler + " |\n"
|
||||
+ s.line + " | " + line + "\n"
|
||||
+ offset_s.line + " | " + line + "\n"
|
||||
+ filler + " | " + peg$padEnd("", s.column - 1, ' ')
|
||||
+ peg$padEnd("", hatLen, "^");
|
||||
} else {
|
||||
@@ -182,21 +185,21 @@ function peg$parse(input, options) {
|
||||
var peg$c8 = "#";
|
||||
var peg$c9 = "^";
|
||||
var peg$c10 = "_";
|
||||
var peg$c11 = ":";
|
||||
var peg$c12 = "[";
|
||||
var peg$c13 = "]";
|
||||
var peg$c14 = "{";
|
||||
var peg$c15 = "}";
|
||||
var peg$c16 = "%";
|
||||
var peg$c17 = "<";
|
||||
var peg$c18 = ">";
|
||||
var peg$c19 = "@";
|
||||
var peg$c20 = "!";
|
||||
var peg$c21 = "(";
|
||||
var peg$c22 = ")";
|
||||
var peg$c23 = "/";
|
||||
var peg$c24 = "*";
|
||||
var peg$c25 = "?";
|
||||
var peg$c11 = "[";
|
||||
var peg$c12 = "]";
|
||||
var peg$c13 = "{";
|
||||
var peg$c14 = "}";
|
||||
var peg$c15 = "%";
|
||||
var peg$c16 = "<";
|
||||
var peg$c17 = ">";
|
||||
var peg$c18 = "@";
|
||||
var peg$c19 = "!";
|
||||
var peg$c20 = "(";
|
||||
var peg$c21 = ")";
|
||||
var peg$c22 = "/";
|
||||
var peg$c23 = "*";
|
||||
var peg$c24 = "?";
|
||||
var peg$c25 = ":";
|
||||
var peg$c26 = "struct";
|
||||
var peg$c27 = "target";
|
||||
var peg$c28 = "euclid";
|
||||
@@ -237,21 +240,21 @@ function peg$parse(input, options) {
|
||||
var peg$e15 = peg$literalExpectation("#", false);
|
||||
var peg$e16 = peg$literalExpectation("^", false);
|
||||
var peg$e17 = peg$literalExpectation("_", false);
|
||||
var peg$e18 = peg$literalExpectation(":", false);
|
||||
var peg$e19 = peg$literalExpectation("[", false);
|
||||
var peg$e20 = peg$literalExpectation("]", false);
|
||||
var peg$e21 = peg$literalExpectation("{", false);
|
||||
var peg$e22 = peg$literalExpectation("}", false);
|
||||
var peg$e23 = peg$literalExpectation("%", false);
|
||||
var peg$e24 = peg$literalExpectation("<", false);
|
||||
var peg$e25 = peg$literalExpectation(">", false);
|
||||
var peg$e26 = peg$literalExpectation("@", false);
|
||||
var peg$e27 = peg$literalExpectation("!", false);
|
||||
var peg$e28 = peg$literalExpectation("(", false);
|
||||
var peg$e29 = peg$literalExpectation(")", false);
|
||||
var peg$e30 = peg$literalExpectation("/", false);
|
||||
var peg$e31 = peg$literalExpectation("*", false);
|
||||
var peg$e32 = peg$literalExpectation("?", false);
|
||||
var peg$e18 = peg$literalExpectation("[", false);
|
||||
var peg$e19 = peg$literalExpectation("]", false);
|
||||
var peg$e20 = peg$literalExpectation("{", false);
|
||||
var peg$e21 = peg$literalExpectation("}", false);
|
||||
var peg$e22 = peg$literalExpectation("%", false);
|
||||
var peg$e23 = peg$literalExpectation("<", false);
|
||||
var peg$e24 = peg$literalExpectation(">", false);
|
||||
var peg$e25 = peg$literalExpectation("@", false);
|
||||
var peg$e26 = peg$literalExpectation("!", false);
|
||||
var peg$e27 = peg$literalExpectation("(", false);
|
||||
var peg$e28 = peg$literalExpectation(")", false);
|
||||
var peg$e29 = peg$literalExpectation("/", false);
|
||||
var peg$e30 = peg$literalExpectation("*", false);
|
||||
var peg$e31 = peg$literalExpectation("?", false);
|
||||
var peg$e32 = peg$literalExpectation(":", false);
|
||||
var peg$e33 = peg$literalExpectation("struct", false);
|
||||
var peg$e34 = peg$literalExpectation("target", false);
|
||||
var peg$e35 = peg$literalExpectation("euclid", false);
|
||||
@@ -279,36 +282,37 @@ function peg$parse(input, options) {
|
||||
var peg$f8 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
|
||||
var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
|
||||
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
|
||||
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) };
|
||||
var peg$f12 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
|
||||
var peg$f12 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
|
||||
var peg$f13 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||
for (const op of ops) {
|
||||
op(result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
var peg$f13 = function(s) { return new PatternStub(s, 'fastcat'); };
|
||||
var peg$f14 = function(tail) { return { alignment: 'stack', list: tail }; };
|
||||
var peg$f15 = function(tail) { return { alignment: 'rand', list: tail }; };
|
||||
var peg$f16 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } };
|
||||
var peg$f17 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
||||
var peg$f18 = function(sc) { return sc; };
|
||||
var peg$f19 = function(s) { return { name: "struct", args: { mini:s }}};
|
||||
var peg$f20 = function(s) { return { name: "target", args : { name:s}}};
|
||||
var peg$f21 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
|
||||
var peg$f22 = function(a) { return { name: "stretch", args :{ amount: a}}};
|
||||
var peg$f23 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
|
||||
var peg$f24 = function(a) { return { name: "shift", args :{ amount: a}}};
|
||||
var peg$f25 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
|
||||
var peg$f26 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
|
||||
var peg$f27 = function(s, v) { return v};
|
||||
var peg$f28 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
|
||||
var peg$f29 = function(sg) {return sg};
|
||||
var peg$f30 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
|
||||
var peg$f31 = function(sc) { return sc };
|
||||
var peg$f32 = function(c) { return c };
|
||||
var peg$f33 = function(v) { return new CommandStub("setcps", { value: v})};
|
||||
var peg$f34 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
|
||||
var peg$f35 = function() { return new CommandStub("hush")};
|
||||
var peg$f14 = function(s) { return new PatternStub(s, 'fastcat'); };
|
||||
var peg$f15 = function(tail) { return { alignment: 'stack', list: tail }; };
|
||||
var peg$f16 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
|
||||
var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
|
||||
var peg$f18 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
||||
var peg$f19 = function(sc) { return sc; };
|
||||
var peg$f20 = function(s) { return { name: "struct", args: { mini:s }}};
|
||||
var peg$f21 = function(s) { return { name: "target", args : { name:s}}};
|
||||
var peg$f22 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
|
||||
var peg$f23 = function(a) { return { name: "stretch", args :{ amount: a}}};
|
||||
var peg$f24 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
|
||||
var peg$f25 = function(a) { return { name: "shift", args :{ amount: a}}};
|
||||
var peg$f26 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
|
||||
var peg$f27 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
|
||||
var peg$f28 = function(s, v) { return v};
|
||||
var peg$f29 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
|
||||
var peg$f30 = function(sg) {return sg};
|
||||
var peg$f31 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
|
||||
var peg$f32 = function(sc) { return sc };
|
||||
var peg$f33 = function(c) { return c };
|
||||
var peg$f34 = function(v) { return new CommandStub("setcps", { value: v})};
|
||||
var peg$f35 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
|
||||
var peg$f36 = function() { return new CommandStub("hush")};
|
||||
var peg$currPos = 0;
|
||||
var peg$savedPos = 0;
|
||||
var peg$posDetailsCache = [{ line: 1, column: 1 }];
|
||||
@@ -421,11 +425,11 @@ function peg$parse(input, options) {
|
||||
}
|
||||
}
|
||||
|
||||
function peg$computeLocation(startPos, endPos) {
|
||||
function peg$computeLocation(startPos, endPos, offset) {
|
||||
var startPosDetails = peg$computePosDetails(startPos);
|
||||
var endPosDetails = peg$computePosDetails(endPos);
|
||||
|
||||
return {
|
||||
var res = {
|
||||
source: peg$source,
|
||||
start: {
|
||||
offset: startPos,
|
||||
@@ -438,6 +442,11 @@ function peg$parse(input, options) {
|
||||
column: endPosDetails.column
|
||||
}
|
||||
};
|
||||
if (offset && peg$source && (typeof peg$source.offset === "function")) {
|
||||
res.start = peg$source.offset(res.start);
|
||||
res.end = peg$source.offset(res.end);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function peg$fail(expected) {
|
||||
@@ -848,15 +857,6 @@ function peg$parse(input, options) {
|
||||
s0 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e17); }
|
||||
}
|
||||
if (s0 === peg$FAILED) {
|
||||
if (input.charCodeAt(peg$currPos) === 58) {
|
||||
s0 = peg$c11;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s0 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -899,11 +899,11 @@ function peg$parse(input, options) {
|
||||
s0 = peg$currPos;
|
||||
s1 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 91) {
|
||||
s2 = peg$c12;
|
||||
s2 = peg$c11;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s2 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||
}
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
@@ -911,11 +911,11 @@ function peg$parse(input, options) {
|
||||
if (s4 !== peg$FAILED) {
|
||||
s5 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 93) {
|
||||
s6 = peg$c13;
|
||||
s6 = peg$c12;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s6 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
}
|
||||
if (s6 !== peg$FAILED) {
|
||||
s7 = peg$parsews();
|
||||
@@ -943,11 +943,11 @@ function peg$parse(input, options) {
|
||||
s0 = peg$currPos;
|
||||
s1 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 123) {
|
||||
s2 = peg$c14;
|
||||
s2 = peg$c13;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s2 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e21); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
||||
}
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
@@ -955,11 +955,11 @@ function peg$parse(input, options) {
|
||||
if (s4 !== peg$FAILED) {
|
||||
s5 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 125) {
|
||||
s6 = peg$c15;
|
||||
s6 = peg$c14;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s6 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e22); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e21); }
|
||||
}
|
||||
if (s6 !== peg$FAILED) {
|
||||
s7 = peg$parsepolymeter_steps();
|
||||
@@ -990,11 +990,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 37) {
|
||||
s1 = peg$c16;
|
||||
s1 = peg$c15;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e22); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
@@ -1019,11 +1019,11 @@ function peg$parse(input, options) {
|
||||
s0 = peg$currPos;
|
||||
s1 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 60) {
|
||||
s2 = peg$c17;
|
||||
s2 = peg$c16;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s2 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
||||
}
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
@@ -1031,11 +1031,11 @@ function peg$parse(input, options) {
|
||||
if (s4 !== peg$FAILED) {
|
||||
s5 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 62) {
|
||||
s6 = peg$c18;
|
||||
s6 = peg$c17;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s6 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e25); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
||||
}
|
||||
if (s6 !== peg$FAILED) {
|
||||
s7 = peg$parsews();
|
||||
@@ -1088,6 +1088,9 @@ function peg$parse(input, options) {
|
||||
s0 = peg$parseop_replicate();
|
||||
if (s0 === peg$FAILED) {
|
||||
s0 = peg$parseop_degrade();
|
||||
if (s0 === peg$FAILED) {
|
||||
s0 = peg$parseop_tail();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1102,11 +1105,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 64) {
|
||||
s1 = peg$c19;
|
||||
s1 = peg$c18;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e25); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
@@ -1130,11 +1133,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 33) {
|
||||
s1 = peg$c20;
|
||||
s1 = peg$c19;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e27); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
@@ -1158,11 +1161,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 40) {
|
||||
s1 = peg$c21;
|
||||
s1 = peg$c20;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e27); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
@@ -1186,11 +1189,11 @@ function peg$parse(input, options) {
|
||||
}
|
||||
s12 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 41) {
|
||||
s13 = peg$c22;
|
||||
s13 = peg$c21;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s13 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||
}
|
||||
if (s13 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
@@ -1224,11 +1227,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 47) {
|
||||
s1 = peg$c23;
|
||||
s1 = peg$c22;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e30); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
@@ -1252,11 +1255,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 42) {
|
||||
s1 = peg$c24;
|
||||
s1 = peg$c23;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e31); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e30); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
@@ -1280,11 +1283,11 @@ function peg$parse(input, options) {
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 63) {
|
||||
s1 = peg$c25;
|
||||
s1 = peg$c24;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e32); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e31); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
@@ -1301,6 +1304,34 @@ function peg$parse(input, options) {
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parseop_tail() {
|
||||
var s0, s1, s2;
|
||||
|
||||
s0 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 58) {
|
||||
s1 = peg$c25;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e32); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f12(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parseslice_with_ops() {
|
||||
var s0, s1, s2, s3;
|
||||
|
||||
@@ -1314,7 +1345,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parseslice_op();
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f12(s1, s2);
|
||||
s0 = peg$f13(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1339,7 +1370,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f13(s1);
|
||||
s1 = peg$f14(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1388,7 +1419,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f14(s1);
|
||||
s1 = peg$f15(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1437,7 +1468,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f15(s1);
|
||||
s1 = peg$f16(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1458,7 +1489,7 @@ function peg$parse(input, options) {
|
||||
s2 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f16(s1, s2);
|
||||
s0 = peg$f17(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1478,7 +1509,7 @@ function peg$parse(input, options) {
|
||||
s2 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f17(s1, s2);
|
||||
s0 = peg$f18(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1488,18 +1519,20 @@ function peg$parse(input, options) {
|
||||
}
|
||||
|
||||
function peg$parsemini() {
|
||||
var s0, s1, s2, s3, s4;
|
||||
var s0, s1, s2, s3, s4, s5, s6;
|
||||
|
||||
s0 = peg$currPos;
|
||||
s1 = peg$parsews();
|
||||
s2 = peg$parsequote();
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsestack_or_choose();
|
||||
if (s3 !== peg$FAILED) {
|
||||
s4 = peg$parsequote();
|
||||
if (s4 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
s4 = peg$parsestack_or_choose();
|
||||
if (s4 !== peg$FAILED) {
|
||||
s5 = peg$parsews();
|
||||
s6 = peg$parsequote();
|
||||
if (s6 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f18(s3);
|
||||
s0 = peg$f19(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1561,7 +1594,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsemini_or_operator();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f19(s3);
|
||||
s0 = peg$f20(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1594,7 +1627,7 @@ function peg$parse(input, options) {
|
||||
s5 = peg$parsequote();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f20(s4);
|
||||
s0 = peg$f21(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1639,7 +1672,7 @@ function peg$parse(input, options) {
|
||||
s7 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f21(s3, s5, s7);
|
||||
s0 = peg$f22(s3, s5, s7);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1672,7 +1705,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f22(s3);
|
||||
s0 = peg$f23(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1701,7 +1734,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f23(s3);
|
||||
s0 = peg$f24(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1730,7 +1763,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f24(s3);
|
||||
s0 = peg$f25(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1759,7 +1792,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f25(s3);
|
||||
s0 = peg$f26(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1801,7 +1834,7 @@ function peg$parse(input, options) {
|
||||
s5 = peg$parsequote();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f26(s4);
|
||||
s0 = peg$f27(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1876,11 +1909,11 @@ function peg$parse(input, options) {
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 91) {
|
||||
s3 = peg$c12;
|
||||
s3 = peg$c11;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s3 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||
}
|
||||
if (s3 !== peg$FAILED) {
|
||||
s4 = peg$parsews();
|
||||
@@ -1893,7 +1926,7 @@ function peg$parse(input, options) {
|
||||
s9 = peg$parsemini_or_operator();
|
||||
if (s9 !== peg$FAILED) {
|
||||
peg$savedPos = s7;
|
||||
s7 = peg$f27(s5, s9);
|
||||
s7 = peg$f28(s5, s9);
|
||||
} else {
|
||||
peg$currPos = s7;
|
||||
s7 = peg$FAILED;
|
||||
@@ -1910,7 +1943,7 @@ function peg$parse(input, options) {
|
||||
s9 = peg$parsemini_or_operator();
|
||||
if (s9 !== peg$FAILED) {
|
||||
peg$savedPos = s7;
|
||||
s7 = peg$f27(s5, s9);
|
||||
s7 = peg$f28(s5, s9);
|
||||
} else {
|
||||
peg$currPos = s7;
|
||||
s7 = peg$FAILED;
|
||||
@@ -1922,15 +1955,15 @@ function peg$parse(input, options) {
|
||||
}
|
||||
s7 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 93) {
|
||||
s8 = peg$c13;
|
||||
s8 = peg$c12;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s8 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
}
|
||||
if (s8 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f28(s5, s6);
|
||||
s0 = peg$f29(s5, s6);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1976,7 +2009,7 @@ function peg$parse(input, options) {
|
||||
s4 = peg$parsecomment();
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f29(s1);
|
||||
s0 = peg$f30(s1);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1998,7 +2031,7 @@ function peg$parse(input, options) {
|
||||
s5 = peg$parsemini_or_operator();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f30(s1, s5);
|
||||
s0 = peg$f31(s1, s5);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2023,7 +2056,7 @@ function peg$parse(input, options) {
|
||||
s1 = peg$parsemini_or_operator();
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f31(s1);
|
||||
s1 = peg$f32(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
if (s0 === peg$FAILED) {
|
||||
@@ -2056,7 +2089,7 @@ function peg$parse(input, options) {
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f32(s2);
|
||||
s0 = peg$f33(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2081,7 +2114,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f33(s3);
|
||||
s0 = peg$f34(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2110,7 +2143,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f34(s3);
|
||||
s0 = peg$f35(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2136,7 +2169,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f35();
|
||||
s1 = peg$f36();
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -2162,10 +2195,13 @@ function peg$parse(input, options) {
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var PatternStub = function(source, alignment)
|
||||
var PatternStub = function(source, alignment, seed)
|
||||
{
|
||||
this.type_ = "pattern";
|
||||
this.arguments_ = { alignment : alignment};
|
||||
this.arguments_ = { alignment: alignment };
|
||||
if (seed !== undefined) {
|
||||
this.arguments_.seed = seed;
|
||||
}
|
||||
this.source_ = source;
|
||||
}
|
||||
|
||||
@@ -2191,6 +2227,7 @@ function peg$parse(input, options) {
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
var seed = 0;
|
||||
|
||||
peg$result = peg$startRuleFunction();
|
||||
|
||||
|
||||
@@ -19,10 +19,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var PatternStub = function(source, alignment)
|
||||
var PatternStub = function(source, alignment, seed)
|
||||
{
|
||||
this.type_ = "pattern";
|
||||
this.arguments_ = { alignment : alignment};
|
||||
this.arguments_ = { alignment: alignment };
|
||||
if (seed !== undefined) {
|
||||
this.arguments_.seed = seed;
|
||||
}
|
||||
this.source_ = source;
|
||||
}
|
||||
|
||||
@@ -48,6 +51,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
var seed = 0;
|
||||
}
|
||||
|
||||
start = statement
|
||||
@@ -96,7 +100,7 @@ quote = '"' / "'"
|
||||
// ------------------ steps and cycles ---------------------------
|
||||
|
||||
// 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 new AtomStub(chars.join("")) }
|
||||
|
||||
// define a sub cycle e.g. [1 2, 3 [4]]
|
||||
@@ -119,7 +123,7 @@ slice = step / sub_cycle / polymeter / slow_sequence
|
||||
|
||||
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
||||
// at this point, we assume we can represent them as regular sequence operators
|
||||
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade
|
||||
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail
|
||||
|
||||
op_weight = "@" a:number
|
||||
{ return x => x.options_['weight'] = a }
|
||||
@@ -137,7 +141,10 @@ op_fast = "*"a:slice
|
||||
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
|
||||
|
||||
op_degrade = "?"a:number?
|
||||
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
|
||||
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) }
|
||||
|
||||
op_tail = ":" s:slice
|
||||
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
|
||||
|
||||
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
||||
slice_with_ops = s:slice ops:slice_op*
|
||||
@@ -159,12 +166,12 @@ stack_tail = tail:(comma @sequence)+
|
||||
// a choose is a series of pipe-separated sequence, one of which is
|
||||
// chosen at random, each cycle
|
||||
choose_tail = tail:(pipe @sequence)+
|
||||
{ return { alignment: 'rand', list: tail }; }
|
||||
{ return { alignment: 'rand', list: tail, seed: seed++ }; }
|
||||
|
||||
// if the stack contains only one element, we don't create a stack but return the
|
||||
// underlying element
|
||||
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, tail.seed); } else { return head; } }
|
||||
|
||||
polymeter_stack = head:sequence tail:stack_tail?
|
||||
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
|
||||
@@ -175,7 +182,7 @@ polymeter_stack = head:sequence tail:stack_tail?
|
||||
// Experimental haskellish parser begins
|
||||
|
||||
// mini-notation = a quoted stack
|
||||
mini = ws quote sc:stack_or_choose quote
|
||||
mini = ws quote ws sc:stack_or_choose ws quote
|
||||
{ return sc; }
|
||||
|
||||
// ------------------ operators ---------------------------
|
||||
|
||||
+83
-82
@@ -7,17 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
import * as krill from './krill-parser.js';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
/* var _seedState = 0;
|
||||
const randOffset = 0.0002;
|
||||
const randOffset = 0.0003;
|
||||
|
||||
function _nextSeed() {
|
||||
return _seedState++;
|
||||
} */
|
||||
|
||||
const applyOptions = (parent, code) => (pat, i) => {
|
||||
const applyOptions = (parent, enter) => (pat, i) => {
|
||||
const ast = parent.source_[i];
|
||||
const options = ast.options_;
|
||||
const ops = options?.ops;
|
||||
|
||||
if (ops) {
|
||||
for (const op of ops) {
|
||||
switch (op.type_) {
|
||||
@@ -27,43 +23,26 @@ const applyOptions = (parent, code) => (pat, i) => {
|
||||
if (!legalTypes.includes(type)) {
|
||||
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
|
||||
}
|
||||
pat = strudel.reify(pat)[type](patternifyAST(amount, code));
|
||||
pat = strudel.reify(pat)[type](enter(amount));
|
||||
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),
|
||||
);
|
||||
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
|
||||
} else {
|
||||
pat = pat.euclid(patternifyAST(op.arguments_.pulse, code), patternifyAST(op.arguments_.step, code));
|
||||
pat = pat.euclid(enter(op.arguments_.pulse), enter(op.arguments_.step));
|
||||
}
|
||||
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);
|
||||
pat = strudel
|
||||
.reify(pat)
|
||||
._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5);
|
||||
break;
|
||||
}
|
||||
case 'tail': {
|
||||
const friend = enter(op.arguments_.element);
|
||||
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -89,11 +68,14 @@ function resolveReplications(ast) {
|
||||
);
|
||||
}
|
||||
|
||||
export function patternifyAST(ast, code) {
|
||||
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
|
||||
export function patternifyAST(ast, code, onEnter, offset = 0) {
|
||||
onEnter?.(ast);
|
||||
const enter = (node) => patternifyAST(node, code, onEnter, offset);
|
||||
switch (ast.type_) {
|
||||
case 'pattern': {
|
||||
resolveReplications(ast);
|
||||
const children = ast.source_.map((child) => patternifyAST(child, code)).map(applyOptions(ast, code));
|
||||
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
|
||||
const alignment = ast.arguments_.alignment;
|
||||
if (alignment === 'stack') {
|
||||
return strudel.stack(...children);
|
||||
@@ -101,16 +83,14 @@ export function patternifyAST(ast, code) {
|
||||
if (alignment === 'polymeter') {
|
||||
// polymeter
|
||||
const stepsPerCycle = ast.arguments_.stepsPerCycle
|
||||
? patternifyAST(ast.arguments_.stepsPerCycle, code).fmap((x) => strudel.Fraction(x))
|
||||
? enter(ast.arguments_.stepsPerCycle).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);
|
||||
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
|
||||
}
|
||||
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
||||
if (!weightedChildren && alignment === 'slowcat') {
|
||||
@@ -130,7 +110,7 @@ export function patternifyAST(ast, code) {
|
||||
return pat;
|
||||
}
|
||||
case 'element': {
|
||||
return patternifyAST(ast.source_, code);
|
||||
return enter(ast.source_);
|
||||
}
|
||||
case 'atom': {
|
||||
if (ast.source_ === '~') {
|
||||
@@ -140,66 +120,82 @@ export function patternifyAST(ast, code) {
|
||||
console.warn('no location for', ast);
|
||||
return ast.source_;
|
||||
}
|
||||
const { start, end } = ast.location_;
|
||||
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
|
||||
// the following line expects the shapeshifter append .withMiniLocation
|
||||
// because location_ is only relative to the mini string, but we need it relative to whole code
|
||||
// make sure whitespaces are not part of the highlight:
|
||||
const actual = code?.split('').slice(start.offset, end.offset).join('');
|
||||
const [offsetStart = 0, offsetEnd = 0] = actual
|
||||
? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length)
|
||||
: [];
|
||||
return strudel
|
||||
.pure(value)
|
||||
.withLocation(
|
||||
[start.line, start.column + offsetStart, start.offset + offsetStart],
|
||||
[start.line, end.column - offsetEnd, end.offset - offsetEnd],
|
||||
);
|
||||
if (offset === -1) {
|
||||
// skip location handling (used when getting leaves to avoid confusion)
|
||||
return strudel.pure(value);
|
||||
}
|
||||
const [from, to] = getLeafLocation(code, ast, offset);
|
||||
return strudel.pure(value).withLoc(from, to);
|
||||
}
|
||||
case 'stretch':
|
||||
return patternifyAST(ast.source_, code).slow(patternifyAST(ast.arguments_.amount, code));
|
||||
/* case 'scale':
|
||||
let [tonic, scale] = Scale.tokenize(ast.arguments_.scale);
|
||||
const intervals = Scale.get(scale).intervals;
|
||||
const pattern = patternifyAST(ast.source_);
|
||||
tonic = tonic || 'C4';
|
||||
// console.log('scale', ast, pattern, tonic, scale);
|
||||
console.log('tonic', tonic);
|
||||
return pattern.fmap((step: any) => {
|
||||
step = Number(step);
|
||||
if (isNaN(step)) {
|
||||
console.warn(`scale step "${step}" not a number`);
|
||||
return step;
|
||||
}
|
||||
const octaves = Math.floor(step / intervals.length);
|
||||
const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
|
||||
const index = mod(step, intervals.length); // % with negative numbers. e.g. -1 % 3 = 2
|
||||
const interval = Interval.add(intervals[index], Interval.fromSemitones(octaves * 12));
|
||||
return Note.transpose(tonic, interval || '1P');
|
||||
}); */
|
||||
/* case 'struct':
|
||||
// TODO:
|
||||
return strudel.silence; */
|
||||
return enter(ast.source_).slow(enter(ast.arguments_.amount));
|
||||
default:
|
||||
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
|
||||
return strudel.silence;
|
||||
}
|
||||
}
|
||||
|
||||
// takes quoted mini string + leaf node within, returns source location of node (whitespace corrected)
|
||||
export const getLeafLocation = (code, leaf, globalOffset = 0) => {
|
||||
// value is expected without quotes!
|
||||
const { start, end } = leaf.location_;
|
||||
const actual = code?.split('').slice(start.offset, end.offset).join('');
|
||||
// make sure whitespaces are not part of the highlight
|
||||
const [offsetStart = 0, offsetEnd = 0] = actual
|
||||
? actual.split(leaf.source_).map((p) => p.split('').filter((c) => c === ' ').length)
|
||||
: [];
|
||||
return [start.offset + offsetStart + globalOffset, end.offset - offsetEnd + globalOffset];
|
||||
};
|
||||
|
||||
// takes quoted mini string, returns ast
|
||||
export const mini2ast = (code) => krill.parse(code);
|
||||
|
||||
// takes quoted mini string, returns all nodes that are leaves
|
||||
export const getLeaves = (code) => {
|
||||
const ast = mini2ast(code);
|
||||
let leaves = [];
|
||||
patternifyAST(
|
||||
ast,
|
||||
code,
|
||||
(node) => {
|
||||
if (node.type_ === 'atom') {
|
||||
leaves.push(node);
|
||||
}
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return leaves;
|
||||
};
|
||||
|
||||
// takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes
|
||||
export const getLeafLocations = (code, offset = 0) => {
|
||||
return getLeaves(code).map((l) => getLeafLocation(code, l, offset));
|
||||
};
|
||||
|
||||
// mini notation only (wraps in "")
|
||||
export const mini = (...strings) => {
|
||||
const pats = strings.map((str) => {
|
||||
const code = `"${str}"`;
|
||||
const ast = krill.parse(code);
|
||||
const ast = mini2ast(code);
|
||||
return patternifyAST(ast, code);
|
||||
});
|
||||
return strudel.sequence(...pats);
|
||||
};
|
||||
|
||||
// turns str mini string (without quotes) into pattern
|
||||
// offset is the position of the mini string in the JS code
|
||||
// each leaf node will get .withLoc added
|
||||
// this function is used by the transpiler for double quoted strings
|
||||
export const m = (str, offset) => {
|
||||
const code = `"${str}"`;
|
||||
const ast = mini2ast(code);
|
||||
return patternifyAST(ast, code, null, offset);
|
||||
};
|
||||
|
||||
// includes haskell style (raw krill parsing)
|
||||
export const h = (string) => {
|
||||
const ast = krill.parse(string);
|
||||
// console.log('ast', ast);
|
||||
const ast = mini2ast(string);
|
||||
return patternifyAST(ast, string);
|
||||
};
|
||||
|
||||
@@ -209,3 +205,8 @@ export function minify(thing) {
|
||||
}
|
||||
return strudel.reify(thing);
|
||||
}
|
||||
|
||||
// calling this function will cause patterns to parse strings as mini notation by default
|
||||
export function miniAllStrings() {
|
||||
strudel.setStringParser(mini);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.6.0",
|
||||
"version": "0.9.0",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -35,8 +35,8 @@
|
||||
"@strudel.cycles/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"peggy": "^2.0.1",
|
||||
"vite": "^3.2.2",
|
||||
"vitest": "^0.25.7"
|
||||
"peggy": "^3.0.2",
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.33.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
import { mini } from '../mini.mjs';
|
||||
import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs';
|
||||
import '@strudel.cycles/core/euclid.mjs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
@@ -140,7 +140,22 @@ describe('mini', () => {
|
||||
expect(haps.length < 230).toBe(true);
|
||||
// 'Had too many cycles remaining after degradeBy 0.8');
|
||||
});
|
||||
/*it('supports the random choice operator ("|") with nesting', () => {
|
||||
it('supports multiple independent uses of the random choice operator ("|")', () => {
|
||||
const numCycles = 1000;
|
||||
const values = mini('[a|b] [a|b]')
|
||||
.queryArc(0, numCycles)
|
||||
.map((e) => e.value);
|
||||
const observed = { aa: 0, ab: 0, ba: 0, bb: 0 };
|
||||
for (let i = 0; i < values.length; i += 2) {
|
||||
const chunk = values.slice(i, i + 2);
|
||||
observed[chunk.join('')]++;
|
||||
}
|
||||
for (const count of Object.values(observed)) {
|
||||
// Should fall within 99% confidence interval for binomial with p=0.25.
|
||||
expect(215 <= count && count <= 286).toBe(true);
|
||||
}
|
||||
});
|
||||
it('supports the random choice operator ("|") with nesting', () => {
|
||||
const numCycles = 900;
|
||||
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.
|
||||
@@ -165,6 +180,41 @@ describe('mini', () => {
|
||||
// 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
|
||||
// PRNG, this test should succeed
|
||||
expect(chisq <= 15.086).toBe(true);
|
||||
// assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
|
||||
});*/
|
||||
});
|
||||
it('supports lists', () => {
|
||||
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeafLocation', () => {
|
||||
it('gets location of leaf nodes', () => {
|
||||
const code = '"bd sd"';
|
||||
const ast = mini2ast(code);
|
||||
|
||||
const bd = ast.source_[0].source_;
|
||||
expect(getLeafLocation(code, bd)).toEqual([1, 3]);
|
||||
|
||||
const sd = ast.source_[1].source_;
|
||||
expect(getLeafLocation(code, sd)).toEqual([4, 6]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeafLocations', () => {
|
||||
it('gets locations of leaf nodes', () => {
|
||||
expect(getLeafLocations('"bd sd"')).toEqual([
|
||||
[1, 3], // bd columns
|
||||
[4, 6], // sd columns
|
||||
]);
|
||||
expect(getLeafLocations('"bd*2 [sd cp]"')).toEqual([
|
||||
[1, 3], // bd columns
|
||||
[7, 9], // sd columns
|
||||
[10, 12], // cp columns
|
||||
[4, 5], // "2" columns
|
||||
]);
|
||||
expect(getLeafLocations('"bd*<2 3>"')).toEqual([
|
||||
[1, 3], // bd columns
|
||||
[5, 6], // "2" columns
|
||||
[7, 8], // "3" columns
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import OSC from 'osc-js';
|
||||
|
||||
import { logger, parseNumeral, Pattern } from '@strudel.cycles/core';
|
||||
|
||||
let connection; // Promise<OSC>
|
||||
@@ -33,9 +34,6 @@ function connect() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
const latency = 0.1;
|
||||
let startedAt = -1;
|
||||
|
||||
/**
|
||||
*
|
||||
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
|
||||
@@ -51,17 +49,16 @@ Pattern.prototype.osc = function () {
|
||||
const osc = await connect();
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
// time should be audio time of onset
|
||||
// currentTime should be current time of audio context (slightly before time)
|
||||
if (startedAt < 0) {
|
||||
startedAt = Date.now() - currentTime * 1000;
|
||||
}
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
// make sure n and note are numbers
|
||||
controls.n && (controls.n = parseNumeral(controls.n));
|
||||
controls.note && (controls.note = parseNumeral(controls.note));
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
const ts = Math.floor(startedAt + (time + latency) * 1000);
|
||||
// time should be audio time of onset
|
||||
// currentTime should be current time of audio context (slightly before time)
|
||||
const offset = (time - currentTime) * 1000;
|
||||
// timestamp in milliseconds used to trigger the osc bundle at a precise moment
|
||||
const ts = Math.floor(Date.now() + offset);
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
const bundle = new OSC.Bundle([message], ts);
|
||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.6.0",
|
||||
"version": "0.9.0",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"publishConfig": {
|
||||
@@ -40,7 +40,7 @@
|
||||
"osc-js": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"pkg": "^5.7.0",
|
||||
"vite": "^3.2.2"
|
||||
"pkg": "^5.8.1",
|
||||
"vite": "^4.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
+33
-14
@@ -13,24 +13,43 @@ npm i @strudel.cycles/react
|
||||
Here is a minimal example of how to set up a MiniRepl:
|
||||
|
||||
```jsx
|
||||
import { evalScope, controls } from '@strudel.cycles/core';
|
||||
import * as React from 'react';
|
||||
import '@strudel.cycles/react/dist/style.css';
|
||||
import { MiniRepl } from '@strudel.cycles/react';
|
||||
import { prebake } from '../repl/src/prebake.mjs';
|
||||
import { evalScope, controls } from '@strudel.cycles/core';
|
||||
import { samples, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
||||
|
||||
evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
/* probably import other strudel packages */
|
||||
);
|
||||
async function prebake() {
|
||||
await samples(
|
||||
'https://strudel.tidalcycles.org/tidal-drum-machines.json',
|
||||
'github:ritchse/tidal-drum-machines/main/machines/'
|
||||
);
|
||||
await samples(
|
||||
'https://strudel.tidalcycles.org/EmuSP12.json',
|
||||
'https://strudel.tidalcycles.org/EmuSP12/'
|
||||
);
|
||||
}
|
||||
|
||||
prebake();
|
||||
async function init() {
|
||||
await evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/tonal')
|
||||
);
|
||||
await prebake();
|
||||
initAudioOnFirstClick();
|
||||
}
|
||||
|
||||
export function Repl({ tune }) {
|
||||
return <MiniRepl tune={tune} hideOutsideView={true} />;
|
||||
if (typeof window !== 'undefined') {
|
||||
init();
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <MiniRepl tune={`s("bd sd,hh*4")`} />;
|
||||
}
|
||||
```
|
||||
|
||||
For a more sophisticated example, check out the [nano-repl](./examples/nano-repl/)!
|
||||
- Open [example on stackblitz](https://stackblitz.com/edit/react-ts-saaair?file=tune.tsx,App.tsx)
|
||||
- Also check out the [nano-repl](./examples/nano-repl/) for a more sophisticated example
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# nano-repl
|
||||
|
||||
this is an example of how to create a repl with strudel and react.
|
||||
|
||||
## Usage
|
||||
|
||||
after cloning the strudel repo:
|
||||
|
||||
```sh
|
||||
pnpm i
|
||||
cd packages/react/examples/nano-repl
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
you should now have a repl running at `http://localhost:5173/`
|
||||
@@ -15,17 +15,18 @@
|
||||
"@strudel.cycles/osc": "workspace:*",
|
||||
"@strudel.cycles/mini": "workspace:*",
|
||||
"@strudel.cycles/transpiler": "workspace:*",
|
||||
"@strudel.cycles/soundfonts": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"@strudel.cycles/tonal": "workspace:*",
|
||||
"@strudel.cycles/react": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"postcss": "^8.4.16",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"vite": "^3.0.7"
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.23",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"vite": "^4.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import { controls, evalScope } from '@strudel.cycles/core';
|
||||
import { CodeMirror, useHighlighting, useKeydown, useStrudel, flash } from '@strudel.cycles/react';
|
||||
import { getAudioContext, initAudioOnFirstClick, panic, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
import {
|
||||
getAudioContext,
|
||||
initAudioOnFirstClick,
|
||||
panic,
|
||||
webaudioOutput,
|
||||
registerSynthSounds,
|
||||
} from '@strudel.cycles/webaudio';
|
||||
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
|
||||
import { useCallback, useState } from 'react';
|
||||
import './style.css';
|
||||
// import { prebake } from '../../../../../repl/src/prebake.mjs';
|
||||
|
||||
initAudioOnFirstClick();
|
||||
|
||||
// TODO: only import stuff when play is pressed?
|
||||
evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/xen'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/osc'),
|
||||
);
|
||||
async function init() {
|
||||
// TODO: only import stuff when play is pressed?
|
||||
const loadModules = evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/xen'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/osc'),
|
||||
);
|
||||
|
||||
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
|
||||
}
|
||||
init();
|
||||
|
||||
const defaultTune = `samples({
|
||||
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
|
||||
@@ -31,7 +43,7 @@ stack(
|
||||
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.superimpose(add(.05)) // add second, slightly detuned voice
|
||||
.n() // wrap in "n"
|
||||
.note() // wrap in "note"
|
||||
.decay(.15).sustain(0) // make each note of equal length
|
||||
.s('sawtooth') // waveform
|
||||
.gain(.4) // turn down
|
||||
@@ -40,7 +52,7 @@ stack(
|
||||
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
|
||||
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.n() // wrap in "n"
|
||||
.note() // wrap in "n"
|
||||
.s('square') // waveform
|
||||
.gain(.16) // turn down
|
||||
.cutoff(500) // fixed cutoff
|
||||
@@ -49,7 +61,7 @@ stack(
|
||||
,"a4 c5 <e6 a6>".struct("x(5,8)")
|
||||
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.n() // wrap in "n"
|
||||
.note() // wrap in "note"
|
||||
.decay(.1).sustain(0) // make notes short
|
||||
.s('triangle') // waveform
|
||||
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
|
||||
@@ -66,17 +78,18 @@ function App() {
|
||||
const [code, setCode] = useState(defaultTune);
|
||||
const [view, setView] = useState();
|
||||
// const [code, setCode] = useState(`"c3".note().slow(2)`);
|
||||
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern } = useStrudel({
|
||||
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern, started } = useStrudel({
|
||||
code,
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime,
|
||||
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
|
||||
});
|
||||
|
||||
useHighlighting({
|
||||
const { setMiniLocations } = useHighlighting({
|
||||
view,
|
||||
pattern,
|
||||
active: !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.getPhase(),
|
||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.now(),
|
||||
});
|
||||
|
||||
const error = evalError || schedulerError;
|
||||
@@ -103,7 +116,7 @@ function App() {
|
||||
}
|
||||
}
|
||||
},
|
||||
[scheduler, evaluate, view],
|
||||
[scheduler, evaluate, view, code],
|
||||
),
|
||||
);
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: #222;
|
||||
--lineBackground: #22222250;
|
||||
--foreground: #fff;
|
||||
--caret: #ffcc00;
|
||||
--selection: rgba(128, 203, 196, 0.5);
|
||||
--selectionMatch: #036dd626;
|
||||
--lineHighlight: #00000050;
|
||||
--gutterBackground: transparent;
|
||||
--gutterForeground: #8a919966;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #123;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,23 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
module.exports = {
|
||||
// TODO: find out if leaving out tutorial path works now
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}', '../../src/**/*.{html,js,jsx,md,mdx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
colors: {
|
||||
// codemirror-theme settings
|
||||
background: 'var(--background)',
|
||||
lineBackground: 'var(--lineBackground)',
|
||||
foreground: 'var(--foreground)',
|
||||
caret: 'var(--caret)',
|
||||
selection: 'var(--selection)',
|
||||
selectionMatch: 'var(--selectionMatch)',
|
||||
gutterBackground: 'var(--gutterBackground)',
|
||||
gutterForeground: 'var(--gutterForeground)',
|
||||
gutterBorder: 'var(--gutterBorder)',
|
||||
lineHighlight: 'var(--lineHighlight)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
+21
-20
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/react",
|
||||
"version": "0.6.0",
|
||||
"version": "0.9.0",
|
||||
"description": "React components for strudel",
|
||||
"main": "src/index.js",
|
||||
"publishConfig": {
|
||||
@@ -32,33 +32,34 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.4.0",
|
||||
"@codemirror/lang-javascript": "^6.1.1",
|
||||
"@codemirror/autocomplete": "^6.6.0",
|
||||
"@codemirror/lang-javascript": "^6.1.7",
|
||||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/view": "^6.7.3",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@replit/codemirror-emacs": "^6.0.0",
|
||||
"@replit/codemirror-vim": "^6.0.6",
|
||||
"@codemirror/view": "^6.10.0",
|
||||
"@lezer/highlight": "^1.1.4",
|
||||
"@replit/codemirror-emacs": "^6.0.1",
|
||||
"@replit/codemirror-vim": "^6.0.14",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/transpiler": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"@uiw/codemirror-themes": "^4.12.4",
|
||||
"@uiw/react-codemirror": "^4.12.4",
|
||||
"@strudel/codemirror": "workspace:*",
|
||||
"@uiw/codemirror-themes": "^4.19.16",
|
||||
"@uiw/react-codemirror": "^4.19.16",
|
||||
"react-hook-inview": "^4.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"postcss": "^8.4.18",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"vite": "^3.2.2"
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.23",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"vite": "^4.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ evalScope(
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/midi'),
|
||||
import('@strudel.cycles/xen'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,109 +1,38 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import _CodeMirror from '@uiw/react-codemirror';
|
||||
import { EditorView, Decoration } from '@codemirror/view';
|
||||
import { StateField, StateEffect } from '@codemirror/state';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import strudelTheme from '../themes/strudel-theme';
|
||||
import './style.css';
|
||||
import { useCallback } from 'react';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { strudelAutocomplete } from './Autocomplete';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
|
||||
export const setFlash = StateEffect.define();
|
||||
const flashField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(flash, tr) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setFlash)) {
|
||||
if (e.value) {
|
||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||
} else {
|
||||
flash = Decoration.set([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flash;
|
||||
} catch (err) {
|
||||
console.warn('flash error', err);
|
||||
return flash;
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
export const flash = (view) => {
|
||||
view.dispatch({ effects: setFlash.of(true) });
|
||||
setTimeout(() => {
|
||||
view.dispatch({ effects: setFlash.of(false) });
|
||||
}, 200);
|
||||
};
|
||||
|
||||
export const setHighlights = StateEffect.define();
|
||||
const highlightField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(highlights, tr) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setHighlights)) {
|
||||
const { haps } = e.value;
|
||||
const marks =
|
||||
haps
|
||||
.map((hap) =>
|
||||
(hap.context.locations || []).map(({ start, end }) => {
|
||||
// const color = hap.context.color || e.value.color || '#FFCA28';
|
||||
let from = tr.newDoc.line(start.line).from + start.column;
|
||||
let to = tr.newDoc.line(end.line).from + end.column;
|
||||
const l = tr.newDoc.length;
|
||||
if (from > l || to > l) {
|
||||
return; // dont mark outside of range, as it will throw an error
|
||||
}
|
||||
//const mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
|
||||
const mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
|
||||
return mark.range(from, to);
|
||||
}),
|
||||
)
|
||||
.flat()
|
||||
.filter(Boolean) || [];
|
||||
highlights = Decoration.set(marks, true);
|
||||
}
|
||||
}
|
||||
return highlights;
|
||||
} catch (err) {
|
||||
// console.warn('highlighting error', err);
|
||||
return Decoration.set([]);
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
const staticExtensions = [
|
||||
javascript(),
|
||||
highlightField,
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
import _CodeMirror from '@uiw/react-codemirror';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import strudelTheme from '../themes/strudel-theme';
|
||||
import { strudelAutocomplete } from './Autocomplete';
|
||||
import {
|
||||
highlightExtension,
|
||||
flashField,
|
||||
// javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
|
||||
// autocompletion({ override: [strudelAutocomplete] }),
|
||||
autocompletion({ override: [] }), // wait for https://github.com/uiwjs/react-codemirror/pull/458
|
||||
];
|
||||
flash,
|
||||
highlightMiniLocations,
|
||||
updateMiniLocations,
|
||||
} from '@strudel/codemirror';
|
||||
import './style.css';
|
||||
|
||||
export { flash, highlightMiniLocations, updateMiniLocations };
|
||||
|
||||
const staticExtensions = [javascript(), flashField, highlightExtension];
|
||||
|
||||
export default function CodeMirror({
|
||||
value,
|
||||
onChange,
|
||||
onViewChanged,
|
||||
onSelectionChange,
|
||||
onDocChange,
|
||||
theme,
|
||||
keybindings,
|
||||
isLineNumbersDisplayed,
|
||||
isAutoCompletionEnabled,
|
||||
isLineWrappingEnabled,
|
||||
fontSize = 18,
|
||||
fontFamily = 'monospace',
|
||||
options,
|
||||
editorDidMount,
|
||||
}) {
|
||||
const handleOnChange = useCallback(
|
||||
(value) => {
|
||||
@@ -111,30 +40,52 @@ export default function CodeMirror({
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const handleOnCreateEditor = useCallback(
|
||||
(view) => {
|
||||
onViewChanged?.(view);
|
||||
},
|
||||
[onViewChanged],
|
||||
);
|
||||
|
||||
const handleOnUpdate = useCallback(
|
||||
(viewUpdate) => {
|
||||
if (viewUpdate.docChanged && onDocChange) {
|
||||
onDocChange?.(viewUpdate);
|
||||
}
|
||||
if (viewUpdate.selectionSet && onSelectionChange) {
|
||||
onSelectionChange?.(viewUpdate.state.selection);
|
||||
}
|
||||
},
|
||||
[onSelectionChange],
|
||||
);
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
let _extensions = [...staticExtensions];
|
||||
let bindings = {
|
||||
vim,
|
||||
emacs,
|
||||
};
|
||||
|
||||
if (bindings[keybindings]) {
|
||||
return [...staticExtensions, bindings[keybindings]()];
|
||||
_extensions.push(bindings[keybindings]());
|
||||
}
|
||||
return staticExtensions;
|
||||
}, [keybindings]);
|
||||
|
||||
if (isAutoCompletionEnabled) {
|
||||
_extensions.push(javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }));
|
||||
} else {
|
||||
_extensions.push(autocompletion({ override: [] }));
|
||||
}
|
||||
|
||||
if (isLineWrappingEnabled) {
|
||||
_extensions.push(EditorView.lineWrapping);
|
||||
}
|
||||
|
||||
return _extensions;
|
||||
}, [keybindings, isAutoCompletionEnabled, isLineWrappingEnabled]);
|
||||
|
||||
const basicSetup = useMemo(() => ({ lineNumbers: isLineNumbersDisplayed }), [isLineNumbersDisplayed]);
|
||||
|
||||
return (
|
||||
<div style={{ fontSize, fontFamily }} className="w-full">
|
||||
<_CodeMirror
|
||||
@@ -144,107 +95,8 @@ export default function CodeMirror({
|
||||
onCreateEditor={handleOnCreateEditor}
|
||||
onUpdate={handleOnUpdate}
|
||||
extensions={extensions}
|
||||
basicSetup={basicSetup}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let parenMark;
|
||||
export const markParens = (editor, data) => {
|
||||
const v = editor.getDoc().getValue();
|
||||
const marked = getCurrentParenArea(v, data);
|
||||
parenMark?.clear();
|
||||
parenMark = editor.getDoc().markText(...marked, { css: 'background-color: #00007720' }); //
|
||||
};
|
||||
|
||||
// returns { line, ch } from absolute character offset
|
||||
export function offsetToPosition(offset, code) {
|
||||
const lines = code.split('\n');
|
||||
let line = 0;
|
||||
let ch = 0;
|
||||
for (let i = 0; i < offset; i++) {
|
||||
if (ch === lines[line].length) {
|
||||
line++;
|
||||
ch = 0;
|
||||
} else {
|
||||
ch++;
|
||||
}
|
||||
}
|
||||
return { line, ch };
|
||||
}
|
||||
|
||||
// returns absolute character offset from { line, ch }
|
||||
export function positionToOffset(position, code) {
|
||||
const lines = code.split('\n');
|
||||
if (position.line > lines.length) {
|
||||
// throw new Error('positionToOffset: position.line > lines.length');
|
||||
return 0;
|
||||
}
|
||||
let offset = 0;
|
||||
for (let i = 0; i < position.line; i++) {
|
||||
offset += lines[i].length + 1;
|
||||
}
|
||||
offset += position.ch;
|
||||
return offset;
|
||||
}
|
||||
|
||||
// given code and caret position, the functions returns the indices of the parens we are in
|
||||
export function getCurrentParenArea(code, caretPosition) {
|
||||
const caret = positionToOffset(caretPosition, code);
|
||||
let open, i, begin, end;
|
||||
// walk left
|
||||
i = caret;
|
||||
open = 0;
|
||||
while (i > 0) {
|
||||
if (code[i - 1] === '(') {
|
||||
open--;
|
||||
} else if (code[i - 1] === ')') {
|
||||
open++;
|
||||
}
|
||||
if (open === -1) {
|
||||
break;
|
||||
}
|
||||
i--;
|
||||
}
|
||||
begin = i;
|
||||
// walk right
|
||||
i = caret;
|
||||
open = 0;
|
||||
while (i < code.length) {
|
||||
if (code[i] === '(') {
|
||||
open--;
|
||||
} else if (code[i] === ')') {
|
||||
open++;
|
||||
}
|
||||
if (open === 1) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
end = i;
|
||||
return [begin, end].map((o) => offsetToPosition(o, code));
|
||||
}
|
||||
|
||||
/*
|
||||
export const markEvent = (editor) => (time, event) => {
|
||||
const locs = event.context.locations;
|
||||
if (!locs || !editor) {
|
||||
return;
|
||||
}
|
||||
const col = event.context?.color || '#FFCA28';
|
||||
// mark active event
|
||||
const marks = locs.map(({ start, end }) =>
|
||||
editor.getDoc().markText(
|
||||
{ line: start.line - 1, ch: start.column },
|
||||
{ line: end.line - 1, ch: end.column },
|
||||
//{ css: 'background-color: #FFCA28; color: black' } // background-color is now used by parent marking
|
||||
{ css: 'outline: 1px solid ' + col + '; box-sizing:border-box' },
|
||||
//{ css: `background-color: ${col};border-radius:5px` },
|
||||
),
|
||||
);
|
||||
//Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler...
|
||||
setTimeout(() => {
|
||||
marks.forEach((mark) => mark.clear());
|
||||
// }, '+' + event.duration * 0.5);
|
||||
}, event.duration * 1000);
|
||||
}; */
|
||||
|
||||
@@ -7,7 +7,6 @@ import useHighlighting from '../hooks/useHighlighting.mjs';
|
||||
import useStrudel from '../hooks/useStrudel.mjs';
|
||||
import CodeMirror6, { flash } from './CodeMirror6';
|
||||
import { Icon } from './Icon';
|
||||
import styles from './MiniRepl.module.css';
|
||||
import './style.css';
|
||||
import { logger } from '@strudel.cycles/core';
|
||||
import useEvent from '../hooks/useEvent.mjs';
|
||||
@@ -19,16 +18,24 @@ export function MiniRepl({
|
||||
tune,
|
||||
hideOutsideView = false,
|
||||
enableKeyboard,
|
||||
onTrigger,
|
||||
drawTime,
|
||||
punchcard,
|
||||
punchcardLabels,
|
||||
onPaint,
|
||||
canvasHeight = 200,
|
||||
fontSize = 18,
|
||||
fontFamily,
|
||||
hideHeader = false,
|
||||
theme,
|
||||
keybindings,
|
||||
isLineNumbersDisplayed,
|
||||
}) {
|
||||
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
|
||||
const evalOnMount = !!drawTime;
|
||||
const drawContext = useCallback(
|
||||
!!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
|
||||
[drawTime],
|
||||
punchcard ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
|
||||
[punchcard],
|
||||
);
|
||||
const {
|
||||
code,
|
||||
@@ -48,11 +55,23 @@ export function MiniRepl({
|
||||
} = useStrudel({
|
||||
initialCode: tune,
|
||||
defaultOutput: webaudioOutput,
|
||||
editPattern: (pat) => (punchcard ? pat.punchcard() : pat),
|
||||
editPattern: (pat, id) => {
|
||||
//pat = pat.withContext((ctx) => ({ ...ctx, id }));
|
||||
if (onTrigger) {
|
||||
pat = pat.onTrigger(onTrigger, false);
|
||||
}
|
||||
if (onPaint) {
|
||||
pat = pat.onPaint(onPaint);
|
||||
} else if (punchcard) {
|
||||
pat = pat.punchcard({ labels: punchcardLabels });
|
||||
}
|
||||
return pat;
|
||||
},
|
||||
getTime,
|
||||
evalOnMount,
|
||||
drawContext,
|
||||
drawTime,
|
||||
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
|
||||
});
|
||||
|
||||
const [view, setView] = useState();
|
||||
@@ -66,7 +85,7 @@ export function MiniRepl({
|
||||
}
|
||||
return isVisible || wasVisible.current;
|
||||
}, [isVisible, hideOutsideView]);
|
||||
useHighlighting({
|
||||
const { setMiniLocations } = useHighlighting({
|
||||
view,
|
||||
pattern,
|
||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
@@ -83,7 +102,7 @@ export function MiniRepl({
|
||||
e.preventDefault();
|
||||
flash(view);
|
||||
await activateCode();
|
||||
} else if (e.code === 'Period') {
|
||||
} else if (e.key === '.' || e.code === 'Period') {
|
||||
stop();
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -94,26 +113,6 @@ export function MiniRepl({
|
||||
),
|
||||
);
|
||||
|
||||
// set active pattern on ctrl+enter
|
||||
useLayoutEffect(() => {
|
||||
if (enableKeyboard) {
|
||||
const handleKeyPress = async (e) => {
|
||||
if (e.ctrlKey || e.altKey) {
|
||||
if (e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
flash(view);
|
||||
await activateCode();
|
||||
} else if (e.code === 'Period') {
|
||||
stop();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyPress, true);
|
||||
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
||||
}
|
||||
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
|
||||
|
||||
const [log, setLog] = useState([]);
|
||||
useLogger(
|
||||
useCallback((e) => {
|
||||
@@ -122,29 +121,54 @@ export function MiniRepl({
|
||||
// const logId = data?.pattern?.meta?.id;
|
||||
if (logId === replId) {
|
||||
setLog((l) => {
|
||||
return l.concat([e.detail]).slice(-10);
|
||||
return l.concat([e.detail]).slice(-8);
|
||||
});
|
||||
}
|
||||
}, []),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={ref}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.buttons}>
|
||||
<button className={cx(styles.button, started ? 'animate-pulse' : '')} onClick={() => togglePlay()}>
|
||||
<Icon type={started ? 'stop' : 'play'} />
|
||||
</button>
|
||||
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
||||
<Icon type="refresh" />
|
||||
</button>
|
||||
<div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight" ref={ref}>
|
||||
{!hideHeader && (
|
||||
<div className="flex justify-between bg-lineHighlight">
|
||||
<div className="flex">
|
||||
<button
|
||||
className={cx(
|
||||
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
|
||||
started ? 'animate-pulse' : '',
|
||||
)}
|
||||
onClick={() => togglePlay()}
|
||||
>
|
||||
<Icon type={started ? 'stop' : 'play'} />
|
||||
</button>
|
||||
<button
|
||||
className={cx(
|
||||
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
|
||||
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
onClick={() => activateCode()}
|
||||
>
|
||||
<Icon type="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{error && <div className={styles.error}>{error.message}</div>}
|
||||
)}
|
||||
<div className="overflow-auto relative">
|
||||
{show && (
|
||||
<CodeMirror6
|
||||
value={code}
|
||||
onChange={setCode}
|
||||
onViewChanged={setView}
|
||||
theme={theme}
|
||||
fontFamily={fontFamily}
|
||||
fontSize={fontSize}
|
||||
keybindings={keybindings}
|
||||
isLineNumbersDisplayed={isLineNumbersDisplayed}
|
||||
/>
|
||||
)}
|
||||
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} theme={theme} />}
|
||||
</div>
|
||||
{drawTime && (
|
||||
{punchcard && (
|
||||
<canvas
|
||||
id={canvasId}
|
||||
className="w-full pointer-events-none"
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
.container {
|
||||
@apply overflow-hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply flex justify-between bg-lineHighlight border-t border-l border-r border-lineHighlight rounded-t-md overflow-hidden;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
@apply flex;
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground hover:bg-background;
|
||||
}
|
||||
|
||||
.buttonDisabled {
|
||||
@apply w-16 flex items-center justify-center p-1 opacity-50 cursor-not-allowed border-r border-lineHighlight;
|
||||
}
|
||||
|
||||
.error {
|
||||
@apply text-right p-1 text-sm text-red-200;
|
||||
}
|
||||
|
||||
.body {
|
||||
@apply overflow-auto relative;
|
||||
}
|
||||
@@ -1,3 +1,15 @@
|
||||
:root {
|
||||
--background: #222;
|
||||
--lineBackground: #22222299;
|
||||
--foreground: #fff;
|
||||
--caret: #ffcc00;
|
||||
--selection: rgba(128, 203, 196, 0.5);
|
||||
--selectionMatch: #036dd626;
|
||||
--lineHighlight: #00000050;
|
||||
--gutterBackground: transparent;
|
||||
--gutterForeground: #8a919966;
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
background-color: transparent !important;
|
||||
height: 100%;
|
||||
@@ -12,3 +24,7 @@
|
||||
.cm-theme-light {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
footer {
|
||||
z-index: 0 !important;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { setHighlights } from '../components/CodeMirror6';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { highlightMiniLocations, updateMiniLocations } from '../components/CodeMirror6';
|
||||
const round = (x) => Math.round(x * 1000) / 1000;
|
||||
|
||||
function useHighlighting({ view, pattern, active, getTime }) {
|
||||
const highlights = useRef([]);
|
||||
const lastEnd = useRef(0);
|
||||
|
||||
const [miniLocations, setMiniLocations] = useState([]);
|
||||
useEffect(() => {
|
||||
if (view) {
|
||||
updateMiniLocations(view, miniLocations);
|
||||
}
|
||||
}, [view, miniLocations]);
|
||||
|
||||
useEffect(() => {
|
||||
if (view) {
|
||||
if (pattern && active) {
|
||||
@@ -14,14 +23,14 @@ function useHighlighting({ view, pattern, active, getTime }) {
|
||||
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
|
||||
// see https://github.com/tidalcycles/strudel/issues/108
|
||||
const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy
|
||||
const span = [begin, audioTime + 1 / 60];
|
||||
const span = [round(begin), round(audioTime + 1 / 60)];
|
||||
lastEnd.current = span[1];
|
||||
highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
|
||||
highlights.current = highlights.current.filter((hap) => hap.endClipped > audioTime); // keep only highlights that are still active
|
||||
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
||||
highlights.current = highlights.current.concat(haps); // add potential new onsets
|
||||
view.dispatch({ effects: setHighlights.of({ haps: highlights.current }) }); // highlight all still active + new active haps
|
||||
highlightMiniLocations(view, begin, highlights.current);
|
||||
} catch (err) {
|
||||
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
|
||||
highlightMiniLocations(view, 0, []);
|
||||
}
|
||||
frame = requestAnimationFrame(updateHighlights);
|
||||
});
|
||||
@@ -30,10 +39,12 @@ function useHighlighting({ view, pattern, active, getTime }) {
|
||||
};
|
||||
} else {
|
||||
highlights.current = [];
|
||||
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
|
||||
highlightMiniLocations(view, 0, highlights.current);
|
||||
}
|
||||
}
|
||||
}, [pattern, active, view]);
|
||||
|
||||
return { setMiniLocations };
|
||||
}
|
||||
|
||||
export default useHighlighting;
|
||||
|
||||
@@ -25,10 +25,10 @@ function usePatternFrame({ pattern, started, getTime, onDraw, drawTime = [-2, 2]
|
||||
const haps = pattern.queryArc(Math.max(lastFrame.current, phase - 1 / 10), phase);
|
||||
lastFrame.current = phase;
|
||||
visibleHaps.current = (visibleHaps.current || [])
|
||||
.filter((h) => h.whole.end >= phase - lookbehind - lookahead) // in frame
|
||||
.filter((h) => h.endClipped >= phase - lookbehind - lookahead) // in frame
|
||||
.concat(haps.filter((h) => h.hasOnset()));
|
||||
onDraw(pattern, phase - lookahead, visibleHaps.current, drawTime);
|
||||
}, [pattern]),
|
||||
}, [pattern, onDraw]),
|
||||
);
|
||||
useEffect(() => {
|
||||
if (started) {
|
||||
|
||||
@@ -18,6 +18,7 @@ function useStrudel({
|
||||
canvasId,
|
||||
drawContext,
|
||||
drawTime = [-2, 2],
|
||||
paintOptions = {},
|
||||
}) {
|
||||
const id = useMemo(() => s4(), []);
|
||||
canvasId = canvasId || `canvas-${id}`;
|
||||
@@ -29,10 +30,11 @@ function useStrudel({
|
||||
const [pattern, setPattern] = useState();
|
||||
const [started, setStarted] = useState(false);
|
||||
const isDirty = code !== activeCode;
|
||||
const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
|
||||
//const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
|
||||
const shouldPaint = useCallback((pat) => !!pat?.context?.onPaint, []);
|
||||
|
||||
// TODO: make sure this hook reruns when scheduler.started changes
|
||||
const { scheduler, evaluate, start, stop, pause } = useMemo(
|
||||
const { scheduler, evaluate, start, stop, pause, setCps } = useMemo(
|
||||
() =>
|
||||
repl({
|
||||
interval,
|
||||
@@ -46,9 +48,9 @@ function useStrudel({
|
||||
drawContext,
|
||||
transpiler,
|
||||
editPattern,
|
||||
beforeEval: ({ code }) => {
|
||||
beforeEval: async ({ code }) => {
|
||||
setCode(code);
|
||||
beforeEval?.();
|
||||
await beforeEval?.();
|
||||
},
|
||||
afterEval: (res) => {
|
||||
const { pattern: _pattern, code } = res;
|
||||
@@ -84,9 +86,9 @@ function useStrudel({
|
||||
(pattern, time, haps, drawTime) => {
|
||||
const { onPaint } = pattern.context || {};
|
||||
const ctx = typeof drawContext === 'function' ? drawContext(canvasId) : drawContext;
|
||||
onPaint?.(ctx, time, haps, drawTime);
|
||||
onPaint?.(ctx, time, haps, drawTime, paintOptions);
|
||||
},
|
||||
[drawContext, canvasId],
|
||||
[drawContext, canvasId, paintOptions],
|
||||
);
|
||||
|
||||
const drawFirstFrame = useCallback(
|
||||
@@ -153,6 +155,7 @@ function useStrudel({
|
||||
stop,
|
||||
pause,
|
||||
togglePlay,
|
||||
setCps,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// import 'tailwindcss/tailwind.css';
|
||||
|
||||
export { default as CodeMirror, flash } from './components/CodeMirror6'; // !SSR
|
||||
export { default as CodeMirror, flash, updateMiniLocations, highlightMiniLocations } from './components/CodeMirror6'; // !SSR
|
||||
export * from './components/MiniRepl'; // !SSR
|
||||
export { default as useHighlighting } from './hooks/useHighlighting'; // !SSR
|
||||
export { default as useStrudel } from './hooks/useStrudel'; // !SSR
|
||||
@@ -8,4 +8,5 @@ export { default as usePostMessage } from './hooks/usePostMessage';
|
||||
export { default as useKeydown } from './hooks/useKeydown';
|
||||
export { default as useEvent } from './hooks/useEvent';
|
||||
export { default as strudelTheme } from './themes/strudel-theme';
|
||||
export { default as teletext } from './themes/teletext';
|
||||
export { default as cx } from './cx';
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
ReactDOM.render(
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ export const settings = {
|
||||
gutterBackground: 'transparent',
|
||||
gutterForeground: '#0f380f',
|
||||
light: true,
|
||||
customStyle: '.cm-line { line-height: 1 }',
|
||||
};
|
||||
export default createTheme({
|
||||
theme: 'light',
|
||||
@@ -35,5 +36,6 @@ export default createTheme({
|
||||
{ tag: t.propertyName, color: '#0f380f' },
|
||||
{ tag: t.className, color: '#0f380f' },
|
||||
{ tag: t.invalid, color: '#0f380f' },
|
||||
{ tag: [t.unit, t.punctuation], color: '#0f380f' },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -33,5 +33,6 @@ export default createTheme({
|
||||
{ tag: t.propertyName, color: 'white' },
|
||||
{ tag: t.className, color: 'white' },
|
||||
{ tag: t.invalid, color: 'white' },
|
||||
{ tag: [t.unit, t.punctuation], color: 'white' },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -36,5 +36,6 @@ export default createTheme({
|
||||
{ tag: t.propertyName, color: 'white' },
|
||||
{ tag: t.className, color: 'white' },
|
||||
{ tag: t.invalid, color: 'white' },
|
||||
{ tag: [t.unit, t.punctuation], color: 'white' },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -40,5 +40,6 @@ export default createTheme({
|
||||
|
||||
{ tag: t.className, color: '#decb6b' },
|
||||
{ tag: t.invalid, color: '#ffffff' },
|
||||
{ tag: [t.unit, t.punctuation], color: '#82aaff' },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from '@uiw/codemirror-themes';
|
||||
|
||||
let colorA = '#6edee4';
|
||||
//let colorB = 'magenta';
|
||||
let colorB = 'white';
|
||||
let colorC = 'red';
|
||||
let colorD = '#f8fc55';
|
||||
|
||||
export const settings = {
|
||||
background: '#000000',
|
||||
foreground: colorA, // whats that?
|
||||
caret: colorC,
|
||||
selection: colorD,
|
||||
selectionMatch: colorA,
|
||||
lineHighlight: '#6edee440', // panel bg
|
||||
lineBackground: '#00000040',
|
||||
gutterBackground: 'transparent',
|
||||
gutterForeground: '#8a919966',
|
||||
customStyle: '.cm-line { line-height: 1 }',
|
||||
};
|
||||
|
||||
let punctuation = colorD;
|
||||
let mini = colorB;
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.keyword, color: colorA },
|
||||
{ tag: t.operator, color: mini },
|
||||
{ tag: t.special(t.variableName), color: colorA },
|
||||
{ tag: t.typeName, color: colorA },
|
||||
{ tag: t.atom, color: colorA },
|
||||
{ tag: t.number, color: mini },
|
||||
{ tag: t.definition(t.variableName), color: colorA },
|
||||
{ tag: t.string, color: mini },
|
||||
{ tag: t.special(t.string), color: mini },
|
||||
{ tag: t.comment, color: punctuation },
|
||||
{ tag: t.variableName, color: colorA },
|
||||
{ tag: t.tagName, color: colorA },
|
||||
{ tag: t.bracket, color: punctuation },
|
||||
{ tag: t.meta, color: colorA },
|
||||
{ tag: t.attributeName, color: colorA },
|
||||
{ tag: t.propertyName, color: colorA }, // methods
|
||||
{ tag: t.className, color: colorA },
|
||||
{ tag: t.invalid, color: colorC },
|
||||
{ tag: [t.unit, t.punctuation], color: punctuation },
|
||||
],
|
||||
});
|
||||
@@ -1,7 +1,21 @@
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
colors: {
|
||||
// codemirror-theme settings
|
||||
background: 'var(--background)',
|
||||
lineBackground: 'var(--lineBackground)',
|
||||
foreground: 'var(--foreground)',
|
||||
caret: 'var(--caret)',
|
||||
selection: 'var(--selection)',
|
||||
selectionMatch: 'var(--selectionMatch)',
|
||||
gutterBackground: 'var(--gutterBackground)',
|
||||
gutterForeground: 'var(--gutterForeground)',
|
||||
gutterBorder: 'var(--gutterBorder)',
|
||||
lineHighlight: 'var(--lineHighlight)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
corePlugins: {
|
||||
|
||||
@@ -22,8 +22,6 @@ export default defineConfig({
|
||||
...Object.keys(peerDependencies),
|
||||
...Object.keys(dependencies),
|
||||
// TODO: find out which of below names are obsolete now
|
||||
'@strudel.cycles/tone',
|
||||
'@strudel.cycles/eval',
|
||||
'@strudel.cycles/transpiler',
|
||||
'acorn',
|
||||
'@strudel.cycles/core',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/serial",
|
||||
"version": "0.6.0",
|
||||
"version": "0.9.0",
|
||||
"description": "Webserial API for strudel",
|
||||
"main": "serial.mjs",
|
||||
"publishConfig": {
|
||||
@@ -32,6 +32,6 @@
|
||||
"@strudel.cycles/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^3.2.2"
|
||||
"vite": "^4.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { toMidi } from '@strudel.cycles/core';
|
||||
import { noteToMidi, freqToMidi } from '@strudel.cycles/core';
|
||||
import { getAudioContext, registerSound, getEnvelope } from '@strudel.cycles/webaudio';
|
||||
import gm from './gm.mjs';
|
||||
|
||||
let loadCache = {};
|
||||
async function loadFont(name) {
|
||||
@@ -8,7 +10,6 @@ async function loadFont(name) {
|
||||
const load = async () => {
|
||||
// TODO: make soundfont source configurable
|
||||
const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`;
|
||||
console.log('load font', name, url);
|
||||
const preset = await fetch(url).then((res) => res.text());
|
||||
let [_, data] = preset.split('={');
|
||||
return eval('{' + data);
|
||||
@@ -17,15 +18,24 @@ async function loadFont(name) {
|
||||
return loadCache[name];
|
||||
}
|
||||
|
||||
export async function getFontBufferSource(name, pitch, ac) {
|
||||
if (typeof pitch === 'string') {
|
||||
pitch = toMidi(pitch);
|
||||
export async function getFontBufferSource(name, value, ac) {
|
||||
let { note = 'c3', freq } = value;
|
||||
let midi;
|
||||
if (freq) {
|
||||
midi = freqToMidi(freq);
|
||||
} else if (typeof note === 'string') {
|
||||
midi = noteToMidi(note);
|
||||
} else if (typeof note === 'number') {
|
||||
midi = note;
|
||||
} else {
|
||||
throw new Error(`unexpected "note" type "${typeof note}"`);
|
||||
}
|
||||
const { buffer, zone } = await getFontPitch(name, pitch, ac);
|
||||
|
||||
const { buffer, zone } = await getFontPitch(name, midi, ac);
|
||||
const src = ac.createBufferSource();
|
||||
src.buffer = buffer;
|
||||
const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune;
|
||||
const playbackRate = 1.0 * Math.pow(2, (100.0 * pitch - baseDetune) / 1200.0);
|
||||
const playbackRate = 1.0 * Math.pow(2, (100.0 * midi - baseDetune) / 1200.0);
|
||||
// src detune?
|
||||
src.playbackRate.value = playbackRate;
|
||||
const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd;
|
||||
@@ -114,3 +124,32 @@ async function getBuffer(zone, audioContext) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerSoundfonts() {
|
||||
Object.entries(gm).forEach(([name, fonts]) => {
|
||||
registerSound(
|
||||
name,
|
||||
async (time, value, onended) => {
|
||||
const { n = 0 } = value;
|
||||
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
|
||||
const font = fonts[n % fonts.length];
|
||||
const ctx = getAudioContext();
|
||||
const bufferSource = await getFontBufferSource(font, value, ctx);
|
||||
bufferSource.start(time);
|
||||
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 0.3, time);
|
||||
bufferSource.connect(envelope);
|
||||
const stop = (releaseTime) => {
|
||||
bufferSource.stop(releaseTime + release);
|
||||
releaseEnvelope(releaseTime);
|
||||
};
|
||||
bufferSource.onended = () => {
|
||||
bufferSource.disconnect();
|
||||
envelope.disconnect();
|
||||
onended();
|
||||
};
|
||||
return { node: envelope, stop };
|
||||
},
|
||||
{ type: 'soundfont', prebake: true, fonts },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import { getFontBufferSource } from './fontloader.mjs';
|
||||
import { getFontBufferSource, registerSoundfonts } from './fontloader.mjs';
|
||||
import * as soundfontList from './list.mjs';
|
||||
import { startPresetNote } from 'sfumato';
|
||||
import { loadSoundfont } from './sfumato.mjs';
|
||||
|
||||
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList };
|
||||
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts };
|
||||
|
||||
+926
-665
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/soundfonts",
|
||||
"version": "0.6.0",
|
||||
"version": "0.9.0",
|
||||
"description": "Soundsfont support for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -36,7 +36,7 @@
|
||||
"soundfont2": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-fetch": "^3.2.6",
|
||||
"vite": "^3.2.2"
|
||||
"node-fetch": "^3.3.1",
|
||||
"vite": "^4.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Pattern, getPlayableNoteValue, toMidi } from '@strudel.cycles/core';
|
||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||
import { Pattern, getPlayableNoteValue, noteToMidi } from '@strudel.cycles/core';
|
||||
import { getAudioContext, registerSound } from '@strudel.cycles/webaudio';
|
||||
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
||||
|
||||
Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||
@@ -8,7 +8,7 @@ Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||
const note = getPlayableNoteValue(h);
|
||||
const preset = sf.presets[n % sf.presets.length];
|
||||
const deadline = ctx.currentTime + t - ct;
|
||||
const args = [ctx, preset, toMidi(note), deadline];
|
||||
const args = [ctx, preset, noteToMidi(note), deadline];
|
||||
const stop = startPresetNote(...args);
|
||||
stop(deadline + h.duration);
|
||||
});
|
||||
@@ -21,5 +21,29 @@ export function loadSoundfont(url) {
|
||||
}
|
||||
const sf = _loadSoundfont(url);
|
||||
soundfontCache.set(url, sf);
|
||||
/*sf.then((font) => {
|
||||
font.presets.forEach((preset) => {
|
||||
console.log('preset', preset.header.name);
|
||||
registerSound(
|
||||
preset.header.name.replaceAll(' ', '_'),
|
||||
(time, value, onended) => {
|
||||
const ctx = getAudioContext();
|
||||
let { note } = value; // freq ?
|
||||
|
||||
const p = font.presets.find((p) => p.header.name === preset.header.name);
|
||||
|
||||
if (!p) {
|
||||
throw new Error('preset not found');
|
||||
}
|
||||
const deadline = time; // - ctx.currentTime;
|
||||
const args = [ctx, p, noteToMidi(note), deadline];
|
||||
const stop = startPresetNote(...args);
|
||||
return { node: undefined, stop };
|
||||
},
|
||||
{ type: 'soundfont' },
|
||||
);
|
||||
});
|
||||
//console.log('f', f);
|
||||
});*/
|
||||
return sf;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# superdough
|
||||
|
||||
superdough is a simple web audio sampler and synth, intended for live coding.
|
||||
It is the default output of [strudel](https://strudel.tidalcycles.org/).
|
||||
This package has no ties to strudel and can be used to quickly bake your own music system on the web.
|
||||
|
||||
## Install
|
||||
|
||||
via npm:
|
||||
|
||||
```js
|
||||
npm i superdough --save
|
||||
```
|
||||
|
||||
## Use
|
||||
|
||||
```js
|
||||
import { superdough, samples, initAudioOnFirstClick, registerSynthSounds } from 'superdough';
|
||||
|
||||
const init = Promise.all([
|
||||
initAudioOnFirstClick(),
|
||||
samples('github:tidalcycles/Dirt-Samples/master'),
|
||||
registerSynthSounds(),
|
||||
]);
|
||||
|
||||
const loop = (t = 0) => {
|
||||
// superdough(value, time, duration)
|
||||
superdough({ s: 'bd', delay: 0.5 }, t);
|
||||
superdough({ note: 'g1', s: 'sawtooth', cutoff: 600, resonance: 8 }, t, 0.125);
|
||||
superdough({ note: 'g2', s: 'sawtooth', cutoff: 600, resonance: 8 }, t + 0.25, 0.125);
|
||||
superdough({ s: 'hh' }, t + 0.25);
|
||||
superdough({ s: 'sd', room: 0.5 }, t + 0.5);
|
||||
superdough({ s: 'hh' }, t + 0.75);
|
||||
};
|
||||
|
||||
document.getElementById('play').addEventListener('click', async () => {
|
||||
await init;
|
||||
let t = 0.1;
|
||||
while (t < 16) {
|
||||
loop(t++);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
[Open this in Codesandbox](https://codesandbox.io/s/superdough-demo-forked-sf8djh?file=/src/index.js)
|
||||
|
||||
## API
|
||||
|
||||
### superdough(value, deadline, duration)
|
||||
|
||||
```js
|
||||
superdough({ s: 'bd', delay: 0.5 }, 0, 1);
|
||||
```
|
||||
|
||||
- `value`: the sound properties:
|
||||
- `s`: the name of the sound as loaded via `samples` or `registerSound`
|
||||
- `n`: selects sample with given index
|
||||
- `bank`: prefix_ that is attached to the sound, e.g. `{ s: 'bd', bank: 'RolandTR909' }` = `{ s: 'RolandTR909_bd' }`
|
||||
- `gain`: gain from 0 to 1 (higher values also work but might clip)
|
||||
- `velocity`: additional gain multiplier
|
||||
- `cutoff`: low pass filter cutoff
|
||||
- `resonance`: low pass filter resonance
|
||||
- `hcutoff`: high pass filter cutoff
|
||||
- `hresonance`: high pass filter resonance
|
||||
- `bandf`: band pass filter cutoff
|
||||
- `bandq`: band pass filter resonance
|
||||
- `crush`: amplitude bit crusher using given number of bits
|
||||
- `shape`: distortion effect from 0 (none) to 1 (full). might get loud!
|
||||
- `pan`: stereo panning from 0 (left) to 1 (right)
|
||||
- `vowel`: vowel filter. possible values: "a", "e", "i", "o", "u"
|
||||
- `delay`: delay mix
|
||||
- `delayfeedback`: delay feedback
|
||||
- `delaytime`: delay time
|
||||
- `room`: reverb mix
|
||||
- `size`: reverb room size
|
||||
- `orbit`: bus name for global effects `delay` and `room`. same orbits will get the same effects
|
||||
- `freq`: repitches sound to given frequency in Hz
|
||||
- `note`: repitches sound to given note or midi number
|
||||
- `cut`: sets cut group. Sounds of same group will cut each other off
|
||||
- `clip`: multiplies duration with given number
|
||||
- `speed`: repitches sound by given factor
|
||||
- `begin`: moves beginning of sample to given factor (between 0 and 1)
|
||||
- `end`: moves end of sample to given factor (between 0 and 1)
|
||||
- `attack`: seconds of attack phase
|
||||
- `decay`: seconds of decay phase
|
||||
- `sustain`: gain of sustain phase
|
||||
- `release`: seconds of release phase
|
||||
- `deadline`: seconds until the sound should play (0 = immediate)
|
||||
- `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds
|
||||
|
||||
### registerSynthSounds()
|
||||
|
||||
Loads the default waveforms `sawtooth`, `square`, `triangle` and `sine`. Use them like this:
|
||||
|
||||
```js
|
||||
superdough({ s:'sawtooth' }, 0, 1)
|
||||
```
|
||||
|
||||
The duration needs to be set for these sounds!
|
||||
|
||||
### samples(sampleMap)
|
||||
|
||||
allows you to load samples from URLs. There are 3 ways to load samples
|
||||
|
||||
1. sample map object
|
||||
2. url of sample map json file
|
||||
3. github repo
|
||||
|
||||
#### sample map object
|
||||
|
||||
You can pass a sample map like this:
|
||||
|
||||
```js
|
||||
samples({
|
||||
'_base': 'https://raw.githubusercontent.com/felixroos/samples/main/',
|
||||
'bd': 'president/president_bd.mp3',
|
||||
'sd': ['president/president_sd.mp3', 'president/president_sd2.mp3'],
|
||||
'hh': ['president/president_hh.mp3'],
|
||||
})
|
||||
```
|
||||
|
||||
The `_base` property defines the root url while the others declare one or more sample paths for each sound.
|
||||
|
||||
For example the full URL for `bd` would then be `https://raw.githubusercontent.com/felixroos/samples/main/president/president_bd.mp3`
|
||||
|
||||
A loaded sound can then be played with `superdough({ s: 'bd' }, 0)`.
|
||||
|
||||
If you declare multiple sounds, you can select them with `n`: `superdough({ s: 'sd', n: 1 }, 0)`
|
||||
|
||||
The duration property is not needed for samples.
|
||||
|
||||
#### loading samples from a json file
|
||||
|
||||
Instead of passing an object as a sample map, you can also pass a URL to a json that contains a sample map:
|
||||
|
||||
```js
|
||||
samples('https://raw.githubusercontent.com/felixroos/samples/main/strudel.json')
|
||||
```
|
||||
|
||||
The json file is expected to have the same format as described above.
|
||||
|
||||
#### loading samples from a github repo
|
||||
|
||||
Because it is common to use github for samples, there is a short way to load a sample map from github:
|
||||
|
||||
```js
|
||||
samples('github:tidalcycles/Dirt-Samples/master')
|
||||
```
|
||||
|
||||
The format is `github:<user>/<repo>/<branch>`.
|
||||
|
||||
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
|
||||
|
||||
The format is also expected to be the same as explained above.
|
||||
|
||||
### initAudioOnFirstClick()
|
||||
|
||||
Initializes audio and makes sure it is playable after the first click in the document. A click is needed because of the [Autoplay Policy](https://www.w3.org/TR/autoplay-detection/).
|
||||
You can call this function when the document loads.
|
||||
Then just make sure your first call of `superdough` happens after a click of something.
|
||||
|
||||
## Credits
|
||||
|
||||
- [ZZFX](https://github.com/KilledByAPixel/ZzFX) used for synths starting with z
|
||||
- [SuperDirt](https://github.com/musikinformatik/SuperDirt)
|
||||
- [WebDirt](https://github.com/dktr0/WebDirt)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user