mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-19 10:26:41 +01:00
Compare commits
652 commits
0.2.0-rc.7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d930025422 | |||
| 97c563a5ad | |||
| f38db87b17 | |||
| e6bf5c1f6e | |||
| ef81b085a0 | |||
| 2c3bfe4ebb | |||
| 86941305a4 | |||
| 5e2e0438a4 | |||
| 307dab8686 | |||
| 1434adae09 | |||
| 34070de5f7 | |||
| e987aa697d | |||
| 7f03116cb3 | |||
| f81f16b47b | |||
| 559d2fc4a1 | |||
| cfd19062fd | |||
| 5ccbb9719f | |||
| 17c6184f0e | |||
| d5865d941f | |||
| fa5f67f010 | |||
| 0d7998a5a8 | |||
| 08730df042 | |||
| 28aacd7cbc | |||
| f87a5c14f9 | |||
| 74b497cf3a | |||
| ea47d605a6 | |||
| cac3fe044e | |||
| 4604ad66a2 | |||
| 3dada45ea9 | |||
| fcfb7a0915 | |||
| 50728729b7 | |||
| 4d4c470a81 | |||
| b991a49ad7 | |||
| f2d6e7724b | |||
| 525a455f7a | |||
| 525ed15466 | |||
| 72975c861a | |||
| 43c71e874d | |||
| efdc25fded | |||
| 75b5190cfc | |||
| f488811767 | |||
| e9f912f4d9 | |||
| 3c8616deba | |||
| 9e147cda69 | |||
| f60dd2185a | |||
| 121a273788 | |||
| 4fbd6ab408 | |||
| 71c519b711 | |||
| 45dc05acd6 | |||
| 287983c140 | |||
| 4a2e742e56 | |||
| 73748e1fb9 | |||
| 02312e32a5 | |||
| 8c175e22f5 | |||
| e81955890c | |||
| 0192d85a19 | |||
| cb7e4f7a95 | |||
| 447638ee71 | |||
| 573534a9a6 | |||
| fc038dbd97 | |||
| 99d9da6ffd | |||
| 7746abc9ee | |||
| 3e9545fe26 | |||
| baad8254a2 | |||
| 958e602577 | |||
| 50c263b4d3 | |||
| 01db41b75d | |||
| 9fcb5a08c6 | |||
| 2858b01bd4 | |||
| eb0547dc37 | |||
| 3e748fefa7 | |||
| f938ade839 | |||
| 4f575246ef | |||
| aeb1f7a9e0 | |||
| 29b2789be6 | |||
| f1f5ac63e1 | |||
| 62bfb0120b | |||
| 701ea3fc27 | |||
| ef6aa9ab07 | |||
| 5ed69edd02 | |||
| 4f3a50f2d6 | |||
| b0393184fa | |||
| d3d60d69c7 | |||
| 5ff6868a17 | |||
| c7e7c9f68c | |||
| 3f1a2fee80 | |||
| 48603e4812 | |||
| b663c53b0a | |||
| a9288cb0c2 | |||
| 0d9bb709a5 | |||
| a7f37e52cf | |||
| e9b4a2ca78 | |||
| 4d0868add8 | |||
| b5326b578c | |||
| b6d1978a55 | |||
| 17abb3d971 | |||
| e312e442fa | |||
| 0733742685 | |||
|
|
4f6cb7cb8e | ||
| a45e409e6b | |||
| fca1e85611 | |||
| 9aeb792f7d | |||
| 4ba88bfd6d | |||
| 7495dd10f2 | |||
| 5a360d02fa | |||
| 094d5dd451 | |||
| 182107bfa5 | |||
| 2013bac62f | |||
| 03024f8a14 | |||
| c66a006120 | |||
| b7152ef807 | |||
| 4a9e9132f3 | |||
| d45bd2122e | |||
| 0f16c89248 | |||
| e3a3962130 | |||
| d7bbc2a412 | |||
| 872c2d94d6 | |||
| 4fe51b5267 | |||
| 57eff50973 | |||
| f3c67f95b5 | |||
| 254e19db0d | |||
| 8df49850ae | |||
| ebdb8881e9 | |||
| 6ce83fb27a | |||
| 89288f2920 | |||
| fa73821a0b | |||
| b0ef0cfd21 | |||
| 57102d7e6b | |||
| 6f6078e25a | |||
| e81ae58ab5 | |||
| ea48dd6fa1 | |||
| decbb177f0 | |||
| d1be569b48 | |||
| d647fc68e9 | |||
| ed926b9444 | |||
| cdeb355972 | |||
| 85a144798b | |||
| e00d870d70 | |||
| 4e2702f69e | |||
| 997d67a487 | |||
| 329da026d7 | |||
| 836624674e | |||
| ee2efd1c26 | |||
| b9c101081b | |||
| 6db5df5210 | |||
| 7bc37e7659 | |||
| 7690549bdc | |||
| e5752ea4b0 | |||
| 2ef9628ab8 | |||
| 4127c141cc | |||
| 7f255eaea8 | |||
| 5fab1af138 | |||
| c5586c3a35 | |||
| c78b2dc9de | |||
| fb99128650 | |||
| 5648c96c6a | |||
| 7b09d97473 | |||
| 986e0a42a1 | |||
| ccf21cbdfe | |||
| 5fa5a875b7 | |||
| 9e8572ae0f | |||
| 5c74ffd916 | |||
| 4394e03352 | |||
| b69a89aac9 | |||
| 65d054a03b | |||
| b433688f22 | |||
| e684415c2f | |||
| 75f8fd8746 | |||
| 419a07de8c | |||
| bfa0ea1163 | |||
| ec5e2a982b | |||
| 0cb259b7b5 | |||
| 6d4a629311 | |||
| 87cd6099ad | |||
| cc88743054 | |||
| 6f8a677b7a | |||
| 780fd6694b | |||
| 1b48e10d2d | |||
| e3b12a1d36 | |||
| 16d267523b | |||
| ee7f9dcf12 | |||
| a8be2e9dad | |||
| a6100ab1d6 | |||
| 04af945ea0 | |||
| 9f9de3fafb | |||
| 1b926b0338 | |||
| a3beab0f36 | |||
| 6286d69824 | |||
| bb325869c2 | |||
| 16e9405b1f | |||
| ebd0f18c9c | |||
| 0a090765d3 | |||
| a77536c234 | |||
| 55b6745d4d | |||
| 79bf493004 | |||
| ff2e981e18 | |||
| a67481ab04 | |||
| 944fcfa017 | |||
| b4761a9679 | |||
| 866327bbe7 | |||
| 3fd045cf93 | |||
| d427dc409d | |||
| cd8d85bd97 | |||
| aefc147347 | |||
| 0efcb7f0fe | |||
| 26baa8127d | |||
| 6ed0627056 | |||
| 0e5207a79d | |||
| b0c936bda0 | |||
| 0533ea92ac | |||
| 457e6bb7eb | |||
| a22a793c31 | |||
| 77703d83a5 | |||
| c367a0444e | |||
| 8adbdc5bc7 | |||
| 7b432d12b4 | |||
| 3c2d490f83 | |||
| 80964d5b4a | |||
| 57e0f64056 | |||
| daaa4f7bef | |||
| 9f30f77aee | |||
| 9bc4e3fb5f | |||
| 2fd7d7b39f | |||
| 5696cbbebb | |||
| 3561867640 | |||
| dcd0de7710 | |||
| 26b7a9390e | |||
| a2f27dac90 | |||
| efd182f302 | |||
| e9c825e865 | |||
| 397e71edee | |||
| 22155f7acf | |||
| 3ef3d5eb6f | |||
| e4808f8fc1 | |||
| f8994d3e2d | |||
| f2cecb29b3 | |||
| 5d72a1f90e | |||
| cc94b3485e | |||
| a0cc88ff26 | |||
| 38fb348d19 | |||
| fa2e08e81c | |||
| 0f9cb1437f | |||
| d88f4e33eb | |||
| 7af98b7008 | |||
| a9cfaf9767 | |||
| 2c59b1acfd | |||
| 24bc33d3d0 | |||
| 13444dc59a | |||
| 39dc6b803e | |||
| 3b5c23f68c | |||
| 2877545140 | |||
| b376d75396 | |||
| b58fbdfd30 | |||
| 8b0e484114 | |||
| 09edbbe730 | |||
| 1cc3a58826 | |||
| 866d88c8ec | |||
| 9f70441627 | |||
| a9d22bd26f | |||
| 85616f7338 | |||
| 5db97825cc | |||
| ab37e2e7d4 | |||
| 4b998eaae0 | |||
| b8d6194a72 | |||
| 9b754c0f52 | |||
| 6b9099b087 | |||
| 37be2f4add | |||
| aa8eaf2e2b | |||
| bcd747280c | |||
| 8e9d7dc9a1 | |||
| 4d3f308c10 | |||
| 0fa6d31c7e | |||
| 0027952260 | |||
| cbda299260 | |||
| 59372911d2 | |||
| 393634a1a4 | |||
| 8fa0f8a409 | |||
| 2f8882f6cd | |||
| 414650da31 | |||
| c439528cfc | |||
| 88f22c96c6 | |||
| e6ca672b3a | |||
| 610b6b4353 | |||
| 39ab736cbe | |||
| 93a64833e9 | |||
| 997c8e2d6e | |||
| 3beb24d594 | |||
| 02d0a350dd | |||
| 3d154f1853 | |||
| 1b253dc273 | |||
| 0257aa2b61 | |||
| 664cd8942f | |||
| d893ae0309 | |||
| 5120930919 | |||
| e1a5910db3 | |||
| 805b2ee139 | |||
| 9c1a43ec2d | |||
| 53a01d92ae | |||
| e039c19796 | |||
| 09b9da0f61 | |||
| 83b82c9572 | |||
| 5097b61c7f | |||
| 7aa5627371 | |||
| 5e21792a26 | |||
| f58ff407b3 | |||
| ee6f24515f | |||
| 92271f727d | |||
| 8e5c4d0aba | |||
| 6536f1612e | |||
| c7ee3185c1 | |||
| 852522208e | |||
| 98d56e7009 | |||
| d83be2ef1f | |||
| 9c95ca9613 | |||
| 432b916823 | |||
| 09c8e651d4 | |||
| f12a58463c | |||
| fe9c1e38a4 | |||
| 85749a3437 | |||
| 0c6484d733 | |||
| fbcf148293 | |||
| 74ce1b9f55 | |||
| 70fd1efc1e | |||
| a4f0487324 | |||
| 70ad0b343b | |||
| 3d290a9beb | |||
| 36f7c8bd48 | |||
| 5b61c71503 | |||
| 4229364363 | |||
| 4dbbe0340a | |||
| 9214958bd2 | |||
| 30f2cba54d | |||
| 438a2d86a6 | |||
| 64d520e75c | |||
| 7633813ab2 | |||
| b9d1a82314 | |||
| c35d505b9c | |||
| 9d88a7361f | |||
| 8794b2e05b | |||
| 93a14a3040 | |||
| 60e4bb49b2 | |||
| bcc3f5809e | |||
| 8465d64807 | |||
| 3837dbd47b | |||
| 77d617f9a0 | |||
| f71118613b | |||
| f263c84555 | |||
| c66256124a | |||
| 48fcd00926 | |||
| fd32cbc34c | |||
| 5b797342b7 | |||
| ec85224f3a | |||
| 1924d51323 | |||
| 53f443f4bd | |||
| e0f4ec9a15 | |||
| fe9d5a309e | |||
| d1bb33dc41 | |||
| 7b3bbc5590 | |||
| 6755f972f3 | |||
| 357efeea3e | |||
| 0243e24614 | |||
| 10f8fcc84b | |||
| efdf17f11d | |||
| 6a246cf4ad | |||
| 91e5b8dd16 | |||
| 57b3e48830 | |||
| 6bb73e3896 | |||
| 0999c42f12 | |||
| a16e5361d1 | |||
| 29f09a2556 | |||
| 863c028047 | |||
| bd680db230 | |||
| e8e0f5646d | |||
| 2e18ca96fd | |||
| c78dd2453a | |||
| 5d900a303b | |||
| ad1abf6ec8 | |||
| 1004c6a4d6 | |||
| 36c1c9bebb | |||
| df396f2f83 | |||
| 1172615d8b | |||
| e4620b9a92 | |||
| 3502070613 | |||
| e2023bdf53 | |||
| 414b5e1580 | |||
| 13af81c5ad | |||
| cdbcea0a8f | |||
| 30f43eac79 | |||
| deeaaa0b8b | |||
| 58433c0402 | |||
| bf429cdbbe | |||
| 2270eb8cb3 | |||
| 821f373bd2 | |||
| 7a97b07c0a | |||
| 1fb5dfbe11 | |||
| a66a6a9669 | |||
| 4eff4316c6 | |||
| 923568d7f9 | |||
| 451b33b9bc | |||
| 2ef668ef0b | |||
| 5d6592bbdf | |||
| 76cefdca61 | |||
| 7dc435754a | |||
| 77809ca289 | |||
| 854f198e36 | |||
| a386ba1d86 | |||
| 6b463f14f0 | |||
| b2e4b01bfe | |||
| af958b275d | |||
| 9462a3f2a4 | |||
| 41d31e4db6 | |||
| 2433239eaa | |||
| a79a5c57b3 | |||
| 0fa8e5bf15 | |||
| ffe8893bed | |||
| be6bd32b78 | |||
| 7aa99e4692 | |||
| 5b2c2318a5 | |||
| b306059dbc | |||
| bbe49ad463 | |||
| 4028b3bb29 | |||
| b2c9bfc0e2 | |||
| 6c8f85ab84 | |||
| c13eff95ca | |||
| 85507bf27e | |||
| 2ea6a7bd8b | |||
| 415dc444ea | |||
| 751e7d2160 | |||
| 81a74d79dc | |||
| 93462e7501 | |||
| f7dcc28e1f | |||
| f5d84c2450 | |||
| 680a841e3f | |||
| 209f35440a | |||
| 7ad574cf2a | |||
| a31de6e819 | |||
| ee28d431bd | |||
| cfa3cad5cb | |||
| 9d250daa04 | |||
| f9f9051eb7 | |||
| b8726de78f | |||
| 266f59085e | |||
| 323afe4c89 | |||
| a595e2e895 | |||
| 67148a4aa4 | |||
| 9756862091 | |||
| d14d67172c | |||
| a362028ae7 | |||
| cf1fd5b45a | |||
| dc7b713108 | |||
| 92fcb0af8f | |||
| a949117017 | |||
| 452bdf9598 | |||
| 5e7b867aba | |||
| 38f69ddd50 | |||
| 34b35d08be | |||
| 798de37172 | |||
| 297f9b30df | |||
| ff31957fed | |||
| 143cd24e09 | |||
| 1b9da07280 | |||
| d4f962fbfa | |||
| 3a6202464c | |||
| 1463460c4f | |||
| 9b549d7dfe | |||
| dff6f1e279 | |||
| 3030f28ef7 | |||
| a670320533 | |||
| 5bf1bad7be | |||
| 2ad5b27db6 | |||
| 3f2cf57ea8 | |||
| 2a5af2c753 | |||
| c08d1bee5d | |||
| 968441850f | |||
| 6408cd26b8 | |||
| fc3ecfb241 | |||
| 525923d057 | |||
| 26562437bd | |||
| ed90196a60 | |||
| 8e2aed58af | |||
| 385297c59f | |||
| fadaaa1620 | |||
| ce91e1a043 | |||
| 0ce0a07713 | |||
| 0fb7655b53 | |||
| 2f2e97cf71 | |||
| 0c6add7038 | |||
| b9cc594bdb | |||
| dc45edf7e0 | |||
| 228b4bb47c | |||
| a66bc5ca5e | |||
| ca1fb3c414 | |||
| 6fd87ce4ed | |||
| 44201ebf76 | |||
| 12faadef44 | |||
| d393cab2d8 | |||
| 50b7d8a23d | |||
| efbabe6248 | |||
| acfaf757ec | |||
| 43ccfff24a | |||
| 1b7f0e0b93 | |||
| c9677c87d8 | |||
| 9cd6e9f195 | |||
| df50bb9f47 | |||
| e62e36d558 | |||
| 08184f9906 | |||
| 23fe9f0949 | |||
| 585bba6666 | |||
| ddcb967a2c | |||
| 08a6716bb7 | |||
| 57fda5c7ad | |||
| 8eecd75592 | |||
| 811e341cd5 | |||
| fa70a42bad | |||
| 4af6e011b6 | |||
| 93fa3c26b4 | |||
| af2e237b94 | |||
| ceaaeb1fc7 | |||
| 91d6bcc870 | |||
| 5d3e564949 | |||
| 0d7f78e74f | |||
| 2afae4b6aa | |||
| 8c54f8e426 | |||
| 1ff35baea9 | |||
| fc06fb863b | |||
| 794d4210c6 | |||
| 4ab08e48e5 | |||
| 19ed6a24b8 | |||
| f485a068a8 | |||
| 364d617d37 | |||
| 8dcf73c18c | |||
| aad7aa6c5e | |||
| 8850fbf2f8 | |||
| 479988272e | |||
| e73c31d494 | |||
| 744ce21e24 | |||
| 4fb703d05d | |||
| 1aa0551931 | |||
| bb52555183 | |||
| 06b643e2b1 | |||
| 8c6716adce | |||
| 1f10c95ed0 | |||
| f1bd9e7e88 | |||
| 2cd56e7391 | |||
| ed462cd0f6 | |||
| cff87657b9 | |||
| a9cad18891 | |||
| 532b648a9e | |||
| efa35834de | |||
| 156504b5ba | |||
| a92981bb50 | |||
| ba0ff4af98 | |||
| d9f1875c03 | |||
| f7d9d2e107 | |||
| 1fe60bff5f | |||
| 9035353893 | |||
| d5db15f5a1 | |||
| 54414e2114 | |||
| d3d1670af2 | |||
| f6ab777c82 | |||
| 1b82a957aa | |||
| 86188b59db | |||
| e460ceaf48 | |||
| d58ac8de9a | |||
| ccd905d573 | |||
| 7db3d2aa4c | |||
| 1dcce2502f | |||
| 6746844b7b | |||
| a8611db452 | |||
| f64a9731ce | |||
| 07d90228d3 | |||
| eee10cc3eb | |||
| a0ce7522c3 | |||
| 36707fc7eb | |||
| 08f7a62692 | |||
| 709391ff0a | |||
| 2401dc8fcd | |||
| a6643ab990 | |||
| 9ca872cb98 | |||
| 39c44d1e67 | |||
| 69832723b3 | |||
| 2e81549747 | |||
| 7ddb95d521 | |||
| 01835c8077 | |||
| c3de403645 | |||
| fe70b57dc1 | |||
| c23f52c87b | |||
| b995f81a26 | |||
| 9e4406c66a | |||
| 0cca06e054 | |||
| e8430c373f | |||
| 0d9a4d4830 | |||
| 113e7b0bad | |||
| d38dc14e84 | |||
| 3b6ff81dad | |||
| e08c9d1790 | |||
| 60258649fb | |||
| 7bb3f6224d | |||
| 305481adee | |||
| 411fc0c4bc | |||
| 3975837393 | |||
| 9a70fbc416 | |||
| 4d0f98acd2 | |||
| f052891473 | |||
| 0ee3059cf8 | |||
| 0b365e05c8 | |||
| b2fb71b405 | |||
| 38e2e64751 | |||
| 7ff731133c | |||
| abc1cc8fce | |||
| f920d17058 | |||
| 967042e8a6 | |||
| d15fcf66cb | |||
| c159889ad8 | |||
| 647f07c446 | |||
| 795d91abd8 | |||
| 8d519c53dc | |||
| cf9a031c0f | |||
| 7b3de1e68d | |||
| a702170d16 | |||
| 3dd8a7bc0d | |||
| 400fd9b6e9 | |||
| 0e821e098f | |||
| a6efde40f8 | |||
| ce4574ed78 | |||
| 4ae31bbba0 | |||
| 62a0e8c17c | |||
| ea8ba031c3 | |||
| b80f5c5c70 | |||
| ab1301687d | |||
| f24d5dfed0 | |||
| ee40fff168 | |||
| 1faf5bb6df | |||
| 1a9077427c | |||
| 905486edbd | |||
| f6c603bf73 | |||
| f1b3fc0040 | |||
| 174a7ee614 | |||
| 433e4df0f2 | |||
| 140fd22223 | |||
| f3fd88a199 | |||
| 6f51872856 | |||
| 98d2107e4e | |||
| ac3827b8f3 | |||
| 600d0b3aca | |||
| fc82d6ff9b | |||
| f81a04dd44 | |||
| 2b07e7963e | |||
| f359768ba2 | |||
| a4e61c087a | |||
| 83f840a412 | |||
| b20ebbd7be |
234 changed files with 21082 additions and 12168 deletions
|
|
@ -0,0 +1 @@
|
||||||
|
*
|
||||||
3
.editorconfig
Normal file
3
.editorconfig
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
root = true
|
||||||
|
[*]
|
||||||
|
max_line_length = 132
|
||||||
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
use nix
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
{pkgs?import<nixpkgs>{}}: pkgs.mkShell (with pkgs; {
|
|
||||||
nativeBuildInputs = [
|
|
||||||
cargo
|
|
||||||
pkg-config
|
|
||||||
freetype
|
|
||||||
libclang
|
|
||||||
cloc
|
|
||||||
#bear
|
|
||||||
];
|
|
||||||
buildInputs = [
|
|
||||||
jack2
|
|
||||||
lilv
|
|
||||||
serd
|
|
||||||
libclang
|
|
||||||
#suil
|
|
||||||
glib
|
|
||||||
gtk3
|
|
||||||
];
|
|
||||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
|
|
||||||
pipewire.jack
|
|
||||||
# for ChowKick.lv2:
|
|
||||||
freetype
|
|
||||||
libgcc.lib
|
|
||||||
# for Panagement
|
|
||||||
xorg.libX11
|
|
||||||
xorg.libXcursor
|
|
||||||
xorg.libXi
|
|
||||||
libxkbcommon
|
|
||||||
#suil
|
|
||||||
# for Helm:
|
|
||||||
alsa-lib
|
|
||||||
curl
|
|
||||||
libglvnd
|
|
||||||
#xorg_sys_opengl
|
|
||||||
];
|
|
||||||
VST3_SDK_DIR = "/home/user/Lab/Music/tek/vst3sdk/";
|
|
||||||
LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib";
|
|
||||||
})
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
on: [push]
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
container:
|
|
||||||
image: nixos/nix:latest
|
|
||||||
steps:
|
|
||||||
- run: nix-channel --list && nix-channel --update
|
|
||||||
- run: nix-shell -p git --command 'git clone --recursive $GITHUB_SERVER_URL/$GITHUB_REPOSITORY .'
|
|
||||||
- run: whoami && pwd && ls -al
|
|
||||||
- run: nix-shell --command 'cargo version -vv && cargo test && cargo build --release && cloc crates/tek/src' .forgejo/workflows/build.nix
|
|
||||||
- run: nix-shell -p docker --command "docker run --security-opt seccomp=unconfined -v $PWD:/volume xd009642/tarpaulin cargo tarpaulin --out Html --all-features"
|
|
||||||
36
.forgejo/workflows/release.yml.off
Normal file
36
.forgejo/workflows/release.yml.off
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: '*'
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: codeberg-small-lazy
|
||||||
|
container: { image: "alpine:edge" }
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: install deps
|
||||||
|
run: apk add --no-cache bash nodejs tree rustup git just cloc build-base clang20-dev pipewire-jack-dev lilv-dev serd-dev
|
||||||
|
|
||||||
|
- run: git clone --depth 1 --recursive $GITHUB_SERVER_URL/$GITHUB_REPOSITORY .
|
||||||
|
- run: whoami && pwd && tree && cloc src/ && cloc .
|
||||||
|
|
||||||
|
- run: rustup-init -y
|
||||||
|
- run: source "$HOME/.cargo/env" && rustup install nightly && rustup default nightly && cargo version -vv
|
||||||
|
#- run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just doc
|
||||||
|
- run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just test
|
||||||
|
- run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just build-release
|
||||||
|
|
||||||
|
- run: tree && mkdir -p .release && mv -v target/release/tek .release
|
||||||
|
|
||||||
|
- name: publish release
|
||||||
|
uses: https://data.forgejo.org/actions/forgejo-release@v2.6.0
|
||||||
|
with:
|
||||||
|
url: "https://codeberg.org"
|
||||||
|
direction: upload
|
||||||
|
tag: "${{ github.ref_name }}"
|
||||||
|
sha: "${{ github.sha }}"
|
||||||
|
release-dir: .release
|
||||||
|
override: true
|
||||||
|
verbose: true
|
||||||
|
#hide-archive-link: true
|
||||||
|
#token: ${{ secrets.TOKEN }}
|
||||||
|
#release-notes-assistant: true
|
||||||
49
.forgejo/workflows/test.yaml
Normal file
49
.forgejo/workflows/test.yaml
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: '*'
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
container: { image: "alpine:edge" }
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: install deps
|
||||||
|
run: apk add --no-cache nodejs tree rustup git just cloc build-base clang20-dev pipewire-jack-dev lilv-dev serd-dev
|
||||||
|
|
||||||
|
- run: git clone --depth 1 --recursive $GITHUB_SERVER_URL/$GITHUB_REPOSITORY .
|
||||||
|
- run: whoami && pwd && tree && cloc src/ && cloc .
|
||||||
|
|
||||||
|
#- id: cache
|
||||||
|
#name: cache restore
|
||||||
|
#uses: https://data.forgejo.org/actions/cache/restore@v4
|
||||||
|
#with:
|
||||||
|
#key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
#path: |
|
||||||
|
#~/.cargo/bin/
|
||||||
|
#~/.cargo/registry/index/
|
||||||
|
#~/.cargo/registry/cache/
|
||||||
|
#~/.cargo/git/db/
|
||||||
|
#target/
|
||||||
|
|
||||||
|
#- name: cache hit
|
||||||
|
#if: steps.cache.outputs.cache-hit == 'true'
|
||||||
|
#run: echo "cache hit! :)"
|
||||||
|
#- name: cache miss
|
||||||
|
#if: steps.cache.outputs.cache-miss != 'true'
|
||||||
|
#run: echo "cache miss! :("
|
||||||
|
|
||||||
|
- run: cloc src/ && cloc .
|
||||||
|
- run: rustup-init -y
|
||||||
|
- run: source "$HOME/.cargo/env" && rustup install nightly && rustup default nightly && cargo version -vv
|
||||||
|
- run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just test
|
||||||
|
- run: tree
|
||||||
|
|
||||||
|
#- name: cache save
|
||||||
|
#uses: https://data.forgejo.org/actions/cache/save@v4
|
||||||
|
#with:
|
||||||
|
#key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
#path: |
|
||||||
|
#~/.cargo/bin/
|
||||||
|
#~/.cargo/registry/index/
|
||||||
|
#~/.cargo/registry/cache/
|
||||||
|
#~/.cargo/git/db/
|
||||||
|
#target/
|
||||||
12
.gitignore
vendored
12
.gitignore
vendored
|
|
@ -1,5 +1,15 @@
|
||||||
target
|
target/*
|
||||||
|
!target/.gitkeep
|
||||||
perf.data*
|
perf.data*
|
||||||
flamegraph*.svg
|
flamegraph*.svg
|
||||||
vgcore*
|
vgcore*
|
||||||
example.mid
|
example.mid
|
||||||
|
cov
|
||||||
|
*/cov
|
||||||
|
*.profraw
|
||||||
|
build/*
|
||||||
|
!build/README.md
|
||||||
|
!build/*.sh
|
||||||
|
!build/Dockerfile.*
|
||||||
|
.misc
|
||||||
|
.direnv
|
||||||
|
|
|
||||||
6
.gitmodules
vendored
6
.gitmodules
vendored
|
|
@ -2,3 +2,9 @@
|
||||||
path = rust-jack
|
path = rust-jack
|
||||||
url = https://codeberg.org/unspeaker/rust-jack
|
url = https://codeberg.org/unspeaker/rust-jack
|
||||||
branch = timebase
|
branch = timebase
|
||||||
|
[submodule "tengri"]
|
||||||
|
path = deps/tengri
|
||||||
|
url = ../tengri/
|
||||||
|
[submodule "deps/rust-jack"]
|
||||||
|
path = deps/rust-jack
|
||||||
|
url = https://codeberg.org/unspeaker/rust-jack
|
||||||
|
|
|
||||||
|
|
@ -14,20 +14,7 @@ impl Demo<Tui> {
|
||||||
fn new () -> Self {
|
fn new () -> Self {
|
||||||
Self {
|
Self {
|
||||||
index: 0,
|
index: 0,
|
||||||
items: vec![
|
items: vec![]
|
||||||
//Box::new(tek_sequencer::TransportPlayPauseButton {
|
|
||||||
//_engine: Default::default(),
|
|
||||||
//transport: None,
|
|
||||||
//value: Some(TransportState::Stopped),
|
|
||||||
//focused: true
|
|
||||||
//}),
|
|
||||||
//Box::new(tek_sequencer::TransportPlayPauseButton {
|
|
||||||
//_engine: Default::default(),
|
|
||||||
//transport: None,
|
|
||||||
//value: Some(TransportState::Rolling),
|
|
||||||
//focused: false
|
|
||||||
//}),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,7 +91,7 @@ impl Content for Demo<Tui> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handle<Tui> for Demo<Tui> {
|
impl Handle<TuiIn> for Demo<Tui> {
|
||||||
fn handle (&mut self, from: &TuiIn) -> Perhaps<bool> {
|
fn handle (&mut self, from: &TuiIn) -> Perhaps<bool> {
|
||||||
use KeyCode::{PageUp, PageDown};
|
use KeyCode::{PageUp, PageDown};
|
||||||
match from.event() {
|
match from.event() {
|
||||||
|
|
@ -123,22 +110,3 @@ impl Handle<Tui> for Demo<Tui> {
|
||||||
Ok(Some(true))
|
Ok(Some(true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//lisp!(CONTENT Demo (LET
|
|
||||||
//(BORDER-STYLE (STYLE (FG (RGB 0 0 0))))
|
|
||||||
//(BG-COLOR-0 (RGB 0 128 128))
|
|
||||||
//(BG-COLOR-1 (RGB 128 96 0))
|
|
||||||
//(BG-COLOR-2 (RGB 128 64 0))
|
|
||||||
//(BG-COLOR-3 (RGB 96 64 0))
|
|
||||||
//(CENTER (LAYERS
|
|
||||||
//(BACKGROUND BG-COLOR-0)
|
|
||||||
//(OUTSET-XY 1 1 (SPLIT-DOWN
|
|
||||||
//(LAYERS (BACKGROUND BG-COLOR-1)
|
|
||||||
//(BORDER SQUARE BORDER-STYLE)
|
|
||||||
//(OUTSET-XY 2 1 "..."))
|
|
||||||
//(LAYERS (BACKGROUND BG-COLOR-2)
|
|
||||||
//(BORDER LOZENGE BORDER-STYLE)
|
|
||||||
//(OUTSET-XY 4 2 "---"))
|
|
||||||
//(LAYERS (BACKGROUND BG-COLOR-3)
|
|
||||||
//(BORDER SQUARE-BOLD BORDER-STYLE)
|
|
||||||
//(OUTSET-XY 2 1 "~~~"))))))))
|
|
||||||
188
.old/from_arranger.rs
Normal file
188
.old/from_arranger.rs
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
|
||||||
|
|
||||||
|
//pub struct ArrangerVCursor {
|
||||||
|
//cols: Vec<(usize, usize)>,
|
||||||
|
//rows: Vec<(usize, usize)>,
|
||||||
|
//color: ItemPalette,
|
||||||
|
//reticle: Reticle,
|
||||||
|
//selected: ArrangerSelection,
|
||||||
|
//scenes_w: u16,
|
||||||
|
//}
|
||||||
|
|
||||||
|
//pub(crate) const HEADER_H: u16 = 0; // 5
|
||||||
|
//pub(crate) const SCENES_W_OFFSET: u16 = 0;
|
||||||
|
//from!(|args:(&Arranger, usize)|ArrangerVCursor = Self {
|
||||||
|
//cols: Arranger::track_widths(&args.0.tracks),
|
||||||
|
//rows: Arranger::scene_heights(&args.0.scenes, args.1),
|
||||||
|
//selected: args.0.selected(),
|
||||||
|
//scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&args.0.scenes) as u16,
|
||||||
|
//color: args.0.color,
|
||||||
|
//reticle: Reticle(Style {
|
||||||
|
//fg: Some(args.0.color.lighter.rgb),
|
||||||
|
//bg: None,
|
||||||
|
//underline_color: None,
|
||||||
|
//add_modifier: Modifier::empty(),
|
||||||
|
//sub_modifier: Modifier::DIM
|
||||||
|
//}),
|
||||||
|
//});
|
||||||
|
//impl Content<TuiOut> for ArrangerVCursor {
|
||||||
|
//fn render (&self, to: &mut TuiOut) {
|
||||||
|
//let area = to.area();
|
||||||
|
//let focused = true;
|
||||||
|
//let selected = self.selected;
|
||||||
|
//let get_track_area = |t: usize| [
|
||||||
|
//self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(),
|
||||||
|
//self.cols[t].0 as u16, area.h(),
|
||||||
|
//];
|
||||||
|
//let get_scene_area = |s: usize| [
|
||||||
|
//area.x(), HEADER_H + area.y() + (self.rows[s].1 / PPQ) as u16,
|
||||||
|
//area.w(), (self.rows[s].0 / PPQ) as u16
|
||||||
|
//];
|
||||||
|
//let get_clip_area = |t: usize, s: usize| [
|
||||||
|
//(self.scenes_w + area.x() + self.cols[t].1 as u16).saturating_sub(1),
|
||||||
|
//HEADER_H + area.y() + (self.rows[s].1/PPQ) as u16,
|
||||||
|
//self.cols[t].0 as u16 + 2,
|
||||||
|
//(self.rows[s].0 / PPQ) as u16
|
||||||
|
//];
|
||||||
|
//let mut track_area: Option<[u16;4]> = None;
|
||||||
|
//let mut scene_area: Option<[u16;4]> = None;
|
||||||
|
//let mut clip_area: Option<[u16;4]> = None;
|
||||||
|
//let area = match selected {
|
||||||
|
//ArrangerSelection::Mix => area,
|
||||||
|
//ArrangerSelection::Track(t) => {
|
||||||
|
//track_area = Some(get_track_area(t));
|
||||||
|
//area
|
||||||
|
//},
|
||||||
|
//ArrangerSelection::Scene(s) => {
|
||||||
|
//scene_area = Some(get_scene_area(s));
|
||||||
|
//area
|
||||||
|
//},
|
||||||
|
//ArrangerSelection::Clip(t, s) => {
|
||||||
|
//track_area = Some(get_track_area(t));
|
||||||
|
//scene_area = Some(get_scene_area(s));
|
||||||
|
//clip_area = Some(get_clip_area(t, s));
|
||||||
|
//area
|
||||||
|
//},
|
||||||
|
//};
|
||||||
|
//let bg = self.color.lighter.rgb;//Color::Rgb(0, 255, 0);
|
||||||
|
//if let Some([x, y, width, height]) = track_area {
|
||||||
|
//to.fill_fg([x, y, 1, height], bg);
|
||||||
|
//to.fill_fg([x + width, y, 1, height], bg);
|
||||||
|
//}
|
||||||
|
//if let Some([_, y, _, height]) = scene_area {
|
||||||
|
//to.fill_ul([area.x(), y - 1, area.w(), 1], bg);
|
||||||
|
//to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg);
|
||||||
|
//}
|
||||||
|
//if focused {
|
||||||
|
//to.place(if let Some(clip_area) = clip_area {
|
||||||
|
//clip_area
|
||||||
|
//} else if let Some(track_area) = track_area {
|
||||||
|
//track_area.clip_h(HEADER_H)
|
||||||
|
//} else if let Some(scene_area) = scene_area {
|
||||||
|
//scene_area.clip_w(self.scenes_w)
|
||||||
|
//} else {
|
||||||
|
//area.clip_w(self.scenes_w).clip_h(HEADER_H)
|
||||||
|
//}, &self.reticle)
|
||||||
|
//};
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//impl Arranger {
|
||||||
|
//fn render_mode (state: &Self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//match state.mode {
|
||||||
|
//ArrangerMode::H => todo!("horizontal arranger"),
|
||||||
|
//ArrangerMode::V(factor) => Self::render_mode_v(state, factor),
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//render!(TuiOut: (self: Arranger) => {
|
||||||
|
//let pool_w = if self.pool.visible { self.splits[1] } else { 0 };
|
||||||
|
//let color = self.color;
|
||||||
|
//let layout = Bsp::a(Fill::xy(ArrangerStatus::from(self)),
|
||||||
|
//Bsp::n(Fixed::x(pool_w, PoolView(self.pool.visible, &self.pool)),
|
||||||
|
//Bsp::n(TransportView::new(true, &self.clock),
|
||||||
|
//Bsp::s(Fixed::y(1, MidiEditStatus(&self.editor)),
|
||||||
|
//Bsp::n(Fill::x(Fixed::y(20,
|
||||||
|
//Bsp::a(Fill::xy(Tui::bg(color.darkest.rgb, "background")),
|
||||||
|
//Bsp::a(
|
||||||
|
//Fill::xy(Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb))),
|
||||||
|
//Self::render_mode(self))))), Fill::y(&"fixme: self.editor"))))));
|
||||||
|
//self.size.of(layout)
|
||||||
|
//});
|
||||||
|
//Align::n(Fill::xy(lay!(
|
||||||
|
//Align::n(Fill::xy(Tui::bg(self.color.darkest.rgb, " "))),
|
||||||
|
//Align::n(Fill::xy(ArrangerVRowSep::from((self, 1)))),
|
||||||
|
//Align::n(Fill::xy(ArrangerVColSep::from(self))),
|
||||||
|
//Align::n(Fill::xy(ArrangerVClips::new(self, 1))),
|
||||||
|
//Align::n(Fill::xy(ArrangerVCursor::from((self, 1))))))))))))))));
|
||||||
|
//Align::n(Fill::xy(":")))))))))))));
|
||||||
|
//"todo:"))))))));
|
||||||
|
//Bsp::s(
|
||||||
|
//Align::n(Fixed::y(1, Fill::x(ArrangerVIns::from(self)))),
|
||||||
|
//Bsp::s(
|
||||||
|
//Fixed::y(20, Align::n(ArrangerVClips::new(self, 1))),
|
||||||
|
//Fill::x(Fixed::y(1, ArrangerVOuts::from(self)))))))))))));
|
||||||
|
//Bsp::s(
|
||||||
|
//Bsp::s(
|
||||||
|
//Bsp::s(
|
||||||
|
//Fill::xy(ArrangerVClips::new(self, 1)),
|
||||||
|
//Fill::x(ArrangerVOuts::from(self)))))
|
||||||
|
|
||||||
|
//let cell = phat_sel_3(
|
||||||
|
//selected_track == Some(i) && selected_scene == Some(j),
|
||||||
|
//Tui::fg(TuiTheme::g(64), Push::x(1, name)),
|
||||||
|
//Tui::fg(TuiTheme::g(64), Push::x(1, name)),
|
||||||
|
//if selected_track == Some(i) && selected_scene.map(|s|s+1) == Some(j) {
|
||||||
|
//None
|
||||||
|
//} else {
|
||||||
|
//Some(TuiTheme::g(32).into())
|
||||||
|
//},
|
||||||
|
//TuiTheme::g(32).into(),
|
||||||
|
//TuiTheme::g(32).into(),
|
||||||
|
//);
|
||||||
|
// TODO: port per track:
|
||||||
|
//for connection in midi_from.iter() {
|
||||||
|
//let mut split = connection.as_ref().split("=");
|
||||||
|
//let number = split.next().unwrap().trim();
|
||||||
|
//if let Ok(track) = number.parse::<usize>() {
|
||||||
|
//if track < 1 {
|
||||||
|
//panic!("Tracks start from 1")
|
||||||
|
//}
|
||||||
|
//if track > count {
|
||||||
|
//panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.")
|
||||||
|
//}
|
||||||
|
//if let Some(port) = split.next() {
|
||||||
|
//if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() {
|
||||||
|
////jack.read().unwrap().client().connect_ports(port, &self.tracks[track-1].player.midi_ins[0])?;
|
||||||
|
//} else {
|
||||||
|
//panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
|
||||||
|
//}
|
||||||
|
//} else {
|
||||||
|
//panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME")
|
||||||
|
//}
|
||||||
|
//} else {
|
||||||
|
//panic!("Failed to parse track number: {number}")
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//for connection in midi_to.iter() {
|
||||||
|
//let mut split = connection.as_ref().split("=");
|
||||||
|
//let number = split.next().unwrap().trim();
|
||||||
|
//if let Ok(track) = number.parse::<usize>() {
|
||||||
|
//if track < 1 {
|
||||||
|
//panic!("Tracks start from 1")
|
||||||
|
//}
|
||||||
|
//if track > count {
|
||||||
|
//panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.")
|
||||||
|
//}
|
||||||
|
//if let Some(port) = split.next() {
|
||||||
|
//if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() {
|
||||||
|
////jack.read().unwrap().client().connect_ports(&self.tracks[track-1].player.midi_outs[0], port)?;
|
||||||
|
//} else {
|
||||||
|
//panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
|
||||||
|
//}
|
||||||
|
//} else {
|
||||||
|
//panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME")
|
||||||
|
//}
|
||||||
|
//} else {
|
||||||
|
//panic!("Failed to parse track number: {number}")
|
||||||
|
//}
|
||||||
|
//}
|
||||||
31
.old/midi.scratch.rs
Normal file
31
.old/midi.scratch.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand {
|
||||||
|
//key(Up) => SetNoteCursor(s.note_pos() + 1),
|
||||||
|
//key(Char('w')) => SetNoteCursor(s.note_pos() + 1),
|
||||||
|
//key(Down) => SetNoteCursor(s.note_pos().saturating_sub(1)),
|
||||||
|
//key(Char('s')) => SetNoteCursor(s.note_pos().saturating_sub(1)),
|
||||||
|
//key(Left) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())),
|
||||||
|
//key(Char('a')) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())),
|
||||||
|
//key(Right) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()),
|
||||||
|
//ctrl(alt(key(Up))) => SetNoteScroll(s.note_pos() + 3),
|
||||||
|
//ctrl(alt(key(Down))) => SetNoteScroll(s.note_pos().saturating_sub(3)),
|
||||||
|
//ctrl(alt(key(Left))) => SetTimeScroll(s.time_pos().saturating_sub(s.time_zoom().get())),
|
||||||
|
//ctrl(alt(key(Right))) => SetTimeScroll((s.time_pos() + s.time_zoom().get()) % s.clip_length()),
|
||||||
|
//ctrl(key(Up)) => SetNoteScroll(s.note_lo().get() + 1),
|
||||||
|
//ctrl(key(Down)) => SetNoteScroll(s.note_lo().get().saturating_sub(1)),
|
||||||
|
//ctrl(key(Left)) => SetTimeScroll(s.time_start().get().saturating_sub(s.note_len())),
|
||||||
|
//ctrl(key(Right)) => SetTimeScroll(s.time_start().get() + s.note_len()),
|
||||||
|
//alt(key(Up)) => SetNoteCursor(s.note_pos() + 3),
|
||||||
|
//alt(key(Down)) => SetNoteCursor(s.note_pos().saturating_sub(3)),
|
||||||
|
//alt(key(Left)) => SetTimeCursor(s.time_pos().saturating_sub(s.time_zoom().get())),
|
||||||
|
//alt(key(Right)) => SetTimeCursor((s.time_pos() + s.time_zoom().get()) % s.clip_length()),
|
||||||
|
//key(Char('d')) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()),
|
||||||
|
//key(Char('z')) => SetTimeLock(!s.time_lock().get()),
|
||||||
|
//key(Char('-')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }),
|
||||||
|
//key(Char('_')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }),
|
||||||
|
//key(Char('=')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }),
|
||||||
|
//key(Char('+')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }),
|
||||||
|
////// TODO: kpat!(Char('/')) => // toggle 3plet
|
||||||
|
////// TODO: kpat!(Char('?')) => // toggle dotted
|
||||||
|
//});
|
||||||
|
|
||||||
20
.old/midi_import.rs
Normal file
20
.old/midi_import.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
use tek::*;
|
||||||
|
use tengri::input::*;
|
||||||
|
use std::sync::*;
|
||||||
|
struct ExampleClips(Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>);
|
||||||
|
impl HasClips for ExampleClips {
|
||||||
|
fn clips (&self) -> RwLockReadGuard<'_, Vec<Arc<RwLock<MidiClip>>>> {
|
||||||
|
self.0.read().unwrap()
|
||||||
|
}
|
||||||
|
fn clips_mut (&self) -> RwLockWriteGuard<'_, Vec<Arc<RwLock<MidiClip>>>> {
|
||||||
|
self.0.write().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn main () -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut clips = Pool::default();//ExampleClips(Arc::new(vec![].into()));
|
||||||
|
PoolClipCommand::Import {
|
||||||
|
index: 0,
|
||||||
|
path: std::path::PathBuf::from("./example.mid")
|
||||||
|
}.execute(&mut clips)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
105
.old/sampler_scratch.rs
Normal file
105
.old/sampler_scratch.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
//handle!(TuiIn: |self: Sampler, input|SamplerCommand::execute_with_state(self, input.event()));
|
||||||
|
//input_to_command!(SamplerCommand: |state: Sampler, input: Event|match state.mode{
|
||||||
|
//Some(SamplerMode::Import(..)) => Self::Import(
|
||||||
|
//FileBrowserCommand::input_to_command(state, input)?
|
||||||
|
//),
|
||||||
|
//_ => match input {
|
||||||
|
//// load sample
|
||||||
|
//kpat!(Shift-Char('L')) => Self::Import(FileBrowserCommand::Begin),
|
||||||
|
//kpat!(KeyCode::Up) => Self::Select(state.note_pos().overflowing_add(1).0.min(127)),
|
||||||
|
//kpat!(KeyCode::Down) => Self::Select(state.note_pos().overflowing_sub(1).0.min(127)),
|
||||||
|
//_ => return None
|
||||||
|
//}
|
||||||
|
//});
|
||||||
|
//impl Handle<TuiIn> for AddSampleModal {
|
||||||
|
//fn handle (&mut self, from: &TuiIn) -> Perhaps<bool> {
|
||||||
|
//if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? {
|
||||||
|
//return Ok(Some(true))
|
||||||
|
//}
|
||||||
|
//Ok(Some(true))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding<AddSampleModal>] = keymap!(AddSampleModal {
|
||||||
|
//[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{
|
||||||
|
//modal.exit();
|
||||||
|
//Ok(true)
|
||||||
|
//}],
|
||||||
|
//[Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{
|
||||||
|
//modal.prev();
|
||||||
|
//Ok(true)
|
||||||
|
//}],
|
||||||
|
//[Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{
|
||||||
|
//modal.next();
|
||||||
|
//Ok(true)
|
||||||
|
//}],
|
||||||
|
//[Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{
|
||||||
|
//if modal.pick()? {
|
||||||
|
//modal.exit();
|
||||||
|
//}
|
||||||
|
//Ok(true)
|
||||||
|
//}],
|
||||||
|
//[Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{
|
||||||
|
//modal.try_preview()?;
|
||||||
|
//Ok(true)
|
||||||
|
//}]
|
||||||
|
//});
|
||||||
|
//from_atom!("sampler" => |jack: &Jack, args| -> crate::Sampler {
|
||||||
|
//let mut name = String::new();
|
||||||
|
//let mut dir = String::new();
|
||||||
|
//let mut samples = BTreeMap::new();
|
||||||
|
//atom!(atom in args {
|
||||||
|
//Atom::Map(map) => {
|
||||||
|
//if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) {
|
||||||
|
//name = String::from(*n);
|
||||||
|
//}
|
||||||
|
//if let Some(Atom::Str(n)) = map.get(&Atom::Key(":dir")) {
|
||||||
|
//dir = String::from(*n);
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//Atom::List(args) => match args.first() {
|
||||||
|
//Some(Atom::Symbol("sample")) => {
|
||||||
|
//let (midi, sample) = MidiSample::from_atom((jack, &dir), &args[1..])?;
|
||||||
|
//if let Some(midi) = midi {
|
||||||
|
//samples.insert(midi, sample);
|
||||||
|
//} else {
|
||||||
|
//panic!("sample without midi binding: {}", sample.read().unwrap().name);
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//_ => panic!("unexpected in sampler {name}: {args:?}")
|
||||||
|
//},
|
||||||
|
//_ => panic!("unexpected in sampler {name}: {atom:?}")
|
||||||
|
//});
|
||||||
|
//Self::new(jack, &name)
|
||||||
|
//});
|
||||||
|
//from_atom!("sample" => |(_jack, dir): (&Jack, &str), args| -> MidiSample {
|
||||||
|
//let mut name = String::new();
|
||||||
|
//let mut file = String::new();
|
||||||
|
//let mut midi = None;
|
||||||
|
//let mut start = 0usize;
|
||||||
|
//atom!(atom in args {
|
||||||
|
//Atom::Map(map) => {
|
||||||
|
//if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) {
|
||||||
|
//name = String::from(*n);
|
||||||
|
//}
|
||||||
|
//if let Some(Atom::Str(f)) = map.get(&Atom::Key(":file")) {
|
||||||
|
//file = String::from(*f);
|
||||||
|
//}
|
||||||
|
//if let Some(Atom::Int(i)) = map.get(&Atom::Key(":start")) {
|
||||||
|
//start = *i as usize;
|
||||||
|
//}
|
||||||
|
//if let Some(Atom::Int(m)) = map.get(&Atom::Key(":midi")) {
|
||||||
|
//midi = Some(u7::from(*m as u8));
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//_ => panic!("unexpected in sample {name}"),
|
||||||
|
//});
|
||||||
|
//let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?;
|
||||||
|
//Ok((midi, Arc::new(RwLock::new(crate::Sample {
|
||||||
|
//name,
|
||||||
|
//start,
|
||||||
|
//end,
|
||||||
|
//channels: data,
|
||||||
|
//rate: None,
|
||||||
|
//gain: 1.0
|
||||||
|
//}))))
|
||||||
|
//});
|
||||||
2113
.old/tek.rs.old
Normal file
2113
.old/tek.rs.old
Normal file
File diff suppressed because it is too large
Load diff
83
.old/todo_arranger.edn
Normal file
83
.old/todo_arranger.edn
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
This is the unified Tek Arranger.
|
||||||
|
|
||||||
|
Its appearance is defined by the following view definition:
|
||||||
|
|
||||||
|
{def :view (bsp/s (fixed/y 2 :toolbar)
|
||||||
|
(fill/x (align/c (bsp/w (fixed/x :pool-w :pool)
|
||||||
|
(bsp/n (fixed/y 3 :outputs)
|
||||||
|
(bsp/n (fixed/y 3 :inputs)
|
||||||
|
(bsp/n (fixed/y 3 :tracks) :scenes)))))))}
|
||||||
|
|
||||||
|
The arranger's behavior is controlled by the
|
||||||
|
following keymaps:
|
||||||
|
|
||||||
|
{def :keys
|
||||||
|
(@u undo 1)
|
||||||
|
(@shift-u redo 1)
|
||||||
|
(@space clock toggle)
|
||||||
|
(@shift-space clock toggle 0)
|
||||||
|
(@ctrl-a scene add)
|
||||||
|
(@ctrl-t track add)
|
||||||
|
(@tab edit :clip)
|
||||||
|
(@c color)}
|
||||||
|
|
||||||
|
{def :keys-mix
|
||||||
|
(@down select 0 1)
|
||||||
|
(@s select 0 1)
|
||||||
|
|
||||||
|
(@right select 1 0)
|
||||||
|
(@d select 1 0)}
|
||||||
|
|
||||||
|
{def :keys-track
|
||||||
|
(@left select :track-prev :scene)
|
||||||
|
(@a select :track-prev :scene)
|
||||||
|
(@right select :track-next :scene)
|
||||||
|
(@d select :track-next :scene)
|
||||||
|
(@down select :track :scene-next)
|
||||||
|
(@s select :track :scene-next)
|
||||||
|
|
||||||
|
(@q track launch)
|
||||||
|
(@c track color :track)
|
||||||
|
(@comma track swap-prev)
|
||||||
|
(@period track swap-next)
|
||||||
|
(@lt track size-dec)
|
||||||
|
(@gt track size-inc)
|
||||||
|
(@delete track delete)}
|
||||||
|
|
||||||
|
{def :keys-scene
|
||||||
|
(@up select :track :scene-prev)
|
||||||
|
(@w select :track :scene-prev)
|
||||||
|
(@down select :track :scene-next)
|
||||||
|
(@s select :track :scene-next)
|
||||||
|
(@right select :track-next :scene)
|
||||||
|
(@d select :track-next :scene)
|
||||||
|
|
||||||
|
(@q scene launch)
|
||||||
|
(@c scene color :scene)
|
||||||
|
(@comma scene swap-prev)
|
||||||
|
(@period scene swap-next)
|
||||||
|
(@lt scene size-dec)
|
||||||
|
(@gt scene size-inc)
|
||||||
|
(@delete scene delete)}
|
||||||
|
|
||||||
|
{def :keys-clip
|
||||||
|
(@up select :track :scene-prev)
|
||||||
|
(@w select :track :scene-prev)
|
||||||
|
(@down select :track :scene-next)
|
||||||
|
(@s select :track :scene-next)
|
||||||
|
(@left select :track-prev :scene)
|
||||||
|
(@a select :track-prev :scene)
|
||||||
|
(@right select :track-next :scene)
|
||||||
|
(@d select :track-next :scene)
|
||||||
|
|
||||||
|
(@q enqueue :clip)
|
||||||
|
(@c clip color :track :scene)
|
||||||
|
(@g clip get)
|
||||||
|
(@p clip put)
|
||||||
|
(@delete clip del)
|
||||||
|
(@comma clip prev)
|
||||||
|
(@period clip next)
|
||||||
|
(@lt clip swap-prev)
|
||||||
|
(@gt clip swap-next)
|
||||||
|
(@l clip loop-toggle)}
|
||||||
|
|
||||||
18
CONTRIBUTING.md
Normal file
18
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
## development
|
||||||
|
|
||||||
|
you'll need a Rust toolchain and various system libraries.
|
||||||
|
|
||||||
|
you can obtain the former using `rustup` and the latter using `nix-shell`.
|
||||||
|
there's a `shell.nix` provided with the project.
|
||||||
|
|
||||||
|
from there, use the commands in the `Justfile`, e.g.:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just arranger
|
||||||
|
```
|
||||||
|
|
||||||
|
note that `tek > 0.2.0-rc.7` will require rust nightly
|
||||||
|
for the unstable features `type_alias_impl_trait` and
|
||||||
|
`impl_trait_in_assoc_type`. make some noise for lucky
|
||||||
|
[**rust rfc2515**](https://github.com/rust-lang/rust/issues/63063)
|
||||||
|
if you want to see this buildable with stable/beta.
|
||||||
2285
Cargo.lock
generated
2285
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
107
Cargo.toml
107
Cargo.toml
|
|
@ -1,66 +1,53 @@
|
||||||
[package]
|
[workspace]
|
||||||
name = "tek"
|
resolver = "2"
|
||||||
edition = "2021"
|
members = [ "./app", "./engine", "./device" ]
|
||||||
version = "0.2.0"
|
exclude = [ "./deps/tengri" ]
|
||||||
|
|
||||||
[dependencies]
|
[workspace.package]
|
||||||
tek_layout = { path = "./layout" }
|
edition = "2024"
|
||||||
|
version = "0.3.0"
|
||||||
|
|
||||||
atomic_float = "1.0.0"
|
[profile.release]
|
||||||
backtrace = "0.3.72"
|
lto = true
|
||||||
clap = { version = "4.5.4", features = [ "derive" ] }
|
|
||||||
clojure-reader = "0.3.0"
|
[profile.coverage]
|
||||||
jack = { path = "./rust-jack" }
|
inherits = "test"
|
||||||
livi = "0.7.4"
|
lto = false
|
||||||
midly = "0.5"
|
|
||||||
once_cell = "1.19.0"
|
[workspace.dependencies.tengri]
|
||||||
palette = { version = "0.7.6", features = [ "random" ] }
|
path = "./deps/tengri/tengri"
|
||||||
quanta = "0.12.3"
|
features = [ "tui", "dsl" ]
|
||||||
rand = "0.8.5"
|
|
||||||
symphonia = { version = "0.5.4", features = [ "all" ] }
|
[workspace.dependencies.tengri_proc]
|
||||||
toml = "0.8.12"
|
path = "./deps/tengri/proc"
|
||||||
uuid = { version = "1.10.0", features = [ "v4" ] }
|
|
||||||
wavers = "1.4.3"
|
[workspace.dependencies.jack]
|
||||||
|
path = "./deps/rust-jack"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
tek = { path = "./tek" }
|
||||||
|
|
||||||
|
atomic_float = { version = "1.0.0" }
|
||||||
|
backtrace = { version = "0.3.72" }
|
||||||
|
bumpalo = { version = "3.19.0" }
|
||||||
|
clap = { version = "4.5.4", features = [ "derive" ] }
|
||||||
|
gtk = { version = "0.18.1" }
|
||||||
|
konst = { version = "0.3.16", features = [ "rust_1_83" ] }
|
||||||
|
livi = { version = "0.7.4" }
|
||||||
|
midly = { version = "0.5" }
|
||||||
|
palette = { version = "0.7.6", features = [ "random" ] }
|
||||||
|
quanta = { version = "0.12.3" }
|
||||||
|
rand = { version = "0.8.5" }
|
||||||
|
symphonia = { version = "0.5.4", features = [ "all" ] }
|
||||||
|
toml = { version = "0.9.2" }
|
||||||
|
uuid = { version = "1.10.0", features = [ "v4" ] }
|
||||||
|
wavers = { version = "1.4.3" }
|
||||||
|
winit = { version = "0.30.4", features = [ "x11" ] }
|
||||||
|
xdg = { version = "3.0.0" }
|
||||||
|
#once_cell = "1.19.0"
|
||||||
#no_deadlocks = "1.3.2"
|
#no_deadlocks = "1.3.2"
|
||||||
#suil-rs = { path = "../suil" }
|
#suil-rs = { path = "../suil" }
|
||||||
#vst = "0.4.0"
|
#vst = "0.4.0"
|
||||||
#vst3 = "0.1.0"
|
#vst3 = "0.1.0"
|
||||||
#winit = { version = "0.30.4", features = [ "x11" ] }
|
proptest = { version = "^1" }
|
||||||
|
proptest-derive = { version = "^0.5.1" }
|
||||||
[[bin]]
|
|
||||||
name = "tek_arranger"
|
|
||||||
path = "bin/cli_arranger.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "tek_sequencer"
|
|
||||||
path = "bin/cli_sequencer.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "tek_groovebox"
|
|
||||||
path = "bin/cli_groovebox.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "tek_transport"
|
|
||||||
path = "bin/cli_transport.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "tek_sampler"
|
|
||||||
path = "bin/cli_sampler.rs"
|
|
||||||
|
|
||||||
#[[bin]]
|
|
||||||
#name = "tek_mixer"
|
|
||||||
#path = "src/cli_mixer.rs"
|
|
||||||
|
|
||||||
#[[bin]]
|
|
||||||
#name = "tek_track"
|
|
||||||
#path = "src/cli_track.rs"
|
|
||||||
|
|
||||||
#[[bin]]
|
|
||||||
#name = "tek_plugin"
|
|
||||||
#path = "src/cli_plugin.rs"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
path = "src/lib.rs"
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = true
|
|
||||||
|
|
|
||||||
175
Justfile
175
Justfile
|
|
@ -1,120 +1,117 @@
|
||||||
default:
|
export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold"
|
||||||
just -l
|
export RUST_BACKTRACE := "1"
|
||||||
|
|
||||||
status:
|
default:
|
||||||
cargo c
|
@just -l
|
||||||
cloc --by-file src/
|
|
||||||
git status
|
cloc:
|
||||||
|
for src in {cli,edn/src,input/src,jack/src,midi/src,output/src,plugin/src,sampler/src,tek/src,time/src,tui/src}; do echo; echo $src; cloc --quiet $src; done
|
||||||
|
|
||||||
|
bacon:
|
||||||
|
bacon -s
|
||||||
|
|
||||||
|
check:
|
||||||
|
reset && cargo check
|
||||||
|
|
||||||
|
test:
|
||||||
|
cargo test --workspace --exclude jack
|
||||||
|
|
||||||
|
covfig := "CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' RUSTDOCFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cov/cargo-test-%p-%m.profraw'"
|
||||||
|
grcov-binary := "--binary-path ./target/coverage/deps/"
|
||||||
|
grcov-ignore := "--ignore-not-existing --ignore '../*' --ignore \"/*\" --ignore 'target/*'"
|
||||||
|
cov:
|
||||||
|
{{covfig}} time cargo test -j4 --workspace --exclude jack --profile coverage
|
||||||
|
rm -rf target/coverage/html || true
|
||||||
|
{{covfig}} time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t html -o target/coverage/html
|
||||||
|
cov-md:
|
||||||
|
{{covfig}} time cargo test -j4 --workspace --exclude jack --profile coverage
|
||||||
|
{{covfig}} time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t markdown | sort
|
||||||
|
llcov:
|
||||||
|
time cargo llvm-cov --workspace --exclude jack --profile coverage --no-report
|
||||||
|
time cargo llvm-cov --workspace --exclude jack --profile coverage --no-report --doc
|
||||||
|
time cargo llvm-cov report --doctests --html #--output-path target/coverage/html
|
||||||
|
|
||||||
|
build:
|
||||||
|
reset && cargo build
|
||||||
|
|
||||||
|
debug := "reset && cargo run --"
|
||||||
|
run:
|
||||||
|
{{debug}}
|
||||||
|
run-init:
|
||||||
|
rm -rf ~/.config/tek && {{debug}}
|
||||||
|
|
||||||
|
prof:
|
||||||
|
CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --
|
||||||
|
|
||||||
|
doc:
|
||||||
|
cargo doc -j4 --workspace --document-private-items
|
||||||
|
|
||||||
|
release := "reset && cargo run --release --"
|
||||||
|
release:
|
||||||
|
{{release}}
|
||||||
|
build-release:
|
||||||
|
time cargo build -j4 --release
|
||||||
|
|
||||||
amend:
|
amend:
|
||||||
git commit --amend
|
git commit --amend
|
||||||
push:
|
push:
|
||||||
git push -u codeberg main
|
git push -u codeberg main && git push -u origin main
|
||||||
git push -u origin main
|
|
||||||
tpush:
|
tpush:
|
||||||
git push --tags -u codeberg
|
git push --tags -u codeberg && git push --tags -u origin
|
||||||
git push --tags -u origin
|
|
||||||
fpush:
|
fpush:
|
||||||
git push -fu codeberg main
|
git push -fu codeberg main && git push -fu origin main
|
||||||
git push -fu origin main
|
|
||||||
ftpush:
|
ftpush:
|
||||||
git push --tags -fu codeberg
|
git push --tags -fu codeberg && git push --tags -fu origin
|
||||||
git push --tags -fu origin
|
|
||||||
|
|
||||||
transport:
|
name := "-n tek"
|
||||||
reset
|
bpm := "-b 174"
|
||||||
cargo run --bin tek_transport
|
clock:
|
||||||
transport-release:
|
{{debug}} {{name}} {{bpm}} clock
|
||||||
reset
|
clock-release:
|
||||||
cargo run --release --bin tek_transport
|
{{release}} {{name}} {{bpm}} clock
|
||||||
|
|
||||||
|
midi-in := "-i 'Midi-Bridge:.*nanoKEY.*:.*capture.*'"
|
||||||
|
midi-out := "-o 'Midi-Bridge:.*playback.*'"
|
||||||
|
audio-in := "-l 'Komplete Audio 6 Pro:capture_AUX1' -r 'Komplete Audio 6 Pro:capture_AUX1'"
|
||||||
|
audio-out := "-L 'Komplete Audio 6 Pro:playback_AUX1' -R 'Komplete Audio 6 Pro:playback_AUX1'"
|
||||||
|
firefox-in := "-l 'Firefox:output_FL*' -r 'Firefox:output_FR*'"
|
||||||
arranger:
|
arranger:
|
||||||
reset
|
{{debug}} {{name}} {{bpm}} arranger
|
||||||
cargo run --bin tek_arranger
|
|
||||||
arranger-ext:
|
arranger-ext:
|
||||||
reset
|
{{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} arranger
|
||||||
cargo run --bin tek_arranger -- -n tek \
|
|
||||||
-i "1=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \
|
|
||||||
-o "1=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \
|
|
||||||
-i "2=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \
|
|
||||||
-o "2=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \
|
|
||||||
-i "3=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \
|
|
||||||
-o "3=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \
|
|
||||||
-i "4=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \
|
|
||||||
-o "4=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1"
|
|
||||||
arranger-release:
|
arranger-release:
|
||||||
reset
|
{{release}} {{name}} {{bpm}} arranger
|
||||||
cargo run --release --bin tek_arranger
|
|
||||||
arranger-release-ext:
|
arranger-release-ext:
|
||||||
reset
|
{{release}} {{name}} {{bpm}} {{midi-in}} {{firefox-in}} {{midi-out}} arranger
|
||||||
cargo run --release --bin tek_arranger -- -n tek \
|
|
||||||
-i "1=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \
|
|
||||||
-o "1=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \
|
|
||||||
-i "2=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \
|
|
||||||
-o "2=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \
|
|
||||||
-i "3=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \
|
|
||||||
-o "3=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \
|
|
||||||
-i "4=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \
|
|
||||||
-o "4=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1"
|
|
||||||
|
|
||||||
groovebox:
|
groovebox:
|
||||||
reset
|
{{debug}} {{name}} {{bpm}} groovebox
|
||||||
cargo run --bin tek_groovebox -- -b 174
|
|
||||||
groovebox-ext:
|
groovebox-ext:
|
||||||
reset
|
reset
|
||||||
cargo run --bin tek_groovebox -- -n tek \
|
{{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} {{audio-in}} {{audio-out}} groovebox
|
||||||
-i "Midi-Bridge:nanoKEY Studio 1:(capture_0) nanoKEY Studio nanoKEY Studio _" \
|
groovebox-browser:
|
||||||
-l "Komplete Audio 6 Pro:capture_AUX1" \
|
{{debug}} {{name}} {{bpm}} {{audio-in}} groovebox
|
||||||
-r "Komplete Audio 6 Pro:capture_AUX1" \
|
|
||||||
-L "Komplete Audio 6 Pro:playback_AUX1" \
|
|
||||||
-R "Komplete Audio 6 Pro:playback_AUX1"
|
|
||||||
groovebox-release:
|
groovebox-release:
|
||||||
reset
|
{{release}} {{name}} {{bpm}} groovebox
|
||||||
cargo run --release --bin tek_groovebox
|
|
||||||
groovebox-release-ext:
|
groovebox-release-ext:
|
||||||
reset
|
{{release}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} {{audio-in}} {{audio-out}} groovebox
|
||||||
cargo run --release --bin tek_groovebox -- -n tek \
|
|
||||||
-i "Midi-Bridge:nanoKEY Studio 1:(capture_0) nanoKEY Studio nanoKEY Studio _" \
|
|
||||||
-l "Komplete Audio 6 Pro:capture_AUX1" \
|
|
||||||
-r "Komplete Audio 6 Pro:capture_AUX1" \
|
|
||||||
-L "Komplete Audio 6 Pro:playback_AUX1" \
|
|
||||||
-R "Komplete Audio 6 Pro:playback_AUX2"
|
|
||||||
groovebox-release-ext-browser:
|
groovebox-release-ext-browser:
|
||||||
reset
|
{{release}} {{name}} {{bpm}} {{midi-in}} {{firefox-in}} {{audio-out}} groovebox
|
||||||
cargo run --release --bin tek_groovebox -- -n tek \
|
|
||||||
-b 112 \
|
|
||||||
-i "Midi-Bridge:nanoKEY Studio 1:(capture_0) nanoKEY Studio nanoKEY Studio _" \
|
|
||||||
-l "Firefox:output_FL" \
|
|
||||||
-r "Firefox:output_FR" \
|
|
||||||
-L "Komplete Audio 6 Pro:playback_AUX1" \
|
|
||||||
-R "Komplete Audio 6 Pro:playback_AUX2"
|
|
||||||
|
|
||||||
sequencer:
|
sequencer:
|
||||||
reset
|
{{debug}} {{name}} {{bpm}} sequencer
|
||||||
cargo run --bin tek_sequencer
|
|
||||||
sequencer-ext:
|
sequencer-ext:
|
||||||
reset
|
{{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} sequencer
|
||||||
cargo run --bin tek_sequencer -- \
|
|
||||||
-i "Midi-Bridge:nanoKEY Studio 1:(capture_0) nanoKEY Studio nanoKEY Studio _" \
|
|
||||||
-o "Midi-Bridge:Komplete Audio 6 0:(playback_0) Komplete Audio 6 MIDI 1"
|
|
||||||
sequencer-release:
|
sequencer-release:
|
||||||
reset
|
{{release}} {{name}} {{bpm}} sequencer
|
||||||
cargo run --release --bin tek_sequencer
|
|
||||||
sequencer-release-ext:
|
sequencer-release-ext:
|
||||||
reset
|
{{release}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} sequencer
|
||||||
cargo run --release --bin tek_sequencer -- \
|
|
||||||
-i "Midi-Bridge:nanoKEY Studio 1:(capture_0) nanoKEY Studio nanoKEY Studio _" \
|
|
||||||
-o "Midi-Bridge:Komplete Audio 6 0:(playback_0) Komplete Audio 6 MIDI 1"
|
|
||||||
|
|
||||||
mixer:
|
mixer:
|
||||||
reset
|
{{debug}} mixer
|
||||||
cargo run --bin tek_mixer
|
|
||||||
track:
|
track:
|
||||||
reset
|
{{debug}} track
|
||||||
cargo run --bin tek_track
|
|
||||||
sampler:
|
sampler:
|
||||||
reset
|
{{debug}} sampler
|
||||||
cargo run --bin tek_sampler
|
|
||||||
plugin:
|
plugin:
|
||||||
reset
|
{{debug}} plugin
|
||||||
cargo run --bin tek_plugin
|
|
||||||
|
|
|
||||||
669
LICENSE
669
LICENSE
|
|
@ -1,8 +1,661 @@
|
||||||
0. The attached collection of letters, numbers, punctuation and other characters will be
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
collectively referred to as "the work".
|
Version 3, 19 November 2007
|
||||||
1. The work exists as-is. It is composed as an extended meditation on the futility of computing.
|
|
||||||
No implication is made that the work compiles, executes, or that it is good for anything
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
whatsoever.
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
2. You may not copy, modify, or distribute the work for any purpose.
|
of this license document, but changing it is not allowed.
|
||||||
3. You may not affirm to third parties that the work exists, that you are its "author",
|
|
||||||
or that the "author" of the work exists.
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
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/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
|
||||||
112
README.md
112
README.md
|
|
@ -10,81 +10,81 @@ for [jack](https://jackaudio.org/) and [pipewire](https://www.pipewire.org/).
|
||||||
[statically linked binaries](https://codeberg.org/unspeaker/tek/releases), and on the
|
[statically linked binaries](https://codeberg.org/unspeaker/tek/releases), and on the
|
||||||
[aur](https://codeberg.org/unspeaker/tek#arch-linux).
|
[aur](https://codeberg.org/unspeaker/tek#arch-linux).
|
||||||
|
|
||||||
hmu on [**mastodon** `@unspeaker@mastodon.social`](https://mastodon.social/@unspeaker)
|
author is reachable via [**mastodon** `@unspeaker@mastodon.social`](https://mastodon.social/@unspeaker)
|
||||||
or [**matrix** `@unspeaker:matrix.org`](https://matrix.to/#/@unspeaker:matrix.org)
|
or [**matrix** `@unspeaker:matrix.org`](https://matrix.to/#/@unspeaker:matrix.org)
|
||||||
|
|
||||||

|
| | |
|
||||||
|
|-|-|
|
||||||
|
||<br>|
|
||||||
|
|
||||||
this codebase produces the following binaries:
|
## usage
|
||||||
|
|
||||||
* **`tek_sequencer`** is a single-track, multi-pattern MIDI sequencer with properly tempo-synced pattern switch
|
|
||||||
* **`tek_groovebox`** connects the sequencer to a sampler, so that you can sample while you sequence
|
|
||||||
* **`tek_arranger`** is a multi-track sequencer based on a familiar clip launching UI
|
|
||||||
* **`tek_transport`** is a JACK transport controller
|
|
||||||
* **`tek_sampler`** is a MIDI-controlled sampler
|
|
||||||
* **`tek_plugin`** is an audio plugin host.
|
|
||||||
* **`tek_channel`** is a standalone channel strip
|
|
||||||
* **`tek_mixer`** is an audio mixer.
|
|
||||||
|
|
||||||
some of these are currently work in progress.
|
|
||||||
|
|
||||||
the project roadmap is at https://codeberg.org/unspeaker/tek/milestones
|
|
||||||
|
|
||||||
## getting started
|
|
||||||
|
|
||||||
* **requirements:** linux; jack or pipewire; 24-bit terminal (i use `kitty`)
|
* **requirements:** linux; jack or pipewire; 24-bit terminal (i use `kitty`)
|
||||||
* **recommended:** midi controller; samples in wav format; lv2 plugins.
|
* **recommended:** midi controller; samples in wav format; lv2 plugins.
|
||||||
|
|
||||||
### arch linux
|
## keymaps
|
||||||
|
|
||||||
[tek](https://codeberg.org/unspeaker/tek) is available as a package in the AUR.
|
* Arranger:
|
||||||
you can install it using an AUR helper (e.g. `paru`):
|
* [x] arrows: navigate
|
||||||
|
* [x] tab: enter editor
|
||||||
|
* [x] `q`: enqueue clip
|
||||||
|
* [x] space: play/pause
|
||||||
|
* Editor:
|
||||||
|
* [x] arrows: navigate
|
||||||
|
* [x] `,` / `.`: change note length
|
||||||
|
* [x] enter: write note
|
||||||
|
* [x] `-` / `=`: zoom midi editor
|
||||||
|
* [ ] `z`: zoom lock/unlock
|
||||||
|
* [ ] del: delete
|
||||||
|
* Global:
|
||||||
|
* [x] esc: options menu
|
||||||
|
* [x] f1: help/command list
|
||||||
|
* [ ] f2: rename
|
||||||
|
* [ ] f6: save
|
||||||
|
* [ ] f9: load
|
||||||
|
|
||||||
|
## installation
|
||||||
|
|
||||||
|
### binary download
|
||||||
|
|
||||||
|
you can download [tek 0.2.0 "almost static"](https://codeberg.org/unspeaker/tek/releases/tag/0.2.0)
|
||||||
|
from codeberg releases. this standalone binary release, should work on any glibc-based system.
|
||||||
|
|
||||||
|
### from distro repositories
|
||||||
|
|
||||||
|
[](https://repology.org/project/tek/versions)
|
||||||
|
|
||||||
|
#### arch linux
|
||||||
|
|
||||||
|
[tek 0.2.0-rc7](https://aur.archlinux.org/packages/tek) is available as a package in the AUR.
|
||||||
|
you can install it using your preferred AUR helper (e.g. `paru`):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
paru -S tek
|
paru -S tek
|
||||||
```
|
```
|
||||||
|
|
||||||
### downloads
|
|
||||||
|
|
||||||
see the [releases page](https://codeberg.org/unspeaker/tek/releases).
|
|
||||||
|
|
||||||
### building from source
|
### building from source
|
||||||
|
|
||||||
you'll need a Rust toolchain and various system libraries.
|
requires docker.
|
||||||
|
|
||||||
you can obtain the former using `rustup` and the latter using `nix-shell`.
|
```
|
||||||
there's a `shell.nix` provided with the project.
|
git clone --recursive -b 0.2 https://codeberg.org/unspeaker/tek
|
||||||
|
cd tek # enter directory
|
||||||
from there, use the commands in the `Justfile`, e.g.:
|
cat bin/release-glibc.sh # preview build script
|
||||||
|
sudo bin/release-glibc.sh # run build script
|
||||||
```sh
|
sudo cp bin/tek /usr/local/bin/tek # install
|
||||||
just arranger
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## design goals
|
## design goals
|
||||||
|
|
||||||
### lightweight
|
* inspired by trackers and hardware sequencers,
|
||||||
|
but with the critical feature that 90s samplers lack:
|
||||||
|
able to **resample, i.e. record while playing!**
|
||||||
|
|
||||||
* pop-up scratchpad for musical ideas
|
* **pop-up scratchpad for musical ideas.**
|
||||||
* low resource consumption, can stay open in background
|
low resource consumption, can stay open in background.
|
||||||
* advanced toolset allows quickly expanding on compositions
|
but flexible enough to allow expanding on compositions
|
||||||
|
|
||||||
### flexible
|
* **human- and machine- readable project format**
|
||||||
|
simple representation for project data
|
||||||
* inspired by trackers and hardware sequencers
|
enable scripting and remapping.
|
||||||
* able to record while playing!
|
|
||||||
|
|
||||||
### programmable
|
|
||||||
|
|
||||||
* human-readable project format
|
|
||||||
* command architecture allows for scripting and remapping
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Your moral support means a lot to me.
|
|
||||||
> Feel free to [contact me on Mastodon](https://mastodon.social/@unspeaker)![^0]
|
|
||||||
>
|
|
||||||
> Love,
|
|
||||||
> 🤫
|
|
||||||
> (a rogue knowledge worker in a cyberpunk dystopia)
|
|
||||||
|
|
|
||||||
58
app/Cargo.toml
Normal file
58
app/Cargo.toml
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
[package]
|
||||||
|
name = "tek"
|
||||||
|
edition = { workspace = true }
|
||||||
|
version = { workspace = true }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "tek.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "tek"
|
||||||
|
path = "tek_cli.rs"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")']
|
||||||
|
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tek_device = { path = "../device" }
|
||||||
|
|
||||||
|
atomic_float = { workspace = true }
|
||||||
|
backtrace = { workspace = true }
|
||||||
|
clap = { workspace = true, optional = true }
|
||||||
|
jack = { workspace = true }
|
||||||
|
konst = { workspace = true }
|
||||||
|
livi = { workspace = true, optional = true }
|
||||||
|
midly = { workspace = true }
|
||||||
|
palette = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
symphonia = { workspace = true, optional = true }
|
||||||
|
tengri = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
uuid = { workspace = true, optional = true }
|
||||||
|
wavers = { workspace = true, optional = true }
|
||||||
|
winit = { workspace = true, optional = true }
|
||||||
|
xdg = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
proptest = { workspace = true }
|
||||||
|
proptest-derive = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
arranger = ["port", "editor", "sequencer", "editor"]
|
||||||
|
browse = []
|
||||||
|
clap = []
|
||||||
|
cli = ["dep:clap"]
|
||||||
|
clock = []
|
||||||
|
default = ["cli", "arranger", "sampler", "lv2"]
|
||||||
|
editor = []
|
||||||
|
host = ["lv2"]
|
||||||
|
lv2 = ["port", "livi", "winit"]
|
||||||
|
meter = []
|
||||||
|
mixer = []
|
||||||
|
pool = []
|
||||||
|
port = []
|
||||||
|
sampler = ["port", "meter", "mixer", "browse", "symphonia", "wavers"]
|
||||||
|
sequencer = ["port", "clock", "uuid", "pool"]
|
||||||
|
sf2 = []
|
||||||
|
vst2 = []
|
||||||
|
vst3 = []
|
||||||
224
app/tek.edn
Normal file
224
app/tek.edn
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
(keys :axis/x
|
||||||
|
(@left x/dec)
|
||||||
|
(@right x/inc))
|
||||||
|
(keys :axis/x2
|
||||||
|
(@shift/left x2/dec)
|
||||||
|
(@shift/right x2/inc))
|
||||||
|
(keys :axis/y
|
||||||
|
(@up y/dec)
|
||||||
|
(@down y/inc))
|
||||||
|
(keys :axis/y2
|
||||||
|
(@shift/up y2/dec)
|
||||||
|
(@shift/down y2/inc))
|
||||||
|
(keys :axis/z
|
||||||
|
(@minus z/dec)
|
||||||
|
(@equal z/inc))
|
||||||
|
(keys :axis/z2
|
||||||
|
(@underscore z2/dec)
|
||||||
|
(@plus z2/inc))
|
||||||
|
(keys :axis/i
|
||||||
|
(@comma i/dec)
|
||||||
|
(@period z/inc))
|
||||||
|
(keys :axis/i2
|
||||||
|
(@lt i2/dec)
|
||||||
|
(@gt z2/inc))
|
||||||
|
(keys :axis/w
|
||||||
|
(@openbracket w/dec)
|
||||||
|
(@closebracket w/inc))
|
||||||
|
(keys :axis/w2
|
||||||
|
(@openbrace w2/dec)
|
||||||
|
(@closebrace w2/inc))
|
||||||
|
|
||||||
|
(mode :menu (keys :axis/y :confirm) :menu)
|
||||||
|
|
||||||
|
(keys :confirm
|
||||||
|
(@enter confirm))
|
||||||
|
|
||||||
|
(view :menu (bg (g 0) (bsp/s
|
||||||
|
:ports/out
|
||||||
|
(bsp/n :ports/in (bg (g 30) (bsp/s (fixed/y 7 :logo) (fill :dialog/menu)))))))
|
||||||
|
|
||||||
|
(view :menu (bsp/s
|
||||||
|
(push/y 4 (fixed/xy 20 2 (bg (g 0) :debug)))
|
||||||
|
(fixed 20 2 (bg (g 20) (push/x 2 :debug)))))
|
||||||
|
|
||||||
|
(view :menu (bsp/s (fixed/y 4 :debug) :debug))
|
||||||
|
|
||||||
|
(view :ports/out (fill/x (fixed/y 3 (bsp/a
|
||||||
|
(fill/x (align/w (text L-AUDIO-OUT)))
|
||||||
|
(bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R))))))))
|
||||||
|
|
||||||
|
(view :ports/in (fill/x (fixed/y 3 (bsp/a
|
||||||
|
(fill/x (align/w (text L-AUDIO-IN)))
|
||||||
|
(bsp/a (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R))))))))
|
||||||
|
|
||||||
|
(view :browse (bsp/s
|
||||||
|
(padding 3 1 :browse-title)
|
||||||
|
(enclose (fg (g 96)) browser)))
|
||||||
|
|
||||||
|
(keys :help
|
||||||
|
(@f1 dialog :help))
|
||||||
|
|
||||||
|
(keys :back
|
||||||
|
(@escape back))
|
||||||
|
|
||||||
|
(keys :page
|
||||||
|
(@pgup page/up)
|
||||||
|
(@pgdn page/down))
|
||||||
|
|
||||||
|
(keys :delete
|
||||||
|
(@delete delete)
|
||||||
|
(@backspace delete/back))
|
||||||
|
|
||||||
|
(keys :input (see :axis/x :delete)
|
||||||
|
(:char input))
|
||||||
|
|
||||||
|
(keys :list (see :axis/y :confirm))
|
||||||
|
|
||||||
|
(keys :length (see :axis/x :axis/y :confirm))
|
||||||
|
|
||||||
|
(keys :browse (see :list :input :focus))
|
||||||
|
|
||||||
|
(keys :history
|
||||||
|
(@u undo 1)
|
||||||
|
(@r redo 1))
|
||||||
|
|
||||||
|
(keys :clock
|
||||||
|
(@space clock/toggle 0)
|
||||||
|
(@shift/space clock/toggle 0))
|
||||||
|
|
||||||
|
(keys :color
|
||||||
|
(@c color))
|
||||||
|
|
||||||
|
(keys :launch
|
||||||
|
(@q launch))
|
||||||
|
|
||||||
|
(keys :saveload
|
||||||
|
(@f6 dialog :save)
|
||||||
|
(@f9 dialog :load))
|
||||||
|
|
||||||
|
(keys :global (see :history :saveload)
|
||||||
|
(@f8 dialog :options)
|
||||||
|
(@f10 dialog :quit))
|
||||||
|
|
||||||
|
(keys :focus)
|
||||||
|
|
||||||
|
(mode :transport (name Transport) (info A JACK transport controller.) (keys :clock :global)
|
||||||
|
(view :transport))
|
||||||
|
|
||||||
|
(mode :arranger (name Arranger) (info A grid of launchable clips arranged by track and scene.)
|
||||||
|
(mode :editor (keys :editor)) (mode :dialog (keys :dialog)) (mode :message (keys :message))
|
||||||
|
(mode :add-device (keys :add-device)) (mode :browse (keys :browse)) (mode :rename (keys :input))
|
||||||
|
(mode :length (keys :rename)) (mode :clip (keys :clip)) (mode :track (keys :track))
|
||||||
|
(mode :scene (keys :scene)) (mode :mix (keys :mix))
|
||||||
|
(keys :clock :arranger :global) :arranger)
|
||||||
|
|
||||||
|
(view :arranger (bsp/n
|
||||||
|
:status
|
||||||
|
(bsp/w :meters/output (bsp/e :meters/input :arrangement))))
|
||||||
|
|
||||||
|
(view :arrangement (bsp/n
|
||||||
|
:tracks/inputs
|
||||||
|
(bsp/s :tracks/outputs (bsp/s :tracks/names (bsp/s :tracks/devices
|
||||||
|
(fill (either :mode/editor (bsp/e :scenes/names :editor) :scenes)))))))
|
||||||
|
|
||||||
|
(keys :arranger (see :color :launch :scenes :tracks)
|
||||||
|
(@tab project/edit) (@enter project/edit)
|
||||||
|
(@shift/I project/input/add) (@shift/O project/output/add)
|
||||||
|
(@shift/S project/scene/add) (@shift/T project/track/add)
|
||||||
|
(@shift/D dialog/show :dialog/device))
|
||||||
|
|
||||||
|
(keys :tracks
|
||||||
|
(@t select :select/track)
|
||||||
|
(@left select :select/track/dec)
|
||||||
|
(@right select :select/track/inc))
|
||||||
|
|
||||||
|
(keys :scenes
|
||||||
|
(@s select :select/scene)
|
||||||
|
(@up select :select/scene/dec)
|
||||||
|
(@down select :select/scene/inc))
|
||||||
|
|
||||||
|
(keys :track (see :color :launch :axis/z :axis/z2 :delete)
|
||||||
|
(@r toggle :rec)
|
||||||
|
(@m toggle :mon)
|
||||||
|
(@p toggle :play)
|
||||||
|
(@P toggle :solo))
|
||||||
|
(keys :scene (see :color :launch :axis/z :axis/z2 :delete))
|
||||||
|
|
||||||
|
(keys :clip (see :color :launch :axis/z :axis/z2 :delete)
|
||||||
|
(@l toggle :loop))
|
||||||
|
|
||||||
|
(mode :groovebox (name Groovebox) (info A sequencer with built-in sampler.)
|
||||||
|
(mode browse (keys :browse))
|
||||||
|
(mode rename (keys :pool-rename))
|
||||||
|
(mode length (keys :pool-length))
|
||||||
|
(keys :clock :editor :sampler :global) (view :groovebox))
|
||||||
|
|
||||||
|
(view :groovebox (bsp/w
|
||||||
|
:meters/output
|
||||||
|
(bsp/e :meters/input (bsp/w :groove/meta :groove/editor))))
|
||||||
|
|
||||||
|
(view :groove/meta (fill/y (align/n (stack/s
|
||||||
|
:midi-ins/status :midi-outs/status :audio-ins/status :audio-outs/status :pool))))
|
||||||
|
|
||||||
|
(view :groove/editor (bsp/n
|
||||||
|
:groove/sample
|
||||||
|
:groove/sequence))
|
||||||
|
|
||||||
|
(view :groove/sample (fixed/y :h-sample-detail (bsp/e
|
||||||
|
(fill/y (fixed/x 20 (align/nw :sample-status)))
|
||||||
|
:sample-viewer)))
|
||||||
|
|
||||||
|
(view :groove/sequence (bsp/e
|
||||||
|
(fill/y (align/n (bsp/s :status/v :editor-status)))
|
||||||
|
(bsp/e :samples/keys :editor)))
|
||||||
|
|
||||||
|
(mode :sampler (name Sampler) (info A sampling soundboard.)
|
||||||
|
(keys :sampler :global) (view :sampler))
|
||||||
|
|
||||||
|
(view :sampler (bsp/s
|
||||||
|
(fixed/y 1 :transport)
|
||||||
|
(bsp/n (fixed/y 1 :status) (fill :samples/grid))))
|
||||||
|
|
||||||
|
(keys :sampler (see :sampler/directions :sampler/record :sampler/play))
|
||||||
|
|
||||||
|
(keys :sampler/record
|
||||||
|
(@r sampler/record/toggle :sample/selected) (@shift/R sampler/record/back))
|
||||||
|
|
||||||
|
(keys :sampler/play
|
||||||
|
(@p sampler/play/sample :sample/selected) (@P sampler/stop/sample :sample/selected))
|
||||||
|
|
||||||
|
(keys :sampler/import-export
|
||||||
|
(@shift/f6 dialog :dialog/export/sample) (@shift/f9 dialog :dialog/import/sample))
|
||||||
|
|
||||||
|
(keys :sampler/directions
|
||||||
|
(@up sampler/select :sample/above)
|
||||||
|
(@down sampler/select :sample/below)
|
||||||
|
(@left sampler/select :sample/to/left)
|
||||||
|
(@right sampler/select :sample/to/right))
|
||||||
|
|
||||||
|
(mode :sequencer (name Sequencer) (info A MIDI sequencer.)
|
||||||
|
(mode browse (keys :browse)) (mode rename (keys :pool/rename)) (mode length (keys :pool/length))
|
||||||
|
(keys :editor :clock :global) (view :sequencer))
|
||||||
|
|
||||||
|
(view :sequencer (bsp/s
|
||||||
|
(fixed/y 1 :transport)
|
||||||
|
(bsp/n (fixed/y 1 :status) (fill (bsp/a (fill/xy (align/e :pool)) :editor)))))
|
||||||
|
|
||||||
|
(keys :editor (see :editor/view :editor/note))
|
||||||
|
|
||||||
|
(keys :editor/view (see :axis/x :axis/x2 :axis/z :axis/z2)
|
||||||
|
(@z toggle :lock))
|
||||||
|
|
||||||
|
(keys :editor/note (see :axis/i :axis/i2 :axis/y :page)
|
||||||
|
(@a editor/append :true)
|
||||||
|
(@enter editor/append :false)
|
||||||
|
(@del editor/delete/note)
|
||||||
|
(@shift/del editor/delete/note))
|
||||||
|
|
||||||
|
(keys :pool (see :axis-y :axis-w :axis/z2 :color :delete)
|
||||||
|
(@n rename/begin) (@t length/begin) (@m import/begin) (@x export/begin)
|
||||||
|
(@shift/A clip/add :after :new/clip) (@shift/D clip/add :after :cloned/clip))
|
||||||
|
|
||||||
|
(keys :sequencer (see :color :launch)
|
||||||
|
(@shift/I input/add) (@shift/O output/add))
|
||||||
468
app/tek.rs
Normal file
468
app/tek.rs
Normal file
|
|
@ -0,0 +1,468 @@
|
||||||
|
#![feature(
|
||||||
|
adt_const_params,
|
||||||
|
associated_type_defaults,
|
||||||
|
closure_lifetime_binder,
|
||||||
|
if_let_guard,
|
||||||
|
impl_trait_in_assoc_type,
|
||||||
|
trait_alias,
|
||||||
|
type_alias_impl_trait,
|
||||||
|
type_changing_struct_update,
|
||||||
|
)]
|
||||||
|
|
||||||
|
#![allow(
|
||||||
|
clippy::unit_arg
|
||||||
|
)]
|
||||||
|
|
||||||
|
#[cfg(test)] mod tek_test;
|
||||||
|
|
||||||
|
mod tek_bind; pub use self::tek_bind::*;
|
||||||
|
mod tek_cfg; pub use self::tek_cfg::*;
|
||||||
|
mod tek_deps; pub use self::tek_deps::*;
|
||||||
|
mod tek_mode; pub use self::tek_mode::*;
|
||||||
|
mod tek_view; pub use self::tek_view::*;
|
||||||
|
|
||||||
|
/// Total state
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct App {
|
||||||
|
/// Base color.
|
||||||
|
pub color: ItemTheme,
|
||||||
|
/// Must not be dropped for the duration of the process
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// Display size
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
/// Performance counter
|
||||||
|
pub perf: PerfModel,
|
||||||
|
/// Available view modes and input bindings
|
||||||
|
pub config: Config,
|
||||||
|
/// Currently selected mode
|
||||||
|
pub mode: Arc<Mode<Arc<str>>>,
|
||||||
|
/// Undo history
|
||||||
|
pub history: Vec<(AppCommand, Option<AppCommand>)>,
|
||||||
|
/// Dialog overlay
|
||||||
|
pub dialog: Dialog,
|
||||||
|
/// Contains all recently created clips.
|
||||||
|
pub pool: Pool,
|
||||||
|
/// Contains the currently edited musical arrangement
|
||||||
|
pub project: Arrangement,
|
||||||
|
}
|
||||||
|
|
||||||
|
audio!(
|
||||||
|
|self: App, client, scope|{
|
||||||
|
let t0 = self.perf.get_t0();
|
||||||
|
self.clock().update_from_scope(scope).unwrap();
|
||||||
|
let midi_in = self.project.midi_input_collect(scope);
|
||||||
|
if let Some(editor) = &self.editor() {
|
||||||
|
let mut pitch: Option<u7> = None;
|
||||||
|
for port in midi_in.iter() {
|
||||||
|
for event in port.iter() {
|
||||||
|
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..}))
|
||||||
|
= event
|
||||||
|
{
|
||||||
|
pitch = Some(key.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(pitch) = pitch {
|
||||||
|
editor.set_note_pos(pitch.as_int() as usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let result = self.project.process_tracks(client, scope);
|
||||||
|
self.perf.update_from_jack_scope(t0, scope);
|
||||||
|
result
|
||||||
|
};
|
||||||
|
|self, event|{
|
||||||
|
use JackEvent::*;
|
||||||
|
match event {
|
||||||
|
SampleRate(sr) => { self.clock().timebase.sr.set(sr as f64); },
|
||||||
|
PortRegistration(_id, true) => {
|
||||||
|
//let port = self.jack().port_by_id(id);
|
||||||
|
//println!("\rport add: {id} {port:?}");
|
||||||
|
//println!("\rport add: {id}");
|
||||||
|
},
|
||||||
|
PortRegistration(_id, false) => {
|
||||||
|
/*println!("\rport del: {id}")*/
|
||||||
|
},
|
||||||
|
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ },
|
||||||
|
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ },
|
||||||
|
ClientRegistration(_id, true) => {},
|
||||||
|
ClientRegistration(_id, false) => {},
|
||||||
|
ThreadInit => {},
|
||||||
|
XRun => {},
|
||||||
|
GraphReorder => {},
|
||||||
|
_ => { panic!("{event:?}"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Allow source to be read as Literal string
|
||||||
|
dsl_ns!(App: Arc<str> {
|
||||||
|
literal = |dsl|Ok(dsl.src()?.map(|x|x.into()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Provide boolean values.
|
||||||
|
dsl_ns!(App: bool {
|
||||||
|
// TODO literal = ...
|
||||||
|
word = |app| {
|
||||||
|
":mode/editor" => app.project.editor.is_some(),
|
||||||
|
":focused/dialog" => !matches!(app.dialog, Dialog::None),
|
||||||
|
":focused/message" => matches!(app.dialog, Dialog::Message(..)),
|
||||||
|
":focused/add_device" => matches!(app.dialog, Dialog::Device(..)),
|
||||||
|
":focused/browser" => app.dialog.browser().is_some(),
|
||||||
|
":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))),
|
||||||
|
":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))),
|
||||||
|
":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))),
|
||||||
|
":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))),
|
||||||
|
":focused/clip" => !app.editor_focused() && matches!(app.selection(),
|
||||||
|
Selection::TrackClip{..}),
|
||||||
|
":focused/track" => !app.editor_focused() && matches!(app.selection(),
|
||||||
|
Selection::Track(..)),
|
||||||
|
":focused/scene" => !app.editor_focused() && matches!(app.selection(),
|
||||||
|
Selection::Scene(..)),
|
||||||
|
":focused/mix" => !app.editor_focused() && matches!(app.selection(),
|
||||||
|
Selection::Mix),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: provide colors here
|
||||||
|
dsl_ns!(App: ItemTheme {});
|
||||||
|
|
||||||
|
dsl_ns!(App: Dialog {
|
||||||
|
word = |app| {
|
||||||
|
":dialog/none" => Dialog::None,
|
||||||
|
":dialog/options" => Dialog::Options,
|
||||||
|
":dialog/device" => Dialog::Device(0),
|
||||||
|
":dialog/device/prev" => Dialog::Device(0),
|
||||||
|
":dialog/device/next" => Dialog::Device(0),
|
||||||
|
":dialog/help" => Dialog::Help(0),
|
||||||
|
":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject,
|
||||||
|
Browse::new(None).unwrap().into()),
|
||||||
|
":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject,
|
||||||
|
Browse::new(None).unwrap().into()),
|
||||||
|
":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()),
|
||||||
|
Browse::new(None).unwrap().into()),
|
||||||
|
":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()),
|
||||||
|
Browse::new(None).unwrap().into()),
|
||||||
|
":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()),
|
||||||
|
Browse::new(None).unwrap().into()),
|
||||||
|
":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()),
|
||||||
|
Browse::new(None).unwrap().into()),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dsl_ns!(App: Selection {
|
||||||
|
word = |app| {
|
||||||
|
":select/scene" => app.selection().select_scene(app.tracks().len()),
|
||||||
|
":select/scene/next" => app.selection().select_scene_next(app.scenes().len()),
|
||||||
|
":select/scene/prev" => app.selection().select_scene_prev(),
|
||||||
|
":select/track" => app.selection().select_track(app.tracks().len()),
|
||||||
|
":select/track/next" => app.selection().select_track_next(app.tracks().len()),
|
||||||
|
":select/track/prev" => app.selection().select_track_prev(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dsl_ns!(App: Color {
|
||||||
|
word = |app| {
|
||||||
|
":color/bg" => Color::Rgb(28, 32, 36),
|
||||||
|
};
|
||||||
|
expr = |app| {
|
||||||
|
"g" (n: u8) => Color::Rgb(n, n, n),
|
||||||
|
"rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dsl_ns!(App: Option<u7> {
|
||||||
|
word = |app| {
|
||||||
|
":editor/pitch" => Some(
|
||||||
|
(app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dsl_ns!(App: Option<usize> {
|
||||||
|
word = |app| {
|
||||||
|
":selected/scene" => app.selection().scene(),
|
||||||
|
":selected/track" => app.selection().track(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dsl_ns!(App: Option<Arc<RwLock<MidiClip>>> {
|
||||||
|
word = |app| {
|
||||||
|
":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() {
|
||||||
|
app.scenes()[*scene].clips[*track].clone()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dsl_ns!(App: u8 {
|
||||||
|
literal = |dsl|Ok(if let Some(src) = dsl.src()? {
|
||||||
|
Some(to_number(src)? as u8)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dsl_ns!(App: u16 {
|
||||||
|
literal = |dsl|Ok(if let Some(src) = dsl.src()? {
|
||||||
|
Some(to_number(src)? as u16)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
});
|
||||||
|
word = |app| {
|
||||||
|
":w/sidebar" => app.project.w_sidebar(app.editor().is_some()),
|
||||||
|
":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dsl_ns!(App: usize {
|
||||||
|
literal = |dsl|Ok(if let Some(src) = dsl.src()? {
|
||||||
|
Some(to_number(src)? as usize)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
});
|
||||||
|
word = |app| {
|
||||||
|
":scene-count" => app.scenes().len(),
|
||||||
|
":track-count" => app.tracks().len(),
|
||||||
|
":device-kind" => app.dialog.device_kind().unwrap_or(0),
|
||||||
|
":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0),
|
||||||
|
":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dsl_ns!(App: isize {
|
||||||
|
literal = |dsl|Ok(if let Some(src) = dsl.src()? {
|
||||||
|
Some(to_number(src)? as isize)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
has!(Jack<'static>: |self: App|self.jack);
|
||||||
|
has!(Pool: |self: App|self.pool);
|
||||||
|
has!(Dialog: |self: App|self.dialog);
|
||||||
|
has!(Clock: |self: App|self.project.clock);
|
||||||
|
has!(Option<MidiEditor>: |self: App|self.project.editor);
|
||||||
|
has!(Selection: |self: App|self.project.selection);
|
||||||
|
has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
|
||||||
|
has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
|
||||||
|
has!(Vec<Scene>: |self: App|self.project.scenes);
|
||||||
|
has!(Vec<Track>: |self: App|self.project.tracks);
|
||||||
|
has!(Measure<TuiOut>: |self: App|self.size);
|
||||||
|
has_clips!( |self: App|self.pool.clips);
|
||||||
|
maybe_has!(Track: |self: App| { MaybeHas::<Track>::get(&self.project) };
|
||||||
|
{ MaybeHas::<Track>::get_mut(&mut self.project) });
|
||||||
|
maybe_has!(Scene: |self: App| { MaybeHas::<Scene>::get(&self.project) };
|
||||||
|
{ MaybeHas::<Scene>::get_mut(&mut self.project) });
|
||||||
|
|
||||||
|
impl HasClipsSize for App {
|
||||||
|
fn clips_size (&self) -> &Measure<TuiOut> { &self.project.size_inner }
|
||||||
|
}
|
||||||
|
impl HasTrackScroll for App {
|
||||||
|
fn track_scroll (&self) -> usize { self.project.track_scroll() }
|
||||||
|
}
|
||||||
|
impl HasSceneScroll for App {
|
||||||
|
fn scene_scroll (&self) -> usize { self.project.scene_scroll() }
|
||||||
|
}
|
||||||
|
impl HasJack<'static> for App {
|
||||||
|
fn jack (&self) -> &Jack<'static> { &self.jack }
|
||||||
|
}
|
||||||
|
impl ScenesView for App {
|
||||||
|
fn w_side (&self) -> u16 { 20 }
|
||||||
|
fn w_mid (&self) -> u16 { (self.width() as u16).saturating_sub(self.w_side()) }
|
||||||
|
fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
|
||||||
|
pub fn editor_focused (&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_dialog (&mut self, mut dialog: Dialog) -> Dialog {
|
||||||
|
std::mem::swap(&mut self.dialog, &mut dialog);
|
||||||
|
dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_editor (&mut self, value: Option<bool>) {
|
||||||
|
//FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed);
|
||||||
|
let value = value.unwrap_or_else(||!self.editor().is_some());
|
||||||
|
if value {
|
||||||
|
// Create new clip in pool when entering empty cell
|
||||||
|
if let Selection::TrackClip { track, scene } = *self.selection()
|
||||||
|
&& let Some(scene) = self.project.scenes.get_mut(scene)
|
||||||
|
&& let Some(slot) = scene.clips.get_mut(track)
|
||||||
|
&& slot.is_none()
|
||||||
|
&& let Some(track) = self.project.tracks.get_mut(track)
|
||||||
|
{
|
||||||
|
let (index, mut clip) = self.pool.add_new_clip();
|
||||||
|
// autocolor: new clip colors from scene and track color
|
||||||
|
let color = track.color.base.mix(scene.color.base, 0.5);
|
||||||
|
clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into();
|
||||||
|
if let Some(editor) = &mut self.project.editor {
|
||||||
|
editor.set_clip(Some(&clip));
|
||||||
|
}
|
||||||
|
*slot = Some(clip.clone());
|
||||||
|
//Some(clip)
|
||||||
|
} else {
|
||||||
|
//None
|
||||||
|
}
|
||||||
|
} else if let Selection::TrackClip { track, scene } = *self.selection()
|
||||||
|
&& let Some(scene) = self.project.scenes.get_mut(scene)
|
||||||
|
&& let Some(slot) = scene.clips.get_mut(track)
|
||||||
|
&& let Some(clip) = slot.as_mut()
|
||||||
|
{
|
||||||
|
// Remove clip from arrangement when exiting empty clip editor
|
||||||
|
let mut swapped = None;
|
||||||
|
if clip.read().unwrap().count_midi_messages() == 0 {
|
||||||
|
std::mem::swap(&mut swapped, slot);
|
||||||
|
}
|
||||||
|
if let Some(clip) = swapped {
|
||||||
|
self.pool.delete_clip(&clip.read().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn browser (&self) -> Option<&Browse> {
|
||||||
|
if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn device_pick (&mut self, index: usize) {
|
||||||
|
self.dialog = Dialog::Device(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_device (&mut self, index: usize) -> Usually<()> {
|
||||||
|
match index {
|
||||||
|
0 => {
|
||||||
|
let name = self.jack.with_client(|c|c.name().to_string());
|
||||||
|
let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name();
|
||||||
|
let track = self.track().expect("no active track");
|
||||||
|
let port = format!("{}/Sampler", &track.name);
|
||||||
|
let connect = Connect::exact(format!("{name}:{midi}"));
|
||||||
|
let sampler = if let Ok(sampler) = Sampler::new(
|
||||||
|
&self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]]
|
||||||
|
) {
|
||||||
|
self.dialog = Dialog::None;
|
||||||
|
Device::Sampler(sampler)
|
||||||
|
} else {
|
||||||
|
self.dialog = Dialog::Message("Failed to add device.".into());
|
||||||
|
return Err("failed to add device".into())
|
||||||
|
};
|
||||||
|
let track = self.track_mut().expect("no active track");
|
||||||
|
track.devices.push(sampler);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
1 => {
|
||||||
|
todo!();
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_clock (&self) {
|
||||||
|
ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Various possible dialog modes.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
|
pub enum Dialog {
|
||||||
|
#[default] None,
|
||||||
|
Help(usize),
|
||||||
|
Menu(usize, MenuItems),
|
||||||
|
Device(usize),
|
||||||
|
Message(Arc<str>),
|
||||||
|
Browse(BrowseTarget, Arc<Browse>),
|
||||||
|
Options,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
|
pub struct MenuItems(pub Arc<[MenuItem]>);
|
||||||
|
|
||||||
|
impl AsRef<Arc<[MenuItem]>> for MenuItems {
|
||||||
|
fn as_ref (&self) -> &Arc<[MenuItem]> {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MenuItem(
|
||||||
|
/// Label
|
||||||
|
pub Arc<str>,
|
||||||
|
/// Callback
|
||||||
|
pub Arc<Box<dyn Fn(&mut App)->Usually<()> + Send + Sync>>
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Default for MenuItem {
|
||||||
|
fn default () -> Self { Self("".into(), Arc::new(Box::new(|_|Ok(())))) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) });
|
||||||
|
|
||||||
|
impl PartialEq for MenuItem {
|
||||||
|
fn eq (&self, other: &Self) -> bool {
|
||||||
|
self.0 == other.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dialog {
|
||||||
|
pub fn welcome () -> Self {
|
||||||
|
Self::Menu(1, MenuItems([
|
||||||
|
MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))),
|
||||||
|
MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({
|
||||||
|
app.dialog = Dialog::None;
|
||||||
|
app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap();
|
||||||
|
})))),
|
||||||
|
MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))),
|
||||||
|
].into()))
|
||||||
|
}
|
||||||
|
pub fn menu_next (&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()),
|
||||||
|
_ => Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn menu_prev (&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()),
|
||||||
|
_ => Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn menu_selected (&self) -> Option<usize> {
|
||||||
|
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
|
||||||
|
}
|
||||||
|
pub fn device_kind (&self) -> Option<usize> {
|
||||||
|
if let Self::Device(index) = self { Some(*index) } else { None }
|
||||||
|
}
|
||||||
|
pub fn device_kind_next (&self) -> Option<usize> {
|
||||||
|
self.device_kind().map(|index|(index + 1) % device_kinds().len())
|
||||||
|
}
|
||||||
|
pub fn device_kind_prev (&self) -> Option<usize> {
|
||||||
|
self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)))
|
||||||
|
}
|
||||||
|
pub fn message (&self) -> Option<&str> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
pub fn browser (&self) -> Option<&Arc<Browse>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
pub fn browser_target (&self) -> Option<&BrowseTarget> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//has_editor!(|self: App|{
|
||||||
|
//editor = self.editor;
|
||||||
|
//editor_w = {
|
||||||
|
//let size = self.size.w();
|
||||||
|
//let editor = self.editor.as_ref().expect("missing editor");
|
||||||
|
//let time_len = editor.time_len().get();
|
||||||
|
//let time_zoom = editor.time_zoom().get().max(1);
|
||||||
|
//(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16)
|
||||||
|
//};
|
||||||
|
//editor_h = 15;
|
||||||
|
//is_editing = self.editor.is_some();
|
||||||
|
//});
|
||||||
325
app/tek_bind.rs
Normal file
325
app/tek_bind.rs
Normal file
|
|
@ -0,0 +1,325 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, EventMap<TuiEvent, Arc<str>>>>>;
|
||||||
|
|
||||||
|
/// A collection of input bindings.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EventMap<E, C>(
|
||||||
|
/// Map of each event (e.g. key combination) to
|
||||||
|
/// all command expressions bound to it by
|
||||||
|
/// all loaded input layers.
|
||||||
|
pub BTreeMap<E, Vec<Binding<C>>>
|
||||||
|
);
|
||||||
|
|
||||||
|
/// An input binding.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Binding<C> {
|
||||||
|
pub commands: Arc<[C]>,
|
||||||
|
pub condition: Option<Condition>,
|
||||||
|
pub description: Option<Arc<str>>,
|
||||||
|
pub source: Option<Arc<PathBuf>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Input bindings are only returned if this evaluates to true
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Condition(Arc<Box<dyn Fn()->bool + Send + Sync>>);
|
||||||
|
|
||||||
|
/// Default is always empty map regardless if `E` and `C` implement [Default].
|
||||||
|
impl<E, C> Default for EventMap<E, C> {
|
||||||
|
fn default () -> Self { Self(Default::default()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Clone + Ord, C> EventMap<E, C> {
|
||||||
|
/// Create a new event map
|
||||||
|
pub fn new () -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
/// Add a binding to an owned event map.
|
||||||
|
pub fn def (mut self, event: E, binding: Binding<C>) -> Self {
|
||||||
|
self.add(event, binding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Add a binding to an event map.
|
||||||
|
pub fn add (&mut self, event: E, binding: Binding<C>) -> &mut Self {
|
||||||
|
if !self.0.contains_key(&event) {
|
||||||
|
self.0.insert(event.clone(), Default::default());
|
||||||
|
}
|
||||||
|
self.0.get_mut(&event).unwrap().push(binding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Return the binding(s) that correspond to an event.
|
||||||
|
pub fn query (&self, event: &E) -> Option<&[Binding<C>]> {
|
||||||
|
self.0.get(event).map(|x|x.as_slice())
|
||||||
|
}
|
||||||
|
/// Return the first binding that corresponds to an event, considering conditions.
|
||||||
|
pub fn dispatch (&self, event: &E) -> Option<&Binding<C>> {
|
||||||
|
self.query(event)
|
||||||
|
.map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next())
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventMap<TuiEvent, Arc<str>> {
|
||||||
|
pub fn load_into (binds: &Binds, name: &impl AsRef<str>, body: &impl Dsl) -> Usually<()> {
|
||||||
|
println!("EventMap::load_into: {}: {body:?}", name.as_ref());
|
||||||
|
let mut map = Self::new();
|
||||||
|
body.each(|item|if item.expr().head() == Ok(Some("see")) {
|
||||||
|
// TODO
|
||||||
|
Ok(())
|
||||||
|
} else if let Ok(Some(_word)) = item.expr().head().word() {
|
||||||
|
if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? {
|
||||||
|
map.add(key, Binding {
|
||||||
|
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
|
||||||
|
condition: None,
|
||||||
|
description: None,
|
||||||
|
source: None
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
} else if Some(":char") == item.expr()?.head()? {
|
||||||
|
// TODO
|
||||||
|
return Ok(())
|
||||||
|
} else {
|
||||||
|
return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(format!("Config::load_bind: unexpected: {item:?}").into())
|
||||||
|
})?;
|
||||||
|
binds.write().unwrap().insert(name.as_ref().into(), map);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> Binding<C> {
|
||||||
|
pub fn from_dsl (dsl: impl Dsl) -> Usually<Self> {
|
||||||
|
let command: Option<C> = None;
|
||||||
|
let condition: Option<Condition> = None;
|
||||||
|
let description: Option<Arc<str>> = None;
|
||||||
|
let source: Option<Arc<PathBuf>> = None;
|
||||||
|
if let Some(command) = command {
|
||||||
|
Ok(Self { commands: [command].into(), condition, description, source })
|
||||||
|
} else {
|
||||||
|
Err(format!("no command in {dsl:?}").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_debug!(Condition |self, w| { write!(w, "*") });
|
||||||
|
|
||||||
|
handle!(TuiIn:|self: App, input|{
|
||||||
|
let mut commands = vec![];
|
||||||
|
for id in self.mode.keys.iter() {
|
||||||
|
if let Some(event_map) = self.config.binds.clone().read().unwrap().get(id.as_ref()) {
|
||||||
|
if let Some(bindings) = event_map.query(input.event()) {
|
||||||
|
for binding in bindings {
|
||||||
|
for command in binding.commands.iter() {
|
||||||
|
if let Some(command) = self.from(command)? as Option<AppCommand> {
|
||||||
|
commands.push(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for command in commands.into_iter() {
|
||||||
|
let result = command.execute(self);
|
||||||
|
match result {
|
||||||
|
Ok(undo) => {
|
||||||
|
self.history.push((command, undo));
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
self.history.push((command, None));
|
||||||
|
return Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub enum Axis { X, Y, Z, I }
|
||||||
|
|
||||||
|
impl<'a> DslNs<'a, AppCommand> for App {}
|
||||||
|
impl<'a> DslNsExprs<'a, AppCommand> for App {}
|
||||||
|
impl<'a> DslNsWords<'a, AppCommand> for App {
|
||||||
|
dsl_words!('a |app| -> AppCommand {
|
||||||
|
"x/inc" => AppCommand::Inc { axis: Axis::X },
|
||||||
|
"x/dec" => AppCommand::Dec { axis: Axis::X },
|
||||||
|
"y/inc" => AppCommand::Inc { axis: Axis::Y },
|
||||||
|
"y/dec" => AppCommand::Dec { axis: Axis::Y },
|
||||||
|
"confirm" => AppCommand::Confirm,
|
||||||
|
"cancel" => AppCommand::Cancel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppCommand { fn default () -> Self { Self::Nop } }
|
||||||
|
|
||||||
|
def_command!(AppCommand: |app: App| {
|
||||||
|
Nop => Ok(None),
|
||||||
|
Confirm => Ok(match &app.dialog {
|
||||||
|
Dialog::Menu(index, items) => {
|
||||||
|
let callback = items.0[*index].1.clone();
|
||||||
|
callback(app)?;
|
||||||
|
None
|
||||||
|
},
|
||||||
|
_ => todo!(),
|
||||||
|
}),
|
||||||
|
Cancel => todo!(), // TODO delegate:
|
||||||
|
Inc { axis: Axis } => Ok(match (&app.dialog, axis) {
|
||||||
|
(Dialog::None, _) => todo!(),
|
||||||
|
(Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_next() }
|
||||||
|
.execute(app)?,
|
||||||
|
_ => todo!()
|
||||||
|
}),
|
||||||
|
Dec { axis: Axis } => Ok(match (&app.dialog, axis) {
|
||||||
|
(Dialog::None, _) => None,
|
||||||
|
(Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() }
|
||||||
|
.execute(app)?,
|
||||||
|
_ => todo!()
|
||||||
|
}),
|
||||||
|
SetDialog { dialog: Dialog } => {
|
||||||
|
swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog })
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//AppCommand => {
|
||||||
|
//("x/inc" /
|
||||||
|
//("stop-all") => todo!(),//app.project.stop_all(),
|
||||||
|
//("enqueue", clip: Option<Arc<RwLock<MidiClip>>>) => todo!(),
|
||||||
|
//("history", delta: isize) => todo!(),
|
||||||
|
//("zoom", zoom: usize) => todo!(),
|
||||||
|
//("select", selection: Selection) => todo!(),
|
||||||
|
//("dialog" / command: DialogCommand) => todo!(),
|
||||||
|
//("project" / command: ArrangementCommand) => todo!(),
|
||||||
|
//("clock" / command: ClockCommand) => todo!(),
|
||||||
|
//("sampler" / command: SamplerCommand) => todo!(),
|
||||||
|
//("pool" / command: PoolCommand) => todo!(),
|
||||||
|
//("edit" / editor: MidiEditCommand) => todo!(),
|
||||||
|
//};
|
||||||
|
|
||||||
|
//DialogCommand;
|
||||||
|
|
||||||
|
//ArrangementCommand;
|
||||||
|
|
||||||
|
//ClockCommand;
|
||||||
|
|
||||||
|
//SamplerCommand;
|
||||||
|
|
||||||
|
//PoolCommand;
|
||||||
|
|
||||||
|
//MidiEditCommand;
|
||||||
|
|
||||||
|
|
||||||
|
//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter));
|
||||||
|
//#[derive(Clone, Debug)]
|
||||||
|
//pub enum DialogCommand {
|
||||||
|
//Open { dialog: Dialog },
|
||||||
|
//Close
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl Command<Option<Dialog>> for DialogCommand {
|
||||||
|
//fn execute (self, state: &mut Option<Dialog>) -> Perhaps<Self> {
|
||||||
|
//match self {
|
||||||
|
//Self::Open { dialog } => {
|
||||||
|
//*state = Some(dialog);
|
||||||
|
//},
|
||||||
|
//Self::Close => {
|
||||||
|
//*state = None;
|
||||||
|
//}
|
||||||
|
//};
|
||||||
|
//Ok(None)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//dsl!(DialogCommand: |self: Dialog, iter|todo!());
|
||||||
|
//Dsl::take(&mut self.dialog, iter));
|
||||||
|
|
||||||
|
//#[tengri_proc::command(Option<Dialog>)]//Nope.
|
||||||
|
//impl DialogCommand {
|
||||||
|
//fn open (dialog: &mut Option<Dialog>, new: Dialog) -> Perhaps<Self> {
|
||||||
|
//*dialog = Some(new);
|
||||||
|
//Ok(None)
|
||||||
|
//}
|
||||||
|
//fn close (dialog: &mut Option<Dialog>) -> Perhaps<Self> {
|
||||||
|
//*dialog = None;
|
||||||
|
//Ok(None)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//dsl_bind!(AppCommand: App {
|
||||||
|
//enqueue = |app, clip: Option<Arc<RwLock<MidiClip>>>| { todo!() };
|
||||||
|
//history = |app, delta: isize| { todo!() };
|
||||||
|
//zoom = |app, zoom: usize| { todo!() };
|
||||||
|
//stop_all = |app| { app.tracks_stop_all(); Ok(None) };
|
||||||
|
////dialog = |app, command: DialogCommand|
|
||||||
|
////Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?);
|
||||||
|
//project = |app, command: ArrangementCommand|
|
||||||
|
//Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?);
|
||||||
|
//clock = |app, command: ClockCommand|
|
||||||
|
//Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c}));
|
||||||
|
//sampler = |app, command: SamplerCommand|
|
||||||
|
//Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command}))
|
||||||
|
//.transpose()?.flatten());
|
||||||
|
//pool = |app, command: PoolCommand| {
|
||||||
|
//let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?;
|
||||||
|
//// update linked editor after pool action
|
||||||
|
//match command {
|
||||||
|
//// autoselect: automatically load selected clip in editor
|
||||||
|
//PoolCommand::Select { .. } |
|
||||||
|
//// autocolor: update color in all places simultaneously
|
||||||
|
//PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => {
|
||||||
|
//let clip = app.pool.clip().clone();
|
||||||
|
//app.editor_mut().map(|editor|editor.set_clip(clip.as_ref()))
|
||||||
|
//},
|
||||||
|
//_ => None
|
||||||
|
//};
|
||||||
|
//Ok(undo)
|
||||||
|
//};
|
||||||
|
//select = |app, selection: Selection| {
|
||||||
|
//*app.project.selection_mut() = selection;
|
||||||
|
////todo!
|
||||||
|
////if let Some(ref mut editor) = app.editor_mut() {
|
||||||
|
////editor.set_clip(match selection {
|
||||||
|
////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app
|
||||||
|
////.project
|
||||||
|
////.scenes.get(scene)
|
||||||
|
////.map(|s|s.clips.get(track))
|
||||||
|
////=>
|
||||||
|
////Some(clip),
|
||||||
|
////_ =>
|
||||||
|
////None
|
||||||
|
////});
|
||||||
|
////}
|
||||||
|
//Ok(None)
|
||||||
|
////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) {
|
||||||
|
////(0, 0) => Self::Select(Selection::Mix),
|
||||||
|
////(t, 0) => Self::Select(Selection::Track(t)),
|
||||||
|
////(0, s) => Self::Select(Selection::Scene(s)),
|
||||||
|
////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) })))
|
||||||
|
//// autoedit: load focused clip in editor.
|
||||||
|
//};
|
||||||
|
////fn color (app: &mut App, theme: ItemTheme) -> Perhaps<Self> {
|
||||||
|
////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme}))
|
||||||
|
////}
|
||||||
|
////fn launch (app: &mut App) -> Perhaps<Self> {
|
||||||
|
////app.project.launch();
|
||||||
|
////Ok(None)
|
||||||
|
////}
|
||||||
|
//toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) };
|
||||||
|
//editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() {
|
||||||
|
//let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?;
|
||||||
|
//// update linked sampler after editor action
|
||||||
|
//app.project.sampler_mut().map(|sampler|match command {
|
||||||
|
//// autoselect: automatically select sample in sampler
|
||||||
|
//MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); },
|
||||||
|
//_ => {}
|
||||||
|
//});
|
||||||
|
//undo
|
||||||
|
//} else {
|
||||||
|
//None
|
||||||
|
//});
|
||||||
|
//});
|
||||||
|
//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter));
|
||||||
|
//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten()));
|
||||||
|
//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter));
|
||||||
|
//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten()));
|
||||||
|
//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter));
|
||||||
65
app/tek_cfg.rs
Normal file
65
app/tek_cfg.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Configuration.
|
||||||
|
///
|
||||||
|
/// Contains mode, view, and bind definitions.
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
pub dirs: BaseDirectories,
|
||||||
|
pub modes: Modes,
|
||||||
|
pub views: Views,
|
||||||
|
pub binds: Binds,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
const CONFIG: &'static str = "tek.edn";
|
||||||
|
const DEFAULTS: &'static str = include_str!("./tek.edn");
|
||||||
|
|
||||||
|
pub fn new (dirs: Option<BaseDirectories>) -> Self {
|
||||||
|
Self {
|
||||||
|
dirs: dirs.unwrap_or_else(||BaseDirectories::with_profile("tek", "v0")),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn init (&mut self) -> Usually<()> {
|
||||||
|
self.init_file(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.load(&dsl))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn init_file (
|
||||||
|
&mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()>
|
||||||
|
) -> Usually<()> {
|
||||||
|
if self.dirs.find_config_file(path).is_none() {
|
||||||
|
println!("Creating {path:?}");
|
||||||
|
std::fs::write(self.dirs.place_config_file(path)?, defaults)?;
|
||||||
|
}
|
||||||
|
Ok(if let Some(path) = self.dirs.find_config_file(path) {
|
||||||
|
println!("Loading {path:?}");
|
||||||
|
let src = std::fs::read_to_string(&path)?;
|
||||||
|
src.as_str().each(move|item|each(self, item))?;
|
||||||
|
} else {
|
||||||
|
return Err(format!("{path}: not found").into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn load (&mut self, dsl: impl Dsl) -> Usually<()> {
|
||||||
|
dsl.each(|item|if let Some(expr) = item.expr()? {
|
||||||
|
let head = expr.head()?;
|
||||||
|
let tail = expr.tail()?;
|
||||||
|
let name = tail.head()?;
|
||||||
|
let body = tail.tail()?;
|
||||||
|
println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default());
|
||||||
|
match head {
|
||||||
|
Some("mode") if let Some(name) = name =>
|
||||||
|
Mode::<Arc<str>>::load_into(&self.modes, &name, &body)?,
|
||||||
|
Some("keys") if let Some(name) = name =>
|
||||||
|
EventMap::<TuiEvent, Arc<str>>::load_into(&self.binds, &name, &body)?,
|
||||||
|
Some("view") if let Some(name) = name => {
|
||||||
|
self.views.write().unwrap().insert(name.into(), body.src()?.unwrap_or_default().into());
|
||||||
|
},
|
||||||
|
_ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into())
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
return Err(format!("Config::load: expected expr, got: {item:?}").into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
132
app/tek_cli.rs
Normal file
132
app/tek_cli.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
pub(crate) use tek::*;
|
||||||
|
pub(crate) use clap::{self, Parser, Subcommand};
|
||||||
|
|
||||||
|
/// Application entrypoint.
|
||||||
|
pub fn main () -> Usually<()> {
|
||||||
|
Cli::parse().run()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
|
||||||
|
pub struct Cli {
|
||||||
|
/// Pre-defined configuration modes.
|
||||||
|
///
|
||||||
|
/// TODO: Replace these with scripted configurations.
|
||||||
|
#[command(subcommand)] mode: Option<LaunchMode>,
|
||||||
|
/// Name of JACK client
|
||||||
|
#[arg(short='n', long)] name: Option<String>,
|
||||||
|
/// Whether to attempt to become transport master
|
||||||
|
#[arg(short='S', long, default_value_t = false)] sync_lead: bool,
|
||||||
|
/// Whether to sync to external transport master
|
||||||
|
#[arg(short='s', long, default_value_t = true)] sync_follow: bool,
|
||||||
|
/// Initial tempo in beats per minute
|
||||||
|
#[arg(short='b', long, default_value = None)] bpm: Option<f64>,
|
||||||
|
/// Whether to include a transport toolbar (default: true)
|
||||||
|
#[arg(short='t', long, default_value_t = true)] show_clock: bool,
|
||||||
|
/// MIDI outs to connect to (multiple instances accepted)
|
||||||
|
#[arg(short='I', long)] midi_from: Vec<String>,
|
||||||
|
/// MIDI outs to connect to (multiple instances accepted)
|
||||||
|
#[arg(short='i', long)] midi_from_re: Vec<String>,
|
||||||
|
/// MIDI ins to connect to (multiple instances accepted)
|
||||||
|
#[arg(short='O', long)] midi_to: Vec<String>,
|
||||||
|
/// MIDI ins to connect to (multiple instances accepted)
|
||||||
|
#[arg(short='o', long)] midi_to_re: Vec<String>,
|
||||||
|
/// Audio outs to connect to left input
|
||||||
|
#[arg(short='l', long)] left_from: Vec<String>,
|
||||||
|
/// Audio outs to connect to right input
|
||||||
|
#[arg(short='r', long)] right_from: Vec<String>,
|
||||||
|
/// Audio ins to connect from left output
|
||||||
|
#[arg(short='L', long)] left_to: Vec<String>,
|
||||||
|
/// Audio ins to connect from right output
|
||||||
|
#[arg(short='R', long)] right_to: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Application modes
|
||||||
|
#[derive(Debug, Clone, Subcommand)]
|
||||||
|
pub enum LaunchMode {
|
||||||
|
/// Create a new session instead of loading the previous one.
|
||||||
|
New,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cli {
|
||||||
|
pub fn run (&self) -> Usually<()> {
|
||||||
|
let name = self.name.as_ref().map_or("tek", |x|x.as_str());
|
||||||
|
let tracks = vec![];
|
||||||
|
let scenes = vec![];
|
||||||
|
let empty = &[] as &[&str];
|
||||||
|
let left_froms = Connect::collect(&self.left_from, empty, empty);
|
||||||
|
let left_tos = Connect::collect(&self.left_to, empty, empty);
|
||||||
|
let right_froms = Connect::collect(&self.right_from, empty, empty);
|
||||||
|
let right_tos = Connect::collect(&self.right_to, empty, empty);
|
||||||
|
let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()];
|
||||||
|
let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()];
|
||||||
|
let mut config = Config::new(None);
|
||||||
|
config.init()?;
|
||||||
|
Tui::new()?.run(&Jack::new_run(&name, move|jack|{
|
||||||
|
let app = App {
|
||||||
|
jack: jack.clone(),
|
||||||
|
color: ItemTheme::random(),
|
||||||
|
dialog: Dialog::welcome(),
|
||||||
|
mode: config.modes.clone().read().unwrap().get(":menu").cloned().unwrap(),
|
||||||
|
config,
|
||||||
|
project: Arrangement {
|
||||||
|
name: Default::default(),
|
||||||
|
color: ItemTheme::random(),
|
||||||
|
jack: jack.clone(),
|
||||||
|
clock: Clock::new(&jack, self.bpm)?,
|
||||||
|
tracks,
|
||||||
|
scenes,
|
||||||
|
selection: Selection::TrackClip { track: 0, scene: 0 },
|
||||||
|
midi_ins: {
|
||||||
|
let mut midi_ins = vec![];
|
||||||
|
for (index, connect) in self.midi_froms().iter().enumerate() {
|
||||||
|
midi_ins.push(jack.midi_in(&format!("M/{index}"), &[connect.clone()])?);
|
||||||
|
}
|
||||||
|
midi_ins
|
||||||
|
},
|
||||||
|
midi_outs: {
|
||||||
|
let mut midi_outs = vec![];
|
||||||
|
for (index, connect) in self.midi_tos().iter().enumerate() {
|
||||||
|
midi_outs.push(jack.midi_out(&format!("{index}/M"), &[connect.clone()])?);
|
||||||
|
};
|
||||||
|
midi_outs
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
jack.sync_lead(self.sync_lead, |mut state|{
|
||||||
|
let clock = app.clock();
|
||||||
|
clock.playhead.update_from_sample(state.position.frame() as f64);
|
||||||
|
state.position.bbt = Some(clock.bbt());
|
||||||
|
state.position
|
||||||
|
})?;
|
||||||
|
jack.sync_follow(self.sync_follow)?;
|
||||||
|
Ok(app)
|
||||||
|
})?)
|
||||||
|
}
|
||||||
|
fn midi_froms (&self) -> Vec<Connect> {
|
||||||
|
Connect::collect(&self.midi_from, &[] as &[&str], &self.midi_from_re)
|
||||||
|
}
|
||||||
|
fn midi_tos (&self) -> Vec<Connect> {
|
||||||
|
Connect::collect(&self.midi_to, &[] as &[&str], &self.midi_to_re)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CLI header
|
||||||
|
const HEADER: &'static str = r#"
|
||||||
|
~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ ~~~~~~ ~ ~~~
|
||||||
|
~~ ║ ~ ╟─╌ ~╟─< ~ v0.3.0, 2025 sum(m)er @ the nose of the cat. ~
|
||||||
|
~~~ ╨ ~ ╙──╜ ╨ ╜ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ ~~ ~~ ~ ~~
|
||||||
|
On first run, Tek will create configuration and state dirs:
|
||||||
|
* [x] ~/.config/tek - config
|
||||||
|
* [ ] ~/.local/share/tek - projects
|
||||||
|
* [ ] ~/.local/lib/tek - plugins
|
||||||
|
* [ ] ~/.cache/tek - cache
|
||||||
|
~"#;
|
||||||
|
|
||||||
|
#[cfg(test)] #[test] fn test_cli () {
|
||||||
|
use clap::CommandFactory;
|
||||||
|
Cli::command().debug_assert();
|
||||||
|
//let jack = Jack::default();
|
||||||
|
}
|
||||||
38
app/tek_deps.rs
Normal file
38
app/tek_deps.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
pub(crate) use ::{
|
||||||
|
tek_device::{*, tek_engine::*},
|
||||||
|
tengri::{
|
||||||
|
Usually, Perhaps, Has, MaybeHas, has, maybe_has, impl_debug, from,
|
||||||
|
wrap_inc, wrap_dec,
|
||||||
|
dsl::*,
|
||||||
|
input::*,
|
||||||
|
output::*,
|
||||||
|
tui::{
|
||||||
|
*,
|
||||||
|
ratatui::{
|
||||||
|
self,
|
||||||
|
prelude::{Rect, Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}},
|
||||||
|
widgets::{Widget, canvas::{Canvas, Line}},
|
||||||
|
},
|
||||||
|
crossterm::{
|
||||||
|
self,
|
||||||
|
event::{Event, KeyCode::{self, *}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed},
|
||||||
|
error::Error,
|
||||||
|
collections::BTreeMap,
|
||||||
|
fmt::Write,
|
||||||
|
cmp::Ord,
|
||||||
|
ffi::OsString,
|
||||||
|
fmt::{Debug, Formatter},
|
||||||
|
fs::File,
|
||||||
|
ops::{Add, Sub, Mul, Div, Rem},
|
||||||
|
thread::JoinHandle
|
||||||
|
},
|
||||||
|
xdg::BaseDirectories,
|
||||||
|
atomic_float::*
|
||||||
|
};
|
||||||
58
app/tek_mode.rs
Normal file
58
app/tek_mode.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub type Modes = Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>;
|
||||||
|
|
||||||
|
/// A set of currently active view and keys definitions,
|
||||||
|
/// with optional name and description.
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct Mode<D: Dsl + Ord> {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub name: Vec<D>,
|
||||||
|
pub info: Vec<D>,
|
||||||
|
pub view: Vec<D>,
|
||||||
|
pub keys: Vec<D>,
|
||||||
|
pub modes: Modes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D: Dsl + Ord> Draw<TuiOut> for Mode<D> {
|
||||||
|
fn draw (&self, to: &mut TuiOut) {
|
||||||
|
self.content().draw(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mode<Arc<str>> {
|
||||||
|
|
||||||
|
pub fn load_into (modes: &Modes, name: &impl AsRef<str>, body: &impl Dsl) -> Usually<()> {
|
||||||
|
let mut mode = Self::default();
|
||||||
|
println!("Mode::load_into: {}: {body:?}", name.as_ref());
|
||||||
|
body.each(|item|mode.load_one(item))?;
|
||||||
|
modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_one (&mut self, dsl: impl Dsl) -> Usually<()> {
|
||||||
|
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
|
||||||
|
println!("Mode::load_one: {head} {:?}", expr.tail());
|
||||||
|
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
||||||
|
match head {
|
||||||
|
"name" => self.name.push(tail.into()),
|
||||||
|
"info" => self.info.push(tail.into()),
|
||||||
|
"view" => self.view.push(tail.into()),
|
||||||
|
"keys" => tail.each(|expr|{self.keys.push(expr.trim().into()); Ok(())})?,
|
||||||
|
"mode" => if let Some(id) = tail.head()? {
|
||||||
|
Self::load_into(&self.modes, &id, &tail.tail())?;
|
||||||
|
} else {
|
||||||
|
return Err(format!("Mode::load_one: self: incomplete: {expr:?}").into());
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return Err(format!("Mode::load_one: unexpected expr: {head:?} {tail:?}").into())
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if let Ok(Some(word)) = dsl.word() {
|
||||||
|
self.view.push(word.into());
|
||||||
|
} else {
|
||||||
|
return Err(format!("Mode::load_one: unexpected: {dsl:?}").into());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
103
app/tek_test.rs
Normal file
103
app/tek_test.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[cfg(test)] #[test] fn test_app () -> Usually<()> {
|
||||||
|
let mut app = App::default();
|
||||||
|
let _ = app.scene_add(None, None)?;
|
||||||
|
let _ = app.update_clock();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)] #[test] fn test_track () -> Usually<()> {
|
||||||
|
let track = Track::default();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)] #[test] fn test_scene () -> Usually<()> {
|
||||||
|
let scene = Scene::default();
|
||||||
|
let _ = scene.pulses();
|
||||||
|
let _ = scene.is_playing(&[]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)] #[test] fn test_view_layout () {
|
||||||
|
let _ = button_play_pause(true);
|
||||||
|
let _ = button_2("", "", true);
|
||||||
|
let _ = button_2("", "", false);
|
||||||
|
let _ = button_3("", "", "", true);
|
||||||
|
let _ = button_3("", "", "", false);
|
||||||
|
//let _ = heading("", "", 0, "", true);
|
||||||
|
//let _ = heading("", "", 0, "", false);
|
||||||
|
let _ = wrap(Reset, Reset, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)] mod test_view_meter {
|
||||||
|
use super::*;
|
||||||
|
use proptest::prelude::*;
|
||||||
|
#[test] fn test_view_meter () {
|
||||||
|
let _ = view_meter("", 0.0);
|
||||||
|
let _ = view_meters(&[0.0, 0.0]);
|
||||||
|
}
|
||||||
|
proptest! {
|
||||||
|
#[test] fn proptest_view_meter (
|
||||||
|
label in "\\PC*", value in f32::MIN..f32::MAX
|
||||||
|
) {
|
||||||
|
let _ = view_meter(&label, value);
|
||||||
|
}
|
||||||
|
#[test] fn proptest_view_meters (
|
||||||
|
value1 in f32::MIN..f32::MAX,
|
||||||
|
value2 in f32::MIN..f32::MAX
|
||||||
|
) {
|
||||||
|
let _ = view_meters(&[value1, value2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)] #[test] fn test_view_iter () {
|
||||||
|
let mut app = App::default();
|
||||||
|
app.project.editor = Some(Default::default());
|
||||||
|
//let _: Vec<_> = app.project.inputs_with_sizes().collect();
|
||||||
|
//let _: Vec<_> = app.project.outputs_with_sizes().collect();
|
||||||
|
let _: Vec<_> = app.project.tracks_with_sizes().collect();
|
||||||
|
//let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect();
|
||||||
|
//let _: Vec<_> = app.scenes_with_colors(true, 10).collect();
|
||||||
|
//let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)] #[test] fn test_view_sizes () {
|
||||||
|
let app = App::default();
|
||||||
|
let _ = app.project.w();
|
||||||
|
//let _ = app.project.w_sidebar();
|
||||||
|
//let _ = app.project.w_tracks_area();
|
||||||
|
let _ = app.project.h();
|
||||||
|
//let _ = app.project.h_tracks_area();
|
||||||
|
//let _ = app.project.h_inputs();
|
||||||
|
//let _ = app.project.h_outputs();
|
||||||
|
let _ = app.project.h_scenes();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)] #[test] fn test_midi_edit () {
|
||||||
|
let _editor = MidiEditor::default();
|
||||||
|
let mut editor = MidiEditor {
|
||||||
|
mode: PianoHorizontal::new(Some(&Arc::new(RwLock::new(MidiClip::stop_all())))),
|
||||||
|
size: Default::default(),
|
||||||
|
//keys: Default::default(),
|
||||||
|
};
|
||||||
|
let _ = editor.put_note(true);
|
||||||
|
let _ = editor.put_note(false);
|
||||||
|
let _ = editor.clip_status();
|
||||||
|
let _ = editor.edit_status();
|
||||||
|
struct TestEditorHost(Option<MidiEditor>);
|
||||||
|
has!(Option<MidiEditor>: |self: TestEditorHost|self.0);
|
||||||
|
//has_editor!(|self: TestEditorHost|{
|
||||||
|
//editor = self.0;
|
||||||
|
//editor_w = 0;
|
||||||
|
//editor_h = 0;
|
||||||
|
//is_editing = false;
|
||||||
|
//});
|
||||||
|
let mut host = TestEditorHost(Some(editor));
|
||||||
|
let _ = host.editor();
|
||||||
|
let _ = host.editor_mut();
|
||||||
|
let _ = host.is_editing();
|
||||||
|
let _ = host.editor_w();
|
||||||
|
let _ = host.editor_h();
|
||||||
|
}
|
||||||
353
app/tek_view.rs
Normal file
353
app/tek_view.rs
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
|
||||||
|
|
||||||
|
impl Draw<TuiOut> for App {
|
||||||
|
fn draw (&self, to: &mut TuiOut) {
|
||||||
|
for (index, dsl) in self.mode.view.iter().enumerate() {
|
||||||
|
if let Err(e) = self.view(to, dsl) {
|
||||||
|
panic!("render #{index} failed ({e}): {dsl}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View<TuiOut, ()> for App {
|
||||||
|
fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl DslExpr) -> Usually<()> {
|
||||||
|
if evaluate_output_expression(self, to, expr)?
|
||||||
|
|| evaluate_output_expression_tui(self, to, expr)? {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("App::view_expr: unexpected: {expr:?}").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl DslExpr) -> Usually<()> {
|
||||||
|
let mut frags = dsl.src()?.unwrap().split("/");
|
||||||
|
match frags.next() {
|
||||||
|
Some(":logo") => to.place(&view_logo()),
|
||||||
|
Some(":status") => to.place(&Fixed::Y(1, "TODO: Status Bar")),
|
||||||
|
Some(":meters") => match frags.next() {
|
||||||
|
Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Input Meters")))),
|
||||||
|
Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Output Meters")))),
|
||||||
|
_ => panic!()
|
||||||
|
},
|
||||||
|
Some(":tracks") => match frags.next() {
|
||||||
|
None => to.place(&"TODO tracks"),
|
||||||
|
Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Names")))),
|
||||||
|
Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Inputs")))),
|
||||||
|
Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Devices")))),
|
||||||
|
Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Outputs")))),
|
||||||
|
_ => panic!()
|
||||||
|
},
|
||||||
|
Some(":scenes") => match frags.next() {
|
||||||
|
None => to.place(&"TODO scenes"),
|
||||||
|
Some(":scenes/names") => to.place(&"TODO Scene Names"),
|
||||||
|
_ => panic!()
|
||||||
|
},
|
||||||
|
Some(":editor") => to.place(&"TODO Editor"),
|
||||||
|
Some(":dialog") => match frags.next() {
|
||||||
|
Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog {
|
||||||
|
let items = items.clone();
|
||||||
|
let selected = selected;
|
||||||
|
Some(Fill::XY(Thunk::new(move|to: &mut TuiOut|{
|
||||||
|
for (index, MenuItem(item, _)) in items.0.iter().enumerate() {
|
||||||
|
to.place(&Push::Y((2 * index) as u16,
|
||||||
|
Tui::fg_bg(
|
||||||
|
if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) },
|
||||||
|
if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) },
|
||||||
|
Fixed::Y(2, Align::n(Fill::X(item)))
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}),
|
||||||
|
_ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"),
|
||||||
|
},
|
||||||
|
Some(":templates") => to.place(&{
|
||||||
|
let modes = self.config.modes.clone();
|
||||||
|
let height = (modes.read().unwrap().len() * 2) as u16;
|
||||||
|
Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut TuiOut|{
|
||||||
|
for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() {
|
||||||
|
let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) };
|
||||||
|
let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
|
||||||
|
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
|
||||||
|
let fg1 = Rgb(224, 192, 128);
|
||||||
|
let fg2 = Rgb(224, 128, 32);
|
||||||
|
let field_name = Fill::X(Align::w(Tui::fg(fg1, name)));
|
||||||
|
let field_id = Fill::X(Align::e(Tui::fg(fg2, id)));
|
||||||
|
let field_info = Fill::X(Align::w(info));
|
||||||
|
to.place(&Push::Y((2 * index) as u16,
|
||||||
|
Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s(
|
||||||
|
Bsp::a(field_name, field_id), field_info))))));
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
}),
|
||||||
|
Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut TuiOut|{
|
||||||
|
let fg = Rgb(224, 192, 128);
|
||||||
|
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
|
||||||
|
let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
|
||||||
|
to.place(&Push::Y((2 * index) as u16,
|
||||||
|
&Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name)))))));
|
||||||
|
}
|
||||||
|
})))),
|
||||||
|
Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(ItemColor::default(),
|
||||||
|
match self.dialog.browser_target().unwrap() {
|
||||||
|
BrowseTarget::SaveProject => "Save project:",
|
||||||
|
BrowseTarget::LoadProject => "Load project:",
|
||||||
|
BrowseTarget::ImportSample(_) => "Import sample:",
|
||||||
|
BrowseTarget::ExportSample(_) => "Export sample:",
|
||||||
|
BrowseTarget::ImportClip(_) => "Import clip:",
|
||||||
|
BrowseTarget::ExportClip(_) => "Export clip:",
|
||||||
|
}, Shrink::X(3, Fixed::Y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))),
|
||||||
|
Some(":device") => {
|
||||||
|
let selected = self.dialog.device_kind().unwrap();
|
||||||
|
to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1,
|
||||||
|
move||device_kinds().iter(),
|
||||||
|
move|_label: &&'static str, i|{
|
||||||
|
let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
|
||||||
|
let lb = if i == selected { "[ " } else { " " };
|
||||||
|
let rb = if i == selected { " ]" } else { " " };
|
||||||
|
Fill::X(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) })))
|
||||||
|
},
|
||||||
|
Some(":debug") => to.place(&Fixed::Y(1, format!("[{:?}]", to.area()))),
|
||||||
|
Some(_) => {
|
||||||
|
let views = self.config.views.read().unwrap();
|
||||||
|
if let Some(dsl) = views.get(dsl.src()?.unwrap()) {
|
||||||
|
let dsl = dsl.clone();
|
||||||
|
std::mem::drop(views);
|
||||||
|
self.view(to, &dsl)?
|
||||||
|
} else {
|
||||||
|
unimplemented!("{dsl:?}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_logo () -> impl Content<TuiOut> {
|
||||||
|
Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{
|
||||||
|
Fixed::Y(1, ""),
|
||||||
|
Fixed::Y(1, ""),
|
||||||
|
Fixed::Y(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"),
|
||||||
|
Fixed::Y(1, Bsp::e("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))),
|
||||||
|
Fixed::Y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"),
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
//pub fn view_nil (_: &App) -> TuiCb {
|
||||||
|
//|to|to.place(&Fill::XY("·"))
|
||||||
|
//}
|
||||||
|
|
||||||
|
//Bsp::s("",
|
||||||
|
//Map::south(1,
|
||||||
|
//move||app.config.binds.layers.iter()
|
||||||
|
//.filter_map(|a|(a.0)(app).then_some(a.1))
|
||||||
|
//.flat_map(|a|a)
|
||||||
|
//.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None })
|
||||||
|
//.skip(offset)
|
||||||
|
//.take(20),
|
||||||
|
//|mut b,i|Fixed::X(60, Align::w(Bsp::e("(", Bsp::e(
|
||||||
|
//b.next().map(|t|Fixed::X(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))),
|
||||||
|
//Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))),
|
||||||
|
|
||||||
|
//Dialog::Browse(BrowseTarget::Load, browser) => {
|
||||||
|
//"bobcat".boxed()
|
||||||
|
////Bsp::s(
|
||||||
|
////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e(
|
||||||
|
////Tui::bold(true, " Load project: "),
|
||||||
|
////Shrink::X(3, Fixed::Y(1, RepeatH("🭻"))))))),
|
||||||
|
////Outer(true, Style::default().fg(Tui::g(96)))
|
||||||
|
////.enclose(Fill::XY(browser)))
|
||||||
|
//},
|
||||||
|
//Dialog::Browse(BrowseTarget::Export, browser) => {
|
||||||
|
//"bobcat".boxed()
|
||||||
|
////Bsp::s(
|
||||||
|
////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e(
|
||||||
|
////Tui::bold(true, " Export: "),
|
||||||
|
////Shrink::X(3, Fixed::Y(1, RepeatH("🭻"))))))),
|
||||||
|
////Outer(true, Style::default().fg(Tui::g(96)))
|
||||||
|
////.enclose(Fill::XY(browser)))
|
||||||
|
//},
|
||||||
|
//Dialog::Browse(BrowseTarget::Import, browser) => {
|
||||||
|
//"bobcat".boxed()
|
||||||
|
////Bsp::s(
|
||||||
|
////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e(
|
||||||
|
////Tui::bold(true, " Import: "),
|
||||||
|
////Shrink::X(3, Fixed::Y(1, RepeatH("🭻"))))))),
|
||||||
|
////Outer(true, Style::default().fg(Tui::g(96)))
|
||||||
|
////.enclose(Fill::XY(browser)))
|
||||||
|
//},
|
||||||
|
//
|
||||||
|
//pub fn view_history (&self) -> impl Content<TuiOut> {
|
||||||
|
//Fixed::Y(1, Fill::X(Align::w(FieldH(self.color,
|
||||||
|
//format!("History ({})", self.history.len()),
|
||||||
|
//self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))))
|
||||||
|
//}
|
||||||
|
//pub fn view_status_h2 (&self) -> impl Content<TuiOut> {
|
||||||
|
//self.update_clock();
|
||||||
|
//let theme = self.color;
|
||||||
|
//let clock = self.clock();
|
||||||
|
//let playing = clock.is_rolling();
|
||||||
|
//let cache = clock.view_cache.clone();
|
||||||
|
////let selection = self.selection().describe(self.tracks(), self.scenes());
|
||||||
|
//let hist_len = self.history.len();
|
||||||
|
//let hist_last = self.history.last();
|
||||||
|
//Fixed::Y(2, Stack::east(move|add: &mut dyn FnMut(&dyn Draw<TuiOut>)|{
|
||||||
|
//add(&Fixed::X(5, Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
|
||||||
|
//Either::new(false, // TODO
|
||||||
|
//Thunk::new(move||Fixed::X(9, Either::new(playing,
|
||||||
|
//Tui::fg(Rgb(0, 255, 0), " PLAYING "),
|
||||||
|
//Tui::fg(Rgb(255, 128, 0), " STOPPED ")))
|
||||||
|
//),
|
||||||
|
//Thunk::new(move||Fixed::X(5, Either::new(playing,
|
||||||
|
//Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
|
||||||
|
//Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))
|
||||||
|
//)
|
||||||
|
//)
|
||||||
|
//)));
|
||||||
|
//add(&" ");
|
||||||
|
//{
|
||||||
|
//let cache = cache.read().unwrap();
|
||||||
|
//add(&Fixed::X(15, Align::w(Bsp::s(
|
||||||
|
//FieldH(theme, "Beat", cache.beat.view.clone()),
|
||||||
|
//FieldH(theme, "Time", cache.time.view.clone()),
|
||||||
|
//))));
|
||||||
|
//add(&Fixed::X(13, Align::w(Bsp::s(
|
||||||
|
//Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))),
|
||||||
|
//Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))),
|
||||||
|
//))));
|
||||||
|
//add(&Fixed::X(12, Align::w(Bsp::s(
|
||||||
|
//Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))),
|
||||||
|
//Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))),
|
||||||
|
//))));
|
||||||
|
////add(&Bsp::s(
|
||||||
|
//////Fill::X(Align::w(FieldH(theme, "Selected", Align::w(selection)))),
|
||||||
|
////Fill::X(Align::w(FieldH(theme, format!("History ({})", hist_len),
|
||||||
|
////hist_last.map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))),
|
||||||
|
////""
|
||||||
|
////));
|
||||||
|
//////if let Some(last) = self.history.last() {
|
||||||
|
//////add(&FieldV(theme, format!("History ({})", self.history.len()),
|
||||||
|
//////Fill::X(Align::w(format!("{:?}", last.0)))));
|
||||||
|
//////}
|
||||||
|
//}
|
||||||
|
//}))
|
||||||
|
//}
|
||||||
|
//pub fn view_status_v (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.update_clock();
|
||||||
|
//let cache = self.project.clock.view_cache.read().unwrap();
|
||||||
|
//let theme = self.color;
|
||||||
|
//let playing = self.clock().is_rolling();
|
||||||
|
//Tui::bg(theme.darker.rgb, Fixed::XY(20, 5, Outer(true, Style::default().fg(Tui::g(96))).enclose(
|
||||||
|
//col!(
|
||||||
|
//Fill::X(Align::w(Bsp::e(
|
||||||
|
//Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
|
||||||
|
//Either::new(false, // TODO
|
||||||
|
//Thunk::new(move||Fixed::X(9, Either::new(playing,
|
||||||
|
//Tui::fg(Rgb(0, 255, 0), " PLAYING "),
|
||||||
|
//Tui::fg(Rgb(255, 128, 0), " STOPPED ")))
|
||||||
|
//),
|
||||||
|
//Thunk::new(move||Fixed::X(5, Either::new(playing,
|
||||||
|
//Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
|
||||||
|
//Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))
|
||||||
|
//)
|
||||||
|
//)
|
||||||
|
//)),
|
||||||
|
//Bsp::s(
|
||||||
|
//FieldH(theme, "Beat", cache.beat.view.clone()),
|
||||||
|
//FieldH(theme, "Time", cache.time.view.clone()),
|
||||||
|
//),
|
||||||
|
//))),
|
||||||
|
//Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))),
|
||||||
|
//Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))),
|
||||||
|
//Fill::X(Align::w(FieldH(theme, "Buf", Bsp::e(cache.buf.view.clone(), Bsp::e(" = ", cache.lat.view.clone()))))),
|
||||||
|
//))))
|
||||||
|
//}
|
||||||
|
//pub fn view_status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.update_clock();
|
||||||
|
//let cache = self.project.clock.view_cache.read().unwrap();
|
||||||
|
//view_status(Some(self.project.selection.describe(self.tracks(), self.scenes())),
|
||||||
|
//cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone())
|
||||||
|
//}
|
||||||
|
//pub fn view_transport (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.update_clock();
|
||||||
|
//let cache = self.project.clock.view_cache.read().unwrap();
|
||||||
|
//view_transport(self.project.clock.is_rolling(),
|
||||||
|
//cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone())
|
||||||
|
//}
|
||||||
|
//pub fn view_editor (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//let bg = self.editor()
|
||||||
|
//.and_then(|editor|editor.clip().clone())
|
||||||
|
//.map(|clip|clip.read().unwrap().color.darker)
|
||||||
|
//.unwrap_or(self.color.darker);
|
||||||
|
//Fill::XY(Tui::bg(bg.rgb, self.editor()))
|
||||||
|
//}
|
||||||
|
//pub fn view_editor_status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.editor().map(|e|Fixed::X(20, Outer(true, Style::default().fg(Tui::g(96))).enclose(
|
||||||
|
//Fill::Y(Align::n(Bsp::s(e.clip_status(), e.edit_status()))))))
|
||||||
|
//}
|
||||||
|
//pub fn view_midi_ins_status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.project.view_midi_ins_status(self.color)
|
||||||
|
//}
|
||||||
|
//pub fn view_midi_outs_status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.project.view_midi_outs_status(self.color)
|
||||||
|
//}
|
||||||
|
//pub fn view_audio_ins_status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.project.view_audio_ins_status(self.color)
|
||||||
|
//}
|
||||||
|
//pub fn view_audio_outs_status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.project.view_audio_outs_status(self.color)
|
||||||
|
//}
|
||||||
|
//pub fn view_scenes (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//Bsp::e(
|
||||||
|
//Fixed::X(20, Align::nw(self.project.view_scenes_names())),
|
||||||
|
//self.project.view_scenes_clips(),
|
||||||
|
//)
|
||||||
|
//}
|
||||||
|
//pub fn view_scenes_names (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.project.view_scenes_names()
|
||||||
|
//}
|
||||||
|
//pub fn view_scenes_clips (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.project.view_scenes_clips()
|
||||||
|
//}
|
||||||
|
//pub fn view_tracks_inputs <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||||
|
//Fixed::Y(1 + self.project.midi_ins.len() as u16,
|
||||||
|
//self.project.view_inputs(self.color))
|
||||||
|
//}
|
||||||
|
//pub fn view_tracks_outputs <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||||
|
//self.project.view_outputs(self.color)
|
||||||
|
//}
|
||||||
|
//pub fn view_tracks_devices <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||||
|
//Fixed::Y(4, self.project.view_track_devices(self.color))
|
||||||
|
//}
|
||||||
|
//pub fn view_tracks_names <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||||
|
//Fixed::Y(2, self.project.view_track_names(self.color))
|
||||||
|
//}
|
||||||
|
//pub fn view_pool (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//Fixed::X(20, Bsp::s(
|
||||||
|
//Fill::X(Align::w(FieldH(self.color, "Clip pool:", ""))),
|
||||||
|
//Fill::Y(Align::n(Tui::bg(Rgb(0, 0, 0), Outer(true, Style::default().fg(Tui::g(96)))
|
||||||
|
//.enclose(PoolView(&self.pool)))))))
|
||||||
|
//}
|
||||||
|
//pub fn view_samples_keys (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap()))
|
||||||
|
//}
|
||||||
|
//pub fn view_samples_grid (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.project.sampler().map(|s|s.view_grid())
|
||||||
|
//}
|
||||||
|
//pub fn view_sample_viewer (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos()))
|
||||||
|
//}
|
||||||
|
//pub fn view_sample_info (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos()))
|
||||||
|
//}
|
||||||
|
//pub fn view_sample_status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
//self.project.sampler().map(|s|Outer(true, Style::default().fg(Tui::g(96))).enclose(
|
||||||
|
//Fill::Y(Align::n(s.view_sample_status(self.editor().unwrap().get_note_pos())))))
|
||||||
|
//}
|
||||||
|
////let options = ||["Projects", "Settings", "Help", "Quit"].iter();
|
||||||
|
////let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a));
|
||||||
|
////Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option)))
|
||||||
64
bacon.toml
Normal file
64
bacon.toml
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# https://dystroy.org/bacon/config/
|
||||||
|
default_job = "test"
|
||||||
|
env.CARGO_TERM_COLOR = "always"
|
||||||
|
[keybindings]
|
||||||
|
c = "job:check"
|
||||||
|
t = "job:test"
|
||||||
|
n = "job:nextest"
|
||||||
|
l = "job:clippy"
|
||||||
|
[jobs]
|
||||||
|
[jobs.check]
|
||||||
|
command = ["cargo", "check"]
|
||||||
|
need_stdout = false
|
||||||
|
watch = ["deps", "engine", "device", "app"]
|
||||||
|
[jobs.check-all]
|
||||||
|
command = ["cargo", "check", "--all-targets"]
|
||||||
|
need_stdout = false
|
||||||
|
watch = ["deps", "engine", "device", "app"]
|
||||||
|
[jobs.clippy]
|
||||||
|
command = ["cargo", "clippy"]
|
||||||
|
need_stdout = false
|
||||||
|
watch = ["deps", "engine", "device", "app"]
|
||||||
|
[jobs.clippy-all]
|
||||||
|
command = ["cargo", "clippy", "--all-targets"]
|
||||||
|
need_stdout = false
|
||||||
|
watch = ["deps", "engine", "device", "app"]
|
||||||
|
[jobs.test]
|
||||||
|
command = ["cargo", "test", "--workspace", "--exclude", "jack"]
|
||||||
|
need_stdout = true
|
||||||
|
watch = ["deps", "engine", "device", "app"]
|
||||||
|
[jobs.nextest]
|
||||||
|
watch = ["deps", "engine", "device", "app"]
|
||||||
|
command = [
|
||||||
|
"cargo", "nextest", "run",
|
||||||
|
"--hide-progress-bar", "--failure-output", "final"
|
||||||
|
]
|
||||||
|
need_stdout = true
|
||||||
|
analyzer = "nextest"
|
||||||
|
[jobs.doc]
|
||||||
|
command = ["cargo", "doc", "--no-deps"]
|
||||||
|
need_stdout = false
|
||||||
|
[jobs.doc-open]
|
||||||
|
command = ["cargo", "doc", "--no-deps", "--open"]
|
||||||
|
need_stdout = false
|
||||||
|
on_success = "back" # so that we don't open the browser at each change
|
||||||
|
[jobs.run]
|
||||||
|
command = [
|
||||||
|
"cargo", "run",
|
||||||
|
# put launch parameters for your program behind a `--` separator
|
||||||
|
]
|
||||||
|
need_stdout = true
|
||||||
|
allow_warnings = true
|
||||||
|
background = true
|
||||||
|
[jobs.run-long]
|
||||||
|
watch = ["deps", "engine", "device", "app"]
|
||||||
|
command = [ "cargo", "run", ]
|
||||||
|
need_stdout = true
|
||||||
|
allow_warnings = true
|
||||||
|
background = false
|
||||||
|
on_change_strategy = "kill_then_restart"
|
||||||
|
[jobs.ex]
|
||||||
|
watch = ["deps", "engine", "device", "app"]
|
||||||
|
command = ["cargo", "run", "--example"]
|
||||||
|
need_stdout = true
|
||||||
|
allow_warnings = true
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
include!("./lib.rs");
|
|
||||||
use tek::ArrangerTui;
|
|
||||||
pub fn main () -> Usually<()> { ArrangerCli::parse().run() }
|
|
||||||
|
|
||||||
/// Launches an interactive MIDI arranger.
|
|
||||||
#[derive(Debug, Parser)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
pub struct ArrangerCli {
|
|
||||||
/// Name of JACK client
|
|
||||||
#[arg(short, long)]
|
|
||||||
name: Option<String>,
|
|
||||||
/// Whether to include a transport toolbar (default: true)
|
|
||||||
#[arg(short, long, default_value_t = true)]
|
|
||||||
transport: bool,
|
|
||||||
/// Number of tracks
|
|
||||||
#[arg(short = 'x', long, default_value_t = 4)]
|
|
||||||
tracks: usize,
|
|
||||||
/// Number of scenes
|
|
||||||
#[arg(short, long, default_value_t = 8)]
|
|
||||||
scenes: usize,
|
|
||||||
/// MIDI outs to connect each track to.
|
|
||||||
#[arg(short='i', long)]
|
|
||||||
midi_from: Vec<String>,
|
|
||||||
/// MIDI ins to connect each track to.
|
|
||||||
#[arg(short='o', long)]
|
|
||||||
midi_to: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ArrangerCli {
|
|
||||||
/// Run the arranger TUI from CLI arguments.
|
|
||||||
fn run (&self) -> Usually<()> {
|
|
||||||
let name = self.name.as_deref().unwrap_or("tek_arranger");
|
|
||||||
let engine = Tui::new()?;
|
|
||||||
let state = JackConnection::new(name)?.activate_with(|jack|{
|
|
||||||
let mut app = ArrangerTui::try_from(jack)?;
|
|
||||||
let jack = jack.read().unwrap();
|
|
||||||
app.color = ItemPalette::random();
|
|
||||||
add_tracks(&jack, &mut app, self)?;
|
|
||||||
add_scenes(&mut app, self.scenes)?;
|
|
||||||
Ok(app)
|
|
||||||
})?;
|
|
||||||
engine.run(&state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_tracks (jack: &JackConnection, app: &mut ArrangerTui, cli: &ArrangerCli) -> Usually<()> {
|
|
||||||
let n = cli.tracks;
|
|
||||||
let track_color_1 = ItemColor::random();
|
|
||||||
let track_color_2 = ItemColor::random();
|
|
||||||
for i in 0..n {
|
|
||||||
let track = app.track_add(None, Some(
|
|
||||||
track_color_1.mix(track_color_2, i as f32 / n as f32).into()
|
|
||||||
))?;
|
|
||||||
track.width = 8;
|
|
||||||
let name = track.name.read().unwrap();
|
|
||||||
track.player.midi_ins.push(
|
|
||||||
jack.register_port(&format!("{}I", &name), MidiIn::default())?
|
|
||||||
);
|
|
||||||
track.player.midi_outs.push(
|
|
||||||
jack.register_port(&format!("{}O", &name), MidiOut::default())?
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for connection in cli.midi_from.iter() {
|
|
||||||
let mut split = connection.split("=");
|
|
||||||
let number = split.next().unwrap().trim();
|
|
||||||
if let Ok(track) = number.parse::<usize>() {
|
|
||||||
if track < 1 {
|
|
||||||
panic!("Tracks are zero-indexed")
|
|
||||||
}
|
|
||||||
if track > n {
|
|
||||||
panic!("Tried to connect track {track} or {n}. Pass -t {track} to increase number of tracks.")
|
|
||||||
}
|
|
||||||
if let Some(port) = split.next() {
|
|
||||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
|
||||||
jack.client().connect_ports(port, &app.tracks[track-1].player.midi_ins[0])?;
|
|
||||||
} else {
|
|
||||||
panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic!("Failed to parse track number: {number}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for connection in cli.midi_to.iter() {
|
|
||||||
let mut split = connection.split("=");
|
|
||||||
let number = split.next().unwrap().trim();
|
|
||||||
if let Ok(track) = number.parse::<usize>() {
|
|
||||||
if track < 1 {
|
|
||||||
panic!("Tracks are zero-indexed")
|
|
||||||
}
|
|
||||||
if track > n {
|
|
||||||
panic!("Tried to connect track {track} or {n}. Pass -t {track} to increase number of tracks.")
|
|
||||||
}
|
|
||||||
if let Some(port) = split.next() {
|
|
||||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
|
||||||
jack.client().connect_ports(&app.tracks[track-1].player.midi_outs[0], port)?;
|
|
||||||
} else {
|
|
||||||
panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic!("Failed to parse track number: {number}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_scenes (app: &mut ArrangerTui, n: usize) -> Usually<()> {
|
|
||||||
let scene_color_1 = ItemColor::random();
|
|
||||||
let scene_color_2 = ItemColor::random();
|
|
||||||
for i in 0..n {
|
|
||||||
let _scene = app.scene_add(None, Some(
|
|
||||||
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test] fn verify_arranger_cli () {
|
|
||||||
use clap::CommandFactory;
|
|
||||||
ArrangerCli::command().debug_assert();
|
|
||||||
}
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
include!("./lib.rs");
|
|
||||||
pub fn main () -> Usually<()> { GrooveboxCli::parse().run() }
|
|
||||||
#[derive(Debug, Parser)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
pub struct GrooveboxCli {
|
|
||||||
/// Name of JACK client
|
|
||||||
#[arg(short, long)]
|
|
||||||
name: Option<String>,
|
|
||||||
/// Whether to include a transport toolbar (default: true)
|
|
||||||
#[arg(short, long, default_value_t = true)]
|
|
||||||
transport: bool,
|
|
||||||
/// Whether to attempt to become transport master
|
|
||||||
#[arg(short='S', long, default_value_t = false)]
|
|
||||||
sync_lead: bool,
|
|
||||||
/// Whether to attempt to become transport master
|
|
||||||
#[arg(short='s', long, default_value_t = true)]
|
|
||||||
sync_follow: bool,
|
|
||||||
/// Default BPM
|
|
||||||
#[arg(short='b', long, default_value = None)]
|
|
||||||
bpm: Option<f64>,
|
|
||||||
/// MIDI outs to connect to MIDI input
|
|
||||||
#[arg(short='i', long)]
|
|
||||||
midi_from: Vec<String>,
|
|
||||||
/// MIDI ins to connect from MIDI output
|
|
||||||
#[arg(short='o', long)]
|
|
||||||
midi_to: Vec<String>,
|
|
||||||
/// Audio outs to connect to left input
|
|
||||||
#[arg(short='l', long)]
|
|
||||||
l_from: Vec<String>,
|
|
||||||
/// Audio outs to connect to right input
|
|
||||||
#[arg(short='r', long)]
|
|
||||||
r_from: Vec<String>,
|
|
||||||
/// Audio ins to connect from left output
|
|
||||||
#[arg(short='L', long)]
|
|
||||||
l_to: Vec<String>,
|
|
||||||
/// Audio ins to connect from right output
|
|
||||||
#[arg(short='R', long)]
|
|
||||||
r_to: Vec<String>,
|
|
||||||
}
|
|
||||||
impl GrooveboxCli {
|
|
||||||
fn run (&self) -> Usually<()> {
|
|
||||||
let name = self.name.as_deref().unwrap_or("tek_groovebox");
|
|
||||||
let engine = Tui::new()?;
|
|
||||||
let state = JackConnection::new(name)?.activate_with(|jack|{
|
|
||||||
let app = tek::Groovebox::new(
|
|
||||||
jack,
|
|
||||||
&self.midi_from.as_slice(),
|
|
||||||
&self.midi_to.as_slice(),
|
|
||||||
&[&self.l_from.as_slice(), &self.r_from.as_slice()],
|
|
||||||
&[&self.l_to.as_slice(), &self.r_to.as_slice()],
|
|
||||||
)?;
|
|
||||||
if let Some(bpm) = self.bpm {
|
|
||||||
app.clock().timebase.bpm.set(bpm);
|
|
||||||
}
|
|
||||||
if self.sync_lead {
|
|
||||||
jack.read().unwrap().client().register_timebase_callback(false, |mut state|{
|
|
||||||
app.clock().playhead.update_from_sample(state.position.frame() as f64);
|
|
||||||
state.position.bbt = Some(app.clock().bbt());
|
|
||||||
state.position
|
|
||||||
})?
|
|
||||||
} else if self.sync_follow {
|
|
||||||
jack.read().unwrap().client().register_timebase_callback(false, |state|{
|
|
||||||
app.clock().playhead.update_from_sample(state.position.frame() as f64);
|
|
||||||
state.position
|
|
||||||
})?
|
|
||||||
}
|
|
||||||
Ok(app)
|
|
||||||
})?;
|
|
||||||
engine.run(&state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test] fn verify_groovebox_cli () {
|
|
||||||
use clap::CommandFactory;
|
|
||||||
GrooveboxCli::command().debug_assert();
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
include!("./lib.rs");
|
|
||||||
pub fn main () -> Usually<()> { SamplerCli::parse().run() }
|
|
||||||
#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct SamplerCli {
|
|
||||||
/// Name of JACK client
|
|
||||||
#[arg(short, long)] name: Option<String>,
|
|
||||||
/// Path to plugin
|
|
||||||
#[arg(short, long)] path: Option<String>,
|
|
||||||
/// MIDI outs to connect to MIDI input
|
|
||||||
#[arg(short='i', long)]
|
|
||||||
midi_from: Vec<String>,
|
|
||||||
/// Audio outs to connect to left input
|
|
||||||
#[arg(short='l', long)]
|
|
||||||
l_from: Vec<String>,
|
|
||||||
/// Audio outs to connect to right input
|
|
||||||
#[arg(short='r', long)]
|
|
||||||
r_from: Vec<String>,
|
|
||||||
/// Audio ins to connect from left output
|
|
||||||
#[arg(short='L', long)]
|
|
||||||
l_to: Vec<String>,
|
|
||||||
/// Audio ins to connect from right output
|
|
||||||
#[arg(short='R', long)]
|
|
||||||
r_to: Vec<String>,
|
|
||||||
}
|
|
||||||
impl SamplerCli {
|
|
||||||
fn run (&self) -> Usually<()> {
|
|
||||||
let name = self.name.as_deref().unwrap_or("tek_sampler");
|
|
||||||
let engine = Tui::new()?;
|
|
||||||
let state = JackConnection::new(name)?.activate_with(|jack|{
|
|
||||||
Ok(tek::SamplerTui {
|
|
||||||
cursor: (0, 0),
|
|
||||||
editing: None,
|
|
||||||
mode: None,
|
|
||||||
size: Measure::new(),
|
|
||||||
note_lo: 36.into(),
|
|
||||||
note_pt: 36.into(),
|
|
||||||
color: ItemPalette::from(Color::Rgb(64, 128, 32)),
|
|
||||||
state: Sampler::new(
|
|
||||||
jack,
|
|
||||||
&"sampler",
|
|
||||||
&self.midi_from.as_slice(),
|
|
||||||
&[&self.l_from.as_slice(), &self.r_from.as_slice()],
|
|
||||||
&[&self.l_to.as_slice(), &self.r_to.as_slice()],
|
|
||||||
)?,
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
engine.run(&state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
include!("./lib.rs");
|
|
||||||
|
|
||||||
pub fn main () -> Usually<()> {
|
|
||||||
SequencerCli::parse().run()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Launches a single interactive MIDI sequencer.
|
|
||||||
#[derive(Debug, Parser)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
pub struct SequencerCli {
|
|
||||||
/// Name of JACK client
|
|
||||||
#[arg(short, long)]
|
|
||||||
name: Option<String>,
|
|
||||||
/// Whether to include a transport toolbar (default: true)
|
|
||||||
#[arg(short, long, default_value_t = true)]
|
|
||||||
transport: bool,
|
|
||||||
/// MIDI outs to connect to (multiple instances accepted)
|
|
||||||
#[arg(short='i', long)]
|
|
||||||
midi_from: Vec<String>,
|
|
||||||
/// MIDI ins to connect to (multiple instances accepted)
|
|
||||||
#[arg(short='o', long)]
|
|
||||||
midi_to: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SequencerCli {
|
|
||||||
fn run (&self) -> Usually<()> {
|
|
||||||
let name = self.name.as_deref().unwrap_or("tek_sequencer");
|
|
||||||
let engine = Tui::new()?;
|
|
||||||
let state = JackConnection::new(name)?.activate_with(|jack|{
|
|
||||||
let mut app = SequencerTui::try_from(jack)?;
|
|
||||||
let jack = jack.read().unwrap();
|
|
||||||
let midi_in = jack.register_port("i", MidiIn::default())?;
|
|
||||||
let midi_out = jack.register_port("o", MidiOut::default())?;
|
|
||||||
connect_from(&jack, &midi_in, &self.midi_from)?;
|
|
||||||
connect_to(&jack, &midi_out, &self.midi_to)?;
|
|
||||||
app.player.midi_ins.push(midi_in);
|
|
||||||
app.player.midi_outs.push(midi_out);
|
|
||||||
Ok(app)
|
|
||||||
})?;
|
|
||||||
engine.run(&state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test] fn verify_sequencer_cli () {
|
|
||||||
use clap::CommandFactory;
|
|
||||||
SequencerCli::command().debug_assert();
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
include!("./lib.rs");
|
|
||||||
|
|
||||||
/// Application entrypoint.
|
|
||||||
pub fn main () -> Usually<()> {
|
|
||||||
let name = "tek_transport";
|
|
||||||
Tui::new()?.run(&JackConnection::new(name)?
|
|
||||||
.activate_with(|jack|TransportTui::new(jack))?)
|
|
||||||
}
|
|
||||||
56
bin/lib.rs
56
bin/lib.rs
|
|
@ -1,56 +0,0 @@
|
||||||
#[allow(unused_imports)] use std::sync::Arc;
|
|
||||||
#[allow(unused_imports)] use clap::{self, Parser};
|
|
||||||
#[allow(unused_imports)] use tek::{
|
|
||||||
*,
|
|
||||||
jack::*,
|
|
||||||
tek_layout::Measure,
|
|
||||||
tek_engine::{Usually, tui::{Tui, TuiRun, ratatui::prelude::Color}}
|
|
||||||
};
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
fn connect_from (jack: &JackConnection, input: &Port<MidiIn>, ports: &[String]) -> Usually<()> {
|
|
||||||
for port in ports.iter() {
|
|
||||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
|
||||||
jack.client().connect_ports(port, input)?;
|
|
||||||
} else {
|
|
||||||
panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
fn connect_to (jack: &JackConnection, output: &Port<MidiOut>, ports: &[String]) -> Usually<()> {
|
|
||||||
for port in ports.iter() {
|
|
||||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
|
||||||
jack.client().connect_ports(output, port)?;
|
|
||||||
} else {
|
|
||||||
panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
fn connect_audio_from (jack: &JackConnection, input: &Port<AudioIn>, ports: &[String]) -> Usually<()> {
|
|
||||||
for port in ports.iter() {
|
|
||||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
|
||||||
jack.client().connect_ports(port, input)?;
|
|
||||||
} else {
|
|
||||||
panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
fn connect_audio_to (jack: &JackConnection, output: &Port<AudioOut>, ports: &[String]) -> Usually<()> {
|
|
||||||
for port in ports.iter() {
|
|
||||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
|
||||||
jack.client().connect_ports(output, port)?;
|
|
||||||
} else {
|
|
||||||
panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
14
build/Dockerfile.glibc
Normal file
14
build/Dockerfile.glibc
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
FROM docker.io/library/debian:bookworm
|
||||||
|
RUN apt update \
|
||||||
|
&& apt install -y build-essential bash tree git wget \
|
||||||
|
pkg-config libjack-dev liblilv-dev libserd-dev libsord-dev
|
||||||
|
RUN adduser --quiet --uid 1000 --disabled-password build
|
||||||
|
RUN wget https://static.rust-lang.org/rustup/dist/x86_64-unknown-linux-gnu/rustup-init \
|
||||||
|
&& chmod +x ./rustup-init \
|
||||||
|
&& mv rustup-init /usr/bin/rustup-init
|
||||||
|
USER build
|
||||||
|
WORKDIR /home/build
|
||||||
|
RUN rustup-init -yv --profile minimal --default-toolchain nightly \
|
||||||
|
&& rm -rvf "$HOME/.rustup/roolchains/*/share"
|
||||||
|
RUN ls -alh "$HOME" && bash -c '. "$HOME/.cargo/env" \
|
||||||
|
&& cargo version -vv'
|
||||||
13
build/Dockerfile.musl
Normal file
13
build/Dockerfile.musl
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
FROM docker.io/library/alpine:edge
|
||||||
|
|
||||||
|
RUN apk add --no-cache build-base bash tree rustup git just cloc clang20-dev pipewire-jack-dev
|
||||||
|
|
||||||
|
RUN adduser -Du1000 build
|
||||||
|
|
||||||
|
USER 1000
|
||||||
|
|
||||||
|
RUN rustup-init -y --profile minimal --default-toolchain nightly \
|
||||||
|
&& rm -rvf "$HOME/.rustup/roolchains/*/share"
|
||||||
|
|
||||||
|
RUN source "$HOME/.cargo/env" \
|
||||||
|
&& cargo version -vv
|
||||||
11
build/README.md
Normal file
11
build/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
This directory contains Dockerfiles and shell scripts
|
||||||
|
for building Tek in a container. For now, only the
|
||||||
|
GLIBC build works, as the Musl static build is unable
|
||||||
|
to `dlopen` the system's `libjack.so`.
|
||||||
|
|
||||||
|
Invoke from repo root, like this: `build/release-glibc.sh`.
|
||||||
|
This will first build a Docker image, `tek:glibc`, which
|
||||||
|
will contain all build-time dependencies; then, it
|
||||||
|
will invoke a `cargo build --release` in a container
|
||||||
|
spawned from that image, ultimately placing the
|
||||||
|
release build in this directory, as `build/tek`.
|
||||||
11
build/release-glibc-shell.sh
Executable file
11
build/release-glibc-shell.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
set -exo pipefail
|
||||||
|
docker inspect tek:glibc || time docker build --cache-from=internal \
|
||||||
|
-f build/Dockerfile.glibc -t tek:glibc .
|
||||||
|
time docker run \
|
||||||
|
--rm -itu0 \
|
||||||
|
-v .:/build -w /build \
|
||||||
|
-vtek-build-cargo:/home/build/.cargo \
|
||||||
|
-vtek-build-target:/build/target \
|
||||||
|
-eRUST_JACK_DLOPEN=true \
|
||||||
|
tek:glibc $@
|
||||||
14
build/release-glibc.sh
Executable file
14
build/release-glibc.sh
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
set -exo pipefail
|
||||||
|
docker inspect tek:glibc || time docker build --cache-from=internal \
|
||||||
|
-f build/Dockerfile.glibc -t tek:glibc .
|
||||||
|
time docker run \
|
||||||
|
--rm -itu0 \
|
||||||
|
-v .:/build -w /build \
|
||||||
|
-vtek-build-cargo:/home/build/.cargo \
|
||||||
|
-vtek-build-target:/build/target \
|
||||||
|
-eRUST_JACK_DLOPEN=true \
|
||||||
|
tek:glibc sh -c "chown -R 1000:1000 /build/target \
|
||||||
|
&& su build -c '. ~/.cargo/env \
|
||||||
|
&& time cargo build -j4 --release \
|
||||||
|
&& cp target/release/tek build/'"
|
||||||
11
build/release-musl-shell.sh
Executable file
11
build/release-musl-shell.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
set -exo pipefail
|
||||||
|
docker inspect tek:musl || time docker build --cache-from=internal \
|
||||||
|
-f build/Dockerfile.musl -t tek:musl .
|
||||||
|
time docker run \
|
||||||
|
--rm -itu0 \
|
||||||
|
-v .:/build -w /build \
|
||||||
|
-vtek-build-cargo:/home/build/.cargo \
|
||||||
|
-vtek-build-target:/build/target \
|
||||||
|
-eRUST_JACK_DLOPEN=true \
|
||||||
|
tek:musl $@
|
||||||
14
build/release-musl.sh
Executable file
14
build/release-musl.sh
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
set -exo pipefail
|
||||||
|
docker inspect tek:musl || time docker build --cache-from=internal \
|
||||||
|
-f build/Dockerfile.musl -t tek:musl .
|
||||||
|
time docker run \
|
||||||
|
--rm -itu0 \
|
||||||
|
-v .:/build -w /build \
|
||||||
|
-vtek-build-cargo:/home/build/.cargo \
|
||||||
|
-vtek-build-target:/build/target \
|
||||||
|
-eRUST_JACK_DLOPEN=true \
|
||||||
|
tek:musl sh -c "chown -R 1000:1000 /build/target \
|
||||||
|
&& su build -c 'source ~/.cargo/env \
|
||||||
|
&& just build-release \
|
||||||
|
&& cp target/release/tek build/'"
|
||||||
1
deps/rust-jack
vendored
Submodule
1
deps/rust-jack
vendored
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 764a38a880ab4749ea60aa7e53cd814b858e606c
|
||||||
0
suil/Cargo.toml → deps/suil/Cargo.toml
vendored
0
suil/Cargo.toml → deps/suil/Cargo.toml
vendored
0
suil/build.rs → deps/suil/build.rs
vendored
0
suil/build.rs → deps/suil/build.rs
vendored
0
suil/src/bound.rs → deps/suil/src/bound.rs
vendored
0
suil/src/bound.rs → deps/suil/src/bound.rs
vendored
0
suil/src/gtk.rs → deps/suil/src/gtk.rs
vendored
0
suil/src/gtk.rs → deps/suil/src/gtk.rs
vendored
0
suil/src/lib.rs → deps/suil/src/lib.rs
vendored
0
suil/src/lib.rs → deps/suil/src/lib.rs
vendored
0
suil/src/test.rs → deps/suil/src/test.rs
vendored
0
suil/src/test.rs → deps/suil/src/test.rs
vendored
0
suil/stdbool.h → deps/suil/stdbool.h
vendored
0
suil/stdbool.h → deps/suil/stdbool.h
vendored
0
suil/stdint.h → deps/suil/stdint.h
vendored
0
suil/stdint.h → deps/suil/stdint.h
vendored
0
suil/wrapper.h → deps/suil/wrapper.h
vendored
0
suil/wrapper.h → deps/suil/wrapper.h
vendored
1
deps/tengri
vendored
Submodule
1
deps/tengri
vendored
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 8c54510f630e8a81b7d7bdca0a51a69cdb9dffcc
|
||||||
33
deps/vst/.github/workflows/deploy.yml
vendored
Normal file
33
deps/vst/.github/workflows/deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Installs the latest stable rust, and all components needed for the rest of the CI pipeline.
|
||||||
|
- name: Set up CI environment
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
|
||||||
|
# Sanity check: make sure the release builds
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --verbose
|
||||||
|
|
||||||
|
# Sanity check: make sure all tests in the release pass
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --verbose
|
||||||
|
|
||||||
|
# Deploy to crates.io
|
||||||
|
# Only works on github releases (tagged commits)
|
||||||
|
- name: Deploy to crates.io
|
||||||
|
env:
|
||||||
|
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
|
||||||
|
run: cargo publish --token $CRATES_IO_TOKEN --manifest-path Cargo.toml
|
||||||
46
deps/vst/.github/workflows/docs.yml
vendored
Normal file
46
deps/vst/.github/workflows/docs.yml
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
name: Docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Installs the latest stable rust, and all components needed for the rest of the CI pipeline.
|
||||||
|
- name: Set up CI environment
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
|
||||||
|
# Sanity check: make sure the release builds
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --verbose
|
||||||
|
|
||||||
|
# Sanity check: make sure all tests in the release pass
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --verbose
|
||||||
|
|
||||||
|
# Generate docs
|
||||||
|
# TODO: what does the last line here do?
|
||||||
|
- name: Generate docs
|
||||||
|
env:
|
||||||
|
GH_ENCRYPTED_TOKEN: ${{ secrets.GH_ENCRYPTED_TOKEN }}
|
||||||
|
run: |
|
||||||
|
cargo doc --all --no-deps
|
||||||
|
echo '<meta http-equiv=refresh content=0;url=vst/index.html>' > target/doc/index.html
|
||||||
|
|
||||||
|
# Push docs to github pages (branch `gh-pages`)
|
||||||
|
- name: Deploy
|
||||||
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: target/doc
|
||||||
38
deps/vst/.github/workflows/rust.yml
vendored
Normal file
38
deps/vst/.github/workflows/rust.yml
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
name: Rust
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Installs the latest stable rust, and all components needed for the rest of the CI pipeline.
|
||||||
|
- name: Set up CI environment
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
# Makes sure the code builds successfully.
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --verbose
|
||||||
|
|
||||||
|
# Makes sure all of the tests pass.
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --verbose
|
||||||
|
|
||||||
|
# Runs Clippy on the codebase, and makes sure there are no lint warnings.
|
||||||
|
# Disabled for now. Re-enable if you find it useful enough to deal with it constantly breaking.
|
||||||
|
# - name: Clippy
|
||||||
|
# run: cargo clippy --all-targets --all-features -- -D warnings -A clippy::unreadable_literal -A clippy::needless_range_loop -A clippy::float_cmp -A clippy::comparison-chain -A clippy::needless-doctest-main -A clippy::missing-safety-doc
|
||||||
|
|
||||||
|
# Makes sure the codebase is up to `cargo fmt` standards
|
||||||
|
- name: Format check
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
21
deps/vst/.gitignore
vendored
Normal file
21
deps/vst/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Compiled files
|
||||||
|
*.o
|
||||||
|
*.so
|
||||||
|
*.rlib
|
||||||
|
*.dll
|
||||||
|
|
||||||
|
# Executables
|
||||||
|
*.exe
|
||||||
|
|
||||||
|
# Generated by Cargo
|
||||||
|
/target/
|
||||||
|
/examples/*/target/
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# Vim
|
||||||
|
[._]*.s[a-w][a-z]
|
||||||
|
[._]s[a-w][a-z]
|
||||||
|
*.un~
|
||||||
|
Session.vim
|
||||||
|
.netrwhist
|
||||||
|
*~
|
||||||
86
deps/vst/CHANGELOG.md
vendored
Normal file
86
deps/vst/CHANGELOG.md
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Added deprecation notice.
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `SysExEvent` no longer contains invalid data on 64-bit systems ([#170](https://github.com/RustAudio/vst-rs/pull/171)]
|
||||||
|
- Function pointers in `AEffect` marked as `extern` ([#141](https://github.com/RustAudio/vst-rs/pull/141))
|
||||||
|
- Key character fixes ([#152](https://github.com/RustAudio/vst-rs/pull/152))
|
||||||
|
- Doc and deploy actions fixes ([9eb1bef](https://github.com/RustAudio/vst-rs/commit/9eb1bef1826db1581b4162081de05c1090935afb))
|
||||||
|
- Various doc fixes ([#177](https://github.com/RustAudio/vst-rs/pull/177))
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `begin_edit` and `end_edit` now in `Host` trait ([#151](https://github.com/RustAudio/vst-rs/pull/151))
|
||||||
|
- Added a `prelude` for commonly used items when constructing a `Plugin` ([#161](https://github.com/RustAudio/vst-rs/pull/161))
|
||||||
|
- Various useful implementations for `AtomicFloat` ([#150](https://github.com/RustAudio/vst-rs/pull/150))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Major breaking change:** New `Plugin` `Send` requirement ([#140](https://github.com/RustAudio/vst-rs/pull/140))
|
||||||
|
- No longer require `Plugin` to implement `Default` ([#154](https://github.com/RustAudio/vst-rs/pull/154))
|
||||||
|
- `impl_clicke` replaced with `num_enum` ([#168](https://github.com/RustAudio/vst-rs/pull/168))
|
||||||
|
- Reworked `SendEventBuffer` to make it useable in `Plugin::process_events` ([#160](https://github.com/RustAudio/vst-rs/pull/160))
|
||||||
|
- Updated dependencies and removed development dependency on `time` ([#179](https://github.com/RustAudio/vst-rs/pull/179))
|
||||||
|
|
||||||
|
## 0.2.1
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Introduced zero-valued `EventType` variant to enable zero-initialization of `Event`, fixing a panic on Rust 1.48 and newer ([#138](https://github.com/RustAudio/vst-rs/pull/138))
|
||||||
|
- `EditorGetRect` opcode returns `1` on success, ensuring that the provided dimensions are applied by the host ([#115](https://github.com/RustAudio/vst-rs/pull/115))
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `update_display()` method to `Host`, telling the host to update its display (after a parameter change) via the `UpdateDisplay` opcode ([#126](https://github.com/RustAudio/vst-rs/pull/126))
|
||||||
|
- Allow plug-in to return a custom value in `can_do()` via the `Supported::Custom` enum variant ([#130](https://github.com/RustAudio/vst-rs/pull/130))
|
||||||
|
- Added `PartialEq` and `Eq` for `Supported` ([#135](https://github.com/RustAudio/vst-rs/pull/135))
|
||||||
|
- Implemented `get_editor()` and `Editor` interface for `PluginInstance` to enable editor support on the host side ([#136](https://github.com/RustAudio/vst-rs/pull/136))
|
||||||
|
- Default value (`0.0`) for `AtomicFloat` ([#139](https://github.com/RustAudio/vst-rs/pull/139))
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Major breaking change:** Restructured `Plugin` API to make it thread safe ([#65](https://github.com/RustAudio/vst-rs/pull/65))
|
||||||
|
- Fixed a number of unsoundness issues in the `Outputs` API ([#67](https://github.com/RustAudio/vst-rs/pull/67), [#108](https://github.com/RustAudio/vst-rs/pull/108))
|
||||||
|
- Set parameters to be automatable by default ([#99](https://github.com/RustAudio/vst-rs/pull/99))
|
||||||
|
- Moved repository to the [RustAudio](https://github.com/RustAudio) organization and renamed it to `vst-rs` ([#90](https://github.com/RustAudio/vst-rs/pull/90), [#94](https://github.com/RustAudio/vst-rs/pull/94))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed a use-after-move bug in the event iterator ([#93](https://github.com/RustAudio/vst-rs/pull/93), [#111](https://github.com/RustAudio/vst-rs/pull/111))
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Handle `Opcode::GetEffectName` to resolve name display issues on some hosts ([#89](https://github.com/RustAudio/vst-rs/pull/89))
|
||||||
|
- More examples ([#65](https://github.com/RustAudio/vst-rs/pull/65), [#92](https://github.com/RustAudio/vst-rs/pull/92))
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added initial changelog
|
||||||
|
- Initial project files
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- The `#[derive(Copy, Clone)]` attribute from `Outputs`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- The signature of the `Outputs::split_at_mut` now takes an `self` parameter instead of `&mut self`.
|
||||||
|
So calling `split_at_mut` will now move instead of "borrow".
|
||||||
|
- Now `&mut Outputs` (instead of `Outputs`) implements the `IntoIterator` trait.
|
||||||
|
- The return type of the `AudioBuffer::zip()` method (but it still implements the Iterator trait).
|
||||||
75
deps/vst/Cargo.toml
vendored
Normal file
75
deps/vst/Cargo.toml
vendored
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
[package]
|
||||||
|
name = "vst"
|
||||||
|
version = "0.4.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = [
|
||||||
|
"Marko Mijalkovic <marko.mijalkovic97@gmail.com>",
|
||||||
|
"Boscop",
|
||||||
|
"Alex Zywicki <alexander.zywicki@gmail.com>",
|
||||||
|
"doomy <notdoomy@protonmail.com>",
|
||||||
|
"Ms2ger",
|
||||||
|
"Rob Saunders",
|
||||||
|
"David Lu",
|
||||||
|
"Aske Simon Christensen",
|
||||||
|
"kykc",
|
||||||
|
"Jordan Earls",
|
||||||
|
"xnor104",
|
||||||
|
"Nathaniel Theis",
|
||||||
|
"Colin Wallace",
|
||||||
|
"Henrik Nordvik",
|
||||||
|
"Charles Saracco",
|
||||||
|
"Frederik Halkjær" ]
|
||||||
|
|
||||||
|
description = "VST 2.4 API implementation in rust. Create plugins or hosts."
|
||||||
|
|
||||||
|
readme = "README.md"
|
||||||
|
repository = "https://github.com/rustaudio/vst-rs"
|
||||||
|
|
||||||
|
license = "MIT"
|
||||||
|
keywords = ["vst", "vst2", "plugin"]
|
||||||
|
|
||||||
|
autoexamples = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
disable_deprecation_warning = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
log = "0.4"
|
||||||
|
num-traits = "0.2"
|
||||||
|
libc = "0.2"
|
||||||
|
bitflags = "1"
|
||||||
|
libloading = "0.7"
|
||||||
|
num_enum = "0.5.2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rand = "0.8"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "dimension_expander"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "simple_host"
|
||||||
|
crate-type = ["bin"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "sine_synth"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "fwd_midi"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "gain_effect"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "transfer_and_smooth"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "ladder_filter"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
21
deps/vst/LICENSE
vendored
Normal file
21
deps/vst/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Marko Mijalkovic
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
112
deps/vst/README.md
vendored
Normal file
112
deps/vst/README.md
vendored
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
# vst-rs
|
||||||
|
[![crates.io][crates-img]][crates-url]
|
||||||
|
[](https://deps.rs/repo/github/rustaudio/vst-rs)
|
||||||
|
[![Discord Chat][discord-img]][discord-url]
|
||||||
|
[![Discourse topics][dc-img]][dc-url]
|
||||||
|
|
||||||
|
> **Notice**: `vst-rs` is deprecated.
|
||||||
|
>
|
||||||
|
> This crate is no longer actively developed or maintained. VST 2 has been [officially discontinued](http://web.archive.org/web/20210727141622/https://www.steinberg.net/en/newsandevents/news/newsdetail/article/vst-2-coming-to-an-end-4727.html) and it is [no longer possible](https://forum.juce.com/t/steinberg-closing-down-vst2-for-good/27722/25) to acquire a license to distribute VST 2 products. It is highly recommended that you make use of other libraries for developing audio plugins and plugin hosts in Rust.
|
||||||
|
>
|
||||||
|
> If you're looking for a high-level, multi-format framework for developing plugins in Rust, consider using [NIH-plug](https://github.com/robbert-vdh/nih-plug/) or [`baseplug`](https://github.com/wrl/baseplug/). If you're looking for bindings to specific plugin APIs, consider using [`vst3-sys`](https://github.com/RustAudio/vst3-sys/), [`clap-sys`](https://github.com/glowcoil/clap-sys), [`lv2(-sys)`](https://github.com/RustAudio/rust-lv2), or [`auv2-sys`](https://github.com/glowcoil/auv2-sys). If, despite the above warnings, you still have a need to use the VST 2 API from Rust, consider using [`vst2-sys`](https://github.com/RustAudio/vst2-sys) or generating bindings from the original VST 2 SDK using [`bindgen`](https://github.com/rust-lang/rust-bindgen).
|
||||||
|
|
||||||
|
`vst-rs` is a library for creating VST2 plugins in the Rust programming language.
|
||||||
|
|
||||||
|
This library is a work in progress, and as such it does not yet implement all
|
||||||
|
functionality. It can create basic VST plugins without an editor interface.
|
||||||
|
|
||||||
|
**Note:** If you are upgrading from a version prior to 0.2.0, you will need to update
|
||||||
|
your plugin code to be compatible with the new, thread-safe plugin API. See the
|
||||||
|
[`transfer_and_smooth`](examples/transfer_and_smooth.rs) example for a guide on how
|
||||||
|
to port your plugin.
|
||||||
|
|
||||||
|
## Library Documentation
|
||||||
|
|
||||||
|
Documentation for **released** versions can be found [here](https://docs.rs/vst/).
|
||||||
|
|
||||||
|
Development documentation (current `master` branch) can be found [here](https://rustaudio.github.io/vst-rs/vst/).
|
||||||
|
|
||||||
|
## Crate
|
||||||
|
This crate is available on [crates.io](https://crates.io/crates/vst). If you prefer the bleeding-edge, you can also
|
||||||
|
include the crate directly from the official [Github repository](https://github.com/rustaudio/vst-rs).
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# get from crates.io.
|
||||||
|
vst = "0.3"
|
||||||
|
```
|
||||||
|
```toml
|
||||||
|
# get directly from Github. This might be unstable!
|
||||||
|
vst = { git = "https://github.com/rustaudio/vst-rs" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
To create a plugin, simply create a type which implements the `Plugin` trait. Then call the `plugin_main` macro, which will export the necessary functions and handle dealing with the rest of the API.
|
||||||
|
|
||||||
|
## Example Plugin
|
||||||
|
A simple plugin that bears no functionality. The provided `Cargo.toml` has a
|
||||||
|
`crate-type` directive which builds a dynamic library, usable by any VST host.
|
||||||
|
|
||||||
|
`src/lib.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[macro_use]
|
||||||
|
extern crate vst;
|
||||||
|
|
||||||
|
use vst::prelude::*;
|
||||||
|
|
||||||
|
struct BasicPlugin;
|
||||||
|
|
||||||
|
impl Plugin for BasicPlugin {
|
||||||
|
fn new(_host: HostCallback) -> Self {
|
||||||
|
BasicPlugin
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_info(&self) -> Info {
|
||||||
|
Info {
|
||||||
|
name: "Basic Plugin".to_string(),
|
||||||
|
unique_id: 1357, // Used by hosts to differentiate between plugins.
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin_main!(BasicPlugin); // Important!
|
||||||
|
```
|
||||||
|
|
||||||
|
`Cargo.toml`
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[package]
|
||||||
|
name = "basic_vst"
|
||||||
|
version = "0.0.1"
|
||||||
|
authors = ["Author <author@example.com>"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
vst = { git = "https://github.com/rustaudio/vst-rs" }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "basicvst"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
```
|
||||||
|
|
||||||
|
[crates-img]: https://img.shields.io/crates/v/vst.svg
|
||||||
|
[crates-url]: https://crates.io/crates/vst
|
||||||
|
[discord-img]: https://img.shields.io/discord/590254806208217089.svg?label=Discord&logo=discord&color=blue
|
||||||
|
[discord-url]: https://discord.gg/QPdhk2u
|
||||||
|
[dc-img]: https://img.shields.io/discourse/https/rust-audio.discourse.group/topics.svg?logo=discourse&color=blue
|
||||||
|
[dc-url]: https://rust-audio.discourse.group
|
||||||
|
|
||||||
|
#### Packaging on OS X
|
||||||
|
|
||||||
|
On OS X VST plugins are packaged inside loadable bundles.
|
||||||
|
To package your VST as a loadable bundle you may use the `osx_vst_bundler.sh` script this library provides.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
./osx_vst_bundler.sh Plugin target/release/plugin.dylib
|
||||||
|
Creates a Plugin.vst bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
## Special Thanks
|
||||||
|
[Marko Mijalkovic](https://github.com/overdrivenpotato) for [initiating this project](https://github.com/overdrivenpotato/rust-vst2)
|
||||||
222
deps/vst/examples/dimension_expander.rs
vendored
Normal file
222
deps/vst/examples/dimension_expander.rs
vendored
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
// author: Marko Mijalkovic <marko.mijalkovic97@gmail.com>
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate vst;
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::f64::consts::PI;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use vst::prelude::*;
|
||||||
|
|
||||||
|
/// Calculate the length in samples for a delay. Size ranges from 0.0 to 1.0.
|
||||||
|
fn delay(index: usize, mut size: f32) -> isize {
|
||||||
|
const SIZE_OFFSET: f32 = 0.06;
|
||||||
|
const SIZE_MULT: f32 = 1_000.0;
|
||||||
|
|
||||||
|
size += SIZE_OFFSET;
|
||||||
|
|
||||||
|
// Spread ratio between delays
|
||||||
|
const SPREAD: f32 = 0.3;
|
||||||
|
|
||||||
|
let base = size * SIZE_MULT;
|
||||||
|
let mult = (index as f32 * SPREAD) + 1.0;
|
||||||
|
let offset = if index > 2 { base * SPREAD / 2.0 } else { 0.0 };
|
||||||
|
|
||||||
|
(base * mult + offset) as isize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A left channel and right channel sample.
|
||||||
|
type SamplePair = (f32, f32);
|
||||||
|
|
||||||
|
/// The Dimension Expander.
|
||||||
|
struct DimensionExpander {
|
||||||
|
buffers: Vec<VecDeque<SamplePair>>,
|
||||||
|
params: Arc<DimensionExpanderParameters>,
|
||||||
|
old_size: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DimensionExpanderParameters {
|
||||||
|
dry_wet: AtomicFloat,
|
||||||
|
size: AtomicFloat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DimensionExpander {
|
||||||
|
fn new(size: f32, dry_wet: f32) -> DimensionExpander {
|
||||||
|
const NUM_DELAYS: usize = 4;
|
||||||
|
|
||||||
|
let mut buffers = Vec::new();
|
||||||
|
|
||||||
|
// Generate delay buffers
|
||||||
|
for i in 0..NUM_DELAYS {
|
||||||
|
let samples = delay(i, size);
|
||||||
|
let mut buffer = VecDeque::with_capacity(samples as usize);
|
||||||
|
|
||||||
|
// Fill in the delay buffers with empty samples
|
||||||
|
for _ in 0..samples {
|
||||||
|
buffer.push_back((0.0, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
buffers.push(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
DimensionExpander {
|
||||||
|
buffers,
|
||||||
|
params: Arc::new(DimensionExpanderParameters {
|
||||||
|
dry_wet: AtomicFloat::new(dry_wet),
|
||||||
|
size: AtomicFloat::new(size),
|
||||||
|
}),
|
||||||
|
old_size: size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the delay buffers with a new size value.
|
||||||
|
fn resize(&mut self, n: f32) {
|
||||||
|
let old_size = self.old_size;
|
||||||
|
|
||||||
|
for (i, buffer) in self.buffers.iter_mut().enumerate() {
|
||||||
|
// Calculate the size difference between delays
|
||||||
|
let old_delay = delay(i, old_size);
|
||||||
|
let new_delay = delay(i, n);
|
||||||
|
|
||||||
|
let diff = new_delay - old_delay;
|
||||||
|
|
||||||
|
// Add empty samples if the delay was increased, remove if decreased
|
||||||
|
if diff > 0 {
|
||||||
|
for _ in 0..diff {
|
||||||
|
buffer.push_back((0.0, 0.0));
|
||||||
|
}
|
||||||
|
} else if diff < 0 {
|
||||||
|
for _ in 0..-diff {
|
||||||
|
let _ = buffer.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.old_size = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for DimensionExpander {
|
||||||
|
fn new(_host: HostCallback) -> Self {
|
||||||
|
DimensionExpander::new(0.12, 0.66)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_info(&self) -> Info {
|
||||||
|
Info {
|
||||||
|
name: "Dimension Expander".to_string(),
|
||||||
|
vendor: "overdrivenpotato".to_string(),
|
||||||
|
unique_id: 243723071,
|
||||||
|
version: 1,
|
||||||
|
inputs: 2,
|
||||||
|
outputs: 2,
|
||||||
|
parameters: 2,
|
||||||
|
category: Category::Effect,
|
||||||
|
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
||||||
|
let (inputs, outputs) = buffer.split();
|
||||||
|
|
||||||
|
// Assume 2 channels
|
||||||
|
if inputs.len() < 2 || outputs.len() < 2 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize if size changed
|
||||||
|
let size = self.params.size.get();
|
||||||
|
if size != self.old_size {
|
||||||
|
self.resize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over inputs as (&f32, &f32)
|
||||||
|
let (l, r) = inputs.split_at(1);
|
||||||
|
let stereo_in = l[0].iter().zip(r[0].iter());
|
||||||
|
|
||||||
|
// Iterate over outputs as (&mut f32, &mut f32)
|
||||||
|
let (mut l, mut r) = outputs.split_at_mut(1);
|
||||||
|
let stereo_out = l[0].iter_mut().zip(r[0].iter_mut());
|
||||||
|
|
||||||
|
// Zip and process
|
||||||
|
for ((left_in, right_in), (left_out, right_out)) in stereo_in.zip(stereo_out) {
|
||||||
|
// Push the new samples into the delay buffers.
|
||||||
|
for buffer in &mut self.buffers {
|
||||||
|
buffer.push_back((*left_in, *right_in));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut left_processed = 0.0;
|
||||||
|
let mut right_processed = 0.0;
|
||||||
|
|
||||||
|
// Recalculate time per sample
|
||||||
|
let time_s = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs_f64();
|
||||||
|
|
||||||
|
// Use buffer index to offset volume LFO
|
||||||
|
for (n, buffer) in self.buffers.iter_mut().enumerate() {
|
||||||
|
if let Some((left_old, right_old)) = buffer.pop_front() {
|
||||||
|
const LFO_FREQ: f64 = 0.5;
|
||||||
|
const WET_MULT: f32 = 0.66;
|
||||||
|
|
||||||
|
let offset = 0.25 * (n % 4) as f64;
|
||||||
|
|
||||||
|
// Sine wave volume LFO
|
||||||
|
let lfo = ((time_s * LFO_FREQ + offset) * PI * 2.0).sin() as f32;
|
||||||
|
|
||||||
|
let wet = self.params.dry_wet.get() * WET_MULT;
|
||||||
|
let mono = (left_old + right_old) / 2.0;
|
||||||
|
|
||||||
|
// Flip right channel and keep left mono so that the result is
|
||||||
|
// entirely stereo
|
||||||
|
left_processed += mono * wet * lfo;
|
||||||
|
right_processed += -mono * wet * lfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// By only adding to the input, the output value always remains the same in mono
|
||||||
|
*left_out = *left_in + left_processed;
|
||||||
|
*right_out = *right_in + right_processed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
|
||||||
|
Arc::clone(&self.params) as Arc<dyn PluginParameters>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginParameters for DimensionExpanderParameters {
|
||||||
|
fn get_parameter(&self, index: i32) -> f32 {
|
||||||
|
match index {
|
||||||
|
0 => self.size.get(),
|
||||||
|
1 => self.dry_wet.get(),
|
||||||
|
_ => 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parameter_text(&self, index: i32) -> String {
|
||||||
|
match index {
|
||||||
|
0 => format!("{}", (self.size.get() * 1000.0) as isize),
|
||||||
|
1 => format!("{:.1}%", self.dry_wet.get() * 100.0),
|
||||||
|
_ => "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parameter_name(&self, index: i32) -> String {
|
||||||
|
match index {
|
||||||
|
0 => "Size",
|
||||||
|
1 => "Dry/Wet",
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_parameter(&self, index: i32, val: f32) {
|
||||||
|
match index {
|
||||||
|
0 => self.size.set(val),
|
||||||
|
1 => self.dry_wet.set(val),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin_main!(DimensionExpander);
|
||||||
71
deps/vst/examples/fwd_midi.rs
vendored
Normal file
71
deps/vst/examples/fwd_midi.rs
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate vst;
|
||||||
|
|
||||||
|
use vst::api;
|
||||||
|
use vst::prelude::*;
|
||||||
|
|
||||||
|
plugin_main!(MyPlugin); // Important!
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct MyPlugin {
|
||||||
|
host: HostCallback,
|
||||||
|
recv_buffer: SendEventBuffer,
|
||||||
|
send_buffer: SendEventBuffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MyPlugin {
|
||||||
|
fn send_midi(&mut self) {
|
||||||
|
self.send_buffer
|
||||||
|
.send_events(self.recv_buffer.events().events(), &mut self.host);
|
||||||
|
self.recv_buffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for MyPlugin {
|
||||||
|
fn new(host: HostCallback) -> Self {
|
||||||
|
MyPlugin {
|
||||||
|
host,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_info(&self) -> Info {
|
||||||
|
Info {
|
||||||
|
name: "fwd_midi".to_string(),
|
||||||
|
unique_id: 7357001, // Used by hosts to differentiate between plugins.
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_events(&mut self, events: &api::Events) {
|
||||||
|
self.recv_buffer.store_events(events.events());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
||||||
|
for (input, output) in buffer.zip() {
|
||||||
|
for (in_sample, out_sample) in input.iter().zip(output) {
|
||||||
|
*out_sample = *in_sample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.send_midi();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_f64(&mut self, buffer: &mut AudioBuffer<f64>) {
|
||||||
|
for (input, output) in buffer.zip() {
|
||||||
|
for (in_sample, out_sample) in input.iter().zip(output) {
|
||||||
|
*out_sample = *in_sample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.send_midi();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_do(&self, can_do: CanDo) -> vst::api::Supported {
|
||||||
|
use vst::api::Supported::*;
|
||||||
|
use vst::plugin::CanDo::*;
|
||||||
|
|
||||||
|
match can_do {
|
||||||
|
SendEvents | SendMidiEvent | ReceiveEvents | ReceiveMidiEvent => Yes,
|
||||||
|
_ => No,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
deps/vst/examples/gain_effect.rs
vendored
Normal file
129
deps/vst/examples/gain_effect.rs
vendored
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
// author: doomy <notdoomy@protonmail.com>
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate vst;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use vst::prelude::*;
|
||||||
|
|
||||||
|
/// Simple Gain Effect.
|
||||||
|
/// Note that this does not use a proper scale for sound and shouldn't be used in
|
||||||
|
/// a production amplification effect! This is purely for demonstration purposes,
|
||||||
|
/// as well as to keep things simple as this is meant to be a starting point for
|
||||||
|
/// any effect.
|
||||||
|
struct GainEffect {
|
||||||
|
// Store a handle to the plugin's parameter object.
|
||||||
|
params: Arc<GainEffectParameters>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The plugin's parameter object contains the values of parameters that can be
|
||||||
|
/// adjusted from the host. If we were creating an effect that didn't allow the
|
||||||
|
/// user to modify it at runtime or have any controls, we could omit this part.
|
||||||
|
///
|
||||||
|
/// The parameters object is shared between the processing and GUI threads.
|
||||||
|
/// For this reason, all mutable state in the object has to be represented
|
||||||
|
/// through thread-safe interior mutability. The easiest way to achieve this
|
||||||
|
/// is to store the parameters in atomic containers.
|
||||||
|
struct GainEffectParameters {
|
||||||
|
// The plugin's state consists of a single parameter: amplitude.
|
||||||
|
amplitude: AtomicFloat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GainEffectParameters {
|
||||||
|
fn default() -> GainEffectParameters {
|
||||||
|
GainEffectParameters {
|
||||||
|
amplitude: AtomicFloat::new(0.5),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All plugins using `vst` also need to implement the `Plugin` trait. Here, we
|
||||||
|
// define functions that give necessary info to our host.
|
||||||
|
impl Plugin for GainEffect {
|
||||||
|
fn new(_host: HostCallback) -> Self {
|
||||||
|
// Note that controls will always return a value from 0 - 1.
|
||||||
|
// Setting a default to 0.5 means it's halfway up.
|
||||||
|
GainEffect {
|
||||||
|
params: Arc::new(GainEffectParameters::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_info(&self) -> Info {
|
||||||
|
Info {
|
||||||
|
name: "Gain Effect in Rust".to_string(),
|
||||||
|
vendor: "Rust DSP".to_string(),
|
||||||
|
unique_id: 243723072,
|
||||||
|
version: 1,
|
||||||
|
inputs: 2,
|
||||||
|
outputs: 2,
|
||||||
|
// This `parameters` bit is important; without it, none of our
|
||||||
|
// parameters will be shown!
|
||||||
|
parameters: 1,
|
||||||
|
category: Category::Effect,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here is where the bulk of our audio processing code goes.
|
||||||
|
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
||||||
|
// Read the amplitude from the parameter object
|
||||||
|
let amplitude = self.params.amplitude.get();
|
||||||
|
// First, we destructure our audio buffer into an arbitrary number of
|
||||||
|
// input and output buffers. Usually, we'll be dealing with stereo (2 of each)
|
||||||
|
// but that might change.
|
||||||
|
for (input_buffer, output_buffer) in buffer.zip() {
|
||||||
|
// Next, we'll loop through each individual sample so we can apply the amplitude
|
||||||
|
// value to it.
|
||||||
|
for (input_sample, output_sample) in input_buffer.iter().zip(output_buffer) {
|
||||||
|
*output_sample = *input_sample * amplitude;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the parameter object. This method can be omitted if the
|
||||||
|
// plugin has no parameters.
|
||||||
|
fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
|
||||||
|
Arc::clone(&self.params) as Arc<dyn PluginParameters>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginParameters for GainEffectParameters {
|
||||||
|
// the `get_parameter` function reads the value of a parameter.
|
||||||
|
fn get_parameter(&self, index: i32) -> f32 {
|
||||||
|
match index {
|
||||||
|
0 => self.amplitude.get(),
|
||||||
|
_ => 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the `set_parameter` function sets the value of a parameter.
|
||||||
|
fn set_parameter(&self, index: i32, val: f32) {
|
||||||
|
#[allow(clippy::single_match)]
|
||||||
|
match index {
|
||||||
|
0 => self.amplitude.set(val),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is what will display underneath our control. We can
|
||||||
|
// format it into a string that makes the most since.
|
||||||
|
fn get_parameter_text(&self, index: i32) -> String {
|
||||||
|
match index {
|
||||||
|
0 => format!("{:.2}", (self.amplitude.get() - 0.5) * 2f32),
|
||||||
|
_ => "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This shows the control's name.
|
||||||
|
fn get_parameter_name(&self, index: i32) -> String {
|
||||||
|
match index {
|
||||||
|
0 => "Amplitude",
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This part is important! Without it, our plugin won't work.
|
||||||
|
plugin_main!(GainEffect);
|
||||||
248
deps/vst/examples/ladder_filter.rs
vendored
Normal file
248
deps/vst/examples/ladder_filter.rs
vendored
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
//! This zero-delay feedback filter is based on a 4-stage transistor ladder filter.
|
||||||
|
//! It follows the following equations:
|
||||||
|
//! x = input - tanh(self.res * self.vout[3])
|
||||||
|
//! vout[0] = self.params.g.get() * (tanh(x) - tanh(self.vout[0])) + self.s[0]
|
||||||
|
//! vout[1] = self.params.g.get() * (tanh(self.vout[0]) - tanh(self.vout[1])) + self.s[1]
|
||||||
|
//! vout[0] = self.params.g.get() * (tanh(self.vout[1]) - tanh(self.vout[2])) + self.s[2]
|
||||||
|
//! vout[0] = self.params.g.get() * (tanh(self.vout[2]) - tanh(self.vout[3])) + self.s[3]
|
||||||
|
//! since we can't easily solve a nonlinear equation,
|
||||||
|
//! Mystran's fixed-pivot method is used to approximate the tanh() parts.
|
||||||
|
//! Quality can be improved a lot by oversampling a bit.
|
||||||
|
//! Feedback is clipped independently of the input, so it doesn't disappear at high gains.
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate vst;
|
||||||
|
use std::f32::consts::PI;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use vst::prelude::*;
|
||||||
|
|
||||||
|
// this is a 4-pole filter with resonance, which is why there's 4 states and vouts
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct LadderFilter {
|
||||||
|
// Store a handle to the plugin's parameter object.
|
||||||
|
params: Arc<LadderParameters>,
|
||||||
|
// the output of the different filter stages
|
||||||
|
vout: [f32; 4],
|
||||||
|
// s is the "state" parameter. In an IIR it would be the last value from the filter
|
||||||
|
// In this we find it by trapezoidal integration to avoid the unit delay
|
||||||
|
s: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LadderParameters {
|
||||||
|
// the "cutoff" parameter. Determines how heavy filtering is
|
||||||
|
cutoff: AtomicFloat,
|
||||||
|
g: AtomicFloat,
|
||||||
|
// needed to calculate cutoff.
|
||||||
|
sample_rate: AtomicFloat,
|
||||||
|
// makes a peak at cutoff
|
||||||
|
res: AtomicFloat,
|
||||||
|
// used to choose where we want our output to be
|
||||||
|
poles: AtomicUsize,
|
||||||
|
// pole_value is just to be able to use get_parameter on poles
|
||||||
|
pole_value: AtomicFloat,
|
||||||
|
// a drive parameter. Just used to increase the volume, which results in heavier distortion
|
||||||
|
drive: AtomicFloat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LadderParameters {
|
||||||
|
fn default() -> LadderParameters {
|
||||||
|
LadderParameters {
|
||||||
|
cutoff: AtomicFloat::new(1000.),
|
||||||
|
res: AtomicFloat::new(2.),
|
||||||
|
poles: AtomicUsize::new(3),
|
||||||
|
pole_value: AtomicFloat::new(1.),
|
||||||
|
drive: AtomicFloat::new(0.),
|
||||||
|
sample_rate: AtomicFloat::new(44100.),
|
||||||
|
g: AtomicFloat::new(0.07135868),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// member methods for the struct
|
||||||
|
impl LadderFilter {
|
||||||
|
// the state needs to be updated after each process. Found by trapezoidal integration
|
||||||
|
fn update_state(&mut self) {
|
||||||
|
self.s[0] = 2. * self.vout[0] - self.s[0];
|
||||||
|
self.s[1] = 2. * self.vout[1] - self.s[1];
|
||||||
|
self.s[2] = 2. * self.vout[2] - self.s[2];
|
||||||
|
self.s[3] = 2. * self.vout[3] - self.s[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
// performs a complete filter process (mystran's method)
|
||||||
|
fn tick_pivotal(&mut self, input: f32) {
|
||||||
|
if self.params.drive.get() > 0. {
|
||||||
|
self.run_ladder_nonlinear(input * (self.params.drive.get() + 0.7));
|
||||||
|
} else {
|
||||||
|
//
|
||||||
|
self.run_ladder_linear(input);
|
||||||
|
}
|
||||||
|
self.update_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
// nonlinear ladder filter function with distortion.
|
||||||
|
fn run_ladder_nonlinear(&mut self, input: f32) {
|
||||||
|
let mut a = [1f32; 5];
|
||||||
|
let base = [input, self.s[0], self.s[1], self.s[2], self.s[3]];
|
||||||
|
// a[n] is the fixed-pivot approximation for tanh()
|
||||||
|
for n in 0..base.len() {
|
||||||
|
if base[n] != 0. {
|
||||||
|
a[n] = base[n].tanh() / base[n];
|
||||||
|
} else {
|
||||||
|
a[n] = 1.;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// denominators of solutions of individual stages. Simplifies the math a bit
|
||||||
|
let g0 = 1. / (1. + self.params.g.get() * a[1]);
|
||||||
|
let g1 = 1. / (1. + self.params.g.get() * a[2]);
|
||||||
|
let g2 = 1. / (1. + self.params.g.get() * a[3]);
|
||||||
|
let g3 = 1. / (1. + self.params.g.get() * a[4]);
|
||||||
|
// these are just factored out of the feedback solution. Makes the math way easier to read
|
||||||
|
let f3 = self.params.g.get() * a[3] * g3;
|
||||||
|
let f2 = self.params.g.get() * a[2] * g2 * f3;
|
||||||
|
let f1 = self.params.g.get() * a[1] * g1 * f2;
|
||||||
|
let f0 = self.params.g.get() * g0 * f1;
|
||||||
|
// outputs a 24db filter
|
||||||
|
self.vout[3] =
|
||||||
|
(f0 * input * a[0] + f1 * g0 * self.s[0] + f2 * g1 * self.s[1] + f3 * g2 * self.s[2] + g3 * self.s[3])
|
||||||
|
/ (f0 * self.params.res.get() * a[3] + 1.);
|
||||||
|
// since we know the feedback, we can solve the remaining outputs:
|
||||||
|
self.vout[0] = g0
|
||||||
|
* (self.params.g.get() * a[1] * (input * a[0] - self.params.res.get() * a[3] * self.vout[3]) + self.s[0]);
|
||||||
|
self.vout[1] = g1 * (self.params.g.get() * a[2] * self.vout[0] + self.s[1]);
|
||||||
|
self.vout[2] = g2 * (self.params.g.get() * a[3] * self.vout[1] + self.s[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// linear version without distortion
|
||||||
|
pub fn run_ladder_linear(&mut self, input: f32) {
|
||||||
|
// denominators of solutions of individual stages. Simplifies the math a bit
|
||||||
|
let g0 = 1. / (1. + self.params.g.get());
|
||||||
|
let g1 = self.params.g.get() * g0 * g0;
|
||||||
|
let g2 = self.params.g.get() * g1 * g0;
|
||||||
|
let g3 = self.params.g.get() * g2 * g0;
|
||||||
|
// outputs a 24db filter
|
||||||
|
self.vout[3] =
|
||||||
|
(g3 * self.params.g.get() * input + g0 * self.s[3] + g1 * self.s[2] + g2 * self.s[1] + g3 * self.s[0])
|
||||||
|
/ (g3 * self.params.g.get() * self.params.res.get() + 1.);
|
||||||
|
// since we know the feedback, we can solve the remaining outputs:
|
||||||
|
self.vout[0] = g0 * (self.params.g.get() * (input - self.params.res.get() * self.vout[3]) + self.s[0]);
|
||||||
|
self.vout[1] = g0 * (self.params.g.get() * self.vout[0] + self.s[1]);
|
||||||
|
self.vout[2] = g0 * (self.params.g.get() * self.vout[1] + self.s[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LadderParameters {
|
||||||
|
pub fn set_cutoff(&self, value: f32) {
|
||||||
|
// cutoff formula gives us a natural feeling cutoff knob that spends more time in the low frequencies
|
||||||
|
self.cutoff.set(20000. * (1.8f32.powf(10. * value - 10.)));
|
||||||
|
// bilinear transformation for g gives us a very accurate cutoff
|
||||||
|
self.g.set((PI * self.cutoff.get() / (self.sample_rate.get())).tan());
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the value used to set cutoff. for get_parameter function
|
||||||
|
pub fn get_cutoff(&self) -> f32 {
|
||||||
|
1. + 0.17012975 * (0.00005 * self.cutoff.get()).ln()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_poles(&self, value: f32) {
|
||||||
|
self.pole_value.set(value);
|
||||||
|
self.poles.store(((value * 3.).round()) as usize, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginParameters for LadderParameters {
|
||||||
|
// get_parameter has to return the value used in set_parameter
|
||||||
|
fn get_parameter(&self, index: i32) -> f32 {
|
||||||
|
match index {
|
||||||
|
0 => self.get_cutoff(),
|
||||||
|
1 => self.res.get() / 4.,
|
||||||
|
2 => self.pole_value.get(),
|
||||||
|
3 => self.drive.get() / 5.,
|
||||||
|
_ => 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_parameter(&self, index: i32, value: f32) {
|
||||||
|
match index {
|
||||||
|
0 => self.set_cutoff(value),
|
||||||
|
1 => self.res.set(value * 4.),
|
||||||
|
2 => self.set_poles(value),
|
||||||
|
3 => self.drive.set(value * 5.),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parameter_name(&self, index: i32) -> String {
|
||||||
|
match index {
|
||||||
|
0 => "cutoff".to_string(),
|
||||||
|
1 => "resonance".to_string(),
|
||||||
|
2 => "filter order".to_string(),
|
||||||
|
3 => "drive".to_string(),
|
||||||
|
_ => "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parameter_label(&self, index: i32) -> String {
|
||||||
|
match index {
|
||||||
|
0 => "Hz".to_string(),
|
||||||
|
1 => "%".to_string(),
|
||||||
|
2 => "poles".to_string(),
|
||||||
|
3 => "%".to_string(),
|
||||||
|
_ => "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This is what will display underneath our control. We can
|
||||||
|
// format it into a string that makes the most sense.
|
||||||
|
fn get_parameter_text(&self, index: i32) -> String {
|
||||||
|
match index {
|
||||||
|
0 => format!("{:.0}", self.cutoff.get()),
|
||||||
|
1 => format!("{:.3}", self.res.get()),
|
||||||
|
2 => format!("{}", self.poles.load(Ordering::Relaxed) + 1),
|
||||||
|
3 => format!("{:.3}", self.drive.get()),
|
||||||
|
_ => format!(""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for LadderFilter {
|
||||||
|
fn new(_host: HostCallback) -> Self {
|
||||||
|
LadderFilter {
|
||||||
|
vout: [0f32; 4],
|
||||||
|
s: [0f32; 4],
|
||||||
|
params: Arc::new(LadderParameters::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_sample_rate(&mut self, rate: f32) {
|
||||||
|
self.params.sample_rate.set(rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_info(&self) -> Info {
|
||||||
|
Info {
|
||||||
|
name: "LadderFilter".to_string(),
|
||||||
|
unique_id: 9263,
|
||||||
|
inputs: 1,
|
||||||
|
outputs: 1,
|
||||||
|
category: Category::Effect,
|
||||||
|
parameters: 4,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
||||||
|
for (input_buffer, output_buffer) in buffer.zip() {
|
||||||
|
for (input_sample, output_sample) in input_buffer.iter().zip(output_buffer) {
|
||||||
|
self.tick_pivotal(*input_sample);
|
||||||
|
// the poles parameter chooses which filter stage we take our output from.
|
||||||
|
*output_sample = self.vout[self.params.poles.load(Ordering::Relaxed)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
|
||||||
|
Arc::clone(&self.params) as Arc<dyn PluginParameters>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin_main!(LadderFilter);
|
||||||
63
deps/vst/examples/simple_host.rs
vendored
Normal file
63
deps/vst/examples/simple_host.rs
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
extern crate vst;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use vst::host::{Host, PluginLoader};
|
||||||
|
use vst::plugin::Plugin;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct SampleHost;
|
||||||
|
|
||||||
|
impl Host for SampleHost {
|
||||||
|
fn automate(&self, index: i32, value: f32) {
|
||||||
|
println!("Parameter {} had its value changed to {}", index, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() < 2 {
|
||||||
|
println!("usage: simple_host path/to/vst");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = Path::new(&args[1]);
|
||||||
|
|
||||||
|
// Create the host
|
||||||
|
let host = Arc::new(Mutex::new(SampleHost));
|
||||||
|
|
||||||
|
println!("Loading {}...", path.to_str().unwrap());
|
||||||
|
|
||||||
|
// Load the plugin
|
||||||
|
let mut loader =
|
||||||
|
PluginLoader::load(path, Arc::clone(&host)).unwrap_or_else(|e| panic!("Failed to load plugin: {}", e));
|
||||||
|
|
||||||
|
// Create an instance of the plugin
|
||||||
|
let mut instance = loader.instance().unwrap();
|
||||||
|
|
||||||
|
// Get the plugin information
|
||||||
|
let info = instance.get_info();
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Loaded '{}':\n\t\
|
||||||
|
Vendor: {}\n\t\
|
||||||
|
Presets: {}\n\t\
|
||||||
|
Parameters: {}\n\t\
|
||||||
|
VST ID: {}\n\t\
|
||||||
|
Version: {}\n\t\
|
||||||
|
Initial Delay: {} samples",
|
||||||
|
info.name, info.vendor, info.presets, info.parameters, info.unique_id, info.version, info.initial_delay
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize the instance
|
||||||
|
instance.init();
|
||||||
|
println!("Initialized instance!");
|
||||||
|
|
||||||
|
println!("Closing instance...");
|
||||||
|
// Close the instance. This is not necessary as the instance is shut down when
|
||||||
|
// it is dropped as it goes out of scope.
|
||||||
|
// drop(instance);
|
||||||
|
}
|
||||||
160
deps/vst/examples/sine_synth.rs
vendored
Normal file
160
deps/vst/examples/sine_synth.rs
vendored
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
// author: Rob Saunders <hello@robsaunders.io>
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate vst;
|
||||||
|
|
||||||
|
use vst::prelude::*;
|
||||||
|
|
||||||
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
|
/// Convert the midi note's pitch into the equivalent frequency.
|
||||||
|
///
|
||||||
|
/// This function assumes A4 is 440hz.
|
||||||
|
fn midi_pitch_to_freq(pitch: u8) -> f64 {
|
||||||
|
const A4_PITCH: i8 = 69;
|
||||||
|
const A4_FREQ: f64 = 440.0;
|
||||||
|
|
||||||
|
// Midi notes can be 0-127
|
||||||
|
((f64::from(pitch as i8 - A4_PITCH)) / 12.).exp2() * A4_FREQ
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SineSynth {
|
||||||
|
sample_rate: f64,
|
||||||
|
time: f64,
|
||||||
|
note_duration: f64,
|
||||||
|
note: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SineSynth {
|
||||||
|
fn time_per_sample(&self) -> f64 {
|
||||||
|
1.0 / self.sample_rate
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process an incoming midi event.
|
||||||
|
///
|
||||||
|
/// The midi data is split up like so:
|
||||||
|
///
|
||||||
|
/// `data[0]`: Contains the status and the channel. Source: [source]
|
||||||
|
/// `data[1]`: Contains the supplemental data for the message - so, if this was a NoteOn then
|
||||||
|
/// this would contain the note.
|
||||||
|
/// `data[2]`: Further supplemental data. Would be velocity in the case of a NoteOn message.
|
||||||
|
///
|
||||||
|
/// [source]: http://www.midimountain.com/midi/midi_status.htm
|
||||||
|
fn process_midi_event(&mut self, data: [u8; 3]) {
|
||||||
|
match data[0] {
|
||||||
|
128 => self.note_off(data[1]),
|
||||||
|
144 => self.note_on(data[1]),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_on(&mut self, note: u8) {
|
||||||
|
self.note_duration = 0.0;
|
||||||
|
self.note = Some(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_off(&mut self, note: u8) {
|
||||||
|
if self.note == Some(note) {
|
||||||
|
self.note = None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const TAU: f64 = PI * 2.0;
|
||||||
|
|
||||||
|
impl Plugin for SineSynth {
|
||||||
|
fn new(_host: HostCallback) -> Self {
|
||||||
|
SineSynth {
|
||||||
|
sample_rate: 44100.0,
|
||||||
|
note_duration: 0.0,
|
||||||
|
time: 0.0,
|
||||||
|
note: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_info(&self) -> Info {
|
||||||
|
Info {
|
||||||
|
name: "SineSynth".to_string(),
|
||||||
|
vendor: "DeathDisco".to_string(),
|
||||||
|
unique_id: 6667,
|
||||||
|
category: Category::Synth,
|
||||||
|
inputs: 2,
|
||||||
|
outputs: 2,
|
||||||
|
parameters: 0,
|
||||||
|
initial_delay: 0,
|
||||||
|
..Info::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
#[allow(clippy::single_match)]
|
||||||
|
fn process_events(&mut self, events: &Events) {
|
||||||
|
for event in events.events() {
|
||||||
|
match event {
|
||||||
|
Event::Midi(ev) => self.process_midi_event(ev.data),
|
||||||
|
// More events can be handled here.
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_sample_rate(&mut self, rate: f32) {
|
||||||
|
self.sample_rate = f64::from(rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
||||||
|
let samples = buffer.samples();
|
||||||
|
let (_, mut outputs) = buffer.split();
|
||||||
|
let output_count = outputs.len();
|
||||||
|
let per_sample = self.time_per_sample();
|
||||||
|
let mut output_sample;
|
||||||
|
for sample_idx in 0..samples {
|
||||||
|
let time = self.time;
|
||||||
|
let note_duration = self.note_duration;
|
||||||
|
if let Some(current_note) = self.note {
|
||||||
|
let signal = (time * midi_pitch_to_freq(current_note) * TAU).sin();
|
||||||
|
|
||||||
|
// Apply a quick envelope to the attack of the signal to avoid popping.
|
||||||
|
let attack = 0.5;
|
||||||
|
let alpha = if note_duration < attack {
|
||||||
|
note_duration / attack
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
output_sample = (signal * alpha) as f32;
|
||||||
|
|
||||||
|
self.time += per_sample;
|
||||||
|
self.note_duration += per_sample;
|
||||||
|
} else {
|
||||||
|
output_sample = 0.0;
|
||||||
|
}
|
||||||
|
for buf_idx in 0..output_count {
|
||||||
|
let buff = outputs.get_mut(buf_idx);
|
||||||
|
buff[sample_idx] = output_sample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_do(&self, can_do: CanDo) -> Supported {
|
||||||
|
match can_do {
|
||||||
|
CanDo::ReceiveMidiEvent => Supported::Yes,
|
||||||
|
_ => Supported::Maybe,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin_main!(SineSynth);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use midi_pitch_to_freq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_midi_pitch_to_freq() {
|
||||||
|
for i in 0..127 {
|
||||||
|
// expect no panics
|
||||||
|
midi_pitch_to_freq(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
136
deps/vst/examples/transfer_and_smooth.rs
vendored
Normal file
136
deps/vst/examples/transfer_and_smooth.rs
vendored
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
// This example illustrates how an existing plugin can be ported to the new,
|
||||||
|
// thread-safe API with the help of the ParameterTransfer struct.
|
||||||
|
// It shows how the parameter iteration feature of ParameterTransfer can be
|
||||||
|
// used to react explicitly to parameter changes in an efficient way (here,
|
||||||
|
// to implement smoothing of parameters).
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate vst;
|
||||||
|
|
||||||
|
use std::f32;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use vst::prelude::*;
|
||||||
|
|
||||||
|
const PARAMETER_COUNT: usize = 100;
|
||||||
|
const BASE_FREQUENCY: f32 = 5.0;
|
||||||
|
const FILTER_FACTOR: f32 = 0.01; // Set this to 1.0 to disable smoothing.
|
||||||
|
const TWO_PI: f32 = 2.0 * f32::consts::PI;
|
||||||
|
|
||||||
|
// 1. Define a struct to hold parameters. Put a ParameterTransfer inside it,
|
||||||
|
// plus optionally a HostCallback.
|
||||||
|
struct MyPluginParameters {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
host: HostCallback,
|
||||||
|
transfer: ParameterTransfer,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Put an Arc reference to your parameter struct in your main Plugin struct.
|
||||||
|
struct MyPlugin {
|
||||||
|
params: Arc<MyPluginParameters>,
|
||||||
|
states: Vec<Smoothed>,
|
||||||
|
sample_rate: f32,
|
||||||
|
phase: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Implement PluginParameters for your parameter struct.
|
||||||
|
// The set_parameter and get_parameter just access the ParameterTransfer.
|
||||||
|
// The other methods can be implemented on top of this as well.
|
||||||
|
impl PluginParameters for MyPluginParameters {
|
||||||
|
fn set_parameter(&self, index: i32, value: f32) {
|
||||||
|
self.transfer.set_parameter(index as usize, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parameter(&self, index: i32) -> f32 {
|
||||||
|
self.transfer.get_parameter(index as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for MyPlugin {
|
||||||
|
fn new(host: HostCallback) -> Self {
|
||||||
|
MyPlugin {
|
||||||
|
// 4. Initialize your main Plugin struct with a parameter struct
|
||||||
|
// wrapped in an Arc, and put the HostCallback inside it.
|
||||||
|
params: Arc::new(MyPluginParameters {
|
||||||
|
host,
|
||||||
|
transfer: ParameterTransfer::new(PARAMETER_COUNT),
|
||||||
|
}),
|
||||||
|
states: vec![Smoothed::default(); PARAMETER_COUNT],
|
||||||
|
sample_rate: 44100.0,
|
||||||
|
phase: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_info(&self) -> Info {
|
||||||
|
Info {
|
||||||
|
parameters: PARAMETER_COUNT as i32,
|
||||||
|
inputs: 0,
|
||||||
|
outputs: 2,
|
||||||
|
category: Category::Synth,
|
||||||
|
f64_precision: false,
|
||||||
|
|
||||||
|
name: "transfer_and_smooth".to_string(),
|
||||||
|
vendor: "Loonies".to_string(),
|
||||||
|
unique_id: 0x500007,
|
||||||
|
version: 100,
|
||||||
|
|
||||||
|
..Info::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Return a reference to the parameter struct from get_parameter_object.
|
||||||
|
fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
|
||||||
|
Arc::clone(&self.params) as Arc<dyn PluginParameters>
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_sample_rate(&mut self, sample_rate: f32) {
|
||||||
|
self.sample_rate = sample_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
||||||
|
// 6. In the process method, iterate over changed parameters and do
|
||||||
|
// for each what you would previously do in set_parameter. Since this
|
||||||
|
// runs in the processing thread, it has mutable access to the Plugin.
|
||||||
|
for (p, value) in self.params.transfer.iterate(true) {
|
||||||
|
// Example: Update filter state of changed parameter.
|
||||||
|
self.states[p].set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Dummy synth adding together a bunch of sines.
|
||||||
|
let samples = buffer.samples();
|
||||||
|
let mut outputs = buffer.split().1;
|
||||||
|
for i in 0..samples {
|
||||||
|
let mut sum = 0.0;
|
||||||
|
for p in 0..PARAMETER_COUNT {
|
||||||
|
let amp = self.states[p].get();
|
||||||
|
if amp != 0.0 {
|
||||||
|
sum += (self.phase * p as f32 * TWO_PI).sin() * amp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outputs[0][i] = sum;
|
||||||
|
outputs[1][i] = sum;
|
||||||
|
self.phase = (self.phase + BASE_FREQUENCY / self.sample_rate).fract();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Parameter smoothing as an example of non-trivial parameter handling
|
||||||
|
// that has to happen when a parameter changes.
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct Smoothed {
|
||||||
|
state: f32,
|
||||||
|
target: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Smoothed {
|
||||||
|
fn set(&mut self, value: f32) {
|
||||||
|
self.target = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&mut self) -> f32 {
|
||||||
|
self.state += (self.target - self.state) * FILTER_FACTOR;
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin_main!(MyPlugin);
|
||||||
61
deps/vst/osx_vst_bundler.sh
vendored
Executable file
61
deps/vst/osx_vst_bundler.sh
vendored
Executable file
|
|
@ -0,0 +1,61 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Make sure we have the arguments we need
|
||||||
|
if [[ -z $1 || -z $2 ]]; then
|
||||||
|
echo "Generates a macOS bundle from a compiled dylib file"
|
||||||
|
echo "Example:"
|
||||||
|
echo -e "\t$0 Plugin target/release/plugin.dylib"
|
||||||
|
echo -e "\tCreates a Plugin.vst bundle"
|
||||||
|
else
|
||||||
|
# Make the bundle folder
|
||||||
|
mkdir -p "$1.vst/Contents/MacOS"
|
||||||
|
|
||||||
|
# Create the PkgInfo
|
||||||
|
echo "BNDL????" > "$1.vst/Contents/PkgInfo"
|
||||||
|
|
||||||
|
#build the Info.Plist
|
||||||
|
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
||||||
|
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
|
||||||
|
<plist version=\"1.0\">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>English</string>
|
||||||
|
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$1</string>
|
||||||
|
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>vst</string>
|
||||||
|
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string></string>
|
||||||
|
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.rust-vst.$1</string>
|
||||||
|
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$1</string>
|
||||||
|
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>BNDL</string>
|
||||||
|
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>$((RANDOM % 9999))</string>
|
||||||
|
|
||||||
|
<key>CSResourcesFileMapped</key>
|
||||||
|
<string></string>
|
||||||
|
|
||||||
|
</dict>
|
||||||
|
</plist>" > "$1.vst/Contents/Info.plist"
|
||||||
|
|
||||||
|
# move the provided library to the correct location
|
||||||
|
cp "$2" "$1.vst/Contents/MacOS/$1"
|
||||||
|
|
||||||
|
echo "Created bundle $1.vst"
|
||||||
|
fi
|
||||||
1
deps/vst/rustfmt.toml
vendored
Normal file
1
deps/vst/rustfmt.toml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
max_width = 120
|
||||||
927
deps/vst/src/api.rs
vendored
Normal file
927
deps/vst/src/api.rs
vendored
Normal file
|
|
@ -0,0 +1,927 @@
|
||||||
|
//! Structures and types for interfacing with the VST 2.4 API.
|
||||||
|
|
||||||
|
use std::os::raw::c_void;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use self::consts::*;
|
||||||
|
use crate::{
|
||||||
|
editor::Editor,
|
||||||
|
plugin::{Info, Plugin, PluginParameters},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Constant values
|
||||||
|
#[allow(missing_docs)] // For obvious constants
|
||||||
|
pub mod consts {
|
||||||
|
|
||||||
|
pub const MAX_PRESET_NAME_LEN: usize = 24;
|
||||||
|
pub const MAX_PARAM_STR_LEN: usize = 32;
|
||||||
|
pub const MAX_LABEL: usize = 64;
|
||||||
|
pub const MAX_SHORT_LABEL: usize = 8;
|
||||||
|
pub const MAX_PRODUCT_STR_LEN: usize = 64;
|
||||||
|
pub const MAX_VENDOR_STR_LEN: usize = 64;
|
||||||
|
|
||||||
|
/// VST plugins are identified by a magic number. This corresponds to 0x56737450.
|
||||||
|
pub const VST_MAGIC: i32 = ('V' as i32) << 24 | ('s' as i32) << 16 | ('t' as i32) << 8 | ('P' as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `VSTPluginMain` function signature.
|
||||||
|
pub type PluginMain = fn(callback: HostCallbackProc) -> *mut AEffect;
|
||||||
|
|
||||||
|
/// Host callback function passed to plugin.
|
||||||
|
/// Can be used to query host information from plugin side.
|
||||||
|
pub type HostCallbackProc =
|
||||||
|
extern "C" fn(effect: *mut AEffect, opcode: i32, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize;
|
||||||
|
|
||||||
|
/// Dispatcher function used to process opcodes. Called by host.
|
||||||
|
pub type DispatcherProc =
|
||||||
|
extern "C" fn(effect: *mut AEffect, opcode: i32, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize;
|
||||||
|
|
||||||
|
/// Process function used to process 32 bit floating point samples. Called by host.
|
||||||
|
pub type ProcessProc =
|
||||||
|
extern "C" fn(effect: *mut AEffect, inputs: *const *const f32, outputs: *mut *mut f32, sample_frames: i32);
|
||||||
|
|
||||||
|
/// Process function used to process 64 bit floating point samples. Called by host.
|
||||||
|
pub type ProcessProcF64 =
|
||||||
|
extern "C" fn(effect: *mut AEffect, inputs: *const *const f64, outputs: *mut *mut f64, sample_frames: i32);
|
||||||
|
|
||||||
|
/// Callback function used to set parameter values. Called by host.
|
||||||
|
pub type SetParameterProc = extern "C" fn(effect: *mut AEffect, index: i32, parameter: f32);
|
||||||
|
|
||||||
|
/// Callback function used to get parameter values. Called by host.
|
||||||
|
pub type GetParameterProc = extern "C" fn(effect: *mut AEffect, index: i32) -> f32;
|
||||||
|
|
||||||
|
/// Used with the VST API to pass around plugin information.
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct AEffect {
|
||||||
|
/// Magic number. Must be `['V', 'S', 'T', 'P']`.
|
||||||
|
pub magic: i32,
|
||||||
|
|
||||||
|
/// Host to plug-in dispatcher.
|
||||||
|
pub dispatcher: DispatcherProc,
|
||||||
|
|
||||||
|
/// Accumulating process mode is deprecated in VST 2.4! Use `processReplacing` instead!
|
||||||
|
pub _process: ProcessProc,
|
||||||
|
|
||||||
|
/// Set value of automatable parameter.
|
||||||
|
pub setParameter: SetParameterProc,
|
||||||
|
|
||||||
|
/// Get value of automatable parameter.
|
||||||
|
pub getParameter: GetParameterProc,
|
||||||
|
|
||||||
|
/// Number of programs (Presets).
|
||||||
|
pub numPrograms: i32,
|
||||||
|
|
||||||
|
/// Number of parameters. All programs are assumed to have this many parameters.
|
||||||
|
pub numParams: i32,
|
||||||
|
|
||||||
|
/// Number of audio inputs.
|
||||||
|
pub numInputs: i32,
|
||||||
|
|
||||||
|
/// Number of audio outputs.
|
||||||
|
pub numOutputs: i32,
|
||||||
|
|
||||||
|
/// Bitmask made of values from `api::PluginFlags`.
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use vst::api::PluginFlags;
|
||||||
|
/// let flags = PluginFlags::CAN_REPLACING | PluginFlags::CAN_DOUBLE_REPLACING;
|
||||||
|
/// // ...
|
||||||
|
/// ```
|
||||||
|
pub flags: i32,
|
||||||
|
|
||||||
|
/// Reserved for host, must be 0.
|
||||||
|
pub reserved1: isize,
|
||||||
|
|
||||||
|
/// Reserved for host, must be 0.
|
||||||
|
pub reserved2: isize,
|
||||||
|
|
||||||
|
/// For algorithms which need input in the first place (Group delay or latency in samples).
|
||||||
|
///
|
||||||
|
/// This value should be initially in a resume state.
|
||||||
|
pub initialDelay: i32,
|
||||||
|
|
||||||
|
/// Deprecated unused member.
|
||||||
|
pub _realQualities: i32,
|
||||||
|
|
||||||
|
/// Deprecated unused member.
|
||||||
|
pub _offQualities: i32,
|
||||||
|
|
||||||
|
/// Deprecated unused member.
|
||||||
|
pub _ioRatio: f32,
|
||||||
|
|
||||||
|
/// Void pointer usable by api to store object data.
|
||||||
|
pub object: *mut c_void,
|
||||||
|
|
||||||
|
/// User defined pointer.
|
||||||
|
pub user: *mut c_void,
|
||||||
|
|
||||||
|
/// Registered unique identifier (register it at Steinberg 3rd party support Web).
|
||||||
|
/// This is used to identify a plug-in during save+load of preset and project.
|
||||||
|
pub uniqueId: i32,
|
||||||
|
|
||||||
|
/// Plug-in version (e.g. 1100 for v1.1.0.0).
|
||||||
|
pub version: i32,
|
||||||
|
|
||||||
|
/// Process audio samples in replacing mode.
|
||||||
|
pub processReplacing: ProcessProc,
|
||||||
|
|
||||||
|
/// Process double-precision audio samples in replacing mode.
|
||||||
|
pub processReplacingF64: ProcessProcF64,
|
||||||
|
|
||||||
|
/// Reserved for future use (please zero).
|
||||||
|
pub future: [u8; 56],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AEffect {
|
||||||
|
/// Return handle to Plugin object. Only works for plugins created using this library.
|
||||||
|
/// Caller is responsible for not calling this function concurrently.
|
||||||
|
// Suppresses warning about returning a reference to a box
|
||||||
|
#[allow(clippy::borrowed_box)]
|
||||||
|
pub unsafe fn get_plugin(&self) -> &mut Box<dyn Plugin> {
|
||||||
|
//FIXME: find a way to do this without resorting to transmuting via a box
|
||||||
|
&mut *(self.object as *mut Box<dyn Plugin>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return handle to Info object. Only works for plugins created using this library.
|
||||||
|
pub unsafe fn get_info(&self) -> &Info {
|
||||||
|
&(*(self.user as *mut super::PluginCache)).info
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return handle to PluginParameters object. Only works for plugins created using this library.
|
||||||
|
pub unsafe fn get_params(&self) -> &Arc<dyn PluginParameters> {
|
||||||
|
&(*(self.user as *mut super::PluginCache)).params
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return handle to Editor object. Only works for plugins created using this library.
|
||||||
|
/// Caller is responsible for not calling this function concurrently.
|
||||||
|
pub unsafe fn get_editor(&self) -> &mut Option<Box<dyn Editor>> {
|
||||||
|
&mut (*(self.user as *mut super::PluginCache)).editor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop the Plugin object. Only works for plugins created using this library.
|
||||||
|
pub unsafe fn drop_plugin(&mut self) {
|
||||||
|
drop(Box::from_raw(self.object as *mut Box<dyn Plugin>));
|
||||||
|
drop(Box::from_raw(self.user as *mut super::PluginCache));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about a channel. Only some hosts use this information.
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct ChannelProperties {
|
||||||
|
/// Channel name.
|
||||||
|
pub name: [u8; MAX_LABEL as usize],
|
||||||
|
|
||||||
|
/// Flags found in `ChannelFlags`.
|
||||||
|
pub flags: i32,
|
||||||
|
|
||||||
|
/// Type of speaker arrangement this channel is a part of.
|
||||||
|
pub arrangement_type: SpeakerArrangementType,
|
||||||
|
|
||||||
|
/// Name of channel (recommended: 6 characters + delimiter).
|
||||||
|
pub short_name: [u8; MAX_SHORT_LABEL as usize],
|
||||||
|
|
||||||
|
/// Reserved for future use.
|
||||||
|
pub future: [u8; 48],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tells the host how the channels are intended to be used in the plugin. Only useful for some
|
||||||
|
/// hosts.
|
||||||
|
#[repr(i32)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum SpeakerArrangementType {
|
||||||
|
/// User defined arrangement.
|
||||||
|
Custom = -2,
|
||||||
|
/// Empty arrangement.
|
||||||
|
Empty = -1,
|
||||||
|
|
||||||
|
/// Mono.
|
||||||
|
Mono = 0,
|
||||||
|
|
||||||
|
/// L R
|
||||||
|
Stereo,
|
||||||
|
/// Ls Rs
|
||||||
|
StereoSurround,
|
||||||
|
/// Lc Rc
|
||||||
|
StereoCenter,
|
||||||
|
/// Sl Sr
|
||||||
|
StereoSide,
|
||||||
|
/// C Lfe
|
||||||
|
StereoCLfe,
|
||||||
|
|
||||||
|
/// L R C
|
||||||
|
Cinema30,
|
||||||
|
/// L R S
|
||||||
|
Music30,
|
||||||
|
|
||||||
|
/// L R C Lfe
|
||||||
|
Cinema31,
|
||||||
|
/// L R Lfe S
|
||||||
|
Music31,
|
||||||
|
|
||||||
|
/// L R C S (LCRS)
|
||||||
|
Cinema40,
|
||||||
|
/// L R Ls Rs (Quadro)
|
||||||
|
Music40,
|
||||||
|
|
||||||
|
/// L R C Lfe S (LCRS + Lfe)
|
||||||
|
Cinema41,
|
||||||
|
/// L R Lfe Ls Rs (Quadro + Lfe)
|
||||||
|
Music41,
|
||||||
|
|
||||||
|
/// L R C Ls Rs
|
||||||
|
Surround50,
|
||||||
|
/// L R C Lfe Ls Rs
|
||||||
|
Surround51,
|
||||||
|
|
||||||
|
/// L R C Ls Rs Cs
|
||||||
|
Cinema60,
|
||||||
|
/// L R Ls Rs Sl Sr
|
||||||
|
Music60,
|
||||||
|
|
||||||
|
/// L R C Lfe Ls Rs Cs
|
||||||
|
Cinema61,
|
||||||
|
/// L R Lfe Ls Rs Sl Sr
|
||||||
|
Music61,
|
||||||
|
|
||||||
|
/// L R C Ls Rs Lc Rc
|
||||||
|
Cinema70,
|
||||||
|
/// L R C Ls Rs Sl Sr
|
||||||
|
Music70,
|
||||||
|
|
||||||
|
/// L R C Lfe Ls Rs Lc Rc
|
||||||
|
Cinema71,
|
||||||
|
/// L R C Lfe Ls Rs Sl Sr
|
||||||
|
Music71,
|
||||||
|
|
||||||
|
/// L R C Ls Rs Lc Rc Cs
|
||||||
|
Cinema80,
|
||||||
|
/// L R C Ls Rs Cs Sl Sr
|
||||||
|
Music80,
|
||||||
|
|
||||||
|
/// L R C Lfe Ls Rs Lc Rc Cs
|
||||||
|
Cinema81,
|
||||||
|
/// L R C Lfe Ls Rs Cs Sl Sr
|
||||||
|
Music81,
|
||||||
|
|
||||||
|
/// L R C Lfe Ls Rs Tfl Tfc Tfr Trl Trr Lfe2
|
||||||
|
Surround102,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used to specify whether functionality is supported.
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
pub enum Supported {
|
||||||
|
Yes,
|
||||||
|
Maybe,
|
||||||
|
No,
|
||||||
|
Custom(isize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Supported {
|
||||||
|
/// Create a `Supported` value from an integer if possible.
|
||||||
|
pub fn from(val: isize) -> Option<Supported> {
|
||||||
|
use self::Supported::*;
|
||||||
|
|
||||||
|
match val {
|
||||||
|
1 => Some(Yes),
|
||||||
|
0 => Some(Maybe),
|
||||||
|
-1 => Some(No),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<isize> for Supported {
|
||||||
|
/// Convert to integer ordinal for interop with VST api.
|
||||||
|
fn into(self) -> isize {
|
||||||
|
use self::Supported::*;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Yes => 1,
|
||||||
|
Maybe => 0,
|
||||||
|
No => -1,
|
||||||
|
Custom(i) => i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Denotes in which thread the host is in.
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum ProcessLevel {
|
||||||
|
/// Unsupported by host.
|
||||||
|
Unknown = 0,
|
||||||
|
|
||||||
|
/// GUI thread.
|
||||||
|
User,
|
||||||
|
/// Audio process thread.
|
||||||
|
Realtime,
|
||||||
|
/// Sequence thread (MIDI, etc).
|
||||||
|
Prefetch,
|
||||||
|
/// Offline processing thread (therefore GUI/user thread).
|
||||||
|
Offline,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Language that the host is using.
|
||||||
|
#[repr(i32)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub enum HostLanguage {
|
||||||
|
English = 1,
|
||||||
|
German,
|
||||||
|
French,
|
||||||
|
Italian,
|
||||||
|
Spanish,
|
||||||
|
Japanese,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The file operation to perform.
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum FileSelectCommand {
|
||||||
|
/// Load a file.
|
||||||
|
Load = 0,
|
||||||
|
/// Save a file.
|
||||||
|
Save,
|
||||||
|
/// Load multiple files simultaneously.
|
||||||
|
LoadMultipleFiles,
|
||||||
|
/// Choose a directory.
|
||||||
|
SelectDirectory,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: investigate removing this.
|
||||||
|
/// Format to select files.
|
||||||
|
pub enum FileSelectType {
|
||||||
|
/// Regular file selector.
|
||||||
|
Regular,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// File type descriptor.
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct FileType {
|
||||||
|
/// Display name of file type.
|
||||||
|
pub name: [u8; 128],
|
||||||
|
|
||||||
|
/// OS X file type.
|
||||||
|
pub osx_type: [u8; 8],
|
||||||
|
/// Windows file type.
|
||||||
|
pub win_type: [u8; 8],
|
||||||
|
/// Unix file type.
|
||||||
|
pub nix_type: [u8; 8],
|
||||||
|
|
||||||
|
/// MIME type.
|
||||||
|
pub mime_type_1: [u8; 128],
|
||||||
|
/// Additional MIME type.
|
||||||
|
pub mime_type_2: [u8; 128],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// File selector descriptor used in `host::OpCode::OpenFileSelector`.
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct FileSelect {
|
||||||
|
/// The type of file selection to perform.
|
||||||
|
pub command: FileSelectCommand,
|
||||||
|
/// The file selector to open.
|
||||||
|
pub select_type: FileSelectType,
|
||||||
|
/// Unknown. 0 = no creator.
|
||||||
|
pub mac_creator: i32,
|
||||||
|
/// Number of file types.
|
||||||
|
pub num_types: i32,
|
||||||
|
/// List of file types to show.
|
||||||
|
pub file_types: *mut FileType,
|
||||||
|
|
||||||
|
/// File selector's title.
|
||||||
|
pub title: [u8; 1024],
|
||||||
|
/// Initial path.
|
||||||
|
pub initial_path: *mut u8,
|
||||||
|
/// Used when operation returns a single path.
|
||||||
|
pub return_path: *mut u8,
|
||||||
|
/// Size of the path buffer in bytes.
|
||||||
|
pub size_return_path: i32,
|
||||||
|
|
||||||
|
/// Used when operation returns multiple paths.
|
||||||
|
pub return_multiple_paths: *mut *mut u8,
|
||||||
|
/// Number of paths returned.
|
||||||
|
pub num_paths: i32,
|
||||||
|
|
||||||
|
/// Reserved by host.
|
||||||
|
pub reserved: isize,
|
||||||
|
/// Reserved for future use.
|
||||||
|
pub future: [u8; 116],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A struct which contains events.
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct Events {
|
||||||
|
/// Number of events.
|
||||||
|
pub num_events: i32,
|
||||||
|
|
||||||
|
/// Reserved for future use. Should be 0.
|
||||||
|
pub _reserved: isize,
|
||||||
|
|
||||||
|
/// Variable-length array of pointers to `api::Event` objects.
|
||||||
|
///
|
||||||
|
/// The VST standard specifies a variable length array of initial size 2. If there are more
|
||||||
|
/// than 2 elements a larger array must be stored in this structure.
|
||||||
|
pub events: [*mut Event; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Events {
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn events_raw(&self) -> &[*const Event] {
|
||||||
|
use std::slice;
|
||||||
|
unsafe {
|
||||||
|
slice::from_raw_parts(
|
||||||
|
&self.events[0] as *const *mut _ as *const *const _,
|
||||||
|
self.num_events as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn events_raw_mut(&mut self) -> &mut [*const SysExEvent] {
|
||||||
|
use std::slice;
|
||||||
|
unsafe {
|
||||||
|
slice::from_raw_parts_mut(
|
||||||
|
&mut self.events[0] as *mut *mut _ as *mut *const _,
|
||||||
|
self.num_events as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use this in your impl of process_events() to process the incoming midi events.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # use vst::plugin::{Info, Plugin, HostCallback};
|
||||||
|
/// # use vst::buffer::{AudioBuffer, SendEventBuffer};
|
||||||
|
/// # use vst::host::Host;
|
||||||
|
/// # use vst::api;
|
||||||
|
/// # use vst::event::{Event, MidiEvent};
|
||||||
|
/// # struct ExamplePlugin { host: HostCallback, send_buf: SendEventBuffer }
|
||||||
|
/// # impl Plugin for ExamplePlugin {
|
||||||
|
/// # fn new(host: HostCallback) -> Self { Self { host, send_buf: Default::default() } }
|
||||||
|
/// #
|
||||||
|
/// # fn get_info(&self) -> Info { Default::default() }
|
||||||
|
/// #
|
||||||
|
/// fn process_events(&mut self, events: &api::Events) {
|
||||||
|
/// for e in events.events() {
|
||||||
|
/// match e {
|
||||||
|
/// Event::Midi(MidiEvent { data, .. }) => {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
/// _ => ()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
#[allow(clippy::needless_lifetimes)]
|
||||||
|
pub fn events<'a>(&'a self) -> impl Iterator<Item = crate::event::Event<'a>> {
|
||||||
|
self.events_raw()
|
||||||
|
.iter()
|
||||||
|
.map(|ptr| unsafe { crate::event::Event::from_raw_event(*ptr) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of event that has occurred. See `api::Event.event_type`.
|
||||||
|
#[repr(i32)]
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum EventType {
|
||||||
|
/// Value used for uninitialized placeholder events.
|
||||||
|
_Placeholder = 0,
|
||||||
|
|
||||||
|
/// Midi event. See `api::MidiEvent`.
|
||||||
|
Midi = 1,
|
||||||
|
|
||||||
|
/// Deprecated.
|
||||||
|
_Audio,
|
||||||
|
/// Deprecated.
|
||||||
|
_Video,
|
||||||
|
/// Deprecated.
|
||||||
|
_Parameter,
|
||||||
|
/// Deprecated.
|
||||||
|
_Trigger,
|
||||||
|
|
||||||
|
/// System exclusive event. See `api::SysExEvent`.
|
||||||
|
SysEx,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A VST event intended to be casted to a corresponding type.
|
||||||
|
///
|
||||||
|
/// The event types are not all guaranteed to be the same size,
|
||||||
|
/// so casting between them can be done
|
||||||
|
/// via `mem::transmute()` while leveraging pointers, e.g.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use vst::api::{Event, EventType, MidiEvent, SysExEvent};
|
||||||
|
/// # let mut event: *mut Event = &mut unsafe { std::mem::zeroed() };
|
||||||
|
/// // let event: *const Event = ...;
|
||||||
|
/// let midi_event: &MidiEvent = unsafe { std::mem::transmute(event) };
|
||||||
|
/// ```
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct Event {
|
||||||
|
/// The type of event. This lets you know which event this object should be casted to.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use vst::api::{Event, EventType, MidiEvent, SysExEvent};
|
||||||
|
/// #
|
||||||
|
/// # // Valid for test
|
||||||
|
/// # let mut event: *mut Event = &mut unsafe { std::mem::zeroed() };
|
||||||
|
/// #
|
||||||
|
/// // let mut event: *mut Event = ...
|
||||||
|
/// match unsafe { (*event).event_type } {
|
||||||
|
/// EventType::Midi => {
|
||||||
|
/// let midi_event: &MidiEvent = unsafe {
|
||||||
|
/// std::mem::transmute(event)
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
/// EventType::SysEx => {
|
||||||
|
/// let sys_event: &SysExEvent = unsafe {
|
||||||
|
/// std::mem::transmute(event)
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
/// // ...
|
||||||
|
/// # _ => {}
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub event_type: EventType,
|
||||||
|
|
||||||
|
/// Size of this structure; `mem::sizeof::<Event>()`.
|
||||||
|
pub byte_size: i32,
|
||||||
|
|
||||||
|
/// Number of samples into the current processing block that this event occurs on.
|
||||||
|
///
|
||||||
|
/// E.g. if the block size is 512 and this value is 123, the event will occur on sample
|
||||||
|
/// `samples[123]`.
|
||||||
|
pub delta_frames: i32,
|
||||||
|
|
||||||
|
/// Generic flags, none defined in VST api yet.
|
||||||
|
pub _flags: i32,
|
||||||
|
|
||||||
|
/// The `Event` type is cast appropriately, so this acts as reserved space.
|
||||||
|
///
|
||||||
|
/// The actual size of the data may vary
|
||||||
|
///as this type is not guaranteed to be the same size as the other event types.
|
||||||
|
pub _reserved: [u8; 16],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A midi event.
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct MidiEvent {
|
||||||
|
/// Should be `EventType::Midi`.
|
||||||
|
pub event_type: EventType,
|
||||||
|
|
||||||
|
/// Size of this structure; `mem::sizeof::<MidiEvent>()`.
|
||||||
|
pub byte_size: i32,
|
||||||
|
|
||||||
|
/// Number of samples into the current processing block that this event occurs on.
|
||||||
|
///
|
||||||
|
/// E.g. if the block size is 512 and this value is 123, the event will occur on sample
|
||||||
|
/// `samples[123]`.
|
||||||
|
pub delta_frames: i32,
|
||||||
|
|
||||||
|
/// See `MidiEventFlags`.
|
||||||
|
pub flags: i32,
|
||||||
|
|
||||||
|
/// Length in sample frames of entire note if available, otherwise 0.
|
||||||
|
pub note_length: i32,
|
||||||
|
|
||||||
|
/// Offset in samples into note from start if available, otherwise 0.
|
||||||
|
pub note_offset: i32,
|
||||||
|
|
||||||
|
/// 1 to 3 midi bytes. TODO: Doc
|
||||||
|
pub midi_data: [u8; 3],
|
||||||
|
|
||||||
|
/// Reserved midi byte (0).
|
||||||
|
pub _midi_reserved: u8,
|
||||||
|
|
||||||
|
/// Detuning between -63 and +64 cents,
|
||||||
|
/// for scales other than 'well-tempered'. e.g. 'microtuning'
|
||||||
|
pub detune: i8,
|
||||||
|
|
||||||
|
/// Note off velocity between 0 and 127.
|
||||||
|
pub note_off_velocity: u8,
|
||||||
|
|
||||||
|
/// Reserved for future use. Should be 0.
|
||||||
|
pub _reserved1: u8,
|
||||||
|
/// Reserved for future use. Should be 0.
|
||||||
|
pub _reserved2: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A midi system exclusive event.
|
||||||
|
///
|
||||||
|
/// This event only contains raw byte data, and is up to the plugin to interpret it correctly.
|
||||||
|
/// `plugin::CanDo` has a `ReceiveSysExEvent` variant which lets the host query the plugin as to
|
||||||
|
/// whether this event is supported.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SysExEvent {
|
||||||
|
/// Should be `EventType::SysEx`.
|
||||||
|
pub event_type: EventType,
|
||||||
|
|
||||||
|
/// Size of this structure; `mem::sizeof::<SysExEvent>()`.
|
||||||
|
pub byte_size: i32,
|
||||||
|
|
||||||
|
/// Number of samples into the current processing block that this event occurs on.
|
||||||
|
///
|
||||||
|
/// E.g. if the block size is 512 and this value is 123, the event will occur on sample
|
||||||
|
/// `samples[123]`.
|
||||||
|
pub delta_frames: i32,
|
||||||
|
|
||||||
|
/// Generic flags, none defined in VST api yet.
|
||||||
|
pub _flags: i32,
|
||||||
|
|
||||||
|
/// Size of payload in bytes.
|
||||||
|
pub data_size: i32,
|
||||||
|
|
||||||
|
/// Reserved for future use. Should be 0.
|
||||||
|
pub _reserved1: isize,
|
||||||
|
|
||||||
|
/// Pointer to payload.
|
||||||
|
pub system_data: *mut u8,
|
||||||
|
|
||||||
|
/// Reserved for future use. Should be 0.
|
||||||
|
pub _reserved2: isize,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for SysExEvent {}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Default, Copy)]
|
||||||
|
/// Describes the time at the start of the block currently being processed
|
||||||
|
pub struct TimeInfo {
|
||||||
|
/// current Position in audio samples (always valid)
|
||||||
|
pub sample_pos: f64,
|
||||||
|
|
||||||
|
/// current Sample Rate in Hertz (always valid)
|
||||||
|
pub sample_rate: f64,
|
||||||
|
|
||||||
|
/// System Time in nanoseconds (10^-9 second)
|
||||||
|
pub nanoseconds: f64,
|
||||||
|
|
||||||
|
/// Musical Position, in Quarter Note (1.0 equals 1 Quarter Note)
|
||||||
|
pub ppq_pos: f64,
|
||||||
|
|
||||||
|
/// current Tempo in BPM (Beats Per Minute)
|
||||||
|
pub tempo: f64,
|
||||||
|
|
||||||
|
/// last Bar Start Position, in Quarter Note
|
||||||
|
pub bar_start_pos: f64,
|
||||||
|
|
||||||
|
/// Cycle Start (left locator), in Quarter Note
|
||||||
|
pub cycle_start_pos: f64,
|
||||||
|
|
||||||
|
/// Cycle End (right locator), in Quarter Note
|
||||||
|
pub cycle_end_pos: f64,
|
||||||
|
|
||||||
|
/// Time Signature Numerator (e.g. 3 for 3/4)
|
||||||
|
pub time_sig_numerator: i32,
|
||||||
|
|
||||||
|
/// Time Signature Denominator (e.g. 4 for 3/4)
|
||||||
|
pub time_sig_denominator: i32,
|
||||||
|
|
||||||
|
/// SMPTE offset in SMPTE subframes (bits; 1/80 of a frame).
|
||||||
|
/// The current SMPTE position can be calculated using `sample_pos`, `sample_rate`, and `smpte_frame_rate`.
|
||||||
|
pub smpte_offset: i32,
|
||||||
|
|
||||||
|
/// See `SmpteFrameRate`
|
||||||
|
pub smpte_frame_rate: SmpteFrameRate,
|
||||||
|
|
||||||
|
/// MIDI Clock Resolution (24 Per Quarter Note), can be negative (nearest clock)
|
||||||
|
pub samples_to_next_clock: i32,
|
||||||
|
|
||||||
|
/// See `TimeInfoFlags`
|
||||||
|
pub flags: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(i32)]
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
/// SMPTE Frame Rates.
|
||||||
|
pub enum SmpteFrameRate {
|
||||||
|
/// 24 fps
|
||||||
|
Smpte24fps = 0,
|
||||||
|
/// 25 fps
|
||||||
|
Smpte25fps = 1,
|
||||||
|
/// 29.97 fps
|
||||||
|
Smpte2997fps = 2,
|
||||||
|
/// 30 fps
|
||||||
|
Smpte30fps = 3,
|
||||||
|
|
||||||
|
/// 29.97 drop
|
||||||
|
Smpte2997dfps = 4,
|
||||||
|
/// 30 drop
|
||||||
|
Smpte30dfps = 5,
|
||||||
|
|
||||||
|
/// Film 16mm
|
||||||
|
SmpteFilm16mm = 6,
|
||||||
|
/// Film 35mm
|
||||||
|
SmpteFilm35mm = 7,
|
||||||
|
|
||||||
|
/// HDTV: 23.976 fps
|
||||||
|
Smpte239fps = 10,
|
||||||
|
/// HDTV: 24.976 fps
|
||||||
|
Smpte249fps = 11,
|
||||||
|
/// HDTV: 59.94 fps
|
||||||
|
Smpte599fps = 12,
|
||||||
|
/// HDTV: 60 fps
|
||||||
|
Smpte60fps = 13,
|
||||||
|
}
|
||||||
|
impl Default for SmpteFrameRate {
|
||||||
|
fn default() -> Self {
|
||||||
|
SmpteFrameRate::Smpte24fps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
/// Flags for VST channels.
|
||||||
|
pub struct ChannelFlags: i32 {
|
||||||
|
/// Indicates channel is active. Ignored by host.
|
||||||
|
const ACTIVE = 1;
|
||||||
|
/// Indicates channel is first of stereo pair.
|
||||||
|
const STEREO = 1 << 1;
|
||||||
|
/// Use channel's specified speaker_arrangement instead of stereo flag.
|
||||||
|
const SPEAKER = 1 << 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
/// Flags for VST plugins.
|
||||||
|
pub struct PluginFlags: i32 {
|
||||||
|
/// Plugin has an editor.
|
||||||
|
const HAS_EDITOR = 1;
|
||||||
|
/// Plugin can process 32 bit audio. (Mandatory in VST 2.4).
|
||||||
|
const CAN_REPLACING = 1 << 4;
|
||||||
|
/// Plugin preset data is handled in formatless chunks.
|
||||||
|
const PROGRAM_CHUNKS = 1 << 5;
|
||||||
|
/// Plugin is a synth.
|
||||||
|
const IS_SYNTH = 1 << 8;
|
||||||
|
/// Plugin does not produce sound when all input is silence.
|
||||||
|
const NO_SOUND_IN_STOP = 1 << 9;
|
||||||
|
/// Supports 64 bit audio processing.
|
||||||
|
const CAN_DOUBLE_REPLACING = 1 << 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
/// Cross platform modifier key flags.
|
||||||
|
pub struct ModifierKey: u8 {
|
||||||
|
/// Shift key.
|
||||||
|
const SHIFT = 1;
|
||||||
|
/// Alt key.
|
||||||
|
const ALT = 1 << 1;
|
||||||
|
/// Control on mac.
|
||||||
|
const COMMAND = 1 << 2;
|
||||||
|
/// Command on mac, ctrl on other.
|
||||||
|
const CONTROL = 1 << 3; // Ctrl on PC, Apple on Mac
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
/// MIDI event flags.
|
||||||
|
pub struct MidiEventFlags: i32 {
|
||||||
|
/// This event is played live (not in playback from a sequencer track). This allows the
|
||||||
|
/// plugin to handle these flagged events with higher priority, especially when the
|
||||||
|
/// plugin has a big latency as per `plugin::Info::initial_delay`.
|
||||||
|
const REALTIME_EVENT = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
/// Used in the `flags` field of `TimeInfo`, and for querying the host for specific values
|
||||||
|
pub struct TimeInfoFlags : i32 {
|
||||||
|
/// Indicates that play, cycle or record state has changed.
|
||||||
|
const TRANSPORT_CHANGED = 1;
|
||||||
|
/// Set if Host sequencer is currently playing.
|
||||||
|
const TRANSPORT_PLAYING = 1 << 1;
|
||||||
|
/// Set if Host sequencer is in cycle mode.
|
||||||
|
const TRANSPORT_CYCLE_ACTIVE = 1 << 2;
|
||||||
|
/// Set if Host sequencer is in record mode.
|
||||||
|
const TRANSPORT_RECORDING = 1 << 3;
|
||||||
|
|
||||||
|
/// Set if automation write mode active (record parameter changes).
|
||||||
|
const AUTOMATION_WRITING = 1 << 6;
|
||||||
|
/// Set if automation read mode active (play parameter changes).
|
||||||
|
const AUTOMATION_READING = 1 << 7;
|
||||||
|
|
||||||
|
/// Set if TimeInfo::nanoseconds is valid.
|
||||||
|
const NANOSECONDS_VALID = 1 << 8;
|
||||||
|
/// Set if TimeInfo::ppq_pos is valid.
|
||||||
|
const PPQ_POS_VALID = 1 << 9;
|
||||||
|
/// Set if TimeInfo::tempo is valid.
|
||||||
|
const TEMPO_VALID = 1 << 10;
|
||||||
|
/// Set if TimeInfo::bar_start_pos is valid.
|
||||||
|
const BARS_VALID = 1 << 11;
|
||||||
|
/// Set if both TimeInfo::cycle_start_pos and VstTimeInfo::cycle_end_pos are valid.
|
||||||
|
const CYCLE_POS_VALID = 1 << 12;
|
||||||
|
/// Set if both TimeInfo::time_sig_numerator and TimeInfo::time_sig_denominator are valid.
|
||||||
|
const TIME_SIG_VALID = 1 << 13;
|
||||||
|
/// Set if both TimeInfo::smpte_offset and VstTimeInfo::smpte_frame_rate are valid.
|
||||||
|
const SMPTE_VALID = 1 << 14;
|
||||||
|
/// Set if TimeInfo::samples_to_next_clock is valid.
|
||||||
|
const VST_CLOCK_VALID = 1 << 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::super::event;
|
||||||
|
use super::*;
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
// This container is used because we have to store somewhere the events
|
||||||
|
// that are pointed to by raw pointers in the events object. We heap allocate
|
||||||
|
// the event so the pointer in events stays consistent when the container is moved.
|
||||||
|
pub struct EventContainer {
|
||||||
|
stored_event: Box<Event>,
|
||||||
|
pub events: Events,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A convenience method which creates an api::Events object representing a midi event.
|
||||||
|
// This represents code that might be found in a VST host using this API.
|
||||||
|
fn encode_midi_message_as_events(message: [u8; 3]) -> EventContainer {
|
||||||
|
let midi_event: MidiEvent = MidiEvent {
|
||||||
|
event_type: EventType::Midi,
|
||||||
|
byte_size: mem::size_of::<MidiEvent>() as i32,
|
||||||
|
delta_frames: 0,
|
||||||
|
flags: 0,
|
||||||
|
note_length: 0,
|
||||||
|
note_offset: 0,
|
||||||
|
midi_data: [message[0], message[1], message[2]],
|
||||||
|
_midi_reserved: 0,
|
||||||
|
detune: 0,
|
||||||
|
note_off_velocity: 0,
|
||||||
|
_reserved1: 0,
|
||||||
|
_reserved2: 0,
|
||||||
|
};
|
||||||
|
let mut event: Event = unsafe { std::mem::transmute(midi_event) };
|
||||||
|
event.event_type = EventType::Midi;
|
||||||
|
|
||||||
|
let events = Events {
|
||||||
|
num_events: 1,
|
||||||
|
_reserved: 0,
|
||||||
|
events: [&mut event, &mut event], // Second one is a dummy
|
||||||
|
};
|
||||||
|
let mut ec = EventContainer {
|
||||||
|
stored_event: Box::new(event),
|
||||||
|
events,
|
||||||
|
};
|
||||||
|
ec.events.events[0] = &mut *(ec.stored_event); // Overwrite ptrs, since we moved the event into ec
|
||||||
|
ec
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_and_decode_gives_back_original_message() {
|
||||||
|
let message: [u8; 3] = [35, 16, 22];
|
||||||
|
let encoded = encode_midi_message_as_events(message);
|
||||||
|
assert_eq!(encoded.events.num_events, 1);
|
||||||
|
assert_eq!(encoded.events.events.len(), 2);
|
||||||
|
let e_vec: Vec<event::Event> = encoded.events.events().collect();
|
||||||
|
assert_eq!(e_vec.len(), 1);
|
||||||
|
|
||||||
|
match e_vec[0] {
|
||||||
|
event::Event::Midi(event::MidiEvent { data, .. }) => {
|
||||||
|
assert_eq!(data, message);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
panic!("Not a midi event!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a regression test for a bug fixed in PR #93
|
||||||
|
// We check here that calling events() on an api::Events object
|
||||||
|
// does not mutate the underlying events.
|
||||||
|
#[test]
|
||||||
|
fn message_survives_calling_events() {
|
||||||
|
let message: [u8; 3] = [35, 16, 22];
|
||||||
|
let encoded = encode_midi_message_as_events(message);
|
||||||
|
|
||||||
|
for e in encoded.events.events() {
|
||||||
|
match e {
|
||||||
|
event::Event::Midi(event::MidiEvent { data, .. }) => {
|
||||||
|
assert_eq!(data, message);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
panic!("Not a midi event!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for e in encoded.events.events() {
|
||||||
|
match e {
|
||||||
|
event::Event::Midi(event::MidiEvent { data, .. }) => {
|
||||||
|
assert_eq!(data, message);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
panic!("Not a midi event!"); // FAILS here!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
606
deps/vst/src/buffer.rs
vendored
Normal file
606
deps/vst/src/buffer.rs
vendored
Normal file
|
|
@ -0,0 +1,606 @@
|
||||||
|
//! Buffers to safely work with audio samples.
|
||||||
|
|
||||||
|
use num_traits::Float;
|
||||||
|
|
||||||
|
use std::slice;
|
||||||
|
|
||||||
|
/// `AudioBuffer` contains references to the audio buffers for all input and output channels.
|
||||||
|
///
|
||||||
|
/// To create an `AudioBuffer` in a host, use a [`HostBuffer`](../host/struct.HostBuffer.html).
|
||||||
|
pub struct AudioBuffer<'a, T: 'a + Float> {
|
||||||
|
inputs: &'a [*const T],
|
||||||
|
outputs: &'a mut [*mut T],
|
||||||
|
samples: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: 'a + Float> AudioBuffer<'a, T> {
|
||||||
|
/// Create an `AudioBuffer` from raw pointers.
|
||||||
|
/// Only really useful for interacting with the VST API.
|
||||||
|
#[inline]
|
||||||
|
pub unsafe fn from_raw(
|
||||||
|
input_count: usize,
|
||||||
|
output_count: usize,
|
||||||
|
inputs_raw: *const *const T,
|
||||||
|
outputs_raw: *mut *mut T,
|
||||||
|
samples: usize,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
inputs: slice::from_raw_parts(inputs_raw, input_count),
|
||||||
|
outputs: slice::from_raw_parts_mut(outputs_raw, output_count),
|
||||||
|
samples,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of input channels that this buffer was created for
|
||||||
|
#[inline]
|
||||||
|
pub fn input_count(&self) -> usize {
|
||||||
|
self.inputs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of output channels that this buffer was created for
|
||||||
|
#[inline]
|
||||||
|
pub fn output_count(&self) -> usize {
|
||||||
|
self.outputs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of samples in this buffer (same for all channels)
|
||||||
|
#[inline]
|
||||||
|
pub fn samples(&self) -> usize {
|
||||||
|
self.samples
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The raw inputs to pass to processReplacing
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn raw_inputs(&self) -> &[*const T] {
|
||||||
|
self.inputs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The raw outputs to pass to processReplacing
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn raw_outputs(&mut self) -> &mut [*mut T] {
|
||||||
|
&mut self.outputs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split this buffer into separate inputs and outputs.
|
||||||
|
#[inline]
|
||||||
|
pub fn split<'b>(&'b mut self) -> (Inputs<'b, T>, Outputs<'b, T>)
|
||||||
|
where
|
||||||
|
'a: 'b,
|
||||||
|
{
|
||||||
|
(
|
||||||
|
Inputs {
|
||||||
|
bufs: self.inputs,
|
||||||
|
samples: self.samples,
|
||||||
|
},
|
||||||
|
Outputs {
|
||||||
|
bufs: self.outputs,
|
||||||
|
samples: self.samples,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an iterator over pairs of input buffers and output buffers.
|
||||||
|
#[inline]
|
||||||
|
pub fn zip<'b>(&'b mut self) -> AudioBufferIterator<'a, 'b, T> {
|
||||||
|
AudioBufferIterator {
|
||||||
|
audio_buffer: self,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator over pairs of buffers of input channels and output channels.
|
||||||
|
pub struct AudioBufferIterator<'a, 'b, T>
|
||||||
|
where
|
||||||
|
T: 'a + Float,
|
||||||
|
'a: 'b,
|
||||||
|
{
|
||||||
|
audio_buffer: &'b mut AudioBuffer<'a, T>,
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, T> Iterator for AudioBufferIterator<'a, 'b, T>
|
||||||
|
where
|
||||||
|
T: 'b + Float,
|
||||||
|
{
|
||||||
|
type Item = (&'b [T], &'b mut [T]);
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.index < self.audio_buffer.inputs.len() && self.index < self.audio_buffer.outputs.len() {
|
||||||
|
let input =
|
||||||
|
unsafe { slice::from_raw_parts(self.audio_buffer.inputs[self.index], self.audio_buffer.samples) };
|
||||||
|
let output =
|
||||||
|
unsafe { slice::from_raw_parts_mut(self.audio_buffer.outputs[self.index], self.audio_buffer.samples) };
|
||||||
|
let val = (input, output);
|
||||||
|
self.index += 1;
|
||||||
|
Some(val)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::ops::{Index, IndexMut};
|
||||||
|
|
||||||
|
/// Wrapper type to access the buffers for the input channels of an `AudioBuffer` in a safe way.
|
||||||
|
/// Behaves like a slice.
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct Inputs<'a, T: 'a> {
|
||||||
|
bufs: &'a [*const T],
|
||||||
|
samples: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Inputs<'a, T> {
|
||||||
|
/// Number of channels
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.bufs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the buffer is empty
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.len() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access channel at the given index
|
||||||
|
pub fn get(&self, i: usize) -> &'a [T] {
|
||||||
|
unsafe { slice::from_raw_parts(self.bufs[i], self.samples) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split borrowing at the given index, like for slices
|
||||||
|
pub fn split_at(&self, i: usize) -> (Inputs<'a, T>, Inputs<'a, T>) {
|
||||||
|
let (l, r) = self.bufs.split_at(i);
|
||||||
|
(
|
||||||
|
Inputs {
|
||||||
|
bufs: l,
|
||||||
|
samples: self.samples,
|
||||||
|
},
|
||||||
|
Inputs {
|
||||||
|
bufs: r,
|
||||||
|
samples: self.samples,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Index<usize> for Inputs<'a, T> {
|
||||||
|
type Output = [T];
|
||||||
|
|
||||||
|
fn index(&self, i: usize) -> &Self::Output {
|
||||||
|
self.get(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator over buffers for input channels of an `AudioBuffer`.
|
||||||
|
pub struct InputIterator<'a, T: 'a> {
|
||||||
|
data: Inputs<'a, T>,
|
||||||
|
i: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Iterator for InputIterator<'a, T> {
|
||||||
|
type Item = &'a [T];
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.i < self.data.len() {
|
||||||
|
let val = self.data.get(self.i);
|
||||||
|
self.i += 1;
|
||||||
|
Some(val)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Sized> IntoIterator for Inputs<'a, T> {
|
||||||
|
type Item = &'a [T];
|
||||||
|
type IntoIter = InputIterator<'a, T>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
InputIterator { data: self, i: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper type to access the buffers for the output channels of an `AudioBuffer` in a safe way.
|
||||||
|
/// Behaves like a slice.
|
||||||
|
pub struct Outputs<'a, T: 'a> {
|
||||||
|
bufs: &'a [*mut T],
|
||||||
|
samples: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Outputs<'a, T> {
|
||||||
|
/// Number of channels
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.bufs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the buffer is empty
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.len() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access channel at the given index
|
||||||
|
pub fn get(&self, i: usize) -> &'a [T] {
|
||||||
|
unsafe { slice::from_raw_parts(self.bufs[i], self.samples) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mutably access channel at the given index
|
||||||
|
pub fn get_mut(&mut self, i: usize) -> &'a mut [T] {
|
||||||
|
unsafe { slice::from_raw_parts_mut(self.bufs[i], self.samples) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split borrowing at the given index, like for slices
|
||||||
|
pub fn split_at_mut(self, i: usize) -> (Outputs<'a, T>, Outputs<'a, T>) {
|
||||||
|
let (l, r) = self.bufs.split_at(i);
|
||||||
|
(
|
||||||
|
Outputs {
|
||||||
|
bufs: l,
|
||||||
|
samples: self.samples,
|
||||||
|
},
|
||||||
|
Outputs {
|
||||||
|
bufs: r,
|
||||||
|
samples: self.samples,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Index<usize> for Outputs<'a, T> {
|
||||||
|
type Output = [T];
|
||||||
|
|
||||||
|
fn index(&self, i: usize) -> &Self::Output {
|
||||||
|
self.get(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> IndexMut<usize> for Outputs<'a, T> {
|
||||||
|
fn index_mut(&mut self, i: usize) -> &mut Self::Output {
|
||||||
|
self.get_mut(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator over buffers for output channels of an `AudioBuffer`.
|
||||||
|
pub struct OutputIterator<'a, 'b, T>
|
||||||
|
where
|
||||||
|
T: 'a,
|
||||||
|
'a: 'b,
|
||||||
|
{
|
||||||
|
data: &'b mut Outputs<'a, T>,
|
||||||
|
i: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, T> Iterator for OutputIterator<'a, 'b, T>
|
||||||
|
where
|
||||||
|
T: 'b,
|
||||||
|
{
|
||||||
|
type Item = &'b mut [T];
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.i < self.data.len() {
|
||||||
|
let val = self.data.get_mut(self.i);
|
||||||
|
self.i += 1;
|
||||||
|
Some(val)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, T: Sized> IntoIterator for &'b mut Outputs<'a, T> {
|
||||||
|
type Item = &'b mut [T];
|
||||||
|
type IntoIter = OutputIterator<'a, 'b, T>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
OutputIterator { data: self, i: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::event::{Event, MidiEvent, SysExEvent};
|
||||||
|
|
||||||
|
/// This is used as a placeholder to pre-allocate space for a fixed number of
|
||||||
|
/// midi events in the re-useable `SendEventBuffer`, because `SysExEvent` is
|
||||||
|
/// larger than `MidiEvent`, so either one can be stored in a `SysExEvent`.
|
||||||
|
pub type PlaceholderEvent = api::SysExEvent;
|
||||||
|
|
||||||
|
/// This trait is used by `SendEventBuffer::send_events` to accept iterators over midi events
|
||||||
|
pub trait WriteIntoPlaceholder {
|
||||||
|
/// writes an event into the given placeholder memory location
|
||||||
|
fn write_into(&self, out: &mut PlaceholderEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: WriteIntoPlaceholder> WriteIntoPlaceholder for &'a T {
|
||||||
|
fn write_into(&self, out: &mut PlaceholderEvent) {
|
||||||
|
(*self).write_into(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WriteIntoPlaceholder for MidiEvent {
|
||||||
|
fn write_into(&self, out: &mut PlaceholderEvent) {
|
||||||
|
let out = unsafe { &mut *(out as *mut _ as *mut _) };
|
||||||
|
*out = api::MidiEvent {
|
||||||
|
event_type: api::EventType::Midi,
|
||||||
|
byte_size: mem::size_of::<api::MidiEvent>() as i32,
|
||||||
|
delta_frames: self.delta_frames,
|
||||||
|
flags: if self.live {
|
||||||
|
api::MidiEventFlags::REALTIME_EVENT.bits()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
note_length: self.note_length.unwrap_or(0),
|
||||||
|
note_offset: self.note_offset.unwrap_or(0),
|
||||||
|
midi_data: self.data,
|
||||||
|
_midi_reserved: 0,
|
||||||
|
detune: self.detune,
|
||||||
|
note_off_velocity: self.note_off_velocity,
|
||||||
|
_reserved1: 0,
|
||||||
|
_reserved2: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> WriteIntoPlaceholder for SysExEvent<'a> {
|
||||||
|
fn write_into(&self, out: &mut PlaceholderEvent) {
|
||||||
|
*out = PlaceholderEvent {
|
||||||
|
event_type: api::EventType::SysEx,
|
||||||
|
byte_size: mem::size_of::<PlaceholderEvent>() as i32,
|
||||||
|
delta_frames: self.delta_frames,
|
||||||
|
_flags: 0,
|
||||||
|
data_size: self.payload.len() as i32,
|
||||||
|
_reserved1: 0,
|
||||||
|
system_data: self.payload.as_ptr() as *const u8 as *mut u8,
|
||||||
|
_reserved2: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> WriteIntoPlaceholder for Event<'a> {
|
||||||
|
fn write_into(&self, out: &mut PlaceholderEvent) {
|
||||||
|
match *self {
|
||||||
|
Event::Midi(ref ev) => {
|
||||||
|
ev.write_into(out);
|
||||||
|
}
|
||||||
|
Event::SysEx(ref ev) => {
|
||||||
|
ev.write_into(out);
|
||||||
|
}
|
||||||
|
Event::Deprecated(e) => {
|
||||||
|
let out = unsafe { &mut *(out as *mut _ as *mut _) };
|
||||||
|
*out = e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::{api, host::Host};
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
/// This buffer is used for sending midi events through the VST interface.
|
||||||
|
/// The purpose of this is to convert outgoing midi events from `event::Event` to `api::Events`.
|
||||||
|
/// It only allocates memory in new() and reuses the memory between calls.
|
||||||
|
pub struct SendEventBuffer {
|
||||||
|
buf: Vec<u8>,
|
||||||
|
api_events: Vec<PlaceholderEvent>, // using SysExEvent to store both because it's larger than MidiEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SendEventBuffer {
|
||||||
|
fn default() -> Self {
|
||||||
|
SendEventBuffer::new(1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendEventBuffer {
|
||||||
|
/// Creates a buffer for sending up to the given number of midi events per frame
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn new(capacity: usize) -> Self {
|
||||||
|
let header_size = mem::size_of::<api::Events>() - (mem::size_of::<*mut api::Event>() * 2);
|
||||||
|
let body_size = mem::size_of::<*mut api::Event>() * capacity;
|
||||||
|
let mut buf = vec![0u8; header_size + body_size];
|
||||||
|
let api_events = vec![unsafe { mem::zeroed::<PlaceholderEvent>() }; capacity];
|
||||||
|
{
|
||||||
|
let ptrs = {
|
||||||
|
let e = Self::buf_as_api_events(&mut buf);
|
||||||
|
e.num_events = capacity as i32;
|
||||||
|
e.events_raw_mut()
|
||||||
|
};
|
||||||
|
for (ptr, event) in ptrs.iter_mut().zip(&api_events) {
|
||||||
|
let (ptr, event): (&mut *const PlaceholderEvent, &PlaceholderEvent) = (ptr, event);
|
||||||
|
*ptr = event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self { buf, api_events }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends events to the host. See the `fwd_midi` example.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # use vst::plugin::{Info, Plugin, HostCallback};
|
||||||
|
/// # use vst::buffer::{AudioBuffer, SendEventBuffer};
|
||||||
|
/// # use vst::host::Host;
|
||||||
|
/// # use vst::event::*;
|
||||||
|
/// # struct ExamplePlugin { host: HostCallback, send_buffer: SendEventBuffer }
|
||||||
|
/// # impl Plugin for ExamplePlugin {
|
||||||
|
/// # fn new(host: HostCallback) -> Self { Self { host, send_buffer: Default::default() } }
|
||||||
|
/// #
|
||||||
|
/// # fn get_info(&self) -> Info { Default::default() }
|
||||||
|
/// #
|
||||||
|
/// fn process(&mut self, buffer: &mut AudioBuffer<f32>){
|
||||||
|
/// let events: Vec<MidiEvent> = vec![
|
||||||
|
/// // ...
|
||||||
|
/// ];
|
||||||
|
/// self.send_buffer.send_events(&events, &mut self.host);
|
||||||
|
/// }
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn send_events<T: IntoIterator<Item = U>, U: WriteIntoPlaceholder>(&mut self, events: T, host: &mut dyn Host) {
|
||||||
|
self.store_events(events);
|
||||||
|
host.process_events(self.events());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores events in the buffer, replacing the buffer's current content.
|
||||||
|
/// Use this in [`process_events`](crate::Plugin::process_events) to store received input events, then read them in [`process`](crate::Plugin::process) using [`events`](SendEventBuffer::events).
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn store_events<T: IntoIterator<Item = U>, U: WriteIntoPlaceholder>(&mut self, events: T) {
|
||||||
|
#[allow(clippy::suspicious_map)]
|
||||||
|
let count = events
|
||||||
|
.into_iter()
|
||||||
|
.zip(self.api_events.iter_mut())
|
||||||
|
.map(|(ev, out)| ev.write_into(out))
|
||||||
|
.count();
|
||||||
|
self.set_num_events(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the stored events
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn events(&self) -> &api::Events {
|
||||||
|
#[allow(clippy::cast_ptr_alignment)]
|
||||||
|
unsafe {
|
||||||
|
&*(self.buf.as_ptr() as *const api::Events)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the buffer
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.set_num_events(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn buf_as_api_events(buf: &mut [u8]) -> &mut api::Events {
|
||||||
|
#[allow(clippy::cast_ptr_alignment)]
|
||||||
|
unsafe {
|
||||||
|
&mut *(buf.as_mut_ptr() as *mut api::Events)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn set_num_events(&mut self, events_len: usize) {
|
||||||
|
use std::cmp::min;
|
||||||
|
let e = Self::buf_as_api_events(&mut self.buf);
|
||||||
|
e.num_events = min(self.api_events.len(), events_len) as i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::buffer::AudioBuffer;
|
||||||
|
|
||||||
|
/// Size of buffers used in tests.
|
||||||
|
const SIZE: usize = 1024;
|
||||||
|
|
||||||
|
/// Test that creating and zipping buffers works.
|
||||||
|
///
|
||||||
|
/// This test creates a channel for 2 inputs and 2 outputs.
|
||||||
|
/// The input channels are simply values
|
||||||
|
/// from 0 to `SIZE-1` (e.g. [0, 1, 2, 3, 4, .. , SIZE - 1])
|
||||||
|
/// and the output channels are just 0.
|
||||||
|
/// This test assures that when the buffers are zipped together,
|
||||||
|
/// the input values do not change.
|
||||||
|
#[test]
|
||||||
|
fn buffer_zip() {
|
||||||
|
let in1: Vec<f32> = (0..SIZE).map(|x| x as f32).collect();
|
||||||
|
let in2 = in1.clone();
|
||||||
|
|
||||||
|
let mut out1 = vec![0.0; SIZE];
|
||||||
|
let mut out2 = out1.clone();
|
||||||
|
|
||||||
|
let inputs = vec![in1.as_ptr(), in2.as_ptr()];
|
||||||
|
let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr()];
|
||||||
|
let mut buffer = unsafe { AudioBuffer::from_raw(2, 2, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) };
|
||||||
|
|
||||||
|
for (input, output) in buffer.zip() {
|
||||||
|
input.iter().zip(output.iter_mut()).fold(0, |acc, (input, output)| {
|
||||||
|
assert_eq!(*input, acc as f32);
|
||||||
|
assert_eq!(*output, 0.0);
|
||||||
|
acc + 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that the `zip()` method returns an iterator that gives `n` elements
|
||||||
|
// where n is the number of inputs when this is lower than the number of outputs.
|
||||||
|
#[test]
|
||||||
|
fn buffer_zip_fewer_inputs_than_outputs() {
|
||||||
|
let in1 = vec![1.0; SIZE];
|
||||||
|
let in2 = vec![2.0; SIZE];
|
||||||
|
|
||||||
|
let mut out1 = vec![3.0; SIZE];
|
||||||
|
let mut out2 = vec![4.0; SIZE];
|
||||||
|
let mut out3 = vec![5.0; SIZE];
|
||||||
|
|
||||||
|
let inputs = vec![in1.as_ptr(), in2.as_ptr()];
|
||||||
|
let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr(), out3.as_mut_ptr()];
|
||||||
|
let mut buffer = unsafe { AudioBuffer::from_raw(2, 3, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) };
|
||||||
|
|
||||||
|
let mut iter = buffer.zip();
|
||||||
|
if let Some((observed_in1, observed_out1)) = iter.next() {
|
||||||
|
assert_eq!(1.0, observed_in1[0]);
|
||||||
|
assert_eq!(3.0, observed_out1[0]);
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((observed_in2, observed_out2)) = iter.next() {
|
||||||
|
assert_eq!(2.0, observed_in2[0]);
|
||||||
|
assert_eq!(4.0, observed_out2[0]);
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(None, iter.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that the `zip()` method returns an iterator that gives `n` elements
|
||||||
|
// where n is the number of outputs when this is lower than the number of inputs.
|
||||||
|
#[test]
|
||||||
|
fn buffer_zip_more_inputs_than_outputs() {
|
||||||
|
let in1 = vec![1.0; SIZE];
|
||||||
|
let in2 = vec![2.0; SIZE];
|
||||||
|
let in3 = vec![3.0; SIZE];
|
||||||
|
|
||||||
|
let mut out1 = vec![4.0; SIZE];
|
||||||
|
let mut out2 = vec![5.0; SIZE];
|
||||||
|
|
||||||
|
let inputs = vec![in1.as_ptr(), in2.as_ptr(), in3.as_ptr()];
|
||||||
|
let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr()];
|
||||||
|
let mut buffer = unsafe { AudioBuffer::from_raw(3, 2, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) };
|
||||||
|
|
||||||
|
let mut iter = buffer.zip();
|
||||||
|
|
||||||
|
if let Some((observed_in1, observed_out1)) = iter.next() {
|
||||||
|
assert_eq!(1.0, observed_in1[0]);
|
||||||
|
assert_eq!(4.0, observed_out1[0]);
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((observed_in2, observed_out2)) = iter.next() {
|
||||||
|
assert_eq!(2.0, observed_in2[0]);
|
||||||
|
assert_eq!(5.0, observed_out2[0]);
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(None, iter.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that creating buffers from raw pointers works.
|
||||||
|
#[test]
|
||||||
|
fn from_raw() {
|
||||||
|
let in1: Vec<f32> = (0..SIZE).map(|x| x as f32).collect();
|
||||||
|
let in2 = in1.clone();
|
||||||
|
|
||||||
|
let mut out1 = vec![0.0; SIZE];
|
||||||
|
let mut out2 = out1.clone();
|
||||||
|
|
||||||
|
let inputs = vec![in1.as_ptr(), in2.as_ptr()];
|
||||||
|
let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr()];
|
||||||
|
let mut buffer = unsafe { AudioBuffer::from_raw(2, 2, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) };
|
||||||
|
|
||||||
|
for (input, output) in buffer.zip() {
|
||||||
|
input.iter().zip(output.iter_mut()).fold(0, |acc, (input, output)| {
|
||||||
|
assert_eq!(*input, acc as f32);
|
||||||
|
assert_eq!(*output, 0.0);
|
||||||
|
acc + 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
deps/vst/src/cache.rs
vendored
Normal file
19
deps/vst/src/cache.rs
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{editor::Editor, prelude::*};
|
||||||
|
|
||||||
|
pub(crate) struct PluginCache {
|
||||||
|
pub info: Info,
|
||||||
|
pub params: Arc<dyn PluginParameters>,
|
||||||
|
pub editor: Option<Box<dyn Editor>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginCache {
|
||||||
|
pub fn new(info: &Info, params: Arc<dyn PluginParameters>, editor: Option<Box<dyn Editor>>) -> Self {
|
||||||
|
Self {
|
||||||
|
info: info.clone(),
|
||||||
|
params,
|
||||||
|
editor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
352
deps/vst/src/channels.rs
vendored
Normal file
352
deps/vst/src/channels.rs
vendored
Normal file
|
|
@ -0,0 +1,352 @@
|
||||||
|
//! Meta data for dealing with input / output channels. Not all hosts use this so it is not
|
||||||
|
//! necessary for plugin functionality.
|
||||||
|
|
||||||
|
use crate::api;
|
||||||
|
use crate::api::consts::{MAX_LABEL, MAX_SHORT_LABEL};
|
||||||
|
|
||||||
|
/// Information about an input / output channel. This isn't necessary for a channel to function but
|
||||||
|
/// informs the host how the channel is meant to be used.
|
||||||
|
pub struct ChannelInfo {
|
||||||
|
name: String,
|
||||||
|
short_name: String,
|
||||||
|
active: bool,
|
||||||
|
arrangement_type: SpeakerArrangementType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelInfo {
|
||||||
|
/// Construct a new `ChannelInfo` object.
|
||||||
|
///
|
||||||
|
/// `name` is a user friendly name for this channel limited to `MAX_LABEL` characters.
|
||||||
|
/// `short_name` is an optional field which provides a short name limited to `MAX_SHORT_LABEL`.
|
||||||
|
/// `active` determines whether this channel is active.
|
||||||
|
/// `arrangement_type` describes the arrangement type for this channel.
|
||||||
|
pub fn new(
|
||||||
|
name: String,
|
||||||
|
short_name: Option<String>,
|
||||||
|
active: bool,
|
||||||
|
arrangement_type: Option<SpeakerArrangementType>,
|
||||||
|
) -> ChannelInfo {
|
||||||
|
ChannelInfo {
|
||||||
|
name: name.clone(),
|
||||||
|
|
||||||
|
short_name: if let Some(short_name) = short_name {
|
||||||
|
short_name
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
},
|
||||||
|
|
||||||
|
active,
|
||||||
|
|
||||||
|
arrangement_type: arrangement_type.unwrap_or(SpeakerArrangementType::Custom),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<api::ChannelProperties> for ChannelInfo {
|
||||||
|
/// Convert to the VST api equivalent of this structure.
|
||||||
|
fn into(self) -> api::ChannelProperties {
|
||||||
|
api::ChannelProperties {
|
||||||
|
name: {
|
||||||
|
let mut label = [0; MAX_LABEL as usize];
|
||||||
|
for (b, c) in self.name.bytes().zip(label.iter_mut()) {
|
||||||
|
*c = b;
|
||||||
|
}
|
||||||
|
label
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
let mut flag = api::ChannelFlags::empty();
|
||||||
|
if self.active {
|
||||||
|
flag |= api::ChannelFlags::ACTIVE
|
||||||
|
}
|
||||||
|
if self.arrangement_type.is_left_stereo() {
|
||||||
|
flag |= api::ChannelFlags::STEREO
|
||||||
|
}
|
||||||
|
if self.arrangement_type.is_speaker_type() {
|
||||||
|
flag |= api::ChannelFlags::SPEAKER
|
||||||
|
}
|
||||||
|
flag.bits()
|
||||||
|
},
|
||||||
|
arrangement_type: self.arrangement_type.into(),
|
||||||
|
short_name: {
|
||||||
|
let mut label = [0; MAX_SHORT_LABEL as usize];
|
||||||
|
for (b, c) in self.short_name.bytes().zip(label.iter_mut()) {
|
||||||
|
*c = b;
|
||||||
|
}
|
||||||
|
label
|
||||||
|
},
|
||||||
|
future: [0; 48],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<api::ChannelProperties> for ChannelInfo {
|
||||||
|
fn from(api: api::ChannelProperties) -> ChannelInfo {
|
||||||
|
ChannelInfo {
|
||||||
|
name: String::from_utf8_lossy(&api.name).to_string(),
|
||||||
|
short_name: String::from_utf8_lossy(&api.short_name).to_string(),
|
||||||
|
active: api::ChannelFlags::from_bits(api.flags)
|
||||||
|
.expect("Invalid bits in channel info")
|
||||||
|
.intersects(api::ChannelFlags::ACTIVE),
|
||||||
|
arrangement_type: SpeakerArrangementType::from(api),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Target for Speaker arrangement type. Can be a cinema configuration or music configuration. Both
|
||||||
|
/// are technically identical but this provides extra information to the host.
|
||||||
|
pub enum ArrangementTarget {
|
||||||
|
/// Music arrangement. Technically identical to Cinema.
|
||||||
|
Music,
|
||||||
|
/// Cinematic arrangement. Technically identical to Music.
|
||||||
|
Cinema,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An enum for all channels in a stereo configuration.
|
||||||
|
pub enum StereoChannel {
|
||||||
|
/// Left channel.
|
||||||
|
Left,
|
||||||
|
/// Right channel.
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Possible stereo speaker configurations.
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub enum StereoConfig {
|
||||||
|
/// Regular.
|
||||||
|
L_R,
|
||||||
|
/// Left surround, right surround.
|
||||||
|
Ls_Rs,
|
||||||
|
/// Left center, right center.
|
||||||
|
Lc_Rc,
|
||||||
|
/// Side left, side right.
|
||||||
|
Sl_Sr,
|
||||||
|
/// Center, low frequency effects.
|
||||||
|
C_Lfe,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Possible surround speaker configurations.
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub enum SurroundConfig {
|
||||||
|
/// 3.0 surround sound.
|
||||||
|
/// Cinema: L R C
|
||||||
|
/// Music: L R S
|
||||||
|
S3_0(ArrangementTarget),
|
||||||
|
/// 3.1 surround sound.
|
||||||
|
/// Cinema: L R C Lfe
|
||||||
|
/// Music: L R Lfe S
|
||||||
|
S3_1(ArrangementTarget),
|
||||||
|
/// 4.0 surround sound.
|
||||||
|
/// Cinema: L R C S (LCRS)
|
||||||
|
/// Music: L R Ls Rs (Quadro)
|
||||||
|
S4_0(ArrangementTarget),
|
||||||
|
/// 4.1 surround sound.
|
||||||
|
/// Cinema: L R C Lfe S (LCRS + Lfe)
|
||||||
|
/// Music: L R Ls Rs (Quadro + Lfe)
|
||||||
|
S4_1(ArrangementTarget),
|
||||||
|
/// 5.0 surround sound.
|
||||||
|
/// Cinema and music: L R C Ls Rs
|
||||||
|
S5_0,
|
||||||
|
/// 5.1 surround sound.
|
||||||
|
/// Cinema and music: L R C Lfe Ls Rs
|
||||||
|
S5_1,
|
||||||
|
/// 6.0 surround sound.
|
||||||
|
/// Cinema: L R C Ls Rs Cs
|
||||||
|
/// Music: L R Ls Rs Sl Sr
|
||||||
|
S6_0(ArrangementTarget),
|
||||||
|
/// 6.1 surround sound.
|
||||||
|
/// Cinema: L R C Lfe Ls Rs Cs
|
||||||
|
/// Music: L R Ls Rs Sl Sr
|
||||||
|
S6_1(ArrangementTarget),
|
||||||
|
/// 7.0 surround sound.
|
||||||
|
/// Cinema: L R C Ls Rs Lc Rc
|
||||||
|
/// Music: L R C Ls Rs Sl Sr
|
||||||
|
S7_0(ArrangementTarget),
|
||||||
|
/// 7.1 surround sound.
|
||||||
|
/// Cinema: L R C Lfe Ls Rs Lc Rc
|
||||||
|
/// Music: L R C Lfe Ls Rs Sl Sr
|
||||||
|
S7_1(ArrangementTarget),
|
||||||
|
/// 8.0 surround sound.
|
||||||
|
/// Cinema: L R C Ls Rs Lc Rc Cs
|
||||||
|
/// Music: L R C Ls Rs Cs Sl Sr
|
||||||
|
S8_0(ArrangementTarget),
|
||||||
|
/// 8.1 surround sound.
|
||||||
|
/// Cinema: L R C Lfe Ls Rs Lc Rc Cs
|
||||||
|
/// Music: L R C Lfe Ls Rs Cs Sl Sr
|
||||||
|
S8_1(ArrangementTarget),
|
||||||
|
/// 10.2 surround sound.
|
||||||
|
/// Cinema + Music: L R C Lfe Ls Rs Tfl Tfc Tfr Trl Trr Lfe2
|
||||||
|
S10_2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type representing how a channel is used. Only useful for some hosts.
|
||||||
|
pub enum SpeakerArrangementType {
|
||||||
|
/// Custom arrangement not specified to host.
|
||||||
|
Custom,
|
||||||
|
/// Empty arrangement.
|
||||||
|
Empty,
|
||||||
|
/// Mono channel.
|
||||||
|
Mono,
|
||||||
|
/// Stereo channel. Contains type of stereo arrangement and speaker represented.
|
||||||
|
Stereo(StereoConfig, StereoChannel),
|
||||||
|
/// Surround channel. Contains surround arrangement and target (cinema or music).
|
||||||
|
Surround(SurroundConfig),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SpeakerArrangementType {
|
||||||
|
fn default() -> SpeakerArrangementType {
|
||||||
|
SpeakerArrangementType::Mono
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpeakerArrangementType {
|
||||||
|
/// Determine whether this channel is part of a surround speaker arrangement.
|
||||||
|
pub fn is_speaker_type(&self) -> bool {
|
||||||
|
if let SpeakerArrangementType::Surround(..) = *self {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine whether this channel is the left speaker in a stereo pair.
|
||||||
|
pub fn is_left_stereo(&self) -> bool {
|
||||||
|
if let SpeakerArrangementType::Stereo(_, StereoChannel::Left) = *self {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<api::SpeakerArrangementType> for SpeakerArrangementType {
|
||||||
|
/// Convert to VST API arrangement type.
|
||||||
|
fn into(self) -> api::SpeakerArrangementType {
|
||||||
|
use self::ArrangementTarget::{Cinema, Music};
|
||||||
|
use self::SpeakerArrangementType::*;
|
||||||
|
use api::SpeakerArrangementType as Raw;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Custom => Raw::Custom,
|
||||||
|
Empty => Raw::Empty,
|
||||||
|
Mono => Raw::Mono,
|
||||||
|
Stereo(conf, _) => {
|
||||||
|
match conf {
|
||||||
|
// Stereo channels.
|
||||||
|
StereoConfig::L_R => Raw::Stereo,
|
||||||
|
StereoConfig::Ls_Rs => Raw::StereoSurround,
|
||||||
|
StereoConfig::Lc_Rc => Raw::StereoCenter,
|
||||||
|
StereoConfig::Sl_Sr => Raw::StereoSide,
|
||||||
|
StereoConfig::C_Lfe => Raw::StereoCLfe,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Surround(conf) => {
|
||||||
|
match conf {
|
||||||
|
// Surround channels.
|
||||||
|
SurroundConfig::S3_0(Music) => Raw::Music30,
|
||||||
|
SurroundConfig::S3_0(Cinema) => Raw::Cinema30,
|
||||||
|
|
||||||
|
SurroundConfig::S3_1(Music) => Raw::Music31,
|
||||||
|
SurroundConfig::S3_1(Cinema) => Raw::Cinema31,
|
||||||
|
|
||||||
|
SurroundConfig::S4_0(Music) => Raw::Music40,
|
||||||
|
SurroundConfig::S4_0(Cinema) => Raw::Cinema40,
|
||||||
|
|
||||||
|
SurroundConfig::S4_1(Music) => Raw::Music41,
|
||||||
|
SurroundConfig::S4_1(Cinema) => Raw::Cinema41,
|
||||||
|
|
||||||
|
SurroundConfig::S5_0 => Raw::Surround50,
|
||||||
|
SurroundConfig::S5_1 => Raw::Surround51,
|
||||||
|
|
||||||
|
SurroundConfig::S6_0(Music) => Raw::Music60,
|
||||||
|
SurroundConfig::S6_0(Cinema) => Raw::Cinema60,
|
||||||
|
|
||||||
|
SurroundConfig::S6_1(Music) => Raw::Music61,
|
||||||
|
SurroundConfig::S6_1(Cinema) => Raw::Cinema61,
|
||||||
|
|
||||||
|
SurroundConfig::S7_0(Music) => Raw::Music70,
|
||||||
|
SurroundConfig::S7_0(Cinema) => Raw::Cinema70,
|
||||||
|
|
||||||
|
SurroundConfig::S7_1(Music) => Raw::Music71,
|
||||||
|
SurroundConfig::S7_1(Cinema) => Raw::Cinema71,
|
||||||
|
|
||||||
|
SurroundConfig::S8_0(Music) => Raw::Music80,
|
||||||
|
SurroundConfig::S8_0(Cinema) => Raw::Cinema80,
|
||||||
|
|
||||||
|
SurroundConfig::S8_1(Music) => Raw::Music81,
|
||||||
|
SurroundConfig::S8_1(Cinema) => Raw::Cinema81,
|
||||||
|
|
||||||
|
SurroundConfig::S10_2 => Raw::Surround102,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert the VST API equivalent struct into something more usable.
|
||||||
|
///
|
||||||
|
/// We implement `From<ChannelProperties>` as `SpeakerArrangementType` contains extra info about
|
||||||
|
/// stereo speakers found in the channel flags.
|
||||||
|
impl From<api::ChannelProperties> for SpeakerArrangementType {
|
||||||
|
fn from(api: api::ChannelProperties) -> SpeakerArrangementType {
|
||||||
|
use self::ArrangementTarget::{Cinema, Music};
|
||||||
|
use self::SpeakerArrangementType::*;
|
||||||
|
use self::SurroundConfig::*;
|
||||||
|
use api::SpeakerArrangementType as Raw;
|
||||||
|
|
||||||
|
let stereo = if api::ChannelFlags::from_bits(api.flags)
|
||||||
|
.expect("Invalid Channel Flags")
|
||||||
|
.intersects(api::ChannelFlags::STEREO)
|
||||||
|
{
|
||||||
|
StereoChannel::Left
|
||||||
|
} else {
|
||||||
|
StereoChannel::Right
|
||||||
|
};
|
||||||
|
|
||||||
|
match api.arrangement_type {
|
||||||
|
Raw::Custom => Custom,
|
||||||
|
Raw::Empty => Empty,
|
||||||
|
Raw::Mono => Mono,
|
||||||
|
|
||||||
|
Raw::Stereo => Stereo(StereoConfig::L_R, stereo),
|
||||||
|
Raw::StereoSurround => Stereo(StereoConfig::Ls_Rs, stereo),
|
||||||
|
Raw::StereoCenter => Stereo(StereoConfig::Lc_Rc, stereo),
|
||||||
|
Raw::StereoSide => Stereo(StereoConfig::Sl_Sr, stereo),
|
||||||
|
Raw::StereoCLfe => Stereo(StereoConfig::C_Lfe, stereo),
|
||||||
|
|
||||||
|
Raw::Music30 => Surround(S3_0(Music)),
|
||||||
|
Raw::Cinema30 => Surround(S3_0(Cinema)),
|
||||||
|
|
||||||
|
Raw::Music31 => Surround(S3_1(Music)),
|
||||||
|
Raw::Cinema31 => Surround(S3_1(Cinema)),
|
||||||
|
|
||||||
|
Raw::Music40 => Surround(S4_0(Music)),
|
||||||
|
Raw::Cinema40 => Surround(S4_0(Cinema)),
|
||||||
|
|
||||||
|
Raw::Music41 => Surround(S4_1(Music)),
|
||||||
|
Raw::Cinema41 => Surround(S4_1(Cinema)),
|
||||||
|
|
||||||
|
Raw::Surround50 => Surround(S5_0),
|
||||||
|
Raw::Surround51 => Surround(S5_1),
|
||||||
|
|
||||||
|
Raw::Music60 => Surround(S6_0(Music)),
|
||||||
|
Raw::Cinema60 => Surround(S6_0(Cinema)),
|
||||||
|
|
||||||
|
Raw::Music61 => Surround(S6_1(Music)),
|
||||||
|
Raw::Cinema61 => Surround(S6_1(Cinema)),
|
||||||
|
|
||||||
|
Raw::Music70 => Surround(S7_0(Music)),
|
||||||
|
Raw::Cinema70 => Surround(S7_0(Cinema)),
|
||||||
|
|
||||||
|
Raw::Music71 => Surround(S7_1(Music)),
|
||||||
|
Raw::Cinema71 => Surround(S7_1(Cinema)),
|
||||||
|
|
||||||
|
Raw::Music80 => Surround(S8_0(Music)),
|
||||||
|
Raw::Cinema80 => Surround(S8_0(Cinema)),
|
||||||
|
|
||||||
|
Raw::Music81 => Surround(S8_1(Music)),
|
||||||
|
Raw::Cinema81 => Surround(S8_1(Cinema)),
|
||||||
|
|
||||||
|
Raw::Surround102 => Surround(S10_2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
155
deps/vst/src/editor.rs
vendored
Normal file
155
deps/vst/src/editor.rs
vendored
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
//! All VST plugin editor related functionality.
|
||||||
|
|
||||||
|
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||||
|
|
||||||
|
use std::os::raw::c_void;
|
||||||
|
|
||||||
|
/// Implemented by plugin editors.
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
pub trait Editor {
|
||||||
|
/// Get the size of the editor window.
|
||||||
|
fn size(&self) -> (i32, i32);
|
||||||
|
|
||||||
|
/// Get the coordinates of the editor window.
|
||||||
|
fn position(&self) -> (i32, i32);
|
||||||
|
|
||||||
|
/// Editor idle call. Called by host.
|
||||||
|
fn idle(&mut self) {}
|
||||||
|
|
||||||
|
/// Called when the editor window is closed.
|
||||||
|
fn close(&mut self) {}
|
||||||
|
|
||||||
|
/// Called when the editor window is opened.
|
||||||
|
///
|
||||||
|
/// `parent` is a window pointer that the new window should attach itself to.
|
||||||
|
/// **It is dependent upon the platform you are targeting.**
|
||||||
|
///
|
||||||
|
/// A few examples:
|
||||||
|
///
|
||||||
|
/// - On Windows, it should be interpreted as a `HWND`
|
||||||
|
/// - On Mac OS X (64 bit), it should be interpreted as a `NSView*`
|
||||||
|
/// - On X11 platforms, it should be interpreted as a `u32` (the ID number of the parent window)
|
||||||
|
///
|
||||||
|
/// Return `true` if the window opened successfully, `false` otherwise.
|
||||||
|
fn open(&mut self, parent: *mut c_void) -> bool;
|
||||||
|
|
||||||
|
/// Return whether the window is currently open.
|
||||||
|
fn is_open(&mut self) -> bool;
|
||||||
|
|
||||||
|
/// Set the knob mode for this editor (if supported by host).
|
||||||
|
///
|
||||||
|
/// Return `true` if the knob mode was set.
|
||||||
|
fn set_knob_mode(&mut self, mode: KnobMode) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive key up event. Return `true` if the key was used.
|
||||||
|
fn key_up(&mut self, keycode: KeyCode) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive key down event. Return `true` if the key was used.
|
||||||
|
fn key_down(&mut self, keycode: KeyCode) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rectangle used to specify dimensions of editor window.
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct Rect {
|
||||||
|
/// Y value in pixels of top side.
|
||||||
|
pub top: i16,
|
||||||
|
/// X value in pixels of left side.
|
||||||
|
pub left: i16,
|
||||||
|
/// Y value in pixels of bottom side.
|
||||||
|
pub bottom: i16,
|
||||||
|
/// X value in pixels of right side.
|
||||||
|
pub right: i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A platform independent key code. Includes modifier keys.
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct KeyCode {
|
||||||
|
/// ASCII character for key pressed (if applicable).
|
||||||
|
pub character: char,
|
||||||
|
/// Key pressed. See `enums::Key`.
|
||||||
|
pub key: Key,
|
||||||
|
/// Modifier key bitflags. See `enums::flags::modifier_key`.
|
||||||
|
pub modifier: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allows host to set how a parameter knob works.
|
||||||
|
#[repr(isize)]
|
||||||
|
#[derive(Copy, Clone, Debug, TryFromPrimitive, IntoPrimitive)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub enum KnobMode {
|
||||||
|
Circular,
|
||||||
|
CircularRelative,
|
||||||
|
Linear,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Platform independent key codes.
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[repr(isize)]
|
||||||
|
#[derive(Debug, Copy, Clone, TryFromPrimitive, IntoPrimitive)]
|
||||||
|
pub enum Key {
|
||||||
|
None = 0,
|
||||||
|
Back,
|
||||||
|
Tab,
|
||||||
|
Clear,
|
||||||
|
Return,
|
||||||
|
Pause,
|
||||||
|
Escape,
|
||||||
|
Space,
|
||||||
|
Next,
|
||||||
|
End,
|
||||||
|
Home,
|
||||||
|
Left,
|
||||||
|
Up,
|
||||||
|
Right,
|
||||||
|
Down,
|
||||||
|
PageUp,
|
||||||
|
PageDown,
|
||||||
|
Select,
|
||||||
|
Print,
|
||||||
|
Enter,
|
||||||
|
Snapshot,
|
||||||
|
Insert,
|
||||||
|
Delete,
|
||||||
|
Help,
|
||||||
|
Numpad0,
|
||||||
|
Numpad1,
|
||||||
|
Numpad2,
|
||||||
|
Numpad3,
|
||||||
|
Numpad4,
|
||||||
|
Numpad5,
|
||||||
|
Numpad6,
|
||||||
|
Numpad7,
|
||||||
|
Numpad8,
|
||||||
|
Numpad9,
|
||||||
|
Multiply,
|
||||||
|
Add,
|
||||||
|
Separator,
|
||||||
|
Subtract,
|
||||||
|
Decimal,
|
||||||
|
Divide,
|
||||||
|
F1,
|
||||||
|
F2,
|
||||||
|
F3,
|
||||||
|
F4,
|
||||||
|
F5,
|
||||||
|
F6,
|
||||||
|
F7,
|
||||||
|
F8,
|
||||||
|
F9,
|
||||||
|
F10,
|
||||||
|
F11,
|
||||||
|
F12,
|
||||||
|
Numlock,
|
||||||
|
Scroll,
|
||||||
|
Shift,
|
||||||
|
Control,
|
||||||
|
Alt,
|
||||||
|
Equals,
|
||||||
|
}
|
||||||
133
deps/vst/src/event.rs
vendored
Normal file
133
deps/vst/src/event.rs
vendored
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
//! Interfaces to VST events.
|
||||||
|
// TODO: Update and explain both host and plugin events
|
||||||
|
|
||||||
|
use std::{mem, slice};
|
||||||
|
|
||||||
|
use crate::api;
|
||||||
|
|
||||||
|
/// A VST event.
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum Event<'a> {
|
||||||
|
/// A midi event.
|
||||||
|
///
|
||||||
|
/// These are sent to the plugin before `Plugin::processing()` or `Plugin::processing_f64()` is
|
||||||
|
/// called.
|
||||||
|
Midi(MidiEvent),
|
||||||
|
|
||||||
|
/// A system exclusive event.
|
||||||
|
///
|
||||||
|
/// This is just a block of data and it is up to the plugin to interpret this. Generally used
|
||||||
|
/// by midi controllers.
|
||||||
|
SysEx(SysExEvent<'a>),
|
||||||
|
|
||||||
|
/// A deprecated event.
|
||||||
|
///
|
||||||
|
/// Passes the raw midi event structure along with this so that implementors can handle
|
||||||
|
/// optionally handle this event.
|
||||||
|
Deprecated(api::Event),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A midi event.
|
||||||
|
///
|
||||||
|
/// These are sent to the plugin before `Plugin::processing()` or `Plugin::processing_f64()` is
|
||||||
|
/// called.
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct MidiEvent {
|
||||||
|
/// The raw midi data associated with this event.
|
||||||
|
pub data: [u8; 3],
|
||||||
|
|
||||||
|
/// Number of samples into the current processing block that this event occurs on.
|
||||||
|
///
|
||||||
|
/// E.g. if the block size is 512 and this value is 123, the event will occur on sample
|
||||||
|
/// `samples[123]`.
|
||||||
|
// TODO: Don't repeat this value in all event types
|
||||||
|
pub delta_frames: i32,
|
||||||
|
|
||||||
|
/// This midi event was created live as opposed to being played back in the sequencer.
|
||||||
|
///
|
||||||
|
/// This can give the plugin priority over this event if it introduces a lot of latency.
|
||||||
|
pub live: bool,
|
||||||
|
|
||||||
|
/// The length of the midi note associated with this event, if available.
|
||||||
|
pub note_length: Option<i32>,
|
||||||
|
|
||||||
|
/// Offset in samples into note from note start, if available.
|
||||||
|
pub note_offset: Option<i32>,
|
||||||
|
|
||||||
|
/// Detuning between -63 and +64 cents.
|
||||||
|
pub detune: i8,
|
||||||
|
|
||||||
|
/// Note off velocity between 0 and 127.
|
||||||
|
pub note_off_velocity: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A system exclusive event.
|
||||||
|
///
|
||||||
|
/// This is just a block of data and it is up to the plugin to interpret this. Generally used
|
||||||
|
/// by midi controllers.
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct SysExEvent<'a> {
|
||||||
|
/// The SysEx payload.
|
||||||
|
pub payload: &'a [u8],
|
||||||
|
|
||||||
|
/// Number of samples into the current processing block that this event occurs on.
|
||||||
|
///
|
||||||
|
/// E.g. if the block size is 512 and this value is 123, the event will occur on sample
|
||||||
|
/// `samples[123]`.
|
||||||
|
pub delta_frames: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Event<'a> {
|
||||||
|
/// Creates a high-level event from the given low-level API event.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// You must ensure that the given pointer refers to a valid event of the correct event type.
|
||||||
|
/// For example, if the event type is [`api::EventType::SysEx`], it should point to a
|
||||||
|
/// [`SysExEvent`]. In case of a [`SysExEvent`], `system_data` and `data_size` must be correct.
|
||||||
|
pub unsafe fn from_raw_event(event: *const api::Event) -> Event<'a> {
|
||||||
|
use api::EventType::*;
|
||||||
|
let event = &*event;
|
||||||
|
match event.event_type {
|
||||||
|
Midi => {
|
||||||
|
let event: api::MidiEvent = mem::transmute(*event);
|
||||||
|
|
||||||
|
let length = if event.note_length > 0 {
|
||||||
|
Some(event.note_length)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let offset = if event.note_offset > 0 {
|
||||||
|
Some(event.note_offset)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let flags = api::MidiEventFlags::from_bits(event.flags).unwrap();
|
||||||
|
|
||||||
|
Event::Midi(MidiEvent {
|
||||||
|
data: event.midi_data,
|
||||||
|
delta_frames: event.delta_frames,
|
||||||
|
live: flags.intersects(api::MidiEventFlags::REALTIME_EVENT),
|
||||||
|
note_length: length,
|
||||||
|
note_offset: offset,
|
||||||
|
detune: event.detune,
|
||||||
|
note_off_velocity: event.note_off_velocity,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
SysEx => Event::SysEx(SysExEvent {
|
||||||
|
payload: {
|
||||||
|
// We can safely cast the event pointer to a `SysExEvent` pointer as
|
||||||
|
// event_type refers to a `SysEx` type.
|
||||||
|
#[allow(clippy::cast_ptr_alignment)]
|
||||||
|
let event: &api::SysExEvent = &*(event as *const api::Event as *const api::SysExEvent);
|
||||||
|
slice::from_raw_parts(event.system_data, event.data_size as usize)
|
||||||
|
},
|
||||||
|
|
||||||
|
delta_frames: event.delta_frames,
|
||||||
|
}),
|
||||||
|
|
||||||
|
_ => Event::Deprecated(*event),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
962
deps/vst/src/host.rs
vendored
Normal file
962
deps/vst/src/host.rs
vendored
Normal file
|
|
@ -0,0 +1,962 @@
|
||||||
|
//! Host specific structures.
|
||||||
|
|
||||||
|
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||||
|
use num_traits::Float;
|
||||||
|
|
||||||
|
use libloading::Library;
|
||||||
|
use std::cell::UnsafeCell;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::mem::MaybeUninit;
|
||||||
|
use std::os::raw::c_void;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::{fmt, ptr, slice};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::{self, consts::*, AEffect, PluginFlags, PluginMain, Supported, TimeInfo},
|
||||||
|
buffer::AudioBuffer,
|
||||||
|
channels::ChannelInfo,
|
||||||
|
editor::{Editor, Rect},
|
||||||
|
interfaces,
|
||||||
|
plugin::{self, Category, HostCallback, Info, Plugin, PluginParameters},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[repr(i32)]
|
||||||
|
#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub enum OpCode {
|
||||||
|
/// [index]: parameter index
|
||||||
|
/// [opt]: parameter value
|
||||||
|
Automate = 0,
|
||||||
|
/// [return]: host vst version (e.g. 2400 for VST 2.4)
|
||||||
|
Version,
|
||||||
|
/// [return]: current plugin ID (useful for shell plugins to figure out which plugin to load in
|
||||||
|
/// `VSTPluginMain()`).
|
||||||
|
CurrentId,
|
||||||
|
/// No arguments. Give idle time to Host application, e.g. if plug-in editor is doing mouse
|
||||||
|
/// tracking in a modal loop.
|
||||||
|
Idle,
|
||||||
|
/// Deprecated.
|
||||||
|
_PinConnected = 4,
|
||||||
|
|
||||||
|
/// Deprecated.
|
||||||
|
_WantMidi = 6, // Not a typo
|
||||||
|
/// [value]: request mask. see `VstTimeInfoFlags`
|
||||||
|
/// [return]: `VstTimeInfo` pointer or null if not supported.
|
||||||
|
GetTime,
|
||||||
|
/// Inform host that the plugin has MIDI events ready to be processed. Should be called at the
|
||||||
|
/// end of `Plugin::process`.
|
||||||
|
/// [ptr]: `VstEvents*` the events to be processed.
|
||||||
|
/// [return]: 1 if supported and processed OK.
|
||||||
|
ProcessEvents,
|
||||||
|
/// Deprecated.
|
||||||
|
_SetTime,
|
||||||
|
/// Deprecated.
|
||||||
|
_TempoAt,
|
||||||
|
/// Deprecated.
|
||||||
|
_GetNumAutomatableParameters,
|
||||||
|
/// Deprecated.
|
||||||
|
_GetParameterQuantization,
|
||||||
|
|
||||||
|
/// Notifies the host that the input/output setup has changed. This can allow the host to check
|
||||||
|
/// numInputs/numOutputs or call `getSpeakerArrangement()`.
|
||||||
|
/// [return]: 1 if supported.
|
||||||
|
IOChanged,
|
||||||
|
|
||||||
|
/// Deprecated.
|
||||||
|
_NeedIdle,
|
||||||
|
|
||||||
|
/// Request the host to resize the plugin window.
|
||||||
|
/// [index]: new width.
|
||||||
|
/// [value]: new height.
|
||||||
|
SizeWindow,
|
||||||
|
/// [return]: the current sample rate.
|
||||||
|
GetSampleRate,
|
||||||
|
/// [return]: the current block size.
|
||||||
|
GetBlockSize,
|
||||||
|
/// [return]: the input latency in samples.
|
||||||
|
GetInputLatency,
|
||||||
|
/// [return]: the output latency in samples.
|
||||||
|
GetOutputLatency,
|
||||||
|
|
||||||
|
/// Deprecated.
|
||||||
|
_GetPreviousPlug,
|
||||||
|
/// Deprecated.
|
||||||
|
_GetNextPlug,
|
||||||
|
/// Deprecated.
|
||||||
|
_WillReplaceOrAccumulate,
|
||||||
|
|
||||||
|
/// [return]: the current process level, see `VstProcessLevels`
|
||||||
|
GetCurrentProcessLevel,
|
||||||
|
/// [return]: the current automation state, see `VstAutomationStates`
|
||||||
|
GetAutomationState,
|
||||||
|
|
||||||
|
/// The plugin is ready to begin offline processing.
|
||||||
|
/// [index]: number of new audio files.
|
||||||
|
/// [value]: number of audio files.
|
||||||
|
/// [ptr]: `AudioFile*` the host audio files. Flags can be updated from plugin.
|
||||||
|
OfflineStart,
|
||||||
|
/// Called by the plugin to read data.
|
||||||
|
/// [index]: (bool)
|
||||||
|
/// VST offline processing allows a plugin to overwrite existing files. If this value is
|
||||||
|
/// true then the host will read the original file's samples, but if it is false it will
|
||||||
|
/// read the samples which the plugin has written via `OfflineWrite`
|
||||||
|
/// [value]: see `OfflineOption`
|
||||||
|
/// [ptr]: `OfflineTask*` describing the task.
|
||||||
|
/// [return]: 1 on success
|
||||||
|
OfflineRead,
|
||||||
|
/// Called by the plugin to write data.
|
||||||
|
/// [value]: see `OfflineOption`
|
||||||
|
/// [ptr]: `OfflineTask*` describing the task.
|
||||||
|
OfflineWrite,
|
||||||
|
/// Unknown. Used in offline processing.
|
||||||
|
OfflineGetCurrentPass,
|
||||||
|
/// Unknown. Used in offline processing.
|
||||||
|
OfflineGetCurrentMetaPass,
|
||||||
|
|
||||||
|
/// Deprecated.
|
||||||
|
_SetOutputSampleRate,
|
||||||
|
/// Deprecated.
|
||||||
|
_GetOutputSpeakerArrangement,
|
||||||
|
|
||||||
|
/// Get the vendor string.
|
||||||
|
/// [ptr]: `char*` for vendor string, limited to `MAX_VENDOR_STR_LEN`.
|
||||||
|
GetVendorString,
|
||||||
|
/// Get the product string.
|
||||||
|
/// [ptr]: `char*` for vendor string, limited to `MAX_PRODUCT_STR_LEN`.
|
||||||
|
GetProductString,
|
||||||
|
/// [return]: vendor-specific version
|
||||||
|
GetVendorVersion,
|
||||||
|
/// Vendor specific handling.
|
||||||
|
VendorSpecific,
|
||||||
|
|
||||||
|
/// Deprecated.
|
||||||
|
_SetIcon,
|
||||||
|
|
||||||
|
/// Check if the host supports a feature.
|
||||||
|
/// [ptr]: `char*` can do string
|
||||||
|
/// [return]: 1 if supported
|
||||||
|
CanDo,
|
||||||
|
/// Get the language of the host.
|
||||||
|
/// [return]: `VstHostLanguage`
|
||||||
|
GetLanguage,
|
||||||
|
|
||||||
|
/// Deprecated.
|
||||||
|
_OpenWindow,
|
||||||
|
/// Deprecated.
|
||||||
|
_CloseWindow,
|
||||||
|
|
||||||
|
/// Get the current directory.
|
||||||
|
/// [return]: `FSSpec` on OS X, `char*` otherwise
|
||||||
|
GetDirectory,
|
||||||
|
/// Tell the host that the plugin's parameters have changed, refresh the UI.
|
||||||
|
///
|
||||||
|
/// No arguments.
|
||||||
|
UpdateDisplay,
|
||||||
|
/// Tell the host that if needed, it should record automation data for a control.
|
||||||
|
///
|
||||||
|
/// Typically called when the plugin editor begins changing a control.
|
||||||
|
///
|
||||||
|
/// [index]: index of the control.
|
||||||
|
/// [return]: true on success.
|
||||||
|
BeginEdit,
|
||||||
|
/// A control is no longer being changed.
|
||||||
|
///
|
||||||
|
/// Typically called after the plugin editor is done.
|
||||||
|
///
|
||||||
|
/// [index]: index of the control.
|
||||||
|
/// [return]: true on success.
|
||||||
|
EndEdit,
|
||||||
|
/// Open the host file selector.
|
||||||
|
/// [ptr]: `VstFileSelect*`
|
||||||
|
/// [return]: true on success.
|
||||||
|
OpenFileSelector,
|
||||||
|
/// Close the host file selector.
|
||||||
|
/// [ptr]: `VstFileSelect*`
|
||||||
|
/// [return]: true on success.
|
||||||
|
CloseFileSelector,
|
||||||
|
|
||||||
|
/// Deprecated.
|
||||||
|
_EditFile,
|
||||||
|
/// Deprecated.
|
||||||
|
/// [ptr]: char[2048] or sizeof (FSSpec).
|
||||||
|
/// [return]: 1 if supported.
|
||||||
|
_GetChunkFile,
|
||||||
|
/// Deprecated.
|
||||||
|
_GetInputSpeakerArrangement,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implemented by all VST hosts.
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
pub trait Host {
|
||||||
|
/// Automate a parameter; the value has been changed.
|
||||||
|
fn automate(&self, index: i32, value: f32) {}
|
||||||
|
|
||||||
|
/// Signal that automation of a parameter started (the knob has been touched / mouse button down).
|
||||||
|
fn begin_edit(&self, index: i32) {}
|
||||||
|
|
||||||
|
/// Signal that automation of a parameter ended (the knob is no longer been touched / mouse button up).
|
||||||
|
fn end_edit(&self, index: i32) {}
|
||||||
|
|
||||||
|
/// Get the plugin ID of the currently loading plugin.
|
||||||
|
///
|
||||||
|
/// This is only useful for shell plugins where this value will change the plugin returned.
|
||||||
|
/// `TODO: implement shell plugins`
|
||||||
|
fn get_plugin_id(&self) -> i32 {
|
||||||
|
// TODO: Handle this properly
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An idle call.
|
||||||
|
///
|
||||||
|
/// This is useful when the plugin is doing something such as mouse tracking in the UI.
|
||||||
|
fn idle(&self) {}
|
||||||
|
|
||||||
|
/// Get vendor and product information.
|
||||||
|
///
|
||||||
|
/// Returns a tuple in the form of `(version, vendor_name, product_name)`.
|
||||||
|
fn get_info(&self) -> (isize, String, String) {
|
||||||
|
(1, "vendor string".to_owned(), "product string".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle incoming events from the plugin.
|
||||||
|
fn process_events(&self, events: &api::Events) {}
|
||||||
|
|
||||||
|
/// Get time information.
|
||||||
|
fn get_time_info(&self, mask: i32) -> Option<TimeInfo> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get block size.
|
||||||
|
fn get_block_size(&self) -> isize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh UI after the plugin's parameters changed.
|
||||||
|
///
|
||||||
|
/// Note: some hosts will call some `PluginParameters` methods from within the `update_display`
|
||||||
|
/// call, including `get_parameter`, `get_parameter_label`, `get_parameter_name`
|
||||||
|
/// and `get_parameter_text`.
|
||||||
|
fn update_display(&self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All possible errors that can occur when loading a VST plugin.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PluginLoadError {
|
||||||
|
/// Could not load given path.
|
||||||
|
InvalidPath,
|
||||||
|
|
||||||
|
/// Given path is not a VST plugin.
|
||||||
|
NotAPlugin,
|
||||||
|
|
||||||
|
/// Failed to create an instance of this plugin.
|
||||||
|
///
|
||||||
|
/// This can happen for many reasons, such as if the plugin requires a different version of
|
||||||
|
/// the VST API to be used, or due to improper licensing.
|
||||||
|
InstanceFailed,
|
||||||
|
|
||||||
|
/// The API version which the plugin used is not supported by this library.
|
||||||
|
InvalidApiVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PluginLoadError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
use self::PluginLoadError::*;
|
||||||
|
let description = match self {
|
||||||
|
InvalidPath => "Could not open the requested path",
|
||||||
|
NotAPlugin => "The given path does not contain a VST2.4 compatible library",
|
||||||
|
InstanceFailed => "Failed to create a plugin instance",
|
||||||
|
InvalidApiVersion => "The plugin API version is not compatible with this library",
|
||||||
|
};
|
||||||
|
write!(f, "{}", description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for PluginLoadError {}
|
||||||
|
|
||||||
|
/// Wrapper for an externally loaded VST plugin.
|
||||||
|
///
|
||||||
|
/// The only functionality this struct provides is loading plugins, which can be done via the
|
||||||
|
/// [`load`](#method.load) method.
|
||||||
|
pub struct PluginLoader<T: Host> {
|
||||||
|
main: PluginMain,
|
||||||
|
lib: Arc<Library>,
|
||||||
|
host: Arc<Mutex<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An instance of an externally loaded VST plugin.
|
||||||
|
#[allow(dead_code)] // To keep `lib` around.
|
||||||
|
pub struct PluginInstance {
|
||||||
|
params: Arc<PluginParametersInstance>,
|
||||||
|
lib: Arc<Library>,
|
||||||
|
info: Info,
|
||||||
|
is_editor_active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PluginParametersInstance {
|
||||||
|
effect: UnsafeCell<*mut AEffect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for PluginParametersInstance {}
|
||||||
|
unsafe impl Sync for PluginParametersInstance {}
|
||||||
|
|
||||||
|
impl Drop for PluginInstance {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.dispatch(plugin::OpCode::Shutdown, 0, 0, ptr::null_mut(), 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The editor of an externally loaded VST plugin.
|
||||||
|
struct EditorInstance {
|
||||||
|
params: Arc<PluginParametersInstance>,
|
||||||
|
is_open: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditorInstance {
|
||||||
|
fn get_rect(&self) -> Option<Rect> {
|
||||||
|
let mut rect: *mut Rect = std::ptr::null_mut();
|
||||||
|
let rect_ptr: *mut *mut Rect = &mut rect;
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.params
|
||||||
|
.dispatch(plugin::OpCode::EditorGetRect, 0, 0, rect_ptr as *mut c_void, 0.0);
|
||||||
|
|
||||||
|
if result == 0 || rect.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(unsafe { *rect }) // TODO: Who owns rect? Who should free the memory?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Editor for EditorInstance {
|
||||||
|
fn size(&self) -> (i32, i32) {
|
||||||
|
// Assuming coordinate origins from top-left
|
||||||
|
match self.get_rect() {
|
||||||
|
None => (0, 0),
|
||||||
|
Some(rect) => ((rect.right - rect.left) as i32, (rect.bottom - rect.top) as i32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn position(&self) -> (i32, i32) {
|
||||||
|
// Assuming coordinate origins from top-left
|
||||||
|
match self.get_rect() {
|
||||||
|
None => (0, 0),
|
||||||
|
Some(rect) => (rect.left as i32, rect.top as i32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(&mut self) {
|
||||||
|
self.params
|
||||||
|
.dispatch(plugin::OpCode::EditorClose, 0, 0, ptr::null_mut(), 0.0);
|
||||||
|
self.is_open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open(&mut self, parent: *mut c_void) -> bool {
|
||||||
|
let result = self.params.dispatch(plugin::OpCode::EditorOpen, 0, 0, parent, 0.0);
|
||||||
|
|
||||||
|
let opened = result == 1;
|
||||||
|
if opened {
|
||||||
|
self.is_open = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
opened
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_open(&mut self) -> bool {
|
||||||
|
self.is_open
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Host> PluginLoader<T> {
|
||||||
|
/// Load a plugin at the given path with the given host.
|
||||||
|
///
|
||||||
|
/// Because of the possibility of multi-threading problems that can occur when using plugins,
|
||||||
|
/// the host must be passed in via an `Arc<Mutex<T>>` object. This makes sure that even if the
|
||||||
|
/// plugins are multi-threaded no data race issues can occur.
|
||||||
|
///
|
||||||
|
/// Upon success, this method returns a [`PluginLoader`](.) object which you can use to call
|
||||||
|
/// [`instance`](#method.instance) to create a new instance of the plugin.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # use std::path::Path;
|
||||||
|
/// # use std::sync::{Arc, Mutex};
|
||||||
|
/// # use vst::host::{Host, PluginLoader};
|
||||||
|
/// # let path = Path::new(".");
|
||||||
|
/// # struct MyHost;
|
||||||
|
/// # impl MyHost { fn new() -> MyHost { MyHost } }
|
||||||
|
/// # impl Host for MyHost {
|
||||||
|
/// # fn automate(&self, _: i32, _: f32) {}
|
||||||
|
/// # fn get_plugin_id(&self) -> i32 { 0 }
|
||||||
|
/// # }
|
||||||
|
/// // ...
|
||||||
|
/// let host = Arc::new(Mutex::new(MyHost::new()));
|
||||||
|
///
|
||||||
|
/// let mut plugin = PluginLoader::load(path, host.clone()).unwrap();
|
||||||
|
///
|
||||||
|
/// let instance = plugin.instance().unwrap();
|
||||||
|
/// // ...
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Linux/Windows
|
||||||
|
/// * This should be a path to the library, typically ending in `.so`/`.dll`.
|
||||||
|
/// * Possible full path: `/home/overdrivenpotato/.vst/u-he/Zebra2.64.so`
|
||||||
|
/// * Possible full path: `C:\Program Files (x86)\VSTPlugins\iZotope Ozone 5.dll`
|
||||||
|
///
|
||||||
|
/// # OS X
|
||||||
|
/// * This should point to the mach-o file within the `.vst` bundle.
|
||||||
|
/// * Plugin: `/Library/Audio/Plug-Ins/VST/iZotope Ozone 5.vst`
|
||||||
|
/// * Possible full path:
|
||||||
|
/// `/Library/Audio/Plug-Ins/VST/iZotope Ozone 5.vst/Contents/MacOS/PluginHooksVST`
|
||||||
|
pub fn load(path: &Path, host: Arc<Mutex<T>>) -> Result<PluginLoader<T>, PluginLoadError> {
|
||||||
|
// Try loading the library at the given path
|
||||||
|
unsafe {
|
||||||
|
let lib = match Library::new(path) {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(_) => return Err(PluginLoadError::InvalidPath),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(PluginLoader {
|
||||||
|
main:
|
||||||
|
// Search the library for the VSTAPI entry point
|
||||||
|
match lib.get(b"VSTPluginMain") {
|
||||||
|
Ok(s) => *s,
|
||||||
|
_ => return Err(PluginLoadError::NotAPlugin),
|
||||||
|
}
|
||||||
|
,
|
||||||
|
lib: Arc::new(lib),
|
||||||
|
host,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call the VST entry point and retrieve a (possibly null) pointer.
|
||||||
|
unsafe fn call_main(&mut self) -> *mut AEffect {
|
||||||
|
LOAD_POINTER = Box::into_raw(Box::new(Arc::clone(&self.host))) as *mut c_void;
|
||||||
|
(self.main)(callback_wrapper::<T>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to create an instance of this VST plugin.
|
||||||
|
///
|
||||||
|
/// If the instance is successfully created, a [`PluginInstance`](struct.PluginInstance.html)
|
||||||
|
/// is returned. This struct implements the [`Plugin` trait](../plugin/trait.Plugin.html).
|
||||||
|
pub fn instance(&mut self) -> Result<PluginInstance, PluginLoadError> {
|
||||||
|
// Call the plugin main function. This also passes the plugin main function as the closure
|
||||||
|
// could not return an error if the symbol wasn't found
|
||||||
|
let effect = unsafe { self.call_main() };
|
||||||
|
|
||||||
|
if effect.is_null() {
|
||||||
|
return Err(PluginLoadError::InstanceFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// Move the host to the heap and add it to the `AEffect` struct for future reference
|
||||||
|
(*effect).reserved1 = Box::into_raw(Box::new(Arc::clone(&self.host))) as isize;
|
||||||
|
}
|
||||||
|
|
||||||
|
let instance = PluginInstance::new(effect, Arc::clone(&self.lib));
|
||||||
|
|
||||||
|
let api_ver = instance.dispatch(plugin::OpCode::GetApiVersion, 0, 0, ptr::null_mut(), 0.0);
|
||||||
|
if api_ver >= 2400 {
|
||||||
|
Ok(instance)
|
||||||
|
} else {
|
||||||
|
trace!("Could not load plugin with api version {}", api_ver);
|
||||||
|
Err(PluginLoadError::InvalidApiVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginInstance {
|
||||||
|
fn new(effect: *mut AEffect, lib: Arc<Library>) -> PluginInstance {
|
||||||
|
use plugin::OpCode as op;
|
||||||
|
|
||||||
|
let params = Arc::new(PluginParametersInstance {
|
||||||
|
effect: UnsafeCell::new(effect),
|
||||||
|
});
|
||||||
|
let mut plug = PluginInstance {
|
||||||
|
params,
|
||||||
|
lib,
|
||||||
|
info: Default::default(),
|
||||||
|
is_editor_active: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let effect: &AEffect = &*effect;
|
||||||
|
let flags = PluginFlags::from_bits_truncate(effect.flags);
|
||||||
|
|
||||||
|
plug.info = Info {
|
||||||
|
name: plug.read_string(op::GetProductName, MAX_PRODUCT_STR_LEN),
|
||||||
|
vendor: plug.read_string(op::GetVendorName, MAX_VENDOR_STR_LEN),
|
||||||
|
|
||||||
|
presets: effect.numPrograms,
|
||||||
|
parameters: effect.numParams,
|
||||||
|
inputs: effect.numInputs,
|
||||||
|
outputs: effect.numOutputs,
|
||||||
|
|
||||||
|
midi_inputs: 0,
|
||||||
|
midi_outputs: 0,
|
||||||
|
|
||||||
|
unique_id: effect.uniqueId,
|
||||||
|
version: effect.version,
|
||||||
|
|
||||||
|
category: Category::try_from(plug.opcode(op::GetCategory)).unwrap_or(Category::Unknown),
|
||||||
|
|
||||||
|
initial_delay: effect.initialDelay,
|
||||||
|
|
||||||
|
preset_chunks: flags.intersects(PluginFlags::PROGRAM_CHUNKS),
|
||||||
|
f64_precision: flags.intersects(PluginFlags::CAN_DOUBLE_REPLACING),
|
||||||
|
silent_when_stopped: flags.intersects(PluginFlags::NO_SOUND_IN_STOP),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
plug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Dispatch {
|
||||||
|
fn get_effect(&self) -> *mut AEffect;
|
||||||
|
|
||||||
|
/// Send a dispatch message to the plugin.
|
||||||
|
fn dispatch(&self, opcode: plugin::OpCode, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize {
|
||||||
|
let dispatcher = unsafe { (*self.get_effect()).dispatcher };
|
||||||
|
if (dispatcher as *mut u8).is_null() {
|
||||||
|
panic!("Plugin was not loaded correctly.");
|
||||||
|
}
|
||||||
|
dispatcher(self.get_effect(), opcode.into(), index, value, ptr, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a lone opcode with no parameters.
|
||||||
|
fn opcode(&self, opcode: plugin::OpCode) -> isize {
|
||||||
|
self.dispatch(opcode, 0, 0, ptr::null_mut(), 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like `dispatch`, except takes a `&str` to send via `ptr`.
|
||||||
|
fn write_string(&self, opcode: plugin::OpCode, index: i32, value: isize, string: &str, opt: f32) -> isize {
|
||||||
|
let string = CString::new(string).expect("Invalid string data");
|
||||||
|
self.dispatch(opcode, index, value, string.as_bytes().as_ptr() as *mut c_void, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_string(&self, opcode: plugin::OpCode, max: usize) -> String {
|
||||||
|
self.read_string_param(opcode, 0, 0, 0.0, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_string_param(&self, opcode: plugin::OpCode, index: i32, value: isize, opt: f32, max: usize) -> String {
|
||||||
|
let mut buf = vec![0; max];
|
||||||
|
self.dispatch(opcode, index, value, buf.as_mut_ptr() as *mut c_void, opt);
|
||||||
|
String::from_utf8_lossy(&buf)
|
||||||
|
.chars()
|
||||||
|
.take_while(|c| *c != '\0')
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch for PluginInstance {
|
||||||
|
fn get_effect(&self) -> *mut AEffect {
|
||||||
|
self.params.get_effect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch for PluginParametersInstance {
|
||||||
|
fn get_effect(&self) -> *mut AEffect {
|
||||||
|
unsafe { *self.effect.get() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for PluginInstance {
|
||||||
|
fn get_info(&self) -> plugin::Info {
|
||||||
|
self.info.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(_host: HostCallback) -> Self {
|
||||||
|
// Plugin::new is only called on client side and PluginInstance is only used on host side
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(&mut self) {
|
||||||
|
self.opcode(plugin::OpCode::Initialize);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_sample_rate(&mut self, rate: f32) {
|
||||||
|
self.dispatch(plugin::OpCode::SetSampleRate, 0, 0, ptr::null_mut(), rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_block_size(&mut self, size: i64) {
|
||||||
|
self.dispatch(plugin::OpCode::SetBlockSize, 0, size as isize, ptr::null_mut(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resume(&mut self) {
|
||||||
|
self.dispatch(plugin::OpCode::StateChanged, 0, 1, ptr::null_mut(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn suspend(&mut self) {
|
||||||
|
self.dispatch(plugin::OpCode::StateChanged, 0, 0, ptr::null_mut(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vendor_specific(&mut self, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize {
|
||||||
|
self.dispatch(plugin::OpCode::VendorSpecific, index, value, ptr, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_do(&self, can_do: plugin::CanDo) -> Supported {
|
||||||
|
let s: String = can_do.into();
|
||||||
|
Supported::from(self.write_string(plugin::OpCode::CanDo, 0, 0, &s, 0.0))
|
||||||
|
.expect("Invalid response received when querying plugin CanDo")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_tail_size(&self) -> isize {
|
||||||
|
self.opcode(plugin::OpCode::GetTailSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
|
||||||
|
if buffer.input_count() < self.info.inputs as usize {
|
||||||
|
panic!("Too few inputs in AudioBuffer");
|
||||||
|
}
|
||||||
|
if buffer.output_count() < self.info.outputs as usize {
|
||||||
|
panic!("Too few outputs in AudioBuffer");
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
((*self.get_effect()).processReplacing)(
|
||||||
|
self.get_effect(),
|
||||||
|
buffer.raw_inputs().as_ptr() as *const *const _,
|
||||||
|
buffer.raw_outputs().as_mut_ptr() as *mut *mut _,
|
||||||
|
buffer.samples() as i32,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_f64(&mut self, buffer: &mut AudioBuffer<f64>) {
|
||||||
|
if buffer.input_count() < self.info.inputs as usize {
|
||||||
|
panic!("Too few inputs in AudioBuffer");
|
||||||
|
}
|
||||||
|
if buffer.output_count() < self.info.outputs as usize {
|
||||||
|
panic!("Too few outputs in AudioBuffer");
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
((*self.get_effect()).processReplacingF64)(
|
||||||
|
self.get_effect(),
|
||||||
|
buffer.raw_inputs().as_ptr() as *const *const _,
|
||||||
|
buffer.raw_outputs().as_mut_ptr() as *mut *mut _,
|
||||||
|
buffer.samples() as i32,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_events(&mut self, events: &api::Events) {
|
||||||
|
self.dispatch(plugin::OpCode::ProcessEvents, 0, 0, events as *const _ as *mut _, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_input_info(&self, input: i32) -> ChannelInfo {
|
||||||
|
let mut props: MaybeUninit<api::ChannelProperties> = MaybeUninit::uninit();
|
||||||
|
let ptr = props.as_mut_ptr() as *mut c_void;
|
||||||
|
|
||||||
|
self.dispatch(plugin::OpCode::GetInputInfo, input, 0, ptr, 0.0);
|
||||||
|
|
||||||
|
ChannelInfo::from(unsafe { props.assume_init() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_output_info(&self, output: i32) -> ChannelInfo {
|
||||||
|
let mut props: MaybeUninit<api::ChannelProperties> = MaybeUninit::uninit();
|
||||||
|
let ptr = props.as_mut_ptr() as *mut c_void;
|
||||||
|
|
||||||
|
self.dispatch(plugin::OpCode::GetOutputInfo, output, 0, ptr, 0.0);
|
||||||
|
|
||||||
|
ChannelInfo::from(unsafe { props.assume_init() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
|
||||||
|
Arc::clone(&self.params) as Arc<dyn PluginParameters>
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_editor(&mut self) -> Option<Box<dyn Editor>> {
|
||||||
|
if self.is_editor_active {
|
||||||
|
// An editor is already active, the caller should be using the active editor instead of
|
||||||
|
// requesting for a new one.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.is_editor_active = true;
|
||||||
|
Some(Box::new(EditorInstance {
|
||||||
|
params: self.params.clone(),
|
||||||
|
is_open: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginParameters for PluginParametersInstance {
|
||||||
|
fn change_preset(&self, preset: i32) {
|
||||||
|
self.dispatch(plugin::OpCode::ChangePreset, 0, preset as isize, ptr::null_mut(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_preset_num(&self) -> i32 {
|
||||||
|
self.opcode(plugin::OpCode::GetCurrentPresetNum) as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_preset_name(&self, name: String) {
|
||||||
|
self.write_string(plugin::OpCode::SetCurrentPresetName, 0, 0, &name, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_preset_name(&self, preset: i32) -> String {
|
||||||
|
self.read_string_param(plugin::OpCode::GetPresetName, preset, 0, 0.0, MAX_PRESET_NAME_LEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parameter_label(&self, index: i32) -> String {
|
||||||
|
self.read_string_param(plugin::OpCode::GetParameterLabel, index, 0, 0.0, MAX_PARAM_STR_LEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parameter_text(&self, index: i32) -> String {
|
||||||
|
self.read_string_param(plugin::OpCode::GetParameterDisplay, index, 0, 0.0, MAX_PARAM_STR_LEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parameter_name(&self, index: i32) -> String {
|
||||||
|
self.read_string_param(plugin::OpCode::GetParameterName, index, 0, 0.0, MAX_PARAM_STR_LEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parameter(&self, index: i32) -> f32 {
|
||||||
|
unsafe { ((*self.get_effect()).getParameter)(self.get_effect(), index) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_parameter(&self, index: i32, value: f32) {
|
||||||
|
unsafe { ((*self.get_effect()).setParameter)(self.get_effect(), index, value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_be_automated(&self, index: i32) -> bool {
|
||||||
|
self.dispatch(plugin::OpCode::CanBeAutomated, index, 0, ptr::null_mut(), 0.0) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_to_parameter(&self, index: i32, text: String) -> bool {
|
||||||
|
self.write_string(plugin::OpCode::StringToParameter, index, 0, &text, 0.0) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Editor
|
||||||
|
|
||||||
|
fn get_preset_data(&self) -> Vec<u8> {
|
||||||
|
// Create a pointer that can be updated from the plugin.
|
||||||
|
let mut ptr: *mut u8 = ptr::null_mut();
|
||||||
|
let len = self.dispatch(
|
||||||
|
plugin::OpCode::GetData,
|
||||||
|
1, /*preset*/
|
||||||
|
0,
|
||||||
|
&mut ptr as *mut *mut u8 as *mut c_void,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
let slice = unsafe { slice::from_raw_parts(ptr, len as usize) };
|
||||||
|
slice.to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_bank_data(&self) -> Vec<u8> {
|
||||||
|
// Create a pointer that can be updated from the plugin.
|
||||||
|
let mut ptr: *mut u8 = ptr::null_mut();
|
||||||
|
let len = self.dispatch(
|
||||||
|
plugin::OpCode::GetData,
|
||||||
|
0, /*bank*/
|
||||||
|
0,
|
||||||
|
&mut ptr as *mut *mut u8 as *mut c_void,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
let slice = unsafe { slice::from_raw_parts(ptr, len as usize) };
|
||||||
|
slice.to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_preset_data(&self, data: &[u8]) {
|
||||||
|
self.dispatch(
|
||||||
|
plugin::OpCode::SetData,
|
||||||
|
1,
|
||||||
|
data.len() as isize,
|
||||||
|
data.as_ptr() as *mut c_void,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_bank_data(&self, data: &[u8]) {
|
||||||
|
self.dispatch(
|
||||||
|
plugin::OpCode::SetData,
|
||||||
|
0,
|
||||||
|
data.len() as isize,
|
||||||
|
data.as_ptr() as *mut c_void,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used for constructing `AudioBuffer` instances on the host.
|
||||||
|
///
|
||||||
|
/// This struct contains all necessary allocations for an `AudioBuffer` apart
|
||||||
|
/// from the actual sample arrays. This way, the inner processing loop can
|
||||||
|
/// be allocation free even if `AudioBuffer` instances are repeatedly created.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use vst::host::HostBuffer;
|
||||||
|
/// # use vst::plugin::Plugin;
|
||||||
|
/// # fn test<P: Plugin>(plugin: &mut P) {
|
||||||
|
/// let mut host_buffer: HostBuffer<f32> = HostBuffer::new(2, 2);
|
||||||
|
/// let inputs = vec![vec![0.0; 1000]; 2];
|
||||||
|
/// let mut outputs = vec![vec![0.0; 1000]; 2];
|
||||||
|
/// let mut audio_buffer = host_buffer.bind(&inputs, &mut outputs);
|
||||||
|
/// plugin.process(&mut audio_buffer);
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub struct HostBuffer<T: Float> {
|
||||||
|
inputs: Vec<*const T>,
|
||||||
|
outputs: Vec<*mut T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Float> HostBuffer<T> {
|
||||||
|
/// Create a `HostBuffer` for a given number of input and output channels.
|
||||||
|
pub fn new(input_count: usize, output_count: usize) -> HostBuffer<T> {
|
||||||
|
HostBuffer {
|
||||||
|
inputs: vec![ptr::null(); input_count],
|
||||||
|
outputs: vec![ptr::null_mut(); output_count],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a `HostBuffer` for the number of input and output channels
|
||||||
|
/// specified in an `Info` struct.
|
||||||
|
pub fn from_info(info: &Info) -> HostBuffer<T> {
|
||||||
|
HostBuffer::new(info.inputs as usize, info.outputs as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bind sample arrays to the `HostBuffer` to create an `AudioBuffer` to pass to a plugin.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// This function will panic if more inputs or outputs are supplied than the `HostBuffer`
|
||||||
|
/// was created for, or if the sample arrays do not all have the same length.
|
||||||
|
pub fn bind<'a, I, O>(&'a mut self, input_arrays: &[I], output_arrays: &mut [O]) -> AudioBuffer<'a, T>
|
||||||
|
where
|
||||||
|
I: AsRef<[T]> + 'a,
|
||||||
|
O: AsMut<[T]> + 'a,
|
||||||
|
{
|
||||||
|
// Check that number of desired inputs and outputs fit in allocation
|
||||||
|
if input_arrays.len() > self.inputs.len() {
|
||||||
|
panic!("Too many inputs for HostBuffer");
|
||||||
|
}
|
||||||
|
if output_arrays.len() > self.outputs.len() {
|
||||||
|
panic!("Too many outputs for HostBuffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize raw pointers and find common length
|
||||||
|
let mut length = None;
|
||||||
|
for (i, input) in input_arrays.iter().map(|r| r.as_ref()).enumerate() {
|
||||||
|
self.inputs[i] = input.as_ptr();
|
||||||
|
match length {
|
||||||
|
None => length = Some(input.len()),
|
||||||
|
Some(old_length) => {
|
||||||
|
if input.len() != old_length {
|
||||||
|
panic!("Mismatching lengths of input arrays");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i, output) in output_arrays.iter_mut().map(|r| r.as_mut()).enumerate() {
|
||||||
|
self.outputs[i] = output.as_mut_ptr();
|
||||||
|
match length {
|
||||||
|
None => length = Some(output.len()),
|
||||||
|
Some(old_length) => {
|
||||||
|
if output.len() != old_length {
|
||||||
|
panic!("Mismatching lengths of output arrays");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let length = length.unwrap_or(0);
|
||||||
|
|
||||||
|
// Construct AudioBuffer
|
||||||
|
unsafe {
|
||||||
|
AudioBuffer::from_raw(
|
||||||
|
input_arrays.len(),
|
||||||
|
output_arrays.len(),
|
||||||
|
self.inputs.as_ptr(),
|
||||||
|
self.outputs.as_mut_ptr(),
|
||||||
|
length,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of input channels supported by this `HostBuffer`.
|
||||||
|
pub fn input_count(&self) -> usize {
|
||||||
|
self.inputs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of output channels supported by this `HostBuffer`.
|
||||||
|
pub fn output_count(&self) -> usize {
|
||||||
|
self.outputs.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HACK: a pointer to store the host so that it can be accessed from the `callback_wrapper`
|
||||||
|
/// function passed to the plugin.
|
||||||
|
///
|
||||||
|
/// When the plugin is being loaded, a `Box<Arc<Mutex<T>>>` is transmuted to a `*mut c_void` pointer
|
||||||
|
/// and placed here. When the plugin calls the callback during initialization, the host refers to
|
||||||
|
/// this pointer to get a handle to the Host. After initialization, this pointer is invalidated and
|
||||||
|
/// the host pointer is placed into a [reserved field] in the instance `AEffect` struct.
|
||||||
|
///
|
||||||
|
/// The issue with this approach is that if 2 plugins are simultaneously loaded with 2 different
|
||||||
|
/// host instances, this might fail as one host may receive a pointer to the other one. In practice
|
||||||
|
/// this is a rare situation as you normally won't have 2 separate host instances loading at once.
|
||||||
|
///
|
||||||
|
/// [reserved field]: ../api/struct.AEffect.html#structfield.reserved1
|
||||||
|
static mut LOAD_POINTER: *mut c_void = 0 as *mut c_void;
|
||||||
|
|
||||||
|
/// Function passed to plugin to handle dispatching host opcodes.
|
||||||
|
extern "C" fn callback_wrapper<T: Host>(
|
||||||
|
effect: *mut AEffect,
|
||||||
|
opcode: i32,
|
||||||
|
index: i32,
|
||||||
|
value: isize,
|
||||||
|
ptr: *mut c_void,
|
||||||
|
opt: f32,
|
||||||
|
) -> isize {
|
||||||
|
unsafe {
|
||||||
|
// If the effect pointer is not null and the host pointer is not null, the plugin has
|
||||||
|
// already been initialized
|
||||||
|
if !effect.is_null() && (*effect).reserved1 != 0 {
|
||||||
|
let reserved = (*effect).reserved1 as *const Arc<Mutex<T>>;
|
||||||
|
let host = &*reserved;
|
||||||
|
|
||||||
|
let host = &mut *host.lock().unwrap();
|
||||||
|
|
||||||
|
interfaces::host_dispatch(host, effect, opcode, index, value, ptr, opt)
|
||||||
|
// In this case, the plugin is still undergoing initialization and so `LOAD_POINTER` is
|
||||||
|
// dereferenced
|
||||||
|
} else {
|
||||||
|
// Used only during the plugin initialization
|
||||||
|
let host = LOAD_POINTER as *const Arc<Mutex<T>>;
|
||||||
|
let host = &*host;
|
||||||
|
let host = &mut *host.lock().unwrap();
|
||||||
|
|
||||||
|
interfaces::host_dispatch(host, effect, opcode, index, value, ptr, opt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::host::HostBuffer;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_buffer() {
|
||||||
|
const LENGTH: usize = 1_000_000;
|
||||||
|
let mut host_buffer: HostBuffer<f32> = HostBuffer::new(2, 2);
|
||||||
|
let input_left = vec![1.0; LENGTH];
|
||||||
|
let input_right = vec![1.0; LENGTH];
|
||||||
|
let mut output_left = vec![0.0; LENGTH];
|
||||||
|
let mut output_right = vec![0.0; LENGTH];
|
||||||
|
{
|
||||||
|
let mut audio_buffer = {
|
||||||
|
// Slices given to `bind` need not persist, but the sample arrays do.
|
||||||
|
let inputs = [&input_left, &input_right];
|
||||||
|
let mut outputs = [&mut output_left, &mut output_right];
|
||||||
|
host_buffer.bind(&inputs, &mut outputs)
|
||||||
|
};
|
||||||
|
for (input, output) in audio_buffer.zip() {
|
||||||
|
for (i, o) in input.iter().zip(output) {
|
||||||
|
*o = *i * 2.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(output_left, vec![2.0; LENGTH]);
|
||||||
|
assert_eq!(output_right, vec![2.0; LENGTH]);
|
||||||
|
}
|
||||||
|
}
|
||||||
370
deps/vst/src/interfaces.rs
vendored
Normal file
370
deps/vst/src/interfaces.rs
vendored
Normal file
|
|
@ -0,0 +1,370 @@
|
||||||
|
//! Function interfaces for VST 2.4 API.
|
||||||
|
|
||||||
|
#![doc(hidden)]
|
||||||
|
|
||||||
|
use std::cell::Cell;
|
||||||
|
use std::os::raw::{c_char, c_void};
|
||||||
|
use std::{mem, slice};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::{self, consts::*, AEffect, TimeInfo},
|
||||||
|
buffer::AudioBuffer,
|
||||||
|
editor::{Key, KeyCode, KnobMode, Rect},
|
||||||
|
host::Host,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Deprecated process function.
|
||||||
|
pub extern "C" fn process_deprecated(
|
||||||
|
_effect: *mut AEffect,
|
||||||
|
_raw_inputs: *const *const f32,
|
||||||
|
_raw_outputs: *mut *mut f32,
|
||||||
|
_samples: i32,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// VST2.4 replacing function.
|
||||||
|
pub extern "C" fn process_replacing(
|
||||||
|
effect: *mut AEffect,
|
||||||
|
raw_inputs: *const *const f32,
|
||||||
|
raw_outputs: *mut *mut f32,
|
||||||
|
samples: i32,
|
||||||
|
) {
|
||||||
|
// Handle to the VST
|
||||||
|
let plugin = unsafe { (*effect).get_plugin() };
|
||||||
|
let info = unsafe { (*effect).get_info() };
|
||||||
|
let (input_count, output_count) = (info.inputs as usize, info.outputs as usize);
|
||||||
|
let mut buffer =
|
||||||
|
unsafe { AudioBuffer::from_raw(input_count, output_count, raw_inputs, raw_outputs, samples as usize) };
|
||||||
|
plugin.process(&mut buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// VST2.4 replacing function with `f64` values.
|
||||||
|
pub extern "C" fn process_replacing_f64(
|
||||||
|
effect: *mut AEffect,
|
||||||
|
raw_inputs: *const *const f64,
|
||||||
|
raw_outputs: *mut *mut f64,
|
||||||
|
samples: i32,
|
||||||
|
) {
|
||||||
|
let plugin = unsafe { (*effect).get_plugin() };
|
||||||
|
let info = unsafe { (*effect).get_info() };
|
||||||
|
let (input_count, output_count) = (info.inputs as usize, info.outputs as usize);
|
||||||
|
let mut buffer =
|
||||||
|
unsafe { AudioBuffer::from_raw(input_count, output_count, raw_inputs, raw_outputs, samples as usize) };
|
||||||
|
plugin.process_f64(&mut buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// VST2.4 set parameter function.
|
||||||
|
pub extern "C" fn set_parameter(effect: *mut AEffect, index: i32, value: f32) {
|
||||||
|
unsafe { (*effect).get_params() }.set_parameter(index, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// VST2.4 get parameter function.
|
||||||
|
pub extern "C" fn get_parameter(effect: *mut AEffect, index: i32) -> f32 {
|
||||||
|
unsafe { (*effect).get_params() }.get_parameter(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy a string into a destination buffer.
|
||||||
|
///
|
||||||
|
/// String will be cut at `max` characters.
|
||||||
|
fn copy_string(dst: *mut c_void, src: &str, max: usize) -> isize {
|
||||||
|
unsafe {
|
||||||
|
use libc::{memcpy, memset};
|
||||||
|
use std::cmp::min;
|
||||||
|
|
||||||
|
let dst = dst as *mut c_void;
|
||||||
|
memset(dst, 0, max);
|
||||||
|
memcpy(dst, src.as_ptr() as *const c_void, min(max, src.as_bytes().len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
1 // Success
|
||||||
|
}
|
||||||
|
|
||||||
|
/// VST2.4 dispatch function. This function handles dispatching all opcodes to the VST plugin.
|
||||||
|
pub extern "C" fn dispatch(
|
||||||
|
effect: *mut AEffect,
|
||||||
|
opcode: i32,
|
||||||
|
index: i32,
|
||||||
|
value: isize,
|
||||||
|
ptr: *mut c_void,
|
||||||
|
opt: f32,
|
||||||
|
) -> isize {
|
||||||
|
use crate::plugin::{CanDo, OpCode};
|
||||||
|
|
||||||
|
// Convert passed in opcode to enum
|
||||||
|
let opcode = OpCode::try_from(opcode);
|
||||||
|
// Only query plugin or editor when needed to avoid creating multiple
|
||||||
|
// concurrent mutable references to the same object.
|
||||||
|
let get_plugin = || unsafe { (*effect).get_plugin() };
|
||||||
|
let get_editor = || unsafe { (*effect).get_editor() };
|
||||||
|
let params = unsafe { (*effect).get_params() };
|
||||||
|
|
||||||
|
match opcode {
|
||||||
|
Ok(OpCode::Initialize) => get_plugin().init(),
|
||||||
|
Ok(OpCode::Shutdown) => unsafe {
|
||||||
|
(*effect).drop_plugin();
|
||||||
|
drop(Box::from_raw(effect))
|
||||||
|
},
|
||||||
|
|
||||||
|
Ok(OpCode::ChangePreset) => params.change_preset(value as i32),
|
||||||
|
Ok(OpCode::GetCurrentPresetNum) => return params.get_preset_num() as isize,
|
||||||
|
Ok(OpCode::SetCurrentPresetName) => params.set_preset_name(read_string(ptr)),
|
||||||
|
Ok(OpCode::GetCurrentPresetName) => {
|
||||||
|
let num = params.get_preset_num();
|
||||||
|
return copy_string(ptr, ¶ms.get_preset_name(num), MAX_PRESET_NAME_LEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(OpCode::GetParameterLabel) => {
|
||||||
|
return copy_string(ptr, ¶ms.get_parameter_label(index), MAX_PARAM_STR_LEN)
|
||||||
|
}
|
||||||
|
Ok(OpCode::GetParameterDisplay) => {
|
||||||
|
return copy_string(ptr, ¶ms.get_parameter_text(index), MAX_PARAM_STR_LEN)
|
||||||
|
}
|
||||||
|
Ok(OpCode::GetParameterName) => return copy_string(ptr, ¶ms.get_parameter_name(index), MAX_PARAM_STR_LEN),
|
||||||
|
|
||||||
|
Ok(OpCode::SetSampleRate) => get_plugin().set_sample_rate(opt),
|
||||||
|
Ok(OpCode::SetBlockSize) => get_plugin().set_block_size(value as i64),
|
||||||
|
Ok(OpCode::StateChanged) => {
|
||||||
|
if value == 1 {
|
||||||
|
get_plugin().resume();
|
||||||
|
} else {
|
||||||
|
get_plugin().suspend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(OpCode::EditorGetRect) => {
|
||||||
|
if let Some(ref mut editor) = get_editor() {
|
||||||
|
let size = editor.size();
|
||||||
|
let pos = editor.position();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// Given a Rect** structure
|
||||||
|
// TODO: Investigate whether we are given a valid Rect** pointer already
|
||||||
|
*(ptr as *mut *mut c_void) = Box::into_raw(Box::new(Rect {
|
||||||
|
left: pos.0 as i16, // x coord of position
|
||||||
|
top: pos.1 as i16, // y coord of position
|
||||||
|
right: (pos.0 + size.0) as i16, // x coord of pos + x coord of size
|
||||||
|
bottom: (pos.1 + size.1) as i16, // y coord of pos + y coord of size
|
||||||
|
})) as *mut _; // TODO: free memory
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(OpCode::EditorOpen) => {
|
||||||
|
if let Some(ref mut editor) = get_editor() {
|
||||||
|
// `ptr` is a window handle to the parent window.
|
||||||
|
// See the documentation for `Editor::open` for details.
|
||||||
|
if editor.open(ptr) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(OpCode::EditorClose) => {
|
||||||
|
if let Some(ref mut editor) = get_editor() {
|
||||||
|
editor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(OpCode::EditorIdle) => {
|
||||||
|
if let Some(ref mut editor) = get_editor() {
|
||||||
|
editor.idle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(OpCode::GetData) => {
|
||||||
|
let mut chunks = if index == 0 {
|
||||||
|
params.get_bank_data()
|
||||||
|
} else {
|
||||||
|
params.get_preset_data()
|
||||||
|
};
|
||||||
|
|
||||||
|
chunks.shrink_to_fit();
|
||||||
|
let len = chunks.len() as isize; // eventually we should be using ffi::size_t
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
*(ptr as *mut *mut c_void) = chunks.as_ptr() as *mut c_void;
|
||||||
|
}
|
||||||
|
|
||||||
|
mem::forget(chunks);
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
Ok(OpCode::SetData) => {
|
||||||
|
let chunks = unsafe { slice::from_raw_parts(ptr as *mut u8, value as usize) };
|
||||||
|
|
||||||
|
if index == 0 {
|
||||||
|
params.load_bank_data(chunks);
|
||||||
|
} else {
|
||||||
|
params.load_preset_data(chunks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(OpCode::ProcessEvents) => {
|
||||||
|
get_plugin().process_events(unsafe { &*(ptr as *const api::Events) });
|
||||||
|
}
|
||||||
|
Ok(OpCode::CanBeAutomated) => return params.can_be_automated(index) as isize,
|
||||||
|
Ok(OpCode::StringToParameter) => return params.string_to_parameter(index, read_string(ptr)) as isize,
|
||||||
|
|
||||||
|
Ok(OpCode::GetPresetName) => return copy_string(ptr, ¶ms.get_preset_name(index), MAX_PRESET_NAME_LEN),
|
||||||
|
|
||||||
|
Ok(OpCode::GetInputInfo) => {
|
||||||
|
if index >= 0 && index < get_plugin().get_info().inputs {
|
||||||
|
unsafe {
|
||||||
|
let ptr = ptr as *mut api::ChannelProperties;
|
||||||
|
*ptr = get_plugin().get_input_info(index).into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(OpCode::GetOutputInfo) => {
|
||||||
|
if index >= 0 && index < get_plugin().get_info().outputs {
|
||||||
|
unsafe {
|
||||||
|
let ptr = ptr as *mut api::ChannelProperties;
|
||||||
|
*ptr = get_plugin().get_output_info(index).into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(OpCode::GetCategory) => {
|
||||||
|
return get_plugin().get_info().category.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(OpCode::GetEffectName) => return copy_string(ptr, &get_plugin().get_info().name, MAX_VENDOR_STR_LEN),
|
||||||
|
|
||||||
|
Ok(OpCode::GetVendorName) => return copy_string(ptr, &get_plugin().get_info().vendor, MAX_VENDOR_STR_LEN),
|
||||||
|
Ok(OpCode::GetProductName) => return copy_string(ptr, &get_plugin().get_info().name, MAX_PRODUCT_STR_LEN),
|
||||||
|
Ok(OpCode::GetVendorVersion) => return get_plugin().get_info().version as isize,
|
||||||
|
Ok(OpCode::VendorSpecific) => return get_plugin().vendor_specific(index, value, ptr, opt),
|
||||||
|
Ok(OpCode::CanDo) => {
|
||||||
|
let can_do = CanDo::from_str(&read_string(ptr));
|
||||||
|
return get_plugin().can_do(can_do).into();
|
||||||
|
}
|
||||||
|
Ok(OpCode::GetTailSize) => {
|
||||||
|
if get_plugin().get_tail_size() == 0 {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return get_plugin().get_tail_size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//OpCode::GetParamInfo => { /*TODO*/ }
|
||||||
|
Ok(OpCode::GetApiVersion) => return 2400,
|
||||||
|
|
||||||
|
Ok(OpCode::EditorKeyDown) => {
|
||||||
|
if let Some(ref mut editor) = get_editor() {
|
||||||
|
if let Ok(key) = Key::try_from(value) {
|
||||||
|
editor.key_down(KeyCode {
|
||||||
|
character: index as u8 as char,
|
||||||
|
key,
|
||||||
|
modifier: opt.to_bits() as u8,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(OpCode::EditorKeyUp) => {
|
||||||
|
if let Some(ref mut editor) = get_editor() {
|
||||||
|
if let Ok(key) = Key::try_from(value) {
|
||||||
|
editor.key_up(KeyCode {
|
||||||
|
character: index as u8 as char,
|
||||||
|
key,
|
||||||
|
modifier: opt.to_bits() as u8,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(OpCode::EditorSetKnobMode) => {
|
||||||
|
if let Some(ref mut editor) = get_editor() {
|
||||||
|
if let Ok(knob_mode) = KnobMode::try_from(value) {
|
||||||
|
editor.set_knob_mode(knob_mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(OpCode::StartProcess) => get_plugin().start_process(),
|
||||||
|
Ok(OpCode::StopProcess) => get_plugin().stop_process(),
|
||||||
|
|
||||||
|
Ok(OpCode::GetNumMidiInputs) => return unsafe { (*effect).get_info() }.midi_inputs as isize,
|
||||||
|
Ok(OpCode::GetNumMidiOutputs) => return unsafe { (*effect).get_info() }.midi_outputs as isize,
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
debug!("Unimplemented opcode ({:?})", opcode);
|
||||||
|
trace!(
|
||||||
|
"Arguments; index: {}, value: {}, ptr: {:?}, opt: {}",
|
||||||
|
index,
|
||||||
|
value,
|
||||||
|
ptr,
|
||||||
|
opt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn host_dispatch(
|
||||||
|
host: &mut dyn Host,
|
||||||
|
effect: *mut AEffect,
|
||||||
|
opcode: i32,
|
||||||
|
index: i32,
|
||||||
|
value: isize,
|
||||||
|
ptr: *mut c_void,
|
||||||
|
opt: f32,
|
||||||
|
) -> isize {
|
||||||
|
use crate::host::OpCode;
|
||||||
|
|
||||||
|
let opcode = OpCode::try_from(opcode);
|
||||||
|
match opcode {
|
||||||
|
Ok(OpCode::Version) => return 2400,
|
||||||
|
Ok(OpCode::Automate) => host.automate(index, opt),
|
||||||
|
Ok(OpCode::BeginEdit) => host.begin_edit(index),
|
||||||
|
Ok(OpCode::EndEdit) => host.end_edit(index),
|
||||||
|
|
||||||
|
Ok(OpCode::Idle) => host.idle(),
|
||||||
|
|
||||||
|
// ...
|
||||||
|
Ok(OpCode::CanDo) => {
|
||||||
|
info!("Plugin is asking if host can: {}.", read_string(ptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(OpCode::GetVendorVersion) => return host.get_info().0,
|
||||||
|
Ok(OpCode::GetVendorString) => return copy_string(ptr, &host.get_info().1, MAX_VENDOR_STR_LEN),
|
||||||
|
Ok(OpCode::GetProductString) => return copy_string(ptr, &host.get_info().2, MAX_PRODUCT_STR_LEN),
|
||||||
|
Ok(OpCode::ProcessEvents) => {
|
||||||
|
host.process_events(unsafe { &*(ptr as *const api::Events) });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(OpCode::GetTime) => {
|
||||||
|
return match host.get_time_info(value as i32) {
|
||||||
|
None => 0,
|
||||||
|
Some(result) => {
|
||||||
|
thread_local! {
|
||||||
|
static TIME_INFO: Cell<TimeInfo> =
|
||||||
|
Cell::new(TimeInfo::default());
|
||||||
|
}
|
||||||
|
TIME_INFO.with(|time_info| {
|
||||||
|
(*time_info).set(result);
|
||||||
|
time_info.as_ptr() as isize
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(OpCode::GetBlockSize) => return host.get_block_size(),
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
trace!("VST: Got unimplemented host opcode ({:?})", opcode);
|
||||||
|
trace!(
|
||||||
|
"Arguments; effect: {:?}, index: {}, value: {}, ptr: {:?}, opt: {}",
|
||||||
|
effect,
|
||||||
|
index,
|
||||||
|
value,
|
||||||
|
ptr,
|
||||||
|
opt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a string from the `ptr` buffer
|
||||||
|
fn read_string(ptr: *mut c_void) -> String {
|
||||||
|
use std::ffi::CStr;
|
||||||
|
|
||||||
|
String::from_utf8_lossy(unsafe { CStr::from_ptr(ptr as *mut c_char).to_bytes() }).into_owned()
|
||||||
|
}
|
||||||
416
deps/vst/src/lib.rs
vendored
Executable file
416
deps/vst/src/lib.rs
vendored
Executable file
|
|
@ -0,0 +1,416 @@
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
//! A rust implementation of the VST2.4 API.
|
||||||
|
//!
|
||||||
|
//! The VST API is multi-threaded. A VST host calls into a plugin generally from two threads -
|
||||||
|
//! the *processing* thread and the *UI* thread. The organization of this crate reflects this
|
||||||
|
//! structure to ensure that the threading assumptions of Safe Rust are fulfilled and data
|
||||||
|
//! races are avoided.
|
||||||
|
//!
|
||||||
|
//! # Plugins
|
||||||
|
//! All Plugins must implement the `Plugin` trait and `std::default::Default`.
|
||||||
|
//! The `plugin_main!` macro must also be called in order to export the necessary functions
|
||||||
|
//! for the plugin to function.
|
||||||
|
//!
|
||||||
|
//! ## `Plugin` Trait
|
||||||
|
//! All methods in this trait have a default implementation except for the `get_info` method which
|
||||||
|
//! must be implemented by the plugin. Any of the default implementations may be overridden for
|
||||||
|
//! custom functionality; the defaults do nothing on their own.
|
||||||
|
//!
|
||||||
|
//! ## `PluginParameters` Trait
|
||||||
|
//! The methods in this trait handle access to plugin parameters. Since the host may call these
|
||||||
|
//! methods concurrently with audio processing, it needs to be separate from the main `Plugin`
|
||||||
|
//! trait.
|
||||||
|
//!
|
||||||
|
//! To support parameters, a plugin must provide an implementation of the `PluginParameters`
|
||||||
|
//! trait, wrap it in an `Arc` (so it can be accessed from both threads) and
|
||||||
|
//! return a reference to it from the `get_parameter_object` method in the `Plugin`.
|
||||||
|
//!
|
||||||
|
//! ## `plugin_main!` macro
|
||||||
|
//! `plugin_main!` will export the necessary functions to create a proper VST plugin. This must be
|
||||||
|
//! called with your VST plugin struct name in order for the vst to work.
|
||||||
|
//!
|
||||||
|
//! ## Example plugin
|
||||||
|
//! A barebones VST plugin:
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! #[macro_use]
|
||||||
|
//! extern crate vst;
|
||||||
|
//!
|
||||||
|
//! use vst::plugin::{HostCallback, Info, Plugin};
|
||||||
|
//!
|
||||||
|
//! struct BasicPlugin;
|
||||||
|
//!
|
||||||
|
//! impl Plugin for BasicPlugin {
|
||||||
|
//! fn new(_host: HostCallback) -> Self {
|
||||||
|
//! BasicPlugin
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn get_info(&self) -> Info {
|
||||||
|
//! Info {
|
||||||
|
//! name: "Basic Plugin".to_string(),
|
||||||
|
//! unique_id: 1357, // Used by hosts to differentiate between plugins.
|
||||||
|
//!
|
||||||
|
//! ..Default::default()
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! plugin_main!(BasicPlugin); // Important!
|
||||||
|
//! # fn main() {} // For `extern crate vst`
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! # Hosts
|
||||||
|
//!
|
||||||
|
//! ## `Host` Trait
|
||||||
|
//! All hosts must implement the [`Host` trait](host/trait.Host.html). To load a VST plugin, you
|
||||||
|
//! need to wrap your host in an `Arc<Mutex<T>>` wrapper for thread safety reasons. Along with the
|
||||||
|
//! plugin path, this can be passed to the [`PluginLoader::load`] method to create a plugin loader
|
||||||
|
//! which can spawn plugin instances.
|
||||||
|
//!
|
||||||
|
//! ## Example Host
|
||||||
|
//! ```no_run
|
||||||
|
//! extern crate vst;
|
||||||
|
//!
|
||||||
|
//! use std::sync::{Arc, Mutex};
|
||||||
|
//! use std::path::Path;
|
||||||
|
//!
|
||||||
|
//! use vst::host::{Host, PluginLoader};
|
||||||
|
//! use vst::plugin::Plugin;
|
||||||
|
//!
|
||||||
|
//! struct SampleHost;
|
||||||
|
//!
|
||||||
|
//! impl Host for SampleHost {
|
||||||
|
//! fn automate(&self, index: i32, value: f32) {
|
||||||
|
//! println!("Parameter {} had its value changed to {}", index, value);
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn main() {
|
||||||
|
//! let host = Arc::new(Mutex::new(SampleHost));
|
||||||
|
//! let path = Path::new("/path/to/vst");
|
||||||
|
//!
|
||||||
|
//! let mut loader = PluginLoader::load(path, host.clone()).unwrap();
|
||||||
|
//! let mut instance = loader.instance().unwrap();
|
||||||
|
//!
|
||||||
|
//! println!("Loaded {}", instance.get_info().name);
|
||||||
|
//!
|
||||||
|
//! instance.init();
|
||||||
|
//! println!("Initialized instance!");
|
||||||
|
//!
|
||||||
|
//! println!("Closing instance...");
|
||||||
|
//! // Not necessary as the instance is shut down when it goes out of scope anyway.
|
||||||
|
//! // drop(instance);
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! [`PluginLoader::load`]: host/struct.PluginLoader.html#method.load
|
||||||
|
//!
|
||||||
|
|
||||||
|
extern crate libc;
|
||||||
|
extern crate libloading;
|
||||||
|
extern crate num_enum;
|
||||||
|
extern crate num_traits;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate bitflags;
|
||||||
|
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
pub mod api;
|
||||||
|
pub mod buffer;
|
||||||
|
mod cache;
|
||||||
|
pub mod channels;
|
||||||
|
pub mod editor;
|
||||||
|
pub mod event;
|
||||||
|
pub mod host;
|
||||||
|
mod interfaces;
|
||||||
|
pub mod plugin;
|
||||||
|
pub mod prelude;
|
||||||
|
pub mod util;
|
||||||
|
|
||||||
|
use api::consts::VST_MAGIC;
|
||||||
|
use api::{AEffect, HostCallbackProc};
|
||||||
|
use cache::PluginCache;
|
||||||
|
use plugin::{HostCallback, Plugin};
|
||||||
|
|
||||||
|
/// Exports the necessary symbols for the plugin to be used by a VST host.
|
||||||
|
///
|
||||||
|
/// This macro takes a type which must implement the `Plugin` trait.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! plugin_main {
|
||||||
|
($t:ty) => {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn main_macho(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect {
|
||||||
|
VSTPluginMain(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn MAIN(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect {
|
||||||
|
VSTPluginMain(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn VSTPluginMain(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect {
|
||||||
|
$crate::main::<$t>(callback)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes a VST plugin and returns a raw pointer to an AEffect struct.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn main<T: Plugin>(callback: HostCallbackProc) -> *mut AEffect {
|
||||||
|
// Initialize as much of the AEffect as we can before creating the plugin.
|
||||||
|
// In particular, initialize all the function pointers, since initializing
|
||||||
|
// these to zero is undefined behavior.
|
||||||
|
let boxed_effect = Box::new(AEffect {
|
||||||
|
magic: VST_MAGIC,
|
||||||
|
dispatcher: interfaces::dispatch, // fn pointer
|
||||||
|
|
||||||
|
_process: interfaces::process_deprecated, // fn pointer
|
||||||
|
|
||||||
|
setParameter: interfaces::set_parameter, // fn pointer
|
||||||
|
getParameter: interfaces::get_parameter, // fn pointer
|
||||||
|
|
||||||
|
numPrograms: 0, // To be updated with plugin specific value.
|
||||||
|
numParams: 0, // To be updated with plugin specific value.
|
||||||
|
numInputs: 0, // To be updated with plugin specific value.
|
||||||
|
numOutputs: 0, // To be updated with plugin specific value.
|
||||||
|
|
||||||
|
flags: 0, // To be updated with plugin specific value.
|
||||||
|
|
||||||
|
reserved1: 0,
|
||||||
|
reserved2: 0,
|
||||||
|
|
||||||
|
initialDelay: 0, // To be updated with plugin specific value.
|
||||||
|
|
||||||
|
_realQualities: 0,
|
||||||
|
_offQualities: 0,
|
||||||
|
_ioRatio: 0.0,
|
||||||
|
|
||||||
|
object: ptr::null_mut(),
|
||||||
|
user: ptr::null_mut(),
|
||||||
|
|
||||||
|
uniqueId: 0, // To be updated with plugin specific value.
|
||||||
|
version: 0, // To be updated with plugin specific value.
|
||||||
|
|
||||||
|
processReplacing: interfaces::process_replacing, // fn pointer
|
||||||
|
processReplacingF64: interfaces::process_replacing_f64, //fn pointer
|
||||||
|
|
||||||
|
future: [0u8; 56],
|
||||||
|
});
|
||||||
|
let raw_effect = Box::into_raw(boxed_effect);
|
||||||
|
|
||||||
|
let host = HostCallback::wrap(callback, raw_effect);
|
||||||
|
if host.vst_version() == 0 {
|
||||||
|
// TODO: Better criteria would probably be useful here...
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!("Creating VST plugin instance...");
|
||||||
|
let mut plugin = T::new(host);
|
||||||
|
let info = plugin.get_info();
|
||||||
|
let params = plugin.get_parameter_object();
|
||||||
|
let editor = plugin.get_editor();
|
||||||
|
|
||||||
|
// Update AEffect in place
|
||||||
|
let effect = unsafe { &mut *raw_effect };
|
||||||
|
effect.numPrograms = info.presets;
|
||||||
|
effect.numParams = info.parameters;
|
||||||
|
effect.numInputs = info.inputs;
|
||||||
|
effect.numOutputs = info.outputs;
|
||||||
|
effect.flags = {
|
||||||
|
use api::PluginFlags;
|
||||||
|
|
||||||
|
let mut flag = PluginFlags::CAN_REPLACING;
|
||||||
|
|
||||||
|
if info.f64_precision {
|
||||||
|
flag |= PluginFlags::CAN_DOUBLE_REPLACING;
|
||||||
|
}
|
||||||
|
|
||||||
|
if editor.is_some() {
|
||||||
|
flag |= PluginFlags::HAS_EDITOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.preset_chunks {
|
||||||
|
flag |= PluginFlags::PROGRAM_CHUNKS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let plugin::Category::Synth = info.category {
|
||||||
|
flag |= PluginFlags::IS_SYNTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.silent_when_stopped {
|
||||||
|
flag |= PluginFlags::NO_SOUND_IN_STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.bits()
|
||||||
|
};
|
||||||
|
effect.initialDelay = info.initial_delay;
|
||||||
|
effect.object = Box::into_raw(Box::new(Box::new(plugin) as Box<dyn Plugin>)) as *mut _;
|
||||||
|
effect.user = Box::into_raw(Box::new(PluginCache::new(&info, params, editor))) as *mut _;
|
||||||
|
effect.uniqueId = info.unique_id;
|
||||||
|
effect.version = info.version;
|
||||||
|
|
||||||
|
effect
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
use std::os::raw::c_void;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::{consts::VST_MAGIC, AEffect},
|
||||||
|
interfaces,
|
||||||
|
plugin::{HostCallback, Info, Plugin},
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TestPlugin;
|
||||||
|
|
||||||
|
impl Plugin for TestPlugin {
|
||||||
|
fn new(_host: HostCallback) -> Self {
|
||||||
|
TestPlugin
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_info(&self) -> Info {
|
||||||
|
Info {
|
||||||
|
name: "Test Plugin".to_string(),
|
||||||
|
vendor: "overdrivenpotato".to_string(),
|
||||||
|
|
||||||
|
presets: 1,
|
||||||
|
parameters: 1,
|
||||||
|
|
||||||
|
unique_id: 5678,
|
||||||
|
version: 1234,
|
||||||
|
|
||||||
|
initial_delay: 123,
|
||||||
|
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin_main!(TestPlugin);
|
||||||
|
|
||||||
|
extern "C" fn pass_callback(
|
||||||
|
_effect: *mut AEffect,
|
||||||
|
_opcode: i32,
|
||||||
|
_index: i32,
|
||||||
|
_value: isize,
|
||||||
|
_ptr: *mut c_void,
|
||||||
|
_opt: f32,
|
||||||
|
) -> isize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" fn fail_callback(
|
||||||
|
_effect: *mut AEffect,
|
||||||
|
_opcode: i32,
|
||||||
|
_index: i32,
|
||||||
|
_value: isize,
|
||||||
|
_ptr: *mut c_void,
|
||||||
|
_opt: f32,
|
||||||
|
) -> isize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[test]
|
||||||
|
fn old_hosts() {
|
||||||
|
assert_eq!(MAIN(fail_callback), ptr::null_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[test]
|
||||||
|
fn old_hosts() {
|
||||||
|
assert_eq!(main_macho(fail_callback), ptr::null_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_callback() {
|
||||||
|
assert_eq!(VSTPluginMain(fail_callback), ptr::null_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aeffect_created() {
|
||||||
|
let aeffect = VSTPluginMain(pass_callback);
|
||||||
|
assert!(!aeffect.is_null());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plugin_drop() {
|
||||||
|
static mut DROP_TEST: bool = false;
|
||||||
|
|
||||||
|
impl Drop for TestPlugin {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
DROP_TEST = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let aeffect = VSTPluginMain(pass_callback);
|
||||||
|
assert!(!aeffect.is_null());
|
||||||
|
|
||||||
|
unsafe { (*aeffect).drop_plugin() };
|
||||||
|
|
||||||
|
// Assert that the VST is shut down and dropped.
|
||||||
|
assert!(unsafe { DROP_TEST });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plugin_no_drop() {
|
||||||
|
let aeffect = VSTPluginMain(pass_callback);
|
||||||
|
assert!(!aeffect.is_null());
|
||||||
|
|
||||||
|
// Make sure this doesn't crash.
|
||||||
|
unsafe { (*aeffect).drop_plugin() };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plugin_deref() {
|
||||||
|
let aeffect = VSTPluginMain(pass_callback);
|
||||||
|
assert!(!aeffect.is_null());
|
||||||
|
|
||||||
|
let plugin = unsafe { (*aeffect).get_plugin() };
|
||||||
|
// Assert that deref works correctly.
|
||||||
|
assert!(plugin.get_info().name == "Test Plugin");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aeffect_params() {
|
||||||
|
// Assert that 2 function pointers are equal.
|
||||||
|
macro_rules! assert_fn_eq {
|
||||||
|
($a:expr, $b:expr) => {
|
||||||
|
assert_eq!($a as usize, $b as usize);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let aeffect = unsafe { &mut *VSTPluginMain(pass_callback) };
|
||||||
|
|
||||||
|
assert_eq!(aeffect.magic, VST_MAGIC);
|
||||||
|
assert_fn_eq!(aeffect.dispatcher, interfaces::dispatch);
|
||||||
|
assert_fn_eq!(aeffect._process, interfaces::process_deprecated);
|
||||||
|
assert_fn_eq!(aeffect.setParameter, interfaces::set_parameter);
|
||||||
|
assert_fn_eq!(aeffect.getParameter, interfaces::get_parameter);
|
||||||
|
assert_eq!(aeffect.numPrograms, 1);
|
||||||
|
assert_eq!(aeffect.numParams, 1);
|
||||||
|
assert_eq!(aeffect.numInputs, 2);
|
||||||
|
assert_eq!(aeffect.numOutputs, 2);
|
||||||
|
assert_eq!(aeffect.reserved1, 0);
|
||||||
|
assert_eq!(aeffect.reserved2, 0);
|
||||||
|
assert_eq!(aeffect.initialDelay, 123);
|
||||||
|
assert_eq!(aeffect.uniqueId, 5678);
|
||||||
|
assert_eq!(aeffect.version, 1234);
|
||||||
|
assert_fn_eq!(aeffect.processReplacing, interfaces::process_replacing);
|
||||||
|
assert_fn_eq!(aeffect.processReplacingF64, interfaces::process_replacing_f64);
|
||||||
|
}
|
||||||
|
}
|
||||||
1086
deps/vst/src/plugin.rs
vendored
Normal file
1086
deps/vst/src/plugin.rs
vendored
Normal file
File diff suppressed because it is too large
Load diff
12
deps/vst/src/prelude.rs
vendored
Normal file
12
deps/vst/src/prelude.rs
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
//! A collection of commonly used items for implement a Plugin
|
||||||
|
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use crate::api::{Events, Supported};
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use crate::buffer::{AudioBuffer, SendEventBuffer};
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use crate::event::{Event, MidiEvent};
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use crate::plugin::{CanDo, Category, HostCallback, Info, Plugin, PluginParameters};
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use crate::util::{AtomicFloat, ParameterTransfer};
|
||||||
59
deps/vst/src/util/atomic_float.rs
vendored
Normal file
59
deps/vst/src/util/atomic_float.rs
vendored
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
|
||||||
|
/// Simple atomic floating point variable with relaxed ordering.
|
||||||
|
///
|
||||||
|
/// Designed for the common case of sharing VST parameters between
|
||||||
|
/// multiple threads when no synchronization or change notification
|
||||||
|
/// is needed.
|
||||||
|
pub struct AtomicFloat {
|
||||||
|
atomic: AtomicU32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AtomicFloat {
|
||||||
|
/// New atomic float with initial value `value`.
|
||||||
|
pub fn new(value: f32) -> AtomicFloat {
|
||||||
|
AtomicFloat {
|
||||||
|
atomic: AtomicU32::new(value.to_bits()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current value of the atomic float.
|
||||||
|
pub fn get(&self) -> f32 {
|
||||||
|
f32::from_bits(self.atomic.load(Ordering::Relaxed))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the value of the atomic float to `value`.
|
||||||
|
pub fn set(&self, value: f32) {
|
||||||
|
self.atomic.store(value.to_bits(), Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AtomicFloat {
|
||||||
|
fn default() -> Self {
|
||||||
|
AtomicFloat::new(0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for AtomicFloat {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
std::fmt::Debug::fmt(&self.get(), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for AtomicFloat {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
std::fmt::Display::fmt(&self.get(), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f32> for AtomicFloat {
|
||||||
|
fn from(value: f32) -> Self {
|
||||||
|
AtomicFloat::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AtomicFloat> for f32 {
|
||||||
|
fn from(value: AtomicFloat) -> Self {
|
||||||
|
value.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
7
deps/vst/src/util/mod.rs
vendored
Normal file
7
deps/vst/src/util/mod.rs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
//! Structures for easing the implementation of VST plugins.
|
||||||
|
|
||||||
|
mod atomic_float;
|
||||||
|
mod parameter_transfer;
|
||||||
|
|
||||||
|
pub use self::atomic_float::AtomicFloat;
|
||||||
|
pub use self::parameter_transfer::{ParameterTransfer, ParameterTransferIterator};
|
||||||
187
deps/vst/src/util/parameter_transfer.rs
vendored
Normal file
187
deps/vst/src/util/parameter_transfer.rs
vendored
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
use std::mem::size_of;
|
||||||
|
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
const USIZE_BITS: usize = size_of::<usize>() * 8;
|
||||||
|
|
||||||
|
fn word_and_bit(index: usize) -> (usize, usize) {
|
||||||
|
(index / USIZE_BITS, 1usize << (index & (USIZE_BITS - 1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of parameters that can be shared between threads.
|
||||||
|
///
|
||||||
|
/// Supports efficient iteration over parameters that changed since last iteration.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ParameterTransfer {
|
||||||
|
values: Vec<AtomicU32>,
|
||||||
|
changed: Vec<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParameterTransfer {
|
||||||
|
/// Create a new parameter set with `parameter_count` parameters.
|
||||||
|
pub fn new(parameter_count: usize) -> Self {
|
||||||
|
let bit_words = (parameter_count + USIZE_BITS - 1) / USIZE_BITS;
|
||||||
|
ParameterTransfer {
|
||||||
|
values: (0..parameter_count).map(|_| AtomicU32::new(0)).collect(),
|
||||||
|
changed: (0..bit_words).map(|_| AtomicUsize::new(0)).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the value of the parameter with index `index` to `value` and mark
|
||||||
|
/// it as changed.
|
||||||
|
pub fn set_parameter(&self, index: usize, value: f32) {
|
||||||
|
let (word, bit) = word_and_bit(index);
|
||||||
|
self.values[index].store(value.to_bits(), Ordering::Relaxed);
|
||||||
|
self.changed[word].fetch_or(bit, Ordering::AcqRel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current value of the parameter with index `index`.
|
||||||
|
pub fn get_parameter(&self, index: usize) -> f32 {
|
||||||
|
f32::from_bits(self.values[index].load(Ordering::Relaxed))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all parameters marked as changed. If `acquire` is `true`,
|
||||||
|
/// mark all returned parameters as no longer changed.
|
||||||
|
///
|
||||||
|
/// The iterator returns a pair of `(index, value)` for each changed parameter.
|
||||||
|
///
|
||||||
|
/// When parameters have been changed on the current thread, the iterator is
|
||||||
|
/// precise: it reports all changed parameters with the values they were last
|
||||||
|
/// changed to.
|
||||||
|
///
|
||||||
|
/// When parameters are changed on a different thread, the iterator is
|
||||||
|
/// conservative, in the sense that it is guaranteed to report changed
|
||||||
|
/// parameters eventually, but if a parameter is changed multiple times in
|
||||||
|
/// a short period of time, it may skip some of the changes (but never the
|
||||||
|
/// last) and may report an extra, spurious change at the end.
|
||||||
|
///
|
||||||
|
/// The changed parameters are reported in increasing index order, and the same
|
||||||
|
/// parameter is never reported more than once in the same iteration.
|
||||||
|
pub fn iterate(&self, acquire: bool) -> ParameterTransferIterator {
|
||||||
|
ParameterTransferIterator {
|
||||||
|
pt: self,
|
||||||
|
word: 0,
|
||||||
|
bit: 1,
|
||||||
|
acquire,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An iterator over changed parameters.
|
||||||
|
/// Returned by [`iterate`](struct.ParameterTransfer.html#method.iterate).
|
||||||
|
pub struct ParameterTransferIterator<'pt> {
|
||||||
|
pt: &'pt ParameterTransfer,
|
||||||
|
word: usize,
|
||||||
|
bit: usize,
|
||||||
|
acquire: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'pt> Iterator for ParameterTransferIterator<'pt> {
|
||||||
|
type Item = (usize, f32);
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<(usize, f32)> {
|
||||||
|
let bits = loop {
|
||||||
|
if self.word == self.pt.changed.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let bits = self.pt.changed[self.word].load(Ordering::Acquire) & self.bit.wrapping_neg();
|
||||||
|
if bits != 0 {
|
||||||
|
break bits;
|
||||||
|
}
|
||||||
|
self.word += 1;
|
||||||
|
self.bit = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
let bit_index = bits.trailing_zeros() as usize;
|
||||||
|
let bit = 1usize << bit_index;
|
||||||
|
let index = self.word * USIZE_BITS + bit_index;
|
||||||
|
|
||||||
|
if self.acquire {
|
||||||
|
self.pt.changed[self.word].fetch_and(!bit, Ordering::AcqRel);
|
||||||
|
}
|
||||||
|
|
||||||
|
let next_bit = bit << 1;
|
||||||
|
if next_bit == 0 {
|
||||||
|
self.word += 1;
|
||||||
|
self.bit = 1;
|
||||||
|
} else {
|
||||||
|
self.bit = next_bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((index, self.pt.get_parameter(index)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
extern crate rand;
|
||||||
|
|
||||||
|
use crate::util::ParameterTransfer;
|
||||||
|
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use self::rand::rngs::StdRng;
|
||||||
|
use self::rand::{Rng, SeedableRng};
|
||||||
|
|
||||||
|
const THREADS: usize = 3;
|
||||||
|
const PARAMETERS: usize = 1000;
|
||||||
|
const UPDATES: usize = 1_000_000;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parameter_transfer() {
|
||||||
|
let transfer = Arc::new(ParameterTransfer::new(PARAMETERS));
|
||||||
|
let (tx, rx) = channel();
|
||||||
|
|
||||||
|
// Launch threads that change parameters
|
||||||
|
for t in 0..THREADS {
|
||||||
|
let t_transfer = Arc::clone(&transfer);
|
||||||
|
let t_tx = tx.clone();
|
||||||
|
let mut t_rng = StdRng::seed_from_u64(t as u64);
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut values = vec![0f32; PARAMETERS];
|
||||||
|
for _ in 0..UPDATES {
|
||||||
|
let p: usize = t_rng.gen_range(0..PARAMETERS);
|
||||||
|
let v: f32 = t_rng.gen_range(0.0..1.0);
|
||||||
|
values[p] = v;
|
||||||
|
t_transfer.set_parameter(p, v);
|
||||||
|
}
|
||||||
|
t_tx.send(values).unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continually receive updates from threads
|
||||||
|
let mut values = vec![0f32; PARAMETERS];
|
||||||
|
let mut results = vec![];
|
||||||
|
let mut acquire_rng = StdRng::seed_from_u64(42);
|
||||||
|
while results.len() < THREADS {
|
||||||
|
let mut last_p = -1;
|
||||||
|
for (p, v) in transfer.iterate(acquire_rng.gen_bool(0.9)) {
|
||||||
|
assert!(p as isize > last_p);
|
||||||
|
last_p = p as isize;
|
||||||
|
values[p] = v;
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_micros(100));
|
||||||
|
while let Ok(result) = rx.try_recv() {
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One last iteration to pick up all updates
|
||||||
|
let mut last_p = -1;
|
||||||
|
for (p, v) in transfer.iterate(true) {
|
||||||
|
assert!(p as isize > last_p);
|
||||||
|
last_p = p as isize;
|
||||||
|
values[p] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now there should be no more updates
|
||||||
|
assert!(transfer.iterate(true).next().is_none());
|
||||||
|
|
||||||
|
// Verify final values
|
||||||
|
for p in 0..PARAMETERS {
|
||||||
|
assert!((0..THREADS).any(|t| results[t][p] == values[p]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
device/Cargo.toml
Normal file
59
device/Cargo.toml
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_device"
|
||||||
|
edition = { workspace = true }
|
||||||
|
version = { workspace = true }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "device.rs"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")']
|
||||||
|
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tek_engine = { path = "../engine" }
|
||||||
|
|
||||||
|
atomic_float = { workspace = true }
|
||||||
|
backtrace = { workspace = true }
|
||||||
|
clap = { workspace = true, optional = true }
|
||||||
|
jack = { workspace = true }
|
||||||
|
konst = { workspace = true }
|
||||||
|
livi = { workspace = true, optional = true }
|
||||||
|
midly = { workspace = true }
|
||||||
|
palette = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
symphonia = { workspace = true, optional = true }
|
||||||
|
tengri = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
uuid = { workspace = true, optional = true }
|
||||||
|
wavers = { workspace = true, optional = true }
|
||||||
|
winit = { workspace = true, optional = true }
|
||||||
|
xdg = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
proptest = { workspace = true }
|
||||||
|
proptest-derive = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
arranger = ["port", "editor", "sequencer", "editor", "track", "scene", "clip", "select"]
|
||||||
|
browse = []
|
||||||
|
clap = []
|
||||||
|
cli = ["dep:clap"]
|
||||||
|
clip = []
|
||||||
|
clock = []
|
||||||
|
default = ["cli", "arranger", "sampler", "track", "lv2"]
|
||||||
|
editor = []
|
||||||
|
host = ["lv2"]
|
||||||
|
lv2 = ["port", "livi"]
|
||||||
|
lv2_gui = ["lv2", "winit"]
|
||||||
|
meter = []
|
||||||
|
mixer = []
|
||||||
|
pool = []
|
||||||
|
port = []
|
||||||
|
sampler = ["port", "meter", "mixer", "browse", "symphonia", "wavers"]
|
||||||
|
select = []
|
||||||
|
scene = []
|
||||||
|
sequencer = ["port", "clock", "uuid", "pool"]
|
||||||
|
sf2 = []
|
||||||
|
track = []
|
||||||
|
vst2 = []
|
||||||
|
vst3 = []
|
||||||
626
device/arranger.rs
Normal file
626
device/arranger.rs
Normal file
|
|
@ -0,0 +1,626 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Default, Debug)] pub struct Arrangement {
|
||||||
|
/// Project name.
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Base color.
|
||||||
|
pub color: ItemTheme,
|
||||||
|
/// JACK client handle.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// FIXME a render of the project arrangement, redrawn on update.
|
||||||
|
/// TODO rename to "render_cache" or smth
|
||||||
|
pub arranger: Arc<RwLock<Buffer>>,
|
||||||
|
/// Display size
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
/// Display size of clips area
|
||||||
|
pub size_inner: Measure<TuiOut>,
|
||||||
|
/// Source of time
|
||||||
|
#[cfg(feature = "clock")] pub clock: Clock,
|
||||||
|
/// Allows one MIDI clip to be edited
|
||||||
|
#[cfg(feature = "editor")] pub editor: Option<MidiEditor>,
|
||||||
|
/// List of global midi inputs
|
||||||
|
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
||||||
|
/// List of global midi outputs
|
||||||
|
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
||||||
|
/// List of global audio inputs
|
||||||
|
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
||||||
|
/// List of global audio outputs
|
||||||
|
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
||||||
|
/// Selected UI element
|
||||||
|
#[cfg(feature = "select")] pub selection: Selection,
|
||||||
|
/// Last track number (to avoid duplicate port names)
|
||||||
|
#[cfg(feature = "track")] pub track_last: usize,
|
||||||
|
/// List of tracks
|
||||||
|
#[cfg(feature = "track")] pub tracks: Vec<Track>,
|
||||||
|
/// Scroll offset of tracks
|
||||||
|
#[cfg(feature = "track")] pub track_scroll: usize,
|
||||||
|
/// List of scenes
|
||||||
|
#[cfg(feature = "scene")] pub scenes: Vec<Scene>,
|
||||||
|
/// Scroll offset of scenes
|
||||||
|
#[cfg(feature = "scene")] pub scene_scroll: usize,
|
||||||
|
}
|
||||||
|
impl HasJack<'static> for Arrangement {
|
||||||
|
fn jack (&self) -> &Jack<'static> {
|
||||||
|
&self.jack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
has!(Jack<'static>: |self: Arrangement|self.jack);
|
||||||
|
has!(Measure<TuiOut>: |self: Arrangement|self.size);
|
||||||
|
#[cfg(feature = "editor")] has!(Option<MidiEditor>: |self: Arrangement|self.editor);
|
||||||
|
#[cfg(feature = "port")] has!(Vec<MidiInput>: |self: Arrangement|self.midi_ins);
|
||||||
|
#[cfg(feature = "port")] has!(Vec<MidiOutput>: |self: Arrangement|self.midi_outs);
|
||||||
|
#[cfg(feature = "clock")] has!(Clock: |self: Arrangement|self.clock);
|
||||||
|
#[cfg(feature = "select")] has!(Selection: |self: Arrangement|self.selection);
|
||||||
|
#[cfg(all(feature = "select", feature = "track"))] has!(Vec<Track>: |self: Arrangement|self.tracks);
|
||||||
|
#[cfg(all(feature = "select", feature = "track"))] maybe_has!(Track: |self: Arrangement|
|
||||||
|
{ Has::<Selection>::get(self).track().map(|index|Has::<Vec<Track>>::get(self).get(index)).flatten() };
|
||||||
|
{ Has::<Selection>::get(self).track().map(|index|Has::<Vec<Track>>::get_mut(self).get_mut(index)).flatten() });
|
||||||
|
#[cfg(all(feature = "select", feature = "scene"))] has!(Vec<Scene>: |self: Arrangement|self.scenes);
|
||||||
|
#[cfg(all(feature = "select", feature = "scene"))] maybe_has!(Scene: |self: Arrangement|
|
||||||
|
{ Has::<Selection>::get(self).track().map(|index|Has::<Vec<Scene>>::get(self).get(index)).flatten() };
|
||||||
|
{ Has::<Selection>::get(self).track().map(|index|Has::<Vec<Scene>>::get_mut(self).get_mut(index)).flatten() });
|
||||||
|
|
||||||
|
#[cfg(feature = "select")]
|
||||||
|
impl Arrangement {
|
||||||
|
#[cfg(feature = "clip")] fn selected_clip (&self) -> Option<MidiClip> { todo!() }
|
||||||
|
#[cfg(feature = "scene")] fn selected_scene (&self) -> Option<Scene> { todo!() }
|
||||||
|
#[cfg(feature = "track")] fn selected_track (&self) -> Option<Track> { todo!() }
|
||||||
|
#[cfg(feature = "port")] fn selected_midi_in (&self) -> Option<MidiInput> { todo!() }
|
||||||
|
#[cfg(feature = "port")] fn selected_midi_out (&self) -> Option<MidiOutput> { todo!() }
|
||||||
|
fn selected_device (&self) -> Option<Device> { todo!() }
|
||||||
|
fn unselect (&self) -> Selection {
|
||||||
|
Selection::Nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Arrangement {
|
||||||
|
/// Width of display
|
||||||
|
pub fn w (&self) -> u16 {
|
||||||
|
self.size.w() as u16
|
||||||
|
}
|
||||||
|
/// Width allocated for sidebar.
|
||||||
|
pub fn w_sidebar (&self, is_editing: bool) -> u16 {
|
||||||
|
self.w() / if is_editing { 16 } else { 8 } as u16
|
||||||
|
}
|
||||||
|
/// Width available to display tracks.
|
||||||
|
pub fn w_tracks_area (&self, is_editing: bool) -> u16 {
|
||||||
|
self.w().saturating_sub(self.w_sidebar(is_editing))
|
||||||
|
}
|
||||||
|
/// Height of display
|
||||||
|
pub fn h (&self) -> u16 {
|
||||||
|
self.size.h() as u16
|
||||||
|
}
|
||||||
|
/// Height taken by visible device slots.
|
||||||
|
pub fn h_devices (&self) -> u16 {
|
||||||
|
2
|
||||||
|
//1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "track")]
|
||||||
|
impl TracksView for Arrangement {}
|
||||||
|
|
||||||
|
#[cfg(feature = "track")]
|
||||||
|
impl Arrangement {
|
||||||
|
/// Get the active track
|
||||||
|
pub fn get_track (&self) -> Option<&Track> {
|
||||||
|
let index = self.selection().track()?;
|
||||||
|
Has::<Vec<Track>>::get(self).get(index)
|
||||||
|
}
|
||||||
|
/// Get a mutable reference to the active track
|
||||||
|
pub fn get_track_mut (&mut self) -> Option<&mut Track> {
|
||||||
|
let index = self.selection().track()?;
|
||||||
|
Has::<Vec<Track>>::get_mut(self).get_mut(index)
|
||||||
|
}
|
||||||
|
/// Add multiple tracks
|
||||||
|
pub fn tracks_add (
|
||||||
|
&mut self,
|
||||||
|
count: usize, width: Option<usize>,
|
||||||
|
mins: &[Connect], mouts: &[Connect],
|
||||||
|
) -> Usually<()> {
|
||||||
|
let track_color_1 = ItemColor::random();
|
||||||
|
let track_color_2 = ItemColor::random();
|
||||||
|
for i in 0..count {
|
||||||
|
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
|
||||||
|
let track = self.track_add(None, Some(color), mins, mouts)?.1;
|
||||||
|
if let Some(width) = width {
|
||||||
|
track.width = width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Add a track
|
||||||
|
pub fn track_add (
|
||||||
|
&mut self,
|
||||||
|
name: Option<&str>, color: Option<ItemTheme>,
|
||||||
|
mins: &[Connect], mouts: &[Connect],
|
||||||
|
) -> Usually<(usize, &mut Track)> {
|
||||||
|
let name: Arc<str> = name.map_or_else(
|
||||||
|
||format!("trk{:02}", self.track_last).into(),
|
||||||
|
|x|x.to_string().into()
|
||||||
|
);
|
||||||
|
self.track_last += 1;
|
||||||
|
let track = Track {
|
||||||
|
width: (name.len() + 2).max(12),
|
||||||
|
color: color.unwrap_or_else(ItemTheme::random),
|
||||||
|
sequencer: Sequencer::new(
|
||||||
|
&format!("{name}"),
|
||||||
|
self.jack(),
|
||||||
|
Some(self.clock()),
|
||||||
|
None,
|
||||||
|
mins,
|
||||||
|
mouts
|
||||||
|
)?,
|
||||||
|
name,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
self.tracks_mut().push(track);
|
||||||
|
let len = self.tracks().len();
|
||||||
|
let index = len - 1;
|
||||||
|
for scene in self.scenes_mut().iter_mut() {
|
||||||
|
while scene.clips.len() < len {
|
||||||
|
scene.clips.push(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((index, &mut self.tracks_mut()[index]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_inputs (&self, _theme: ItemTheme) -> impl Content<TuiOut> + '_ {
|
||||||
|
Bsp::s(
|
||||||
|
Fixed::Y(1, self.view_inputs_header()),
|
||||||
|
Thunk::new(|to: &mut TuiOut|{
|
||||||
|
for (index, port) in self.midi_ins().iter().enumerate() {
|
||||||
|
to.place(&Push::X(index as u16 * 10, Fixed::Y(1, self.view_inputs_row(port))))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_inputs_header (&self) -> impl Content<TuiOut> + '_ {
|
||||||
|
Bsp::e(Fixed::X(20, Align::w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))),
|
||||||
|
Bsp::w(Fixed::X(4, button_2("I", "+", false)), Thunk::new(move|to: &mut TuiOut|for (_index, track, x1, _x2) in self.tracks_with_sizes() {
|
||||||
|
#[cfg(feature = "track")]
|
||||||
|
to.place(&Push::X(x1 as u16, Tui::bg(track.color.dark.rgb, Align::w(Fixed::X(track.width as u16, row!(
|
||||||
|
Either::new(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "),
|
||||||
|
Either::new(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "),
|
||||||
|
Either::new(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "),
|
||||||
|
))))))
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_inputs_row (&self, port: &MidiInput) -> impl Content<TuiOut> {
|
||||||
|
Bsp::e(Fixed::X(20, Align::w(Bsp::e(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))),
|
||||||
|
Bsp::w(Fixed::X(4, ()), Thunk::new(move|to: &mut TuiOut|for (_index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||||||
|
#[cfg(feature = "track")]
|
||||||
|
to.place(&Tui::bg(track.color.darker.rgb, Align::w(Fixed::X(track.width as u16, row!(
|
||||||
|
Either::new(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "),
|
||||||
|
Either::new(track.sequencer.recording, Tui::fg(Red, " ● "), " · "),
|
||||||
|
Either::new(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "),
|
||||||
|
)))))
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_outputs (&self, theme: ItemTheme) -> impl Content<TuiOut> {
|
||||||
|
let mut h = 1;
|
||||||
|
for output in self.midi_outs().iter() {
|
||||||
|
h += 1 + output.connections.len();
|
||||||
|
}
|
||||||
|
let h = h as u16;
|
||||||
|
let list = Bsp::s(
|
||||||
|
Fixed::Y(1, Fill::X(Align::w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))),
|
||||||
|
Fixed::Y(h - 1, Fill::XY(Align::nw(Thunk::new(|to: &mut TuiOut|{
|
||||||
|
for (_index, port) in self.midi_outs().iter().enumerate() {
|
||||||
|
to.place(&Fixed::Y(1,Fill::X(Bsp::e(
|
||||||
|
Align::w(Bsp::e(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))),
|
||||||
|
Fill::X(Align::e(format!("{}/{} ",
|
||||||
|
port.port().get_connections().len(),
|
||||||
|
port.connections.len())))))));
|
||||||
|
for (index, conn) in port.connections.iter().enumerate() {
|
||||||
|
to.place(&Fixed::Y(1, Fill::X(Align::w(format!(" c{index:02}{}", conn.info())))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})))));
|
||||||
|
Fixed::Y(h, view_track_row_section(theme, list, button_2("O", "+", false),
|
||||||
|
Tui::bg(theme.darker.rgb, Align::w(Fill::X(
|
||||||
|
Thunk::new(|to: &mut TuiOut|{
|
||||||
|
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||||||
|
to.place(&Fixed::X(track_width(index, track),
|
||||||
|
Thunk::new(|to: &mut TuiOut|{
|
||||||
|
to.place(&Fixed::Y(1, Align::w(Bsp::e(
|
||||||
|
Either::new(true, Tui::fg(Green, "play "), "play "),
|
||||||
|
Either::new(false, Tui::fg(Yellow, "solo "), "solo "),
|
||||||
|
))));
|
||||||
|
for (_index, port) in self.midi_outs().iter().enumerate() {
|
||||||
|
to.place(&Fixed::Y(1, Align::w(Bsp::e(
|
||||||
|
Either::new(true, Tui::fg(Green, " ● "), " · "),
|
||||||
|
Either::new(false, Tui::fg(Yellow, " ● "), " · "),
|
||||||
|
))));
|
||||||
|
for (_index, _conn) in port.connections.iter().enumerate() {
|
||||||
|
to.place(&Fixed::Y(1, Fill::X("")));
|
||||||
|
}
|
||||||
|
}})))}}))))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_track_devices (&self, theme: ItemTheme) -> impl Content<TuiOut> {
|
||||||
|
let mut h = 2u16;
|
||||||
|
for track in self.tracks().iter() {
|
||||||
|
h = h.max(track.devices.len() as u16 * 2);
|
||||||
|
}
|
||||||
|
view_track_row_section(theme,
|
||||||
|
button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false),
|
||||||
|
button_2("D", "+", false),
|
||||||
|
Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||||||
|
to.place(&Fixed::XY(track_width(index, track), h + 1,
|
||||||
|
Tui::bg(track.color.dark.rgb, Align::nw(Map::south(2, move||0..h,
|
||||||
|
|_, _index|Fixed::XY(track.width as u16, 2,
|
||||||
|
Tui::fg_bg(
|
||||||
|
ItemTheme::G[32].lightest.rgb,
|
||||||
|
ItemTheme::G[32].dark.rgb,
|
||||||
|
Align::nw(format!(" · {}", "--")))))))));
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "track")]
|
||||||
|
pub fn view_track_row_section (
|
||||||
|
_theme: ItemTheme,
|
||||||
|
button: impl Content<TuiOut>,
|
||||||
|
button_add: impl Content<TuiOut>,
|
||||||
|
content: impl Content<TuiOut>,
|
||||||
|
) -> impl Content<TuiOut> {
|
||||||
|
Bsp::w(Fill::Y(Fixed::X(4, Align::nw(button_add))),
|
||||||
|
Bsp::e(Fixed::X(20, Fill::Y(Align::nw(button))), Fill::XY(Align::c(content))))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "scene")]
|
||||||
|
impl Arrangement {
|
||||||
|
/// Get the active scene
|
||||||
|
pub fn get_scene (&self) -> Option<&Scene> {
|
||||||
|
let index = self.selection().scene()?;
|
||||||
|
Has::<Vec<Scene>>::get(self).get(index)
|
||||||
|
}
|
||||||
|
/// Get a mutable reference to the active scene
|
||||||
|
pub fn get_scene_mut (&mut self) -> Option<&mut Scene> {
|
||||||
|
let index = self.selection().scene()?;
|
||||||
|
Has::<Vec<Scene>>::get_mut(self).get_mut(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "scene")]
|
||||||
|
impl ScenesView for Arrangement {
|
||||||
|
fn h_scenes (&self) -> u16 {
|
||||||
|
(self.height() as u16).saturating_sub(20)
|
||||||
|
}
|
||||||
|
fn w_side (&self) -> u16 {
|
||||||
|
(self.width() as u16 * 2 / 10).max(20)
|
||||||
|
}
|
||||||
|
fn w_mid (&self) -> u16 {
|
||||||
|
(self.width() as u16).saturating_sub(2 * self.w_side()).max(40)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "clip")]
|
||||||
|
impl Arrangement {
|
||||||
|
/// Get the active clip
|
||||||
|
pub fn get_clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
|
||||||
|
self.get_scene()?.clips.get(self.selection().track()?)?.clone()
|
||||||
|
}
|
||||||
|
/// Put a clip in a slot
|
||||||
|
pub fn clip_put (
|
||||||
|
&mut self, track: usize, scene: usize, clip: Option<Arc<RwLock<MidiClip>>>
|
||||||
|
) -> Option<Arc<RwLock<MidiClip>>> {
|
||||||
|
let old = self.scenes[scene].clips[track].clone();
|
||||||
|
self.scenes[scene].clips[track] = clip;
|
||||||
|
old
|
||||||
|
}
|
||||||
|
/// Change the color of a clip, returning the previous one
|
||||||
|
pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme)
|
||||||
|
-> Option<ItemTheme>
|
||||||
|
{
|
||||||
|
self.scenes[scene].clips[track].as_ref().map(|clip|{
|
||||||
|
let mut clip = clip.write().unwrap();
|
||||||
|
let old = clip.color.clone();
|
||||||
|
clip.color = color.clone();
|
||||||
|
panic!("{color:?} {old:?}");
|
||||||
|
old
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// Toggle looping for the active clip
|
||||||
|
pub fn toggle_loop (&mut self) {
|
||||||
|
if let Some(clip) = self.get_clip() {
|
||||||
|
clip.write().unwrap().toggle_loop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sampler")]
|
||||||
|
impl Arrangement {
|
||||||
|
/// Get the first sampler of the active track
|
||||||
|
pub fn sampler (&self) -> Option<&Sampler> {
|
||||||
|
self.get_track()?.sampler(0)
|
||||||
|
}
|
||||||
|
/// Get the first sampler of the active track
|
||||||
|
pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
|
||||||
|
self.get_track_mut()?.sampler_mut(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn wrap (bg: Color, fg: Color, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
||||||
|
let left = Tui::fg_bg(bg, Reset, Fixed::X(1, RepeatV("▐")));
|
||||||
|
let right = Tui::fg_bg(bg, Reset, Fixed::X(1, RepeatV("▌")));
|
||||||
|
Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasClipsSize {
|
||||||
|
fn clips_size (&self) -> &Measure<TuiOut>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasClipsSize for Arrangement {
|
||||||
|
fn clips_size (&self) -> &Measure<TuiOut> { &self.size_inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasWidth {
|
||||||
|
const MIN_WIDTH: usize;
|
||||||
|
/// Increment track width.
|
||||||
|
fn width_inc (&mut self);
|
||||||
|
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
|
||||||
|
fn width_dec (&mut self);
|
||||||
|
}
|
||||||
|
|
||||||
|
//def_command!(ArrangementCommand: |arranger: Arrangement| {
|
||||||
|
|
||||||
|
//Home => { arranger.editor = None; Ok(None) },
|
||||||
|
|
||||||
|
//Edit => {
|
||||||
|
//let selection = arranger.selection().clone();
|
||||||
|
//arranger.editor = if arranger.editor.is_some() {
|
||||||
|
//None
|
||||||
|
//} else {
|
||||||
|
//match selection {
|
||||||
|
//Selection::TrackClip { track, scene } => {
|
||||||
|
//let clip = &mut arranger.scenes_mut()[scene].clips[track];
|
||||||
|
//if clip.is_none() {
|
||||||
|
////app.clip_auto_create();
|
||||||
|
//*clip = Some(Arc::new(RwLock::new(MidiClip::new(
|
||||||
|
//&format!("t{track:02}s{scene:02}"),
|
||||||
|
//false, 384, None, Some(ItemTheme::random())
|
||||||
|
//))));
|
||||||
|
//}
|
||||||
|
//clip.as_ref().map(|c|c.into())
|
||||||
|
//}
|
||||||
|
//_ => {
|
||||||
|
//None
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//};
|
||||||
|
//if let Some(editor) = arranger.editor.as_mut() {
|
||||||
|
//if let Some(clip) = editor.clip() {
|
||||||
|
//let length = clip.read().unwrap().length.max(1);
|
||||||
|
//let width = arranger.size_inner.w().saturating_sub(20).max(1);
|
||||||
|
//editor.set_time_zoom(length / width);
|
||||||
|
//editor.redraw();
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
////// Set the selection
|
||||||
|
//Select { selection: Selection } => { *arranger.selection_mut() = *selection; Ok(None) },
|
||||||
|
|
||||||
|
////// Launch the selected clip or scene
|
||||||
|
//Launch => {
|
||||||
|
//match *arranger.selection() {
|
||||||
|
//Selection::Track(t) => {
|
||||||
|
//arranger.tracks[t].sequencer.enqueue_next(None)
|
||||||
|
//},
|
||||||
|
//Selection::TrackClip { track, scene } => {
|
||||||
|
//arranger.tracks[track].sequencer.enqueue_next(arranger.scenes[scene].clips[track].as_ref())
|
||||||
|
//},
|
||||||
|
//Selection::Scene(s) => {
|
||||||
|
//for t in 0..arranger.tracks.len() {
|
||||||
|
//arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref())
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//_ => {}
|
||||||
|
//};
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
////// Set the color of the selected entity
|
||||||
|
//SetColor { palette: Option<ItemTheme> } => {
|
||||||
|
//let mut palette = palette.unwrap_or_else(||ItemTheme::random());
|
||||||
|
//let selection = *arranger.selection();
|
||||||
|
//Ok(Some(Self::SetColor { palette: Some(match selection {
|
||||||
|
//Selection::Mix => {
|
||||||
|
//std::mem::swap(&mut palette, &mut arranger.color);
|
||||||
|
//palette
|
||||||
|
//},
|
||||||
|
//Selection::Scene(s) => {
|
||||||
|
//std::mem::swap(&mut palette, &mut arranger.scenes[s].color);
|
||||||
|
//palette
|
||||||
|
//}
|
||||||
|
//Selection::Track(t) => {
|
||||||
|
//std::mem::swap(&mut palette, &mut arranger.tracks[t].color);
|
||||||
|
//palette
|
||||||
|
//}
|
||||||
|
//Selection::TrackClip { track, scene } => {
|
||||||
|
//if let Some(ref clip) = arranger.scenes[scene].clips[track] {
|
||||||
|
//let mut clip = clip.write().unwrap();
|
||||||
|
//std::mem::swap(&mut palette, &mut clip.color);
|
||||||
|
//palette
|
||||||
|
//} else {
|
||||||
|
//return Ok(None)
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//_ => todo!()
|
||||||
|
//}) }))
|
||||||
|
//},
|
||||||
|
|
||||||
|
//Track { track: TrackCommand } => { todo!("delegate") },
|
||||||
|
|
||||||
|
//TrackAdd => {
|
||||||
|
//let index = arranger.track_add(None, None, &[], &[])?.0;
|
||||||
|
//*arranger.selection_mut() = match arranger.selection() {
|
||||||
|
//Selection::Track(_) => Selection::Track(index),
|
||||||
|
//Selection::TrackClip { track: _, scene } => Selection::TrackClip {
|
||||||
|
//track: index, scene: *scene
|
||||||
|
//},
|
||||||
|
//_ => *arranger.selection()
|
||||||
|
//};
|
||||||
|
//Ok(Some(Self::TrackDelete { index }))
|
||||||
|
//},
|
||||||
|
|
||||||
|
//TrackSwap { index: usize, other: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//let other = *other;
|
||||||
|
//Ok(Some(Self::TrackSwap { index, other }))
|
||||||
|
//},
|
||||||
|
|
||||||
|
//TrackDelete { index: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//let exists = arranger.tracks().get(index).is_some();
|
||||||
|
//if exists {
|
||||||
|
//let track = arranger.tracks_mut().remove(index);
|
||||||
|
//let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track;
|
||||||
|
//for port in midi_ins.into_iter() {
|
||||||
|
//port.close()?;
|
||||||
|
//}
|
||||||
|
//for port in midi_outs.into_iter() {
|
||||||
|
//port.close()?;
|
||||||
|
//}
|
||||||
|
//for scene in arranger.scenes_mut().iter_mut() {
|
||||||
|
//scene.clips.remove(index);
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//Ok(None)
|
||||||
|
////TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) })
|
||||||
|
//},
|
||||||
|
|
||||||
|
//MidiIn { input: MidiInputCommand } => {
|
||||||
|
//todo!("delegate"); Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//MidiInAdd => {
|
||||||
|
//arranger.midi_in_add()?;
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//MidiOut { output: MidiOutputCommand } => {
|
||||||
|
//todo!("delegate");
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//MidiOutAdd => {
|
||||||
|
//arranger.midi_out_add()?;
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//Device { command: DeviceCommand } => {
|
||||||
|
//todo!("delegate");
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//DeviceAdd { index: usize } => {
|
||||||
|
//todo!("delegate");
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//Scene { scene: SceneCommand } => {
|
||||||
|
//todo!("delegate");
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//OutputAdd => {
|
||||||
|
//arranger.midi_outs.push(MidiOutput::new(
|
||||||
|
//arranger.jack(),
|
||||||
|
//&format!("/M{}", arranger.midi_outs.len() + 1),
|
||||||
|
//&[]
|
||||||
|
//)?);
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//InputAdd => {
|
||||||
|
//arranger.midi_ins.push(MidiInput::new(
|
||||||
|
//arranger.jack(),
|
||||||
|
//&format!("M{}/", arranger.midi_ins.len() + 1),
|
||||||
|
//&[]
|
||||||
|
//)?);
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//SceneAdd => {
|
||||||
|
//let index = arranger.scene_add(None, None)?.0;
|
||||||
|
//*arranger.selection_mut() = match arranger.selection() {
|
||||||
|
//Selection::Scene(_) => Selection::Scene(index),
|
||||||
|
//Selection::TrackClip { track, scene } => Selection::TrackClip {
|
||||||
|
//track: *track,
|
||||||
|
//scene: index
|
||||||
|
//},
|
||||||
|
//_ => *arranger.selection()
|
||||||
|
//};
|
||||||
|
//Ok(None) // TODO
|
||||||
|
//},
|
||||||
|
|
||||||
|
//SceneSwap { index: usize, other: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//let other = *other;
|
||||||
|
//Ok(Some(Self::SceneSwap { index, other }))
|
||||||
|
//},
|
||||||
|
|
||||||
|
//SceneDelete { index: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//let scenes = arranger.scenes_mut();
|
||||||
|
//Ok(if scenes.get(index).is_some() {
|
||||||
|
//let _scene = scenes.remove(index);
|
||||||
|
//None
|
||||||
|
//} else {
|
||||||
|
//None
|
||||||
|
//})
|
||||||
|
//},
|
||||||
|
|
||||||
|
//SceneLaunch { index: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//for track in 0..arranger.tracks.len() {
|
||||||
|
//let clip = arranger.scenes[index].clips[track].as_ref();
|
||||||
|
//arranger.tracks[track].sequencer.enqueue_next(clip);
|
||||||
|
//}
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//Clip { scene: ClipCommand } => {
|
||||||
|
//todo!("delegate")
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipGet { a: usize, b: usize } => {
|
||||||
|
////(Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}"))
|
||||||
|
////("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap())))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipPut { a: usize, b: usize } => {
|
||||||
|
////(Put [t: usize, s: usize, c: MaybeClip]
|
||||||
|
////Some(Self::Put(t, s, arranger.clip_put(t, s, c))))
|
||||||
|
////("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap())))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipDel { a: usize, b: usize } => {
|
||||||
|
////("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None))))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipEnqueue { a: usize, b: usize } => {
|
||||||
|
////(Enqueue [t: usize, s: usize]
|
||||||
|
////cmd!(arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref())))
|
||||||
|
////("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap())))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipSwap { a: usize, b: usize }=> {
|
||||||
|
////(Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}"))
|
||||||
|
////("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap())))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//});
|
||||||
220
device/browse.rs
Normal file
220
device/browse.rs
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
use crate::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::ffi::OsString;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum BrowseTarget {
|
||||||
|
SaveProject,
|
||||||
|
LoadProject,
|
||||||
|
ImportSample(Arc<RwLock<Option<Sample>>>),
|
||||||
|
ExportSample(Arc<RwLock<Option<Sample>>>),
|
||||||
|
ImportClip(Arc<RwLock<Option<MidiClip>>>),
|
||||||
|
ExportClip(Arc<RwLock<Option<MidiClip>>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for BrowseTarget {
|
||||||
|
fn eq (&self, other: &Self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::ImportSample(_) => false,
|
||||||
|
Self::ExportSample(_) => false,
|
||||||
|
Self::ImportClip(_) => false,
|
||||||
|
Self::ExportClip(_) => false,
|
||||||
|
t => matches!(other, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Browses for phrase to import/export
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
|
pub struct Browse {
|
||||||
|
pub cwd: PathBuf,
|
||||||
|
pub dirs: Vec<(OsString, String)>,
|
||||||
|
pub files: Vec<(OsString, String)>,
|
||||||
|
pub filter: String,
|
||||||
|
pub index: usize,
|
||||||
|
pub scroll: usize,
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Browse {
|
||||||
|
|
||||||
|
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
|
||||||
|
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
|
||||||
|
let mut dirs = vec![];
|
||||||
|
let mut files = vec![];
|
||||||
|
for entry in std::fs::read_dir(&cwd)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let name = entry.file_name();
|
||||||
|
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
|
||||||
|
let meta = entry.metadata()?;
|
||||||
|
if meta.is_dir() {
|
||||||
|
dirs.push((name, format!("📁 {decoded}")));
|
||||||
|
} else if meta.is_file() {
|
||||||
|
files.push((name, format!("📄 {decoded}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
cwd,
|
||||||
|
dirs,
|
||||||
|
files,
|
||||||
|
filter: "".to_string(),
|
||||||
|
index: 0,
|
||||||
|
scroll: 0,
|
||||||
|
size: Measure::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len (&self) -> usize {
|
||||||
|
self.dirs.len() + self.files.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_dir (&self) -> bool {
|
||||||
|
self.index < self.dirs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_file (&self) -> bool {
|
||||||
|
self.index >= self.dirs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path (&self) -> PathBuf {
|
||||||
|
self.cwd.join(if self.is_dir() {
|
||||||
|
&self.dirs[self.index].0
|
||||||
|
} else if self.is_file() {
|
||||||
|
&self.files[self.index - self.dirs.len()].0
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chdir (&self) -> Usually<Self> {
|
||||||
|
Self::new(Some(self.path()))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Browse {
|
||||||
|
fn _todo_stub_path_buf (&self) -> PathBuf {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn _todo_stub_usize (&self) -> usize {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn _todo_stub_arc_str (&self) -> Arc<str> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def_command!(BrowseCommand: |browse: Browse| {
|
||||||
|
SetVisible => Ok(None),
|
||||||
|
SetPath { address: PathBuf } => Ok(None),
|
||||||
|
SetSearch { filter: Arc<str> } => Ok(None),
|
||||||
|
SetCursor { cursor: usize } => Ok(None),
|
||||||
|
});
|
||||||
|
|
||||||
|
impl HasContent<TuiOut> for Browse {
|
||||||
|
fn content (&self) -> impl Content<TuiOut> {
|
||||||
|
Map::south(1, ||EntriesIterator {
|
||||||
|
offset: 0,
|
||||||
|
index: 0,
|
||||||
|
length: self.dirs.len() + self.files.len(),
|
||||||
|
browser: self,
|
||||||
|
}, |entry, _index|Fill::X(Align::w(entry)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EntriesIterator<'a> {
|
||||||
|
browser: &'a Browse,
|
||||||
|
offset: usize,
|
||||||
|
length: usize,
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for EntriesIterator<'a> {
|
||||||
|
type Item = Modify<&'a str>;
|
||||||
|
fn next (&mut self) -> Option<Self::Item> {
|
||||||
|
let dirs = self.browser.dirs.len();
|
||||||
|
let files = self.browser.files.len();
|
||||||
|
let index = self.index;
|
||||||
|
if self.index < dirs {
|
||||||
|
self.index += 1;
|
||||||
|
Some(Tui::bold(true, self.browser.dirs[index].1.as_str()))
|
||||||
|
} else if self.index < dirs + files {
|
||||||
|
self.index += 1;
|
||||||
|
Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands supported by [Browse]
|
||||||
|
//#[derive(Debug, Clone, PartialEq)]
|
||||||
|
//pub enum BrowseCommand {
|
||||||
|
//Begin,
|
||||||
|
//Cancel,
|
||||||
|
//Confirm,
|
||||||
|
//Select(usize),
|
||||||
|
//Chdir(PathBuf),
|
||||||
|
//Filter(Arc<str>),
|
||||||
|
//}
|
||||||
|
//fn begin (browse: &mut Browse) => {
|
||||||
|
//unreachable!();
|
||||||
|
//}
|
||||||
|
//fn cancel (browse: &mut Browse) => {
|
||||||
|
//todo!()
|
||||||
|
////browse.mode = None;
|
||||||
|
////Ok(None)
|
||||||
|
//}
|
||||||
|
//fn confirm (browse: &mut Browse) => {
|
||||||
|
//todo!()
|
||||||
|
////Ok(match browse.mode {
|
||||||
|
////Some(PoolMode::Import(index, ref mut browse)) => {
|
||||||
|
////if browse.is_file() {
|
||||||
|
////let path = browse.path();
|
||||||
|
////browse.mode = None;
|
||||||
|
////let _undo = PoolClipCommand::import(browse, index, path)?;
|
||||||
|
////None
|
||||||
|
////} else if browse.is_dir() {
|
||||||
|
////browse.mode = Some(PoolMode::Import(index, browse.chdir()?));
|
||||||
|
////None
|
||||||
|
////} else {
|
||||||
|
////None
|
||||||
|
////}
|
||||||
|
////},
|
||||||
|
////Some(PoolMode::Export(index, ref mut browse)) => {
|
||||||
|
////todo!()
|
||||||
|
////},
|
||||||
|
////_ => unreachable!(),
|
||||||
|
////})
|
||||||
|
//}
|
||||||
|
//fn select (browse: &mut Browse, index: usize) => {
|
||||||
|
//todo!()
|
||||||
|
////Ok(match browse.mode {
|
||||||
|
////Some(PoolMode::Import(index, ref mut browse)) => {
|
||||||
|
////browse.index = index;
|
||||||
|
////None
|
||||||
|
////},
|
||||||
|
////Some(PoolMode::Export(index, ref mut browse)) => {
|
||||||
|
////browse.index = index;
|
||||||
|
////None
|
||||||
|
////},
|
||||||
|
////_ => unreachable!(),
|
||||||
|
////})
|
||||||
|
//}
|
||||||
|
//fn chdir (browse: &mut Browse, dir: PathBuf) => {
|
||||||
|
//todo!()
|
||||||
|
////Ok(match browse.mode {
|
||||||
|
////Some(PoolMode::Import(index, ref mut browse)) => {
|
||||||
|
////browse.mode = Some(PoolMode::Import(index, Browse::new(Some(dir))?));
|
||||||
|
////None
|
||||||
|
////},
|
||||||
|
////Some(PoolMode::Export(index, ref mut browse)) => {
|
||||||
|
////browse.mode = Some(PoolMode::Export(index, Browse::new(Some(dir))?));
|
||||||
|
////None
|
||||||
|
////},
|
||||||
|
////_ => unreachable!(),
|
||||||
|
////})
|
||||||
|
//}
|
||||||
|
//fn filter (browse: &mut Browse, filter: Arc<str>) => {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
215
device/clip.rs
Normal file
215
device/clip.rs
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub trait HasMidiClip {
|
||||||
|
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! has_clip {
|
||||||
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||||
|
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
|
||||||
|
fn clip (&$self) -> Option<Arc<RwLock<MidiClip>>> { $cb }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A MIDI sequence.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct MidiClip {
|
||||||
|
pub uuid: uuid::Uuid,
|
||||||
|
/// Name of clip
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Temporal resolution in pulses per quarter note
|
||||||
|
pub ppq: usize,
|
||||||
|
/// Length of clip in pulses
|
||||||
|
pub length: usize,
|
||||||
|
/// Notes in clip
|
||||||
|
pub notes: MidiData,
|
||||||
|
/// Whether to loop the clip or play it once
|
||||||
|
pub looped: bool,
|
||||||
|
/// Start of loop
|
||||||
|
pub loop_start: usize,
|
||||||
|
/// Length of loop
|
||||||
|
pub loop_length: usize,
|
||||||
|
/// All notes are displayed with minimum length
|
||||||
|
pub percussive: bool,
|
||||||
|
/// Identifying color of clip
|
||||||
|
pub color: ItemTheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MIDI message structural
|
||||||
|
pub type MidiData = Vec<Vec<MidiMessage>>;
|
||||||
|
|
||||||
|
impl MidiClip {
|
||||||
|
pub fn new (
|
||||||
|
name: impl AsRef<str>,
|
||||||
|
looped: bool,
|
||||||
|
length: usize,
|
||||||
|
notes: Option<MidiData>,
|
||||||
|
color: Option<ItemTheme>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
uuid: uuid::Uuid::new_v4(),
|
||||||
|
name: name.as_ref().into(),
|
||||||
|
ppq: PPQ,
|
||||||
|
length,
|
||||||
|
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
||||||
|
looped,
|
||||||
|
loop_start: 0,
|
||||||
|
loop_length: length,
|
||||||
|
percussive: true,
|
||||||
|
color: color.unwrap_or_else(ItemTheme::random)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn count_midi_messages (&self) -> usize {
|
||||||
|
let mut count = 0;
|
||||||
|
for tick in self.notes.iter() {
|
||||||
|
count += tick.len();
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
|
pub fn set_length (&mut self, length: usize) {
|
||||||
|
self.length = length;
|
||||||
|
self.notes = vec![Vec::with_capacity(16);length];
|
||||||
|
}
|
||||||
|
pub fn duplicate (&self) -> Self {
|
||||||
|
let mut clone = self.clone();
|
||||||
|
clone.uuid = uuid::Uuid::new_v4();
|
||||||
|
clone
|
||||||
|
}
|
||||||
|
pub fn toggle_loop (&mut self) { self.looped = !self.looped; }
|
||||||
|
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
||||||
|
if pulse >= self.length { panic!("extend clip first") }
|
||||||
|
self.notes[pulse].push(message);
|
||||||
|
}
|
||||||
|
/// Check if a range `start..end` contains MIDI Note On `k`
|
||||||
|
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
|
||||||
|
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
|
||||||
|
for event in events.iter() {
|
||||||
|
if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
pub fn stop_all () -> Self {
|
||||||
|
Self::new(
|
||||||
|
"Stop",
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
Some(vec![vec![MidiMessage::Controller {
|
||||||
|
controller: 123.into(),
|
||||||
|
value: 0.into()
|
||||||
|
}]]),
|
||||||
|
Some(ItemColor::from_rgb(Color::Rgb(32, 32, 32)).into())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for MidiClip {
|
||||||
|
fn eq (&self, other: &Self) -> bool {
|
||||||
|
self.uuid == other.uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for MidiClip {}
|
||||||
|
|
||||||
|
impl MidiClip {
|
||||||
|
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
|
||||||
|
fn _todo_bool_stub_ (&self) -> bool { todo!() }
|
||||||
|
fn _todo_usize_stub_ (&self) -> usize { todo!() }
|
||||||
|
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
|
||||||
|
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
|
||||||
|
fn _todo_opt_item_theme_stub (&self) -> Option<ItemTheme> { todo!() }
|
||||||
|
}
|
||||||
|
|
||||||
|
def_command!(ClipCommand: |clip: MidiClip| {
|
||||||
|
|
||||||
|
SetColor { color: Option<ItemTheme> } => {
|
||||||
|
//(SetColor [t: usize, s: usize, c: ItemTheme]
|
||||||
|
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
|
||||||
|
//("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random())))
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
|
|
||||||
|
SetLoop { looping: Option<bool> } => {
|
||||||
|
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
|
||||||
|
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
pub trait ClipsView:
|
||||||
|
TracksView +
|
||||||
|
ScenesView +
|
||||||
|
HasClipsSize +
|
||||||
|
Send +
|
||||||
|
Sync
|
||||||
|
{
|
||||||
|
fn view_scenes_clips <'a> (&'a self)
|
||||||
|
-> impl Content<TuiOut> + 'a
|
||||||
|
{
|
||||||
|
self.clips_size().of(Fill::XY(Bsp::a(
|
||||||
|
Fill::XY(Align::se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))),
|
||||||
|
Thunk::new(|to: &mut TuiOut|for (
|
||||||
|
track_index, track, _, _
|
||||||
|
) in self.tracks_with_sizes() {
|
||||||
|
to.place(&Fixed::X(track.width as u16,
|
||||||
|
Fill::Y(self.view_track_clips(track_index, track))))
|
||||||
|
}))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Content<TuiOut> + 'a {
|
||||||
|
Thunk::new(move|to: &mut TuiOut|for (
|
||||||
|
scene_index, scene, ..
|
||||||
|
) in self.scenes_with_sizes() {
|
||||||
|
let (name, theme): (Arc<str>, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) {
|
||||||
|
let clip = clip.read().unwrap();
|
||||||
|
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
||||||
|
} else {
|
||||||
|
(" ⏹ -- ".into(), ItemTheme::G[32])
|
||||||
|
};
|
||||||
|
let fg = theme.lightest.rgb;
|
||||||
|
let mut outline = theme.base.rgb;
|
||||||
|
let bg = if self.selection().track() == Some(track_index)
|
||||||
|
&& self.selection().scene() == Some(scene_index)
|
||||||
|
{
|
||||||
|
outline = theme.lighter.rgb;
|
||||||
|
theme.light.rgb
|
||||||
|
} else if self.selection().track() == Some(track_index)
|
||||||
|
|| self.selection().scene() == Some(scene_index)
|
||||||
|
{
|
||||||
|
outline = theme.darkest.rgb;
|
||||||
|
theme.base.rgb
|
||||||
|
} else {
|
||||||
|
theme.dark.rgb
|
||||||
|
};
|
||||||
|
let w = if self.selection().track() == Some(track_index)
|
||||||
|
&& let Some(editor) = self.editor ()
|
||||||
|
{
|
||||||
|
editor.width().max(24).max(track.width)
|
||||||
|
} else {
|
||||||
|
track.width
|
||||||
|
} as u16;
|
||||||
|
let y = if self.selection().scene() == Some(scene_index)
|
||||||
|
&& let Some(editor) = self.editor ()
|
||||||
|
{
|
||||||
|
editor.height().max(12)
|
||||||
|
} else {
|
||||||
|
Self::H_SCENE
|
||||||
|
} as u16;
|
||||||
|
|
||||||
|
to.place(&Fixed::XY(w, y, Bsp::b(
|
||||||
|
Fill::XY(Outer(true, Style::default().fg(outline))),
|
||||||
|
Fill::XY(Bsp::b(
|
||||||
|
Bsp::b(
|
||||||
|
Tui::fg_bg(outline, bg, Fill::XY("")),
|
||||||
|
Fill::XY(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))),
|
||||||
|
),
|
||||||
|
Fill::XY(When::new(self.selection().track() == Some(track_index)
|
||||||
|
&& self.selection().scene() == Some(scene_index)
|
||||||
|
&& self.is_editing(), self.editor())))))));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref()
|
||||||
|
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
||||||
421
device/clock.rs
Normal file
421
device/clock.rs
Normal file
|
|
@ -0,0 +1,421 @@
|
||||||
|
use crate::*;
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
pub trait HasClock: Send + Sync {
|
||||||
|
fn clock (&self) -> &Clock;
|
||||||
|
fn clock_mut (&mut self) -> &mut Clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Has<Clock>> HasClock for T {
|
||||||
|
fn clock (&self) -> &Clock { self.get() }
|
||||||
|
fn clock_mut (&mut self) -> &mut Clock { self.get_mut() }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct Clock {
|
||||||
|
/// JACK transport handle.
|
||||||
|
pub transport: Arc<Option<Transport>>,
|
||||||
|
/// Global temporal resolution (shared by [Moment] fields)
|
||||||
|
pub timebase: Arc<Timebase>,
|
||||||
|
/// Current global sample and usec (monotonic from JACK clock)
|
||||||
|
pub global: Arc<Moment>,
|
||||||
|
/// Global sample and usec at which playback started
|
||||||
|
pub started: Arc<RwLock<Option<Moment>>>,
|
||||||
|
/// Playback offset (when playing not from start)
|
||||||
|
pub offset: Arc<Moment>,
|
||||||
|
/// Current playhead position
|
||||||
|
pub playhead: Arc<Moment>,
|
||||||
|
/// Note quantization factor
|
||||||
|
pub quant: Arc<Quantize>,
|
||||||
|
/// Launch quantization factor
|
||||||
|
pub sync: Arc<LaunchSync>,
|
||||||
|
/// Size of buffer in samples
|
||||||
|
pub chunk: Arc<AtomicUsize>,
|
||||||
|
// Cache of formatted strings
|
||||||
|
pub view_cache: Arc<RwLock<ViewCache>>,
|
||||||
|
/// For syncing the clock to an external source
|
||||||
|
#[cfg(feature = "port")] pub midi_in: Arc<RwLock<Option<MidiInput>>>,
|
||||||
|
/// For syncing other devices to this clock
|
||||||
|
#[cfg(feature = "port")] pub midi_out: Arc<RwLock<Option<MidiOutput>>>,
|
||||||
|
/// For emitting a metronome
|
||||||
|
#[cfg(feature = "port")] pub click_out: Arc<RwLock<Option<AudioOutput>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Clock {
|
||||||
|
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
|
f.debug_struct("Clock")
|
||||||
|
.field("timebase", &self.timebase)
|
||||||
|
.field("chunk", &self.chunk)
|
||||||
|
.field("quant", &self.quant)
|
||||||
|
.field("sync", &self.sync)
|
||||||
|
.field("global", &self.global)
|
||||||
|
.field("playhead", &self.playhead)
|
||||||
|
.field("started", &self.started)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clock {
|
||||||
|
pub fn new (jack: &Jack<'static>, bpm: Option<f64>) -> Usually<Self> {
|
||||||
|
let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport()));
|
||||||
|
let timebase = Arc::new(Timebase::default());
|
||||||
|
let clock = Self {
|
||||||
|
quant: Arc::new(24.into()),
|
||||||
|
sync: Arc::new(384.into()),
|
||||||
|
transport: Arc::new(Some(transport)),
|
||||||
|
chunk: Arc::new((chunk as usize).into()),
|
||||||
|
global: Arc::new(Moment::zero(&timebase)),
|
||||||
|
playhead: Arc::new(Moment::zero(&timebase)),
|
||||||
|
offset: Arc::new(Moment::zero(&timebase)),
|
||||||
|
started: RwLock::new(None).into(),
|
||||||
|
timebase,
|
||||||
|
midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))),
|
||||||
|
midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))),
|
||||||
|
click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
if let Some(bpm) = bpm {
|
||||||
|
clock.timebase.bpm.set(bpm);
|
||||||
|
}
|
||||||
|
Ok(clock)
|
||||||
|
}
|
||||||
|
pub fn timebase (&self) -> &Arc<Timebase> {
|
||||||
|
&self.timebase
|
||||||
|
}
|
||||||
|
/// Current sample rate
|
||||||
|
pub fn sr (&self) -> &SampleRate {
|
||||||
|
&self.timebase.sr
|
||||||
|
}
|
||||||
|
/// Current tempo
|
||||||
|
pub fn bpm (&self) -> &BeatsPerMinute {
|
||||||
|
&self.timebase.bpm
|
||||||
|
}
|
||||||
|
/// Current MIDI resolution
|
||||||
|
pub fn ppq (&self) -> &PulsesPerQuaver {
|
||||||
|
&self.timebase.ppq
|
||||||
|
}
|
||||||
|
/// Next pulse that matches launch sync (for phrase switchover)
|
||||||
|
pub fn next_launch_pulse (&self) -> usize {
|
||||||
|
let sync = self.sync.get() as usize;
|
||||||
|
let pulse = self.playhead.pulse.get() as usize;
|
||||||
|
if pulse % sync == 0 {
|
||||||
|
pulse
|
||||||
|
} else {
|
||||||
|
(pulse / sync + 1) * sync
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Start playing, optionally seeking to a given location beforehand
|
||||||
|
pub fn play_from (&self, start: Option<u32>) -> Usually<()> {
|
||||||
|
if let Some(transport) = self.transport.as_ref() {
|
||||||
|
if let Some(start) = start {
|
||||||
|
transport.locate(start)?;
|
||||||
|
}
|
||||||
|
transport.start()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Pause, optionally seeking to a given location afterwards
|
||||||
|
pub fn pause_at (&self, pause: Option<u32>) -> Usually<()> {
|
||||||
|
if let Some(transport) = self.transport.as_ref() {
|
||||||
|
transport.stop()?;
|
||||||
|
if let Some(pause) = pause {
|
||||||
|
transport.locate(pause)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Is currently paused?
|
||||||
|
pub fn is_stopped (&self) -> bool {
|
||||||
|
self.started.read().unwrap().is_none()
|
||||||
|
}
|
||||||
|
/// Is currently playing?
|
||||||
|
pub fn is_rolling (&self) -> bool {
|
||||||
|
self.started.read().unwrap().is_some()
|
||||||
|
}
|
||||||
|
/// Update chunk size
|
||||||
|
pub fn set_chunk (&self, n_frames: usize) {
|
||||||
|
self.chunk.store(n_frames, Relaxed);
|
||||||
|
}
|
||||||
|
pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> {
|
||||||
|
// Store buffer length
|
||||||
|
self.set_chunk(scope.n_frames() as usize);
|
||||||
|
|
||||||
|
// Store reported global frame and usec
|
||||||
|
let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?;
|
||||||
|
self.global.sample.set(current_frames as f64);
|
||||||
|
self.global.usec.set(current_usecs as f64);
|
||||||
|
|
||||||
|
let mut started = self.started.write().unwrap();
|
||||||
|
|
||||||
|
// If transport has just started or just stopped,
|
||||||
|
// update starting point:
|
||||||
|
if let Some(transport) = self.transport.as_ref() {
|
||||||
|
match (transport.query_state()?, started.as_ref()) {
|
||||||
|
(TransportState::Rolling, None) => {
|
||||||
|
let moment = Moment::zero(&self.timebase);
|
||||||
|
moment.sample.set(current_frames as f64);
|
||||||
|
moment.usec.set(current_usecs as f64);
|
||||||
|
*started = Some(moment);
|
||||||
|
},
|
||||||
|
(TransportState::Stopped, Some(_)) => {
|
||||||
|
*started = None;
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
self.playhead.update_from_sample(started.as_ref()
|
||||||
|
.map(|started|current_frames as f64 - started.sample.get())
|
||||||
|
.unwrap_or(0.));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bbt (&self) -> PositionBBT {
|
||||||
|
let pulse = self.playhead.pulse.get() as i32;
|
||||||
|
let ppq = self.timebase.ppq.get() as i32;
|
||||||
|
let bpm = self.timebase.bpm.get();
|
||||||
|
let bar = (pulse / ppq) / 4;
|
||||||
|
PositionBBT {
|
||||||
|
bar: 1 + bar,
|
||||||
|
beat: 1 + (pulse / ppq) % 4,
|
||||||
|
tick: (pulse % ppq),
|
||||||
|
bar_start_tick: (bar * 4 * ppq) as f64,
|
||||||
|
beat_type: 4.,
|
||||||
|
beats_per_bar: 4.,
|
||||||
|
beats_per_minute: bpm,
|
||||||
|
ticks_per_beat: ppq as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_launch_instant (&self) -> Moment {
|
||||||
|
Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get index of first sample to populate.
|
||||||
|
///
|
||||||
|
/// Greater than 0 means that the first pulse of the clip
|
||||||
|
/// falls somewhere in the middle of the chunk.
|
||||||
|
pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{
|
||||||
|
(scope.last_frame_time() as usize).saturating_sub(
|
||||||
|
started.sample.get() as usize +
|
||||||
|
self.started.read().unwrap().as_ref().unwrap().sample.get() as usize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get iterator that emits sample paired with pulse.
|
||||||
|
//
|
||||||
|
// * Sample: index into output buffer at which to write MIDI event
|
||||||
|
// * Pulse: index into clip from which to take the MIDI event
|
||||||
|
//
|
||||||
|
// Emitted for each sample of the output buffer that corresponds to a MIDI pulse.
|
||||||
|
pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> TicksIterator {
|
||||||
|
self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clock {
|
||||||
|
fn _todo_provide_u32 (&self) -> u32 {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn _todo_provide_opt_u32 (&self) -> Option<u32> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn _todo_provide_f64 (&self) -> f64 {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: HasClock> Command<T> for ClockCommand {
|
||||||
|
fn execute (&self, state: &mut T) -> Perhaps<Self> {
|
||||||
|
self.execute(state.clock_mut()) // awesome
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def_command!(ClockCommand: |clock: Clock| {
|
||||||
|
SeekUsec { usec: f64 } => {
|
||||||
|
clock.playhead.update_from_usec(*usec); Ok(None) },
|
||||||
|
SeekSample { sample: f64 } => {
|
||||||
|
clock.playhead.update_from_sample(*sample); Ok(None) },
|
||||||
|
SeekPulse { pulse: f64 } => {
|
||||||
|
clock.playhead.update_from_pulse(*pulse); Ok(None) },
|
||||||
|
SetBpm { bpm: f64 } => Ok(Some(
|
||||||
|
Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })),
|
||||||
|
SetQuant { quant: f64 } => Ok(Some(
|
||||||
|
Self::SetQuant { quant: clock.quant.set(*quant) })),
|
||||||
|
SetSync { sync: f64 } => Ok(Some(
|
||||||
|
Self::SetSync { sync: clock.sync.set(*sync) })),
|
||||||
|
|
||||||
|
Play { position: Option<u32> } => {
|
||||||
|
clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ },
|
||||||
|
Pause { position: Option<u32> } => {
|
||||||
|
clock.pause_at(*position)?; Ok(None) },
|
||||||
|
|
||||||
|
TogglePlayback { position: u32 } => Ok(if clock.is_rolling() {
|
||||||
|
clock.pause_at(Some(*position))?; None
|
||||||
|
} else {
|
||||||
|
clock.play_from(Some(*position))?; None
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn view_transport (
|
||||||
|
play: bool,
|
||||||
|
bpm: Arc<RwLock<String>>,
|
||||||
|
beat: Arc<RwLock<String>>,
|
||||||
|
time: Arc<RwLock<String>>,
|
||||||
|
) -> impl Content<TuiOut> {
|
||||||
|
let theme = ItemTheme::G[96];
|
||||||
|
Tui::bg(Black, row!(Bsp::a(
|
||||||
|
Fill::XY(Align::w(button_play_pause(play))),
|
||||||
|
Fill::XY(Align::e(row!(
|
||||||
|
FieldH(theme, "BPM", bpm),
|
||||||
|
FieldH(theme, "Beat", beat),
|
||||||
|
FieldH(theme, "Time", time),
|
||||||
|
)))
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_status (
|
||||||
|
sel: Option<Arc<str>>,
|
||||||
|
sr: Arc<RwLock<String>>,
|
||||||
|
buf: Arc<RwLock<String>>,
|
||||||
|
lat: Arc<RwLock<String>>,
|
||||||
|
) -> impl Content<TuiOut> {
|
||||||
|
let theme = ItemTheme::G[96];
|
||||||
|
Tui::bg(Black, row!(Bsp::a(
|
||||||
|
Fill::XY(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))),
|
||||||
|
Fill::XY(Align::e(row!(
|
||||||
|
FieldH(theme, "SR", sr),
|
||||||
|
FieldH(theme, "Buf", buf),
|
||||||
|
FieldH(theme, "Lat", lat),
|
||||||
|
)))
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn button_play_pause (playing: bool) -> impl Content<TuiOut> {
|
||||||
|
let compact = true;//self.is_editing();
|
||||||
|
Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
|
||||||
|
Either::new(compact,
|
||||||
|
Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(9, Either::new(playing,
|
||||||
|
Tui::fg(Rgb(0, 255, 0), " PLAYING "),
|
||||||
|
Tui::fg(Rgb(255, 128, 0), " STOPPED ")))
|
||||||
|
)),
|
||||||
|
Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(5, Either::new(playing,
|
||||||
|
Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
|
||||||
|
Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)] pub struct ViewCache {
|
||||||
|
pub sr: Memo<Option<(bool, f64)>, String>,
|
||||||
|
pub buf: Memo<Option<f64>, String>,
|
||||||
|
pub lat: Memo<Option<f64>, String>,
|
||||||
|
pub bpm: Memo<Option<f64>, String>,
|
||||||
|
pub beat: Memo<Option<f64>, String>,
|
||||||
|
pub time: Memo<Option<f64>, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ViewCache {
|
||||||
|
fn default () -> Self {
|
||||||
|
let mut beat = String::with_capacity(16);
|
||||||
|
let _ = write!(beat, "{}", Self::BEAT_EMPTY);
|
||||||
|
let mut time = String::with_capacity(16);
|
||||||
|
let _ = write!(time, "{}", Self::TIME_EMPTY);
|
||||||
|
let mut bpm = String::with_capacity(16);
|
||||||
|
let _ = write!(bpm, "{}", Self::BPM_EMPTY);
|
||||||
|
Self {
|
||||||
|
beat: Memo::new(None, beat),
|
||||||
|
time: Memo::new(None, time),
|
||||||
|
bpm: Memo::new(None, bpm),
|
||||||
|
sr: Memo::new(None, String::with_capacity(16)),
|
||||||
|
buf: Memo::new(None, String::with_capacity(16)),
|
||||||
|
lat: Memo::new(None, String::with_capacity(16)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewCache {
|
||||||
|
pub const BEAT_EMPTY: &'static str = "-.-.--";
|
||||||
|
pub const TIME_EMPTY: &'static str = "-.---s";
|
||||||
|
pub const BPM_EMPTY: &'static str = "---.---";
|
||||||
|
|
||||||
|
//pub fn track_counter (cache: &Arc<RwLock<Self>>, track: usize, tracks: usize)
|
||||||
|
//-> Arc<RwLock<String>>
|
||||||
|
//{
|
||||||
|
//let data = (track, tracks);
|
||||||
|
//cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1));
|
||||||
|
//cache.read().unwrap().trks.view.clone()
|
||||||
|
//}
|
||||||
|
|
||||||
|
//pub fn scene_add (cache: &Arc<RwLock<Self>>, scene: usize, scenes: usize, is_editing: bool)
|
||||||
|
//-> impl Content<TuiOut>
|
||||||
|
//{
|
||||||
|
//let data = (scene, scenes);
|
||||||
|
//cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1));
|
||||||
|
//button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing)
|
||||||
|
//}
|
||||||
|
|
||||||
|
pub fn update_clock (cache: &Arc<RwLock<Self>>, clock: &Clock, compact: bool) {
|
||||||
|
let rate = clock.timebase.sr.get();
|
||||||
|
let chunk = clock.chunk.load(Relaxed) as f64;
|
||||||
|
let lat = chunk / rate * 1000.;
|
||||||
|
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
|
||||||
|
let mut cache = cache.write().unwrap();
|
||||||
|
cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}"));
|
||||||
|
cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms"));
|
||||||
|
cache.sr.update(Some((compact, rate)), |buf,_,_|{
|
||||||
|
buf.clear();
|
||||||
|
if compact {
|
||||||
|
write!(buf, "{:.1}kHz", rate / 1000.)
|
||||||
|
} else {
|
||||||
|
write!(buf, "{:.0}Hz", rate)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) {
|
||||||
|
let pulse = clock.timebase.usecs_to_pulse(now);
|
||||||
|
let time = now/1000000.;
|
||||||
|
let bpm = clock.timebase.bpm.get();
|
||||||
|
cache.beat.update(Some(pulse), |buf, _, _|{
|
||||||
|
buf.clear();
|
||||||
|
clock.timebase.format_beats_1_to(buf, pulse)
|
||||||
|
});
|
||||||
|
cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time));
|
||||||
|
cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm));
|
||||||
|
} else {
|
||||||
|
cache.beat.update(None, rewrite!(buf, "{}", ViewCache::BEAT_EMPTY));
|
||||||
|
cache.time.update(None, rewrite!(buf, "{}", ViewCache::TIME_EMPTY));
|
||||||
|
cache.bpm.update(None, rewrite!(buf, "{}", ViewCache::BPM_EMPTY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//pub fn view_h2 (&self) -> impl Content<TuiOut> {
|
||||||
|
//let cache = self.project.clock.view_cache.clone();
|
||||||
|
//let cache = cache.read().unwrap();
|
||||||
|
//add(&Fixed::x(15, Align::w(Bsp::s(
|
||||||
|
//FieldH(theme, "Beat", cache.beat.view.clone()),
|
||||||
|
//FieldH(theme, "Time", cache.time.view.clone()),
|
||||||
|
//))));
|
||||||
|
//add(&Fixed::x(13, Align::w(Bsp::s(
|
||||||
|
//Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))),
|
||||||
|
//Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))),
|
||||||
|
//))));
|
||||||
|
//add(&Fixed::x(12, Align::w(Bsp::s(
|
||||||
|
//Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))),
|
||||||
|
//Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))),
|
||||||
|
//))));
|
||||||
|
//add(&Bsp::s(
|
||||||
|
//Fill::X(Align::w(FieldH(theme, "Selected", Align::w(self.selection().describe(
|
||||||
|
//self.tracks(),
|
||||||
|
//self.scenes()
|
||||||
|
//))))),
|
||||||
|
//Fill::X(Align::w(FieldH(theme, format!("History ({})", self.history.len()),
|
||||||
|
//self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0)))))))
|
||||||
|
//));
|
||||||
|
////if let Some(last) = self.history.last() {
|
||||||
|
////add(&FieldV(theme, format!("History ({})", self.history.len()),
|
||||||
|
////Fill::X(Align::w(format!("{:?}", last.0)))));
|
||||||
|
////}
|
||||||
|
//}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue