Compare commits

..

608 Commits

Author SHA1 Message Date
Felix Roos 6d6114beb6 Merge branch 'main' into global-repl 2023-11-15 17:46:03 +01:00
Felix Roos 1aaaaba619 Merge pull request #798 from daslyfe/phaser
Create phaser effect
2023-11-13 23:30:14 +01:00
Felix Roos b7dd9b7d35 Merge remote-tracking branch 'origin/main' into phaser 2023-11-13 21:59:10 +01:00
Alex McLean d25851ac17 support multiple named serial connections, change default baudrate to 115200 (#551) 2023-11-10 11:17:35 +00:00
Felix Roos d02450c0d9 Merge pull request #662 from gogins/main
Fix for #1. Enables named instruments for csoundm.
2023-11-09 09:27:57 +01:00
Felix Roos 400b2109fb Merge pull request #706 from Bubobubobubobubo/global-vibrato
Adding vibrato to Superdough sampler
2023-11-09 09:26:45 +01:00
Felix Roos 2a09f9ef0b fix: sampler broke without vibrato 2023-11-09 09:21:40 +01:00
Felix Roos 0052d349d9 refactor: move vibrato up + cleanup oscillator 2023-11-09 09:06:50 +01:00
Felix Roos 7c8a8b8b70 simplify 2023-11-09 08:59:23 +01:00
Felix Roos 85835087ba format 2023-11-09 08:48:23 +01:00
Felix Roos 5aac042c80 Merge branch 'main' into global-vibrato 2023-11-09 08:47:37 +01:00
Felix Roos 9dbd073073 Merge pull request #784 from ilesinge/document_pianoroll
Document pianoroll
2023-11-09 08:46:03 +01:00
Felix Roos 035f6c8098 Merge pull request #795 from bwagner/patch-2
Update first-effects.mdx
2023-11-09 08:45:29 +01:00
Felix Roos d2a06654c3 Merge pull request #796 from bwagner/patch-3
Update pattern-effects.mdx
2023-11-09 08:44:43 +01:00
Felix Roos c028686298 Merge pull request #797 from bwagner/patch-4
Update recap.mdx
2023-11-09 08:43:50 +01:00
Felix Roos 858a02634e dedupe / move supertdirt phaser controls 2023-11-09 08:40:21 +01:00
Felix Roos 2682fd38f8 add to doc page 2023-11-09 08:29:21 +01:00
Felix Roos 3ecb701504 add params to superdough readme 2023-11-09 08:19:04 +01:00
Felix Roos ac359a0e9e doc: phaser synonyms 2023-11-09 08:17:16 +01:00
Felix Roos df636cf4f6 phaser shortcuts 2023-11-09 08:16:17 +01:00
Felix Roos 791a363694 snapshots 2023-11-08 23:33:46 +01:00
Jade Rowland 06eb8b5be8 fixed doc 2023-11-07 22:57:00 -05:00
Jade Rowland 4b4290e087 add more parameters 2023-11-07 22:52:39 -05:00
Jade Rowland c89f91215c Merge branch 'tidalcycles:main' into phaser 2023-11-07 22:11:55 -05:00
Jade Rowland 65d07dbff2 create free running lfos for different orbits 2023-11-07 22:09:53 -05:00
Jade Rowland 2e4d28b910 update doc 2023-11-07 21:49:56 -05:00
Jade Rowland 51c63fa996 turn off of depth is 0 2023-11-07 21:44:57 -05:00
Jade Rowland f0a582dc92 cleaning up 2023-11-07 21:10:12 -05:00
Jade Rowland bee36a7a4f cleaning up 2023-11-07 20:25:31 -05:00
Jade Rowland 0edd54dee3 cleaning up 2023-11-07 20:23:45 -05:00
Bernhard Wagner 4a16cd891f FIXES: table 2023-11-07 18:55:35 +01:00
Bernhard Wagner 23dd9d4c01 Update recap.mdx
Adds a few missing translations
2023-11-07 16:16:46 +01:00
Bernhard Wagner df277d6b7b Update pattern-effects.mdx
grammatical amendments
2023-11-07 16:10:57 +01:00
Bernhard Wagner eed2c304e4 Update first-effects.mdx
grammatical amendments
2023-11-07 15:57:31 +01:00
Felix Roos bd5953317e Merge pull request #794 from bwagner/patch-1
Update first-sounds.mdx
2023-11-07 11:53:48 +01:00
Bernhard Wagner b88f58cc21 Update first-sounds.mdx
in eine sogenannte Funktion -> in einer sogenannten Funktion
2023-11-07 11:38:13 +01:00
Jade Rowland 9cbf3079f3 change to bus effect experiment 2023-11-06 19:02:09 -05:00
Felix Roos 682ac25111 Merge pull request #791 from tidalcycles/fix-ref-anchors
don't use anchor links for reference
2023-11-06 23:17:31 +01:00
Felix Roos 07da9a9dd2 don't use anchor links for reference 2023-11-06 23:13:10 +01:00
Felix Roos 395ae4d05a Merge pull request #788 from tidalcycles/github-shortcut
samples loading shortcuts:
2023-11-06 22:40:43 +01:00
Felix Roos 2ef35a8583 samples loading shortcuts:
- use main branch by default in github shortcut
- add bubo shortcut
2023-11-06 22:17:33 +01:00
Felix Roos 5807dbfdac Merge pull request #786 from tidalcycles/fix-scope-flip
Fix scope pos + document
2023-11-05 22:47:48 +01:00
Felix Roos c4f26c3cad snapshot 2023-11-05 22:46:16 +01:00
Felix Roos 26d87b691c document scope functions 2023-11-05 22:43:37 +01:00
Felix Roos 52e2b90eef fix: scope pos 2023-11-05 22:22:35 +01:00
Felix Roos 62eb9ce598 Merge pull request #783 from ilesinge/hover_tooltip
Implement optional hover tooltip with function documentation
2023-11-05 22:10:25 +01:00
Jade Rowland a9efda3bc3 tweaking to make it sound good 2023-11-05 15:40:02 -05:00
Alexandre G.-Raymond 8fe4dca4c2 Update pianoroll documentation 2023-11-05 18:07:42 +01:00
Felix Roos a05d3a47c6 Merge pull request #785 from ilesinge/reference_parameters
Add function params in reference tab
2023-11-05 16:46:06 +01:00
Alexandre G.-Raymond e9265eff35 Add function params in reference tab 2023-11-05 16:40:46 +01:00
Alexandre G.-Raymond 8fa7bf795a Fix pianoroll documentation 2023-11-05 16:25:10 +01:00
Alexandre G.-Raymond f045fb44de Add color function documentation 2023-11-05 16:23:36 +01:00
Alexandre G.-Raymond 0d06840a1b Add label function documentation 2023-11-05 16:23:21 +01:00
Alexandre G.-Raymond 624e540cb4 Add pianoroll function documentation 2023-11-05 16:23:00 +01:00
Alexandre G.-Raymond 353333653f Implement optional hover tooltip with function documentation 2023-11-05 13:00:54 +01:00
Felix Roos 83fec57400 Merge pull request #781 from tidalcycles/fix-styles
fix: style issues
2023-11-05 12:42:00 +01:00
Felix Roos 7ad0ec1600 fix: namespace mini-repl styles 2023-11-05 12:35:49 +01:00
Felix Roos ec611aac45 Merge pull request #780 from tidalcycles/xfade
add xfade
2023-11-05 12:21:28 +01:00
Felix Roos d7752961b8 snapshot 2023-11-05 12:12:25 +01:00
Felix Roos 8c5837e78a add xfade + docs 2023-11-05 12:11:17 +01:00
Jade Rowland b3f8df1783 initial commit 2023-11-04 20:20:25 -04:00
Felix Roos 82380a0f4e make sliders really work 2023-11-03 14:08:13 +01:00
Felix Roos c030f456c3 widget support 2023-11-03 13:50:18 +01:00
Felix Roos 694e44dc8a better init + don't draw first frame by default 2023-11-03 12:07:19 +01:00
Felix Roos 351da196b9 StrudelMirror onPaint support 2023-11-03 11:46:32 +01:00
Felix Roos e27ee03517 format 2023-11-03 11:11:23 +01:00
Felix Roos 19db419c53 handle shuffle 2023-11-03 11:10:53 +01:00
Felix Roos ff60a89668 fix: autocomplete + add missing basic extensions 2023-11-03 10:47:05 +01:00
Felix Roos 7cc17fefcb fix: undo redo key mapping
+ fix hidden line number style
2023-11-03 10:20:07 +01:00
Felix Roos 76ee9485f4 format 2023-11-03 10:05:37 +01:00
Felix Roos 1d65f09f3c - enable most settings:
- keybindings
- line numbers
- line wrapping
- highlighting (new)
- flashing (new)
- autocomplete not working yet
- move themes to codemirror package (currently duplicated)
- generally simplify settings handling
2023-11-03 10:04:58 +01:00
Felix Roos cc97f6395d better keymap 2023-11-02 23:15:41 +01:00
Felix Roos 33eef619e4 no destructuring 2023-11-02 22:49:52 +01:00
Felix Roos e9e3b97621 format 2023-11-02 22:45:52 +01:00
Felix Roos 1f765e54ca Repl: use codemirror custom keys
+ use new prebake
2023-11-02 22:45:32 +01:00
Felix Roos 2b81cc5d0c comment out (probably) dead end 2023-11-02 22:37:42 +01:00
Felix Roos ca054dbd74 restore to old state 2023-11-02 22:37:31 +01:00
Felix Roos a8433f61a4 add missing dep 2023-11-02 22:36:03 +01:00
Felix Roos 5850106eef restore react hooks 2023-11-02 22:33:16 +01:00
Felix Roos 95a9ab6182 revert Repl.jsx 2023-11-02 22:29:27 +01:00
Felix Roos ba19c4ddce duplicate new lighter repl 2023-11-02 22:28:46 +01:00
Felix Roos b49b1c98ae make play and update buttons work 2023-11-02 17:14:59 +01:00
Felix Roos 7c00aa1a36 intermediate /vanilla repl
+ react is now only used for ui around editor!
+ ui still not fully functional
2023-11-02 16:31:45 +01:00
Felix Roos 07d6bb3c44 - encapsulate repl state from react
- might still be buggy!
- smarter way to flush sounds
2023-11-02 14:37:50 +01:00
Felix Roos 3f2e2554be add back missing shortcuts 2023-11-02 11:29:36 +01:00
Felix Roos 9ebb7edde9 remove old export 2023-11-02 11:29:27 +01:00
Felix Roos a89091cbc7 bind custom keyboard shortcuts via codemirror
+ add reevaluate and panic shortcuts
2023-11-02 11:24:23 +01:00
Felix Roos 0d1a99deb2 also pull in audio init to prebake 2023-11-02 10:37:38 +01:00
Felix Roos b71f8a33d7 refactor: pull module loading into prebake 2023-11-02 10:21:39 +01:00
Felix Roos 0ecc6c196f Merge pull request #775 from tidalcycles/astro-3
Update to Astro 3
2023-11-02 09:30:26 +01:00
Felix Roos 77686547eb no double ! 2023-11-02 09:14:45 +01:00
Felix Roos 419cf6c12b update to astro 3 2023-11-02 09:13:29 +01:00
Felix Roos 326571b797 remove obsolete config 2023-11-02 09:01:53 +01:00
Felix Roos c25c2693f5 Merge branch 'main' into astro-3 2023-11-02 09:00:42 +01:00
Felix Roos 8d8a1f552d Merge pull request #773 from Dsm0/vscode-bindings
add vscode bindings
2023-11-02 08:57:14 +01:00
Felix Roos 638f5c7323 rename bindings key 2023-11-02 08:48:11 +01:00
Felix Roos 9ec5d33840 Merge pull request #774 from tidalcycles/tauri-clipboard
fix: share copy to clipboard + alert
2023-11-02 08:31:30 +01:00
Felix Roos 8d3ba2a8cd fix: share copy to clipboard + alert 2023-11-02 08:29:27 +01:00
Dsm0 0c2838cc90 fixed style issues 2023-11-01 17:55:40 -04:00
Felix Roos 1e5edfca26 Update CONTRIBUTING.md
fix prettier instructions
2023-11-01 22:40:23 +01:00
Dsm0 b141e1ee68 (hopefully) fixed broken lock file 2023-11-01 17:30:59 -04:00
Will 2fd4836228 Merge branch 'main' into vscode-bindings 2023-11-01 17:24:05 -04:00
Alex McLean 9ceab0f294 Fix chunk, add fastChunk and repeatCycles (#712)
* Match behaviour of chunk with tidal, add fastChunk for old strudel behaviour. Also adds repeatCycles. fixes #689

* documentation
2023-11-01 21:22:33 +00:00
Dsm0 d4c12dcef4 integrated vscode bindings 2023-11-01 17:14:22 -04:00
Felix Roos 16bb920011 Merge pull request #767 from ilesinge/document_adsr
Document adsr function
2023-11-01 22:12:48 +01:00
Felix Roos 158fa85c46 Merge pull request #771 from daslyfe/add_pick_and_squeeze_functions
Add pick and squeeze functions
2023-11-01 22:11:48 +01:00
Felix Roos 34bd0e11e4 add doc chapters 2023-11-01 22:09:40 +01:00
Felix Roos 9e649a18c4 snapshots 2023-11-01 22:07:19 +01:00
Felix Roos 2bb45afe5f Merge pull request #772 from tidalcycles/update-vite-pwa
Update vite pwa
2023-11-01 22:04:00 +01:00
Felix Roos 41a9098c80 use new flag 2023-11-01 21:59:53 +01:00
Felix Roos 36ef03a406 use 0.1.4 @vite-pwa/astro 2023-11-01 21:57:14 +01:00
Jade Rowland fd6c713119 move to signal file with other choose functions 2023-10-31 12:18:19 -04:00
Jade Rowland 4a57d3ada1 functions 2023-10-31 11:58:11 -04:00
Felix Roos f16cb8f7d2 Merge pull request #768 from tidalcycles/strudel-cc
replace strudel.tidalcycles.org with strudel.cc
2023-10-28 23:55:07 +02:00
Felix Roos af915be142 replace strudel.tidalcycles.org with strudel.cc 2023-10-28 23:49:30 +02:00
Alexandre G.-Raymond 2633f4fcef Document adsr function 2023-10-28 19:50:41 +02:00
Felix Roos 6c2330984e Merge pull request #766 from ilesinge/document_striate
Document striate function
2023-10-28 12:52:36 +02:00
Alexandre G.-Raymond 1e1d5ef442 Document striate function 2023-10-28 11:47:37 +02:00
Felix Roos 248757ac3f Merge pull request #760 from tidalcycles/zen-fix
fix zen mode logo overlap
2023-10-27 23:06:20 +02:00
Felix Roos cdbfb2c08c Merge pull request #764 from tidalcycles/fix-scale-offset2
fix: scale offset
2023-10-27 23:01:18 +02:00
Felix Roos d84c917f19 fix: scale offset 2023-10-27 22:50:21 +02:00
Felix Roos 1c7c82358d Merge pull request #762 from tidalcycles/fix-addivite-synthesis-phases
Fix addivite synthesis phases
2023-10-27 21:59:35 +02:00
Felix Roos c085694a8b fix: additive synthesis 2023-10-27 21:55:49 +02:00
Felix Roos a57c2b9fe5 fix: flip scope in y direction 2023-10-27 21:55:25 +02:00
Felix Roos 9fc87fe20f add px in zen mode to fix logo overlap 2023-10-27 21:29:31 +02:00
Felix Roos 4761e25fe1 Merge pull request #759 from tidalcycles/hydra-integration
Hydra integration
2023-10-26 16:28:16 +02:00
Felix Roos 3ce50d80de ignore hydra.mjs in linter 2023-10-26 16:20:19 +02:00
Felix Roos 8754b023d5 document H function 2023-10-26 16:11:22 +02:00
Felix Roos 22c9829fea add H function to plug patterns into Hydra 2023-10-26 16:05:10 +02:00
Felix Roos 0f7b3e9f84 add docs + fix hydra for mini repl 2023-10-26 16:01:51 +02:00
Felix Roos 1a456828df Merge branch 'main' into hydra-integration 2023-10-26 14:14:49 +02:00
Felix Roos 5958c69d64 Merge pull request #758 from tidalcycles/play-function
add play function
2023-10-26 14:14:26 +02:00
Felix Roos b14aa901ba also return pat 2023-10-26 13:16:15 +02:00
Felix Roos 535dfb5099 Merge pull request #740 from ilesinge/shabda_shortcut
Add shabda shortcut
2023-10-26 13:11:00 +02:00
Felix Roos 19f1aad88c Merge pull request #751 from ilesinge/alphabet_samples
mini notation: international alphabets support
2023-10-26 13:07:23 +02:00
Felix Roos 9009eccc8e add play function 2023-10-26 13:06:58 +02:00
Felix Roos 7f3cdedef9 basic hydra integration 2023-10-26 13:05:54 +02:00
Felix Roos 05095805f1 Merge pull request #753 from tidalcycles/fix-trailing-slash-bug
hopefully fix trainling slashes bug
2023-10-22 23:00:52 +02:00
Felix Roos 5430147572 use new vite-pwa-astro version from file 2023-10-22 22:53:50 +02:00
Alexandre G.-Raymond 9b9f031fe1 Revert error message length limitation 2023-10-21 23:41:56 +02:00
Felix Roos 2ad63247c2 Merge pull request #748 from ilesinge/fix_krill_readme
Fix krill build command in README
2023-10-21 23:38:50 +02:00
Alexandre G.-Raymond 87f37e3d87 Add a human-readable error message for invalid char in step 2023-10-21 23:20:04 +02:00
Alexandre G.-Raymond cb8edd9b8f Support international alphabets in mininotation 2023-10-21 22:51:20 +02:00
Alexandre G.-Raymond 072ae7280e Fix krill build command in README 2023-10-21 22:26:12 +02:00
Felix Roos e99c299c6f Merge pull request #745 from tidalcycles/fix-slash-confusion
completely revert config mess
2023-10-21 00:20:49 +02:00
Felix Roos 46cdbd514f completely revert config mess 2023-10-21 00:18:41 +02:00
Felix Roos a3e0189f0b Merge pull request #744 from tidalcycles/fix-slash-confusion
fix: try different trailing slash behavior
2023-10-20 22:41:39 +02:00
Felix Roos 8f4344f413 fix: try different trailing slash behavior 2023-10-20 22:38:46 +02:00
Felix Roos 7e28de1d24 Merge pull request #743 from tidalcycles/fix-slash-confusion
fix: trailing slash confusion
2023-10-20 22:34:03 +02:00
Felix Roos 624e685212 fix: trailing slash confusion 2023-10-20 22:32:12 +02:00
Felix Roos a23765a1cb fix: trailing slash confusion 2023-10-20 22:30:54 +02:00
Felix Roos 048502ecd1 Merge branch 'main' into astro-3 2023-10-20 22:08:21 +02:00
Alexandre G.-Raymond 1b072448c4 Add test snapshots for shabda shortcut 2023-10-20 21:59:54 +02:00
Felix Roos 86997488fb Merge pull request #741 from daslyfe/bugfix_choose_with
[Bug Fix] chooseWith: prevent pattern from stopping audio when selection is >= 1 or < 0
2023-10-20 12:07:03 +02:00
Felix Roos db6efec417 Merge pull request #742 from tidalcycles/recipes
Recipes
2023-10-20 11:34:26 +02:00
Felix Roos 2cc60c6ee2 format 2023-10-20 11:28:35 +02:00
Felix Roos 80c98b6a35 fix: loud example 2023-10-20 11:23:38 +02:00
Felix Roos f51c01dc88 move stuff around 2023-10-20 11:18:54 +02:00
Felix Roos 7b8a35a311 add understand cycles 2023-10-20 11:18:47 +02:00
Felix Roos d9db5263e8 add recipes 2023-10-20 11:18:40 +02:00
Jade Rowland 912a661c04 fixed example test failure 2023-10-17 23:03:14 -04:00
Jade Rowland d3edc62fc7 document function 2023-10-17 21:31:11 -04:00
Jade Rowland 6b5de5fec8 fixed 2023-10-17 01:33:51 -04:00
Alexandre G.-Raymond fed4c51aa4 Prettier doc file 2023-10-14 16:30:29 +02:00
Alexandre G.-Raymond 1721209f85 Update samples/shabda documentation 2023-10-14 16:20:53 +02:00
Alexandre G.-Raymond 289820188e Add shabda shortcut (#406) 2023-10-14 16:20:17 +02:00
Felix Roos 4fa0a9064b Merge branch 'main' of https://github.com/tidalcycles/strudel 2023-10-14 10:08:53 +02:00
Felix Roos f751d97c29 Merge pull request #737 from tidalcycles/readme-fix
vite-vanilla-repl readme fix
2023-10-13 12:57:24 +02:00
Felix Roos d7028252c2 vite-vanilla-repl readme fix 2023-10-13 12:55:31 +02:00
Felix Roos a2ee70a964 Merge pull request #717 from vasilymilovidov/reverb
Add support for using samples as impulse response buffers for the reverb
2023-10-10 00:17:59 +02:00
Felix Roos 57b166d13a fix: pitched sample as ir 2023-10-09 22:49:38 +02:00
Felix Roos 5c76f2b9a2 bump superdough to 0.9.10 2023-10-09 22:30:34 +02:00
Felix Roos e59f06ff92 Merge branch 'main' into reverb 2023-10-09 22:15:25 +02:00
Felix Roos d3b96dd048 Merge pull request #732 from tidalcycles/reverb-fix-sr
fix: reverb sampleRate
2023-10-09 21:43:42 +02:00
Felix Roos 6544e8135c fix: reverb sampleRate 2023-10-09 21:40:38 +02:00
Felix Roos 54ed415d75 Merge pull request #731 from tidalcycles/reverb-fix
fix: reverb roomsize not required
2023-10-09 21:34:33 +02:00
Felix Roos f34937d4ae fix: roomsize not required 2023-10-09 21:31:49 +02:00
Felix Roos a4fc40875b Merge pull request #729 from tidalcycles/compressor
Compressor
2023-10-08 13:54:51 +02:00
Felix Roos 56c1441494 snapshot 2023-10-08 13:38:18 +02:00
Felix Roos 08ce8213fc format 2023-10-08 13:37:56 +02:00
Felix Roos 0a3692e459 compressor docs 2023-10-08 13:37:39 +02:00
Felix Roos 341546508f Merge pull request #728 from tidalcycles/fix-sample-url-special-chars
fix: hashes in urls
2023-10-08 13:25:57 +02:00
Felix Roos 444d583a49 fix: hashes in urls 2023-10-08 13:23:13 +02:00
Felix Roos 130ad4451b add compressor + postgain 2023-10-08 00:30:12 +02:00
vmilovidov d24cf8e7ec Merge branch 'main' into reverb 2023-10-07 19:50:05 +04:00
Vasilii Milovidov 2d750a14b9 fix: conflicts 2023-10-07 19:48:07 +04:00
Vasilii Milovidov b25b9d815b fix: conflicts 2023-10-07 19:30:07 +04:00
Felix Roos daf965f119 Merge pull request #727 from tidalcycles/fix-n-scale
consume n with scale
2023-10-07 15:48:06 +02:00
Felix Roos 296cd83a55 snapshot 2023-10-07 15:45:44 +02:00
Felix Roos b297888eb4 consume n with scale 2023-10-07 15:40:08 +02:00
Felix Roos e2569ed6dd Merge pull request #726 from tidalcycles/reverb-perf-pitfall
fix: reverb regenerate loophole
2023-10-07 00:27:21 +02:00
Felix Roos 4718bfac25 fix: reverb regenerate loophole 2023-10-07 00:22:55 +02:00
Vasilii Milovidov 2f0798de32 fix: formatting 2023-10-05 13:17:17 +04:00
vmilovidov 11b0a00593 Merge branch 'main' into reverb 2023-10-05 12:16:55 +04:00
Vasilii Milovidov 1d4eb59e94 fix: reverbGen.mjs 2023-10-05 04:10:35 +04:00
Vasilii Milovidov d6c8838fe2 Integrate with the new impulse generation functionality 2023-10-05 03:49:50 +04:00
Felix Roos 0b888ac54d Merge pull request #718 from Bubobubobubobubo/reverb
Better convolution reverb by generating impulse responses
2023-10-05 00:04:17 +02:00
Felix Roos 75099abbc1 remove unused reverb method 2023-10-05 00:03:02 +02:00
Felix Roos 3c4f835d8b consistent naming + simplify 2023-10-05 00:00:47 +02:00
Felix Roos ce842f7561 simplify createReverb 2023-10-04 23:56:00 +02:00
Felix Roos 508af7eb72 update internal reverb param names 2023-10-04 23:48:53 +02:00
Felix Roos a4386a617a Merge branch 'main' into reverb 2023-10-04 22:10:58 +02:00
Felix Roos 767ba713a6 Merge pull request #723 from tidalcycles/slider-afterthoughts
Slider afterthoughts
2023-10-04 10:31:52 +02:00
Felix Roos 020e85906d fix: slider crash on some platforms 2023-10-04 09:42:10 +02:00
Felix Roos d1ffdd57a5 Merge pull request #713 from Bubobubobubobubo/noise
Add 'white', 'pink' and 'brown' oscillators + refactor synth
2023-10-03 16:39:06 +02:00
Felix Roos 9c9323e040 snapshot 2023-10-03 12:42:56 +02:00
Felix Roos 376cf09565 rename zzfx noise to znoise 2023-10-03 12:41:57 +02:00
Felix Roos 047129223e cache noise 2023-10-03 12:25:47 +02:00
Felix Roos 4b64168faa fix: imports 2023-10-03 12:20:28 +02:00
Felix Roos 2bc6d08410 proper dry wet + pull out noise to extra file 2023-10-03 12:19:30 +02:00
Felix Roos 484bb6b11f refactor synth
- separate waveform / noise oscillators
- pull noise out of getOscillator
- put fm into getOscillator
- simplify overall value plumbing
2023-10-03 10:03:09 +02:00
Felix Roos a0884e2a03 add noise heading + hihat example 2023-10-03 09:09:49 +02:00
Felix Roos 1e352fdf80 codeformat 2023-10-03 08:51:35 +02:00
Felix Roos 83dfbb84c2 Merge branch 'main' into noise 2023-10-03 08:50:57 +02:00
Felix Roos 9043064543 snapshot 2023-10-03 08:36:52 +02:00
Felix Roos a3649148c1 more logical naming + update docs 2023-10-03 08:35:15 +02:00
Felix Roos 28966739f6 feat: add step as slider param 2023-10-02 20:44:51 +02:00
Vasilii Milovidov 13cb32903a Prepare to merge with PR #718 2023-10-02 12:13:44 +04:00
Felix Roos dcd75e0384 Merge branch 'main' into reverb 2023-10-01 23:48:48 +02:00
Felix Roos dfdd9e02ca eslint ignore reverbGen + snapshot 2023-10-01 23:22:32 +02:00
Felix Roos 275796c241 fix: reverbs[orbit] is undefined 2023-10-01 23:21:02 +02:00
Felix Roos 6ca99e33ab codeformat 2023-10-01 23:20:05 +02:00
Raphael Forment 1909caf769 Connecting new reverb documentation 2023-10-01 14:50:29 +02:00
Raphael Forment 2d07eeb518 Connecting all parameters to convolution generator 2023-10-01 14:44:34 +02:00
Felix Roos 1c0da7cd49 Merge branch 'main' of https://github.com/tidalcycles/strudel 2023-10-01 14:20:54 +02:00
Alex McLean fd316c81c0 support mininotation '..' range operator, fixes #715 (#716)
* support mininotation .. range operator, fixes #715

* remove logs
2023-10-01 13:20:49 +01:00
Felix Roos edbd437d7b hotfix: add missing dependency 2023-10-01 14:20:30 +02:00
Felix Roos 9be1f9c18b Merge pull request #714 from tidalcycles/ui-widgets
widgets
2023-10-01 14:15:09 +02:00
Felix Roos 96f06d8026 fix: import 2023-10-01 14:09:07 +02:00
Felix Roos 935d8e8aea fix: comment 2023-10-01 14:08:41 +02:00
Felix Roos 21b99b3810 remove comment 2023-10-01 14:07:48 +02:00
Felix Roos c051a1249d remove checkbox 2023-10-01 14:06:32 +02:00
Felix Roos 564697e175 add extra sliderWithID function
+ add warning to slider function
2023-10-01 14:05:16 +02:00
Felix Roos d4bf358eae use raw css instead of tailwind 2023-10-01 13:56:57 +02:00
Felix Roos f84d5ba3a0 add back some margin 2023-10-01 13:44:41 +02:00
Felix Roos b550ff038c simplify: use native events 2023-10-01 13:42:29 +02:00
Vasilii Milovidov ac9c629c0b cleanup 2023-10-01 15:36:52 +04:00
Vasilii Milovidov 755e24315e use sample as an impulse response for the reverb 2023-10-01 15:31:10 +04:00
Felix Roos c6ecd31ea1 improve mouse tracking 2023-10-01 13:27:44 +02:00
Raphael Forment e600b91a85 bugfixes for parameter passing 2023-10-01 12:02:17 +02:00
Raphael Forment abff279707 Document reverb controls 2023-10-01 11:52:24 +02:00
Vasilii Milovidov fd18508266 wip: use sample as an impulse response for the reverb 2023-10-01 05:15:47 +04:00
Felix Roos d7bc309eeb fix: import 2023-10-01 00:36:34 +02:00
Felix Roos 33c40e5ef8 fix some odd number / string problems 2023-10-01 00:33:10 +02:00
Felix Roos 062d926900 make sliders work! 2023-10-01 00:24:31 +02:00
Raphael Forment 0ad0046a76 Replacing old reverb by better convolution 2023-09-30 22:04:22 +02:00
Felix Roos b36cee93f7 add slider function to scope 2023-09-30 21:22:49 +02:00
Felix Roos 2731e70fb7 use loc as slider id 2023-09-30 21:03:28 +02:00
Felix Roos 276cf858fc match slider as function 2023-09-30 20:39:03 +02:00
Raphael Forment b2acff40c4 Add documentation about noise sources 2023-09-30 15:17:22 +02:00
Raphael Forment e3333e716f Cap noise amount to 1 2023-09-30 15:08:09 +02:00
Raphael Forment bb7b8c2fab Fix noise parameter and FM parameters compatibility 2023-09-30 14:59:43 +02:00
Raphael Forment 389c7be264 Add noise parameter for base oscillators 2023-09-30 14:39:44 +02:00
Raphael Forment c2481e460b Add pink, white and brown oscillators 2023-09-30 14:07:33 +02:00
Felix Roos c93e4a951a match number.slider 2023-09-29 12:58:16 +02:00
Felix Roos aa094bf930 checkbox + number slider 2023-09-29 10:40:00 +02:00
Felix Roos 8a741952df Merge pull request #699 from tidalcycles/midi-in
Midi in
2023-09-28 11:03:09 +02:00
Felix Roos 0dcc55ee16 prevent error 2023-09-28 10:48:31 +02:00
Felix Roos 6c3a3b9e29 dedupe 2023-09-28 10:40:44 +02:00
Felix Roos e4eff8c837 Merge remote-tracking branch 'origin/main' into midi-in 2023-09-28 10:40:24 +02:00
Felix Roos 527b895e3c Merge pull request #710 from tidalcycles/midi-clock
add midi clock support
2023-09-27 22:53:49 +02:00
Felix Roos 4eb0a7b7c0 send start with accurate timing 2023-09-27 22:42:35 +02:00
Felix Roos f11462bf41 sync start / stop automatically too 2023-09-27 22:28:52 +02:00
Felix Roos dea1c31701 use midicmd instead of clock 2023-09-27 22:10:21 +02:00
Felix Roos 68ab43b3ab support midi clock via "clock" control
(not on desktop yet)
2023-09-27 21:26:24 +02:00
Felix Roos cf72e3bba5 fix: add conditional imports to eval scope
+ fire postMessage on start
2023-09-27 21:25:30 +02:00
Felix Roos 62743edf45 bump superdough to 0.9.9 2023-09-25 23:57:19 +02:00
Felix Roos eec3752b5a cleanup 2023-09-25 23:57:09 +02:00
Felix Roos 006fd6c733 Merge pull request #707 from tidalcycles/superdough-worklet
add dough function for raw dsp
2023-09-25 23:06:30 +02:00
Felix Roos 7078e20200 less garbage 2023-09-25 22:56:44 +02:00
Felix Roos 52c01abbe9 encapsulate .dough 2023-09-25 22:39:03 +02:00
Felix Roos 6e26f39751 add dough function for raw dsp 2023-09-25 22:34:31 +02:00
Raphael Forment 08aab9d4d8 Adding vibrato to Superdough sampler
Modulating the `playbackRate` to simulate a vibrato!
2023-09-20 19:44:32 +02:00
Felix Roos 30cba7b935 Merge branch 'main' into midi-in 2023-09-17 17:26:42 +02:00
Felix Roos 9e27c0fe2c Merge pull request #705 from vasilymilovidov/main
Update tauri.yml workflow file
2023-09-17 15:53:27 +02:00
vmilovidov 05c09eff19 Update tauri.yml workflow file 2023-09-17 13:26:58 +00:00
vmilovidov 258eb88684 Update tauri.yml workflow 2023-09-17 13:14:13 +00:00
Felix Roos 9c73ef770f snapshot 2023-09-17 13:37:16 +02:00
Felix Roos de9a52366f Publish
- @strudel/codemirror@0.9.0
 - @strudel.cycles/core@0.9.0
 - @strudel.cycles/csound@0.9.0
 - @strudel.cycles/midi@0.9.0
 - @strudel.cycles/mini@0.9.0
 - @strudel.cycles/osc@0.9.0
 - @strudel.cycles/react@0.9.0
 - @strudel.cycles/serial@0.9.0
 - @strudel.cycles/soundfonts@0.9.0
 - superdough@0.9.8
 - @strudel.cycles/tonal@0.9.0
 - @strudel.cycles/transpiler@0.9.0
 - @strudel/web@0.9.0
 - @strudel.cycles/webaudio@0.9.0
 - @strudel.cycles/xen@0.9.0
2023-09-17 12:35:03 +02:00
Felix Roos 57265b9e33 add filter envelopes here and there
+ comment out outroMusic (lame)
2023-09-17 12:34:01 +02:00
Felix Roos b7ef2d97e2 make desktopbridge private for now 2023-09-17 11:50:18 +02:00
Felix Roos 4fb364195c snapshot 2023-09-17 11:18:20 +02:00
Felix Roos 578bede46b fix: tune n -> note 2023-09-17 11:17:37 +02:00
Felix Roos 38a51324b2 Merge pull request #693 from Bubobubobubobubo/vibrato
Adding vibrato to base oscillators
2023-09-17 11:05:05 +02:00
Felix Roos 0e7c9a4001 add vib examples for : notation 2023-09-17 11:01:12 +02:00
Felix Roos 32fee6313b move stuff back 2023-09-17 10:44:15 +02:00
Felix Roos c6a74c040e snapshot 2023-09-17 10:42:51 +02:00
Felix Roos a97384cec1 format 2023-09-17 10:42:21 +02:00
Felix Roos f052adb93a simplify examples 2023-09-17 10:41:22 +02:00
Felix Roos c2560e0cf8 set vib default to .5 2023-09-17 10:41:13 +02:00
Felix Roos cd6d1fb2d2 fix: headings 2023-09-17 10:41:02 +02:00
Felix Roos c354ee32e3 simplify vibrato logic 2023-09-17 10:15:33 +02:00
Felix Roos 5c5df74860 fix: control naming 2023-09-17 10:15:02 +02:00
Felix Roos 887fb6a84c Merge remote-tracking branch 'upstream/main' into vibrato 2023-09-17 09:46:12 +02:00
Felix Roos 80ea9e8de4 Merge pull request #698 from Bubobubobubobubo/sampler
Adding loop points and thus wavetable synthesis
2023-09-17 08:27:36 +02:00
Felix Roos 3879b01ddd snapshots 2023-09-17 08:04:02 +02:00
Felix Roos 4490681fa3 add note about wt_ 2023-09-17 08:03:09 +02:00
Felix Roos b98e24fabf docs: fit + splice 2023-09-17 08:02:02 +02:00
Felix Roos 2deafe214a update loop examples 2023-09-17 07:59:55 +02:00
Felix Roos d0258756e5 Merge remote-tracking branch 'upstream/main' into sampler 2023-09-17 07:31:21 +02:00
Felix Roos 4fecad16e4 superdough 0.9.7 2023-09-16 02:21:10 +02:00
Felix Roos a82eeb44c0 update snapshot 2023-09-16 02:08:25 +02:00
Felix Roos 7ec5ab0c20 fix: tune 2023-09-16 02:05:49 +02:00
Felix Roos 872e10fc95 Merge pull request #692 from Bubobubobubobubo/filters
Adding filter envelopes and filter order selection
2023-09-16 02:03:49 +02:00
Felix Roos e3b7d0f84d drop some acid in there 2023-09-16 02:01:46 +02:00
Felix Roos e0c424eb92 update snapshots 2023-09-16 01:50:45 +02:00
Felix Roos 536427310d add some filter envelopes here and there 2023-09-16 01:50:23 +02:00
Felix Roos a7eb0bb04a snapshot 2023-09-16 01:34:00 +02:00
Felix Roos af6a5b85f5 keep only lp envelopes as examples 2023-09-16 01:33:34 +02:00
Felix Roos b96e8093a0 doc: fenv 2023-09-16 01:33:17 +02:00
Felix Roos 989dc0aabe fix: filters without envelopes 2023-09-16 01:33:09 +02:00
Felix Roos 735f892a08 fix: snapshots 2023-09-16 01:14:53 +02:00
Felix Roos e959c275fd add fanchor 2023-09-16 01:14:14 +02:00
Felix Roos 7dbcc90450 fix: docs after fenv change 2023-09-16 01:07:10 +02:00
Felix Roos 7f86eac8f1 simplify 2023-09-16 00:51:36 +02:00
Felix Roos 40de717cc9 fix: don't add frequency 2023-09-16 00:46:50 +02:00
Felix Roos df211c994d anchored fenv, more logical with negative values
- defaulting to .5 for now...
2023-09-16 00:44:03 +02:00
Felix Roos ee314e9b03 snapshot 2023-09-15 23:38:37 +02:00
Felix Roos 9049a8b964 simplify fenv 2023-09-15 23:37:36 +02:00
Felix Roos 88c029202c simplify fenv examples 2023-09-15 23:37:27 +02:00
Felix Roos 9142fb753a cleaner default filter envelope
- fixes some unexpected envelope glitches
2023-09-15 23:36:55 +02:00
Felix Roos 4688c0bf10 format 2023-09-15 23:13:49 +02:00
Felix Roos 7e55258bbf update snapshots 2023-09-15 23:13:30 +02:00
Felix Roos 828e475921 support negative fenv 2023-09-15 23:12:51 +02:00
Felix Roos 9d5f919023 add all filter envelope examples to doc 2023-09-15 23:12:39 +02:00
Felix Roos b8932390f7 update snapshot 2023-09-15 21:44:24 +02:00
Felix Roos 445881992e slow down examples + simplify 2023-09-15 21:43:03 +02:00
Felix Roos 30d96dcb65 remove log 2023-09-14 16:28:47 +02:00
Felix Roos 3a69fd50bb fix: linting errors 2023-09-14 09:39:31 +02:00
Felix Roos 0f72729f0d midi cc input poc 2023-09-14 09:36:06 +02:00
Felix Roos 0a8874180c generalize getDevice + begin midiIn implementation 2023-09-13 13:48:33 +02:00
Felix Roos a6501474bb Merge branch 'fix-eval-scope' into midi-in 2023-09-13 13:00:52 +02:00
Felix Roos 4cb93ddcde fix: eval scope 2023-09-13 12:59:51 +02:00
Raphael Forment 7f1918ae8c Merge branch 'sampler' of github.com:Bubobubobubobubo/strudel into sampler 2023-09-11 00:56:50 +02:00
Raphael Forment 78adaaedef fixing minor bugs and adding description 2023-09-11 00:56:43 +02:00
Felix Roos 4a65113bff do not panic when no description 2023-09-11 00:52:49 +02:00
Raphael Forment 843d9d52c3 add documentation but it crashes 2023-09-11 00:46:31 +02:00
Raphael Forment 7a74189771 document wavetable synthesis 2023-09-11 00:41:55 +02:00
Raphael Forment 6de04f7781 turn loop on for samples starting with wt_ 2023-09-10 23:29:09 +02:00
Felix Roos ba27f2ba28 fix: looped samples pitch 2023-09-10 21:05:57 +02:00
Felix Roos f09b89ed63 fix: duration for loops 2023-09-10 20:55:59 +02:00
Raphael Forment bac8594c5f beginning documentation work (breaks) 2023-09-10 20:36:09 +02:00
Raphael Forment 3582ae9163 adding loopBegin and loopEnd 2023-09-10 18:34:21 +02:00
Raphael Forment 7a95bafa54 Updating documentation 2023-09-10 09:59:18 +02:00
Raphael Forment 1383af6208 documenting filters more + tests 2023-09-10 09:42:29 +02:00
Raphael Forment bea9a3c2dc replacing fenv by lpenv, hpenv, bpenv 2023-09-10 09:28:07 +02:00
Raphael Forment 78ccec5d7b adding test run 2023-09-10 09:14:41 +02:00
Felix Roos 534e3f7f81 add snapshots 2023-09-10 00:48:04 +02:00
Felix Roos 6a427d6f89 Merge pull request #697 from daslyfe/add_logging_from_tauri
Add logging from tauri
2023-09-09 11:19:57 +02:00
Jade Rowland b4297580a7 changed warning to error 2023-09-09 01:34:35 -04:00
Jade Rowland 26b0e2ae43 its working 2023-09-09 01:27:49 -04:00
Jade Rowland e75ac7b3c0 working 2023-09-09 01:20:08 -04:00
Raphael Forment 74442b0d76 documenting vibrato and removing broken slide/pitchJump mechanism 2023-09-08 13:43:43 +02:00
Raphael Forment 3431803980 documenting vib and vibmod 2023-09-08 13:08:00 +02:00
Raphael Forment c5fb247de6 Documentation for filter envelope 2023-09-08 12:38:39 +02:00
Felix Roos 18640d59da acid party 2023-09-08 03:00:49 +02:00
Felix Roos 48753b2539 fix last crack? 2023-09-08 02:53:08 +02:00
Felix Roos 606dcc8cc3 use fenv to scale above base cutoff? 2023-09-08 02:27:28 +02:00
Felix Roos 9e4c548c69 fix: filter clicks 2023-09-08 02:06:03 +02:00
Felix Roos 75a974643b add missing controls 2023-09-08 01:41:11 +02:00
Felix Roos e2711ba911 fix: filter envelopes + simplify 2023-09-08 01:40:10 +02:00
Raphael Forment 7605198774 parameter renaming 2023-09-05 11:27:42 +02:00
Raphael Forment fc9525e7d8 saner vibrato default 2023-09-05 11:24:21 +02:00
Raphael Forment e597b6473d remove test console log statement 2023-09-05 10:44:41 +02:00
Raphael Forment 16c8dcf788 stable version 2023-09-05 09:56:49 +02:00
Raphael Forment 37842d4fbd Revert "replace odd envelope"
This reverts commit 4319df0cb6.
2023-09-05 09:01:04 +02:00
Felix Roos 133a1d2e8f Merge pull request #694 from daslyfe/tauri_osc_support
Direct OSC Support in Tauri
2023-09-05 00:33:54 +02:00
Jade Rowland 15c15a9c36 remove unused import 2023-09-04 17:09:54 -04:00
Jade Rowland d233d50de3 dont panic if message fails to send 2023-09-04 17:07:50 -04:00
Jade Rowland a9d8e7ca82 cleaning up 2023-09-04 15:33:44 -04:00
Jade Rowland 4001cc9531 cleaning up 2023-09-04 15:33:28 -04:00
Raphael Forment 4319df0cb6 replace odd envelope 2023-09-04 21:24:42 +02:00
Raphael Forment 5be7898522 New technique using ADSR envelope 2023-09-04 21:17:16 +02:00
Jade Rowland 009760c31a correct bit calcuations 2023-09-04 15:00:43 -04:00
Jade Rowland 77349526e4 timetag mishaps 2023-09-04 14:21:30 -04:00
Felix Roos 3ba195c2d9 add new controls
+ rename slide_speed > slidespeed
2023-09-04 18:38:05 +02:00
Raphael Forment 60f5032b12 Implement pitch slide 2023-09-04 06:25:49 +02:00
Raphael Forment fdc201a799 Adding vibrato to synth oscillator 2023-09-04 06:03:05 +02:00
Raphael Forment ec8616a515 Envelope filter and filter order 2023-09-04 04:56:45 +02:00
Jade Rowland 349d11e76d different time strats 2023-09-03 17:36:21 -04:00
Jade Rowland 62e3774ff7 its working 2023-09-03 16:28:52 -04:00
Felix Roos abaeb52b7c Merge pull request #690 from vasilymilovidov/main
fix MIDI CC messages
2023-09-03 22:19:22 +02:00
Jade Rowland 60e600c381 setting up 2023-09-03 13:23:13 -04:00
Vasilii Milovidov 97f84ee87f fix MIDI CC messages 2023-09-03 15:54:34 +04:00
Felix Roos c5738aeed9 Merge pull request #688 from daslyfe/fix_tauri_cpu_midi
add sleep timer + improve message iterating
2023-09-03 10:22:15 +02:00
Jade Rowland 192adccb10 add sleep timer + improve message iterating 2023-09-02 20:59:00 -04:00
Felix Roos defcacb59b bump superdough to 0.9.6 2023-08-31 13:09:16 +02:00
Felix Roos e4ea786956 Merge pull request #684 from Bubobubobubobubo/zzfx
ZZFX Synth support
2023-08-31 13:00:30 +02:00
Felix Roos c9d98bf4d7 fix: tests 2023-08-31 12:58:32 +02:00
Felix Roos 8dd08c360f improve synth docs 2023-08-31 12:56:18 +02:00
Felix Roos d6c4bb0e4e Merge remote-tracking branch 'upstream/main' into zzfx 2023-08-31 12:38:19 +02:00
Felix Roos c54916cdfd Merge pull request #683 from Bubobubobubobubo/betterfmsynth
Wave Selection and Global Envelope on the FM Synth Modulator
2023-08-31 12:36:36 +02:00
Felix Roos e29cf6fcc1 update snapshot 2023-08-31 12:34:49 +02:00
Felix Roos 5b3cd166b0 more clear example 2023-08-31 12:34:16 +02:00
Felix Roos a7246c6518 snapshots 2023-08-31 12:30:01 +02:00
Felix Roos b48fd78cf0 fm envelope docs 2023-08-31 12:29:29 +02:00
Felix Roos 66c895ee3c fix: bad import 2023-08-31 11:52:49 +02:00
Felix Roos 37c450dd83 Merge remote-tracking branch 'upstream/main' into betterfmsynth 2023-08-31 11:52:41 +02:00
Felix Roos 4c744da3e2 add zzfx sound + zzfx control to override params 2023-08-31 10:52:36 +02:00
Felix Roos 388e7b3c2f add doc 2023-08-31 09:46:36 +02:00
Felix Roos 6ec5377381 add lfo alias for repeatTime 2023-08-31 09:46:30 +02:00
Felix Roos 24ec47e4ff fix: comments 2023-08-31 07:58:31 +02:00
Felix Roos 4a122523d9 fix: import 2023-08-31 07:55:09 +02:00
Felix Roos 1241911ec0 rename more stuff + pull out fork 2023-08-31 07:53:41 +02:00
Felix Roos 0f54bb1938 rename some params + use correct duration 2023-08-31 06:57:08 +02:00
Felix Roos a3b55aa5cc Merge pull request #685 from daslyfe/desktop_midi_bridge
Create Midi Integration for Tauri Desktop app
2023-08-31 05:52:17 +02:00
Felix Roos 4fa02a483b Merge pull request #681 from tidalcycles/protracker-font
teletext theme + fonts
2023-08-31 05:32:15 +02:00
Felix Roos a786c642f1 remove weird flags 2023-08-31 05:25:18 +02:00
Felix Roos 1bade7bee8 urgh npm 2023-08-31 05:23:08 +02:00
Felix Roos d399df8798 Merge pull request #674 from tidalcycles/waveformn
control osc partial count with n
2023-08-31 05:20:55 +02:00
Felix Roos 749b11a5ea fix: history 2023-08-31 05:12:33 +02:00
Felix Roos 328af5f560 fix: move desktopbridge dependency to website 2023-08-31 05:10:17 +02:00
Felix Roos 876f97e9a2 fix: exp envelope allow 0 without blowup 2023-08-31 04:35:35 +02:00
Felix Roos 26d7a32f11 remove comments 2023-08-31 04:26:14 +02:00
Felix Roos 3057773009 keep sine as fm default 2023-08-31 04:25:40 +02:00
Felix Roos 6ca89cb907 keep sine as mod default 2023-08-31 04:25:12 +02:00
Felix Roos 88b5b9b21c fix: history 2023-08-31 04:24:44 +02:00
Felix Roos 6bcf8b91cc fix: getExpEnvelope full adsr 2023-08-31 04:22:54 +02:00
Felix Roos 7cf7ebc555 fix: condition flip 2023-08-31 04:17:49 +02:00
Felix Roos ee0aaca2b0 only use fm envelope when needed 2023-08-31 04:13:41 +02:00
Felix Roos b1b084452c fix: format 2023-08-31 03:56:31 +02:00
Felix Roos 213b1a2dae pull out debugging fn 2023-08-31 03:53:52 +02:00
Felix Roos b991cfc493 fix: use custom version of ZZFX
- ZZFX audio context broke tests
- only needed buildSamples function
2023-08-31 03:50:38 +02:00
Felix Roos 90cdea3656 fix: double registering 2023-08-30 15:32:45 +02:00
Felix Roos 1463f27a0f move zzfx stuff to separate file 2023-08-30 15:27:07 +02:00
Felix Roos afb8ab44cf more zzfx wiring 2023-08-30 15:25:23 +02:00
Jade Rowland 282f36c47c fix lock file 2023-08-30 09:17:12 -04:00
Raphael Forment c3f27d1ad9 Boilerplate for ZZFX Synth support 2023-08-30 14:32:20 +02:00
Jade Rowland ad230a8dc7 remove console statement 2023-08-30 08:32:13 -04:00
Jade Rowland c29c4c0a8b configuring package, wiring everything together 2023-08-30 08:24:02 -04:00
Jade Rowland 284f5615cc bridge functionality for midi 2023-08-29 18:14:02 -04:00
Raphael Forment c625096f59 removing comments 2023-08-29 17:29:50 +02:00
Raphael Forment c3cd5d08c9 adding linear and exponential envelope for fm synthesis 2023-08-29 17:25:27 +02:00
Raphael Forment c87c3c6672 Revert "envelopes on fmsynth"
This reverts commit 8fc7688585.
2023-08-29 16:31:47 +02:00
Raphael Forment 8fc7688585 envelopes on fmsynth 2023-08-29 13:46:43 +02:00
Raphael Forment 94bcfb11de removing rogue console.log 2023-08-29 13:16:39 +02:00
Raphael Forment ebeaac3c5f adding fm wave choice 2023-08-29 12:54:17 +02:00
Felix Roos 942ffc038d add font credit 2023-08-29 00:57:41 +02:00
Felix Roos 9de344e48d delete protracker font 2023-08-29 00:45:16 +02:00
Felix Roos 5efea045d2 delete protracker theme 2023-08-29 00:30:16 +02:00
Felix Roos 6bd3274a4b remove protracker theme + font (not good) 2023-08-29 00:27:20 +02:00
Felix Roos a179313db2 teletext: add panel bg 2023-08-29 00:19:57 +02:00
Felix Roos 289376840e add mode7 font
+ can now inject custom theme styles
2023-08-28 23:52:24 +02:00
Felix Roos 0e3d84e5ca add teletext theme + font 2023-08-28 19:21:17 +02:00
Felix Roos cb643f8fd9 add protracker theme
+ fix: make sure all text has a bg
2023-08-28 18:54:33 +02:00
Felix Roos 1faa81099c add protracker3 font 2023-08-28 14:50:06 +02:00
Felix Roos 2dc079b950 Merge pull request #680 from tidalcycles/emoji-support
add emoji support
2023-08-27 22:18:06 +02:00
Felix Roos f6cf740507 add emoji support 2023-08-27 22:11:22 +02:00
Felix Roos 338c866d83 Merge pull request #679 from tidalcycles/pianoroll-improvements
Pianoroll improvements
2023-08-27 16:12:32 +02:00
Felix Roos 038e6c312b dedupe .pianoroll 2023-08-27 16:05:58 +02:00
Felix Roos 6f6def34f7 fix: improve performance of setting patterning 2023-08-27 15:35:44 +02:00
Felix Roos 88651149d3 fix: don't import emoji pkg 2023-08-27 15:28:46 +02:00
Felix Roos e301090704 pianoroll improvements:
- add label / activeLabel controls
- add new pianoroll options:
  - fill
  - fillActive
  - strokeActive
  - stroke
  - hideInactive
  - colorizeInactive
  - fontFamily
- add wordfall method
- fix: some haps were drawn with wrong length
- pianoroll labels now use set fontFamily
- hide fold gutter
2023-08-27 15:27:30 +02:00
Felix Roos d0f8a612ad Merge pull request #677 from tidalcycles/scope
Scope
2023-08-26 21:22:16 +02:00
Felix Roos a7728e3d81 comment new controls 2023-08-26 21:16:46 +02:00
Felix Roos 4dadc18a3a fix: import 2023-08-26 21:12:02 +02:00
Felix Roos 43efe6921d move scope to webaudio package 2023-08-26 21:11:24 +02:00
Felix Roos 2e31c8695a use Float32 for higher precision 2023-08-25 16:29:41 +02:00
Felix Roos 988bd8ccdc rename stuff 2023-08-25 12:40:04 +02:00
Felix Roos 965794712e fscope + scaling options 2023-08-25 12:39:47 +02:00
Felix Roos 68ea086e2a improve canvas quality on retina 2023-08-25 12:39:38 +02:00
Felix Roos 7370f41fa0 basic scope feature 2023-08-25 09:45:30 +02:00
Felix Roos 5cbe30095b control osc partial count with n 2023-08-24 12:21:29 +02:00
Felix Roos f20c5a1bcb Merge pull request #668 from daslyfe/midi_time_fixes
Midi time fixes
2023-08-23 21:50:50 +02:00
Felix Roos 02979926fd bump superdough 2023-08-20 23:50:38 +02:00
Felix Roos b08337af45 Merge pull request #669 from tidalcycles/fm
basic fm
2023-08-20 23:19:07 +02:00
Felix Roos 980c1e4812 simplify 2023-08-20 22:45:05 +02:00
Felix Roos f43dd8db1b add doc 2023-08-20 22:41:49 +02:00
Felix Roos badda165fe add snapshot 2023-08-20 22:39:46 +02:00
Felix Roos f7d17875c0 fix: lint 2023-08-20 22:36:44 +02:00
Felix Roos 0c124bcca3 basic fm 2023-08-20 22:32:27 +02:00
Jade Rowland 3eb63f1730 round duration down 2023-08-19 15:10:53 -04:00
Jade Rowland abb0b3b4c0 dont round duration 2023-08-19 15:09:46 -04:00
Jade Rowland 42662748d3 cleaning up changes 2023-08-19 15:06:55 -04:00
Jade Rowland b5b866a894 time clock improvements 2023-08-19 01:08:41 -04:00
Felix Roos 01cccc6462 hotfix: panel z-index < autocomplete 2023-08-19 00:06:44 +02:00
Felix Roos 9287b8ea42 Merge pull request #667 from tidalcycles/toggle-sidebar
togglable panel position
2023-08-18 23:56:20 +02:00
Felix Roos 5fabc5c7f9 let to const 2023-08-18 23:53:37 +02:00
Felix Roos 61dc6f980d ignore tauri dist for format-check 2023-08-18 23:48:00 +02:00
Felix Roos 84328cfb90 fix: format 2023-08-18 23:46:26 +02:00
Felix Roos 37a35022f5 fix panel scroll
+ move to bottom when minimized
2023-08-18 23:45:55 +02:00
Felix Roos 3afe540111 add hint for zen mode 2023-08-18 23:32:30 +02:00
Felix Roos 35687b1ee0 fix reference style for right panel 2023-08-18 23:32:22 +02:00
Felix Roos 195e6b61f9 fix settings style for right panel 2023-08-18 23:23:12 +02:00
Felix Roos dabc0331cb fix panel scroll 2023-08-18 23:19:19 +02:00
Felix Roos 824abb0406 Merge pull request #666 from daslyfe/osc_fixes
fix osc bundle timestamp glitches caused by drifting clock
2023-08-18 15:59:20 +02:00
Felix Roos f6789aaace togglable panel position 2023-08-18 15:58:51 +02:00
Jade Rowland 10bce85561 fix osc bundle timestamp glitches caused by drifting clock 2023-08-17 18:01:41 -04:00
Felix Roos cd0f1b6c5c bump: superdough 0.9.4 2023-08-17 11:42:24 +02:00
Felix Roos 834bb7d777 Merge pull request #664 from tidalcycles/superdough
superdough: encapsulates web audio output
2023-08-17 11:36:07 +02:00
Felix Roos 5c1d7baf72 superdough readme doc 2023-08-17 11:23:17 +02:00
Felix Roos 8bdbc9ec4c superdough: add logger 2023-08-17 10:08:55 +02:00
Felix Roos 0c6a0720af bump superdough to 0.9.3 2023-08-11 12:35:18 +02:00
Felix Roos c6095fd92a add disableWorklets option to initAudio 2023-08-11 12:35:00 +02:00
Felix Roos 7d0fb0de83 Revert "rename: superdough.mjs > webaudio.mjs"
This reverts commit 75281afe05.
2023-08-11 12:22:03 +02:00
Felix Roos 75281afe05 rename: superdough.mjs > webaudio.mjs
(to keep history?)
2023-08-11 12:20:36 +02:00
Felix Roos 738e714f45 use superdough in webaudio package 2023-08-11 12:13:24 +02:00
Felix Roos 898bfaeecf bump to 0.9.2 2023-08-11 11:13:31 +02:00
Felix Roos 7f44d9cdd2 add link to codesandbox 2023-08-11 11:09:33 +02:00
Felix Roos 8d9be21c8a better code example 2023-08-11 10:56:57 +02:00
Felix Roos 82cb4c8c11 fix: still had strudel core imports 2023-08-11 10:51:59 +02:00
Felix Roos 2778e4d697 fix: build entry file 2023-08-11 10:43:59 +02:00
Felix Roos 30fe2dd502 add superdough package 2023-08-11 10:42:59 +02:00
gogins 1170ff58ee Fix for #1. 2023-08-10 20:26:37 -04:00
Felix Roos 2483bf812f Merge pull request #660 from tidalcycles/fix-multiple-triggers
fix: always run previous trigger
2023-08-11 00:06:21 +02:00
Felix Roos 7a999de602 fix: always run previous trigger
+ make dominantTrigger more dominant
2023-08-11 00:04:09 +02:00
Felix Roos 1929c27225 Merge pull request #659 from tidalcycles/fix-console-welcome
fix: welcome message for latestCode
2023-08-10 23:54:58 +02:00
Felix Roos 0cf5c9c7d6 fix: welcome message for latestCode 2023-08-10 23:51:04 +02:00
Felix Roos c20b691683 Merge pull request #657 from daslyfe/main
[Bug Fix] Midi: Don't treat note 0 as false
2023-08-07 07:45:26 +02:00
Jade Rowland 5c2101de1a [Bug Fix] Midi: Don't treat note 0 as false 2023-07-29 13:34:02 -04:00
Felix Roos 2ec2e81511 Merge pull request #656 from daslyfe/main
[Bug Fix] Account for numeral notation when converting to midi
2023-07-29 09:06:17 +02:00
Jade Rowland ac3d8b832c [Bug Fix] Account for numeral notation when converting to midi 2023-07-29 01:29:05 -04:00
Felix Roos ad174957e8 Merge pull request #653 from tidalcycles/ireal-voicings
ireal voicings
2023-07-23 22:18:48 +02:00
Felix Roos 9b3fa83516 add common chord symbol aliases 2023-07-23 22:05:42 +02:00
Felix Roos 52b8d9d361 add octaves control for arpeggios 2023-07-23 21:18:34 +02:00
Felix Roos d231ded7a2 fix: ^7 voicings 2023-07-23 21:18:18 +02:00
Felix Roos 5f57951e3d fix: add empty string as major alias 2023-07-23 14:10:38 +02:00
Felix Roos b1a4788442 a few more voicings 2023-07-23 14:06:25 +02:00
Felix Roos 6377378a3d sort by topnote and then by bottom note
(easier to track diffs)
2023-07-23 13:31:47 +02:00
Felix Roos 45b91cdea5 fix: major chords 2023-07-23 09:57:34 +02:00
Felix Roos d5647c6760 sort voicings by top note 2023-07-23 09:51:21 +02:00
Felix Roos f419559bde Merge remote-tracking branch 'origin/main' into ireal-voicings 2023-07-22 16:22:49 +02:00
Felix Roos 4cfaa39419 fix: flip subtraction 2023-07-22 13:54:43 +02:00
Felix Roos d6b1762adf Merge pull request #652 from tidalcycles/understand-pitch
Understand pitch
2023-07-22 09:30:21 +02:00
Felix Roos 45c3293385 format 2023-07-22 09:28:51 +02:00
Felix Roos 92d574605e small fixes 2023-07-22 09:26:34 +02:00
Felix Roos cec7b90631 finish pitch page 2023-07-22 09:19:13 +02:00
Felix Roos aa3cb35f09 better exponent display 2023-07-21 18:42:47 +02:00
Felix Roos 7373138e3d improve pitch page 2023-07-21 17:46:45 +02:00
Felix Roos 2d8176f213 begin pitch page 2023-07-21 14:40:47 +02:00
Felix Roos 8589d72aee fix: console error 2023-07-21 14:40:40 +02:00
Felix Roos 626b50087a Merge branch 'main' into docs 2023-07-21 10:02:02 +02:00
Felix Roos 1e62554dfb add links 2023-07-20 23:43:53 +02:00
Felix Roos bcf800482b fix: chord typo + export only voicings 2023-07-20 23:43:08 +02:00
Felix Roos 08f94d2122 add ireal voicings 2023-07-20 22:53:02 +02:00
Felix Roos 559cfd11db Merge pull request #651 from tidalcycles/update-vitest
update vitest
2023-07-17 23:42:59 +02:00
Felix Roos ce820c2314 update vitest 2023-07-17 23:37:51 +02:00
Felix Roos 8583ed0e5c Merge pull request #647 from tidalcycles/tonleiter
stateless voicings + tonleiter lib
2023-07-17 23:34:33 +02:00
Felix Roos f2c16a06a1 fix: note2oct 2023-07-17 23:10:13 +02:00
Felix Roos 584d469a73 Merge pull request #650 from bwagner/tonleiter
FIXES: TODO in rotateChroma
2023-07-17 17:57:16 +02:00
Bernhard Wagner 4298e582fb FIXES: note2pc TODOs 2023-07-14 14:38:26 +02:00
Bernhard Wagner a3fdfbbc16 FIXES: weird gain difference 2023-07-14 14:04:35 +02:00
Bernhard Wagner b8929a0d18 FIXES: prettier formatting 2023-07-14 13:56:36 +02:00
Bernhard Wagner 0fa5832b63 FIXES: TODO in rotateChroma 2023-07-14 13:45:27 +02:00
Felix Roos bbb29ebd7c fix: duck mode with n
+ detect chord obj in rootNotes
2023-07-13 12:52:57 +02:00
Felix Roos 0fd9ac6da3 feat: support strings in voicing function 2023-07-13 12:13:00 +02:00
Felix Roos e53098b179 fix: tests 2023-07-13 11:58:24 +02:00
Felix Roos 78dcab2778 fix: control interference 2023-07-13 11:24:57 +02:00
Felix Roos e05f74cef1 docs + rename under -> duck, quak quak 2023-07-13 10:49:08 +02:00
Felix Roos 3d8724c90d snapshot 2023-07-13 01:11:31 +02:00
Felix Roos ffeda19ce5 snapshots + fix default anchor for lefthand 2023-07-13 01:09:53 +02:00
Felix Roos 70fe8f14e3 refactor: voicings -> voicing in tunes 2023-07-13 01:09:29 +02:00
Felix Roos a639d9f49f fix: remove log 2023-07-13 00:25:24 +02:00
Felix Roos e5251422fe revert changing existing addVoicings
+ add registerVoicings for the new logic
2023-07-13 00:20:28 +02:00
Felix Roos d8f80c058d can now set mode and anchor on voicing dictionary 2023-07-12 23:47:13 +02:00
Felix Roos 4cf3655c08 voicing modes: below, under, above 2023-07-12 23:32:27 +02:00
Felix Roos 58fc786462 noteToMidi: adjustable default octave
+ pull out static objects
2023-07-12 23:32:02 +02:00
Felix Roos 81b142704c change call signature of renderVoicing
+ pull logic in from voicings function
2023-07-12 22:55:34 +02:00
Felix Roos b0cbeda796 fix: support existing voicing dicts in new logic
+ rename voiceBelow to renderVoicing
2023-07-12 22:38:44 +02:00
Felix Roos 0b3a8a5f65 - make dictionary a control
- standalone voicing function
- simplify voicing control names
2023-07-11 22:49:34 +02:00
Felix Roos d7d8d2a3e6 Merge pull request #645 from tidalcycles/slice
slice: list mode
2023-07-10 19:07:45 +02:00
Felix Roos d5c0309885 feat: support n for voicing scales
+ simplify voicing logic
2023-07-09 22:11:40 +02:00
Felix Roos 36fa9d81fb fix: pianoroll isActive still used whole.end 2023-07-09 18:51:26 +02:00
Felix Roos 35f5a5d11a feat: add voiceOffset (2) 2023-07-09 16:53:41 +02:00
Felix Roos d1fe50e271 feat: add voiceOffset 2023-07-09 16:53:29 +02:00
Felix Roos 2d08b31f9a fix: tests 2023-07-08 16:14:25 +02:00
Felix Roos 27643f4a48 doc: slice 2023-07-08 16:12:21 +02:00
Felix Roos 110e179e9e wip: new voicing function 2023-07-08 12:41:17 +02:00
Felix Roos fe6d99d4f1 slice array notation 2023-07-07 21:48:19 +02:00
Felix Roos 06fbcbe24b docs: add slice effect 2023-07-07 21:48:03 +02:00
Felix Roos f40b57ee48 hotfix: remove log 2023-07-06 09:59:20 +02:00
Felix Roos 9438c69bed comment 2023-07-06 09:58:57 +02:00
Felix Roos 70d6f3000a move tonleiter 2023-07-06 09:58:48 +02:00
Felix Roos d47fd0cf18 Merge pull request #639 from tidalcycles/delete-old-packages
Delete old packages
2023-07-04 23:50:30 +02:00
Felix Roos 40117fbe54 package-lock 2023-07-04 23:47:37 +02:00
Felix Roos 7466d8a1de delete webdirt 2023-07-04 23:44:36 +02:00
Felix Roos ce09443e48 delete tone 2023-07-04 23:43:49 +02:00
Felix Roos 1718dbe94e delete eval package 2023-07-04 23:41:29 +02:00
Felix Roos 69894db206 Merge pull request #634 from tidalcycles/highlight-ids
Adaptive Highlighting
2023-07-04 23:38:41 +02:00
Felix Roos 6e4c873248 remove tests from soon removed package 2023-07-04 22:17:44 +02:00
Felix Roos 64693ffd26 fix: remove test flag 2023-07-04 22:16:02 +02:00
Felix Roos 66f8ca72c1 refactor: remove old location methods
docs: update repl chapter to reflect new transpiler behavior
2023-07-04 22:13:40 +02:00
Felix Roos c82f7bd8fe Merge pull request #636 from mindofmatthew/highlight-ids
More work on highlight IDs
2023-07-04 21:55:57 +02:00
Felix Roos 34176ab5f8 refactor:
- dedupe flash / highlighting logic
- codemirror logic now lives only in codemirror package
- remove old highlighting logic
- use codemirror package in react package
- cleanup CodeMirror6.jsx
- pull setMiniLocations into useHighlighting
- migrate MiniRepl, nano-repl + Repl to new highlighting
2023-07-04 21:49:39 +02:00
Felix Roos 7f12ce9b45 fix: format 2023-07-04 18:47:44 +02:00
Felix Roos 9e1b9f7f5c Merge remote-tracking branch 'upstream/main' into highlight-ids 2023-07-04 18:46:05 +02:00
Felix Roos 23e949de1c Merge pull request #637 from tidalcycles/snapshots-sort-haps-by-part
snapshot tests: sort haps by part
2023-07-04 18:44:47 +02:00
Felix Roos 00bd60a855 snapshot tests: sort haps by part 2023-07-04 18:43:26 +02:00
Felix Roos dacd9afac0 comment out opacity logic for now 2023-07-04 18:34:22 +02:00
Felix Roos 3fc5bb31d0 fix: do not recreate haps mapping on update 2023-07-04 18:22:22 +02:00
Felix Roos 78770888a5 remove mock positions 2023-07-04 18:16:58 +02:00
Matthew Kaney f5b092acf2 Split up highlighting logic and add progress flash 2023-07-03 17:06:39 -04:00
Matthew Kaney ba9562f000 Remove highlights when code stops 2023-07-03 10:52:06 -04:00
Matthew Kaney aded178ab7 Pass mininotation locations into highlight state 2023-07-03 03:55:49 -04:00
Felix Roos 0b5d905120 fix: adaptive highlighting
- transpiler now uses m function with globalOffset
- patternifyAST now accepts global offset
- patternifyAST now calls .withLoc with global leaf location
- .withLoc replaces .withLocation + .withMiniLocation
- simple locs (offsets) are now used everywhere
- some tests fail, seems some haps have reordered...
- wip: Repl still uses hardcoded updateMiniLocations
- todo: find way to call updateMiniLocations dynamically
2023-07-03 05:15:32 +02:00
Felix Roos 08abec8fd5 fix: init phase 2023-07-02 18:37:56 +02:00
Felix Roos 63c23736ad wip: adaptive highlighting 2023-07-02 18:33:44 +02:00
Felix Roos f7bd373ce6 update package-lock 2023-07-02 14:17:15 +02:00
Felix Roos 8e717d2ea1 - transpiler now always returns an object
- emit transpiler metadata from evaluate / afterEval
- currently logging miniLocations from Repl.jsx
2023-07-02 14:15:54 +02:00
Felix Roos 5f271ed127 add emitMiniLocations flag to transpiler 2023-07-02 13:42:41 +02:00
Felix Roos d686b65dbe add 2 more tests 2023-07-01 23:55:41 +02:00
Felix Roos c7e882e001 mini: leaf location retrieval
- add onEnter callback for patternifyAST
- add leaf node location helpers
- add tests
2023-07-01 23:52:31 +02:00
Felix Roos f07859996e Merge pull request #631 from tidalcycles/fix-canvas-resize
fix: update canvas size on window resize
2023-06-30 22:47:39 +02:00
Felix Roos fbc73bc78a Merge pull request #630 from tidalcycles/out-of-range-error
fix: out of range error
2023-06-30 22:45:41 +02:00
Felix Roos bd8ad1ed1b fix: out of range error due to rerender 2023-06-30 22:40:02 +02:00
Felix Roos dc2ff83fa5 do not flash when empty 2023-06-30 22:38:14 +02:00
Felix Roos 13545d147b optimize rendering 2023-06-30 22:37:55 +02:00
Felix Roos 1b85aa713b fix: update canvas size on window resize 2023-06-30 16:03:56 +02:00
232 changed files with 21301 additions and 12762 deletions
+4 -1
View File
@@ -17,4 +17,7 @@ vite.config.js
**/*.ts
**/*.json
**/dev-dist
**/dist
**/dist
/src-tauri/target/**/*
reverbGen.mjs
hydra.mjs
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf libasound2-dev
- name: Install app dependencies from lockfile and build web
run: pnpm install
+3 -3
View File
@@ -13,7 +13,7 @@ To get in touch with the contributors, either
## Ask a Question
If you have any questions about strudel, make sure you've glanced through the
[docs](https://strudel.tidalcycles.org/learn/) to find out if it answers your question.
[docs](https://strudel.cc/learn/) to find out if it answers your question.
If not, use one of the Communication Channels above!
Don't be afraid to ask! Your question might be of great value for other people too.
@@ -31,7 +31,7 @@ Use one of the Communication Channels listed above.
## Improve the Docs
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/workshop/getting-started/),
If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/),
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
## Propose a Feature
@@ -83,7 +83,7 @@ Please report any problems you've had with the setup instructions!
To make sure the code changes only where it should, we are using prettier to unify the code style.
- You can format all files at once by running `pnpm prettier` from the project root
- You can format all files at once by running `pnpm codeformat` from the project root
- Run `pnpm format-check` from the project root to check if all files are well formatted
If you use VSCode, you can
+4 -4
View File
@@ -4,8 +4,8 @@
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is slowly stabilising, but please continue to tread carefully.
- Try it here: <https://strudel.tidalcycles.org/>
- Docs: <https://strudel.tidalcycles.org/learn/>
- Try it here: <https://strudel.cc>
- Docs: <https://strudel.cc/learn>
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
@@ -24,8 +24,8 @@ There are multiple npm packages you can use to use strudel, or only parts of it,
- [`core`](./packages/core/): tidal pattern engine
- [`mini`](./packages/mini): mini notation parser + core binding
- [`eval`](./packages/eval): user code evaluator. syntax sugar + highlighting
- [`tone`](./packages/tone): bindings for Tone.js instruments and effects
- [`transpiler`](./packages/transpiler): user code transpiler
- [`webaudio`](./packages/webaudio): webaudio output
- [`osc`](./packages/osc): bindings to communicate via OSC
- [`midi`](./packages/midi): webmidi bindings
- [`serial`](./packages/serial): webserial bindings
+1 -3
View File
@@ -2,7 +2,7 @@
export * from './packages/core/index.mjs';
export * from './packages/csound/index.mjs';
export * from './packages/embed/index.mjs';
export * from './packages/eval/index.mjs';
export * from './packages/desktopbridge/index.mjs';
export * from './packages/midi/index.mjs';
export * from './packages/mini/index.mjs';
export * from './packages/osc/index.mjs';
@@ -10,8 +10,6 @@ export * from './packages/react/index.mjs';
export * from './packages/serial/index.mjs';
export * from './packages/soundfonts/index.mjs';
export * from './packages/tonal/index.mjs';
export * from './packages/tone/index.mjs';
export * from './packages/transpiler/index.mjs';
export * from './packages/webaudio/index.mjs';
export * from './packages/webdirt/index.mjs';
export * from './packages/xen/index.mjs';
+1 -1
View File
@@ -47,7 +47,7 @@ If you want to automatically deploy your site on push, go to `deploy.yml` and ch
## running locally
- install dependencies with `npm run setup`
- run dev server with `npm run repl` and open `http://localhost:3000/strudel/swatch/`
- run dev server with `npm run repl` and open `http://localhost:4321/strudel/swatch/`
## tests fail?
+2 -2
View File
@@ -43,7 +43,7 @@
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://strudel.tidalcycles.org",
"homepage": "https://strudel.cc",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
@@ -67,6 +67,6 @@
"lerna": "^6.6.1",
"prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.8.1",
"vitest": "^0.28.0"
"vitest": "^0.33.0"
}
}
+88
View File
@@ -0,0 +1,88 @@
import { createRoot } from 'react-dom/client';
import jsdoc from '../../doc.json';
// import { javascriptLanguage } from '@codemirror/lang-javascript';
import { autocompletion } from '@codemirror/autocomplete';
const getDocLabel = (doc) => doc.name || doc.longname;
const getInnerText = (html) => {
var div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
};
export function Autocomplete({ doc }) {
return (
<div className="prose dark:prose-invert max-h-[400px] overflow-auto">
<h3 className="pt-0 mt-0">{getDocLabel(doc)}</h3>
<div dangerouslySetInnerHTML={{ __html: doc.description }} />
<ul>
{doc.params?.map(({ name, type, description }, i) => (
<li key={i}>
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
</li>
))}
</ul>
<div>
{doc.examples?.map((example, i) => (
<div key={i}>
<pre
className="cursor-pointer"
onMouseDown={(e) => {
console.log('ola!');
navigator.clipboard.writeText(example);
e.stopPropagation();
}}
>
{example}
</pre>
</div>
))}
</div>
</div>
);
}
const jsdocCompletions = jsdoc.docs
.filter(
(doc) =>
getDocLabel(doc) &&
!getDocLabel(doc).startsWith('_') &&
!['package'].includes(doc.kind) &&
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
)
// https://codemirror.net/docs/ref/#autocomplete.Completion
.map((doc) /*: Completion */ => ({
label: getDocLabel(doc),
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
info: () => {
const node = document.createElement('div');
// if Autocomplete is non-interactive, it could also be rendered at build time..
// .. using renderToStaticMarkup
createRoot(node).render(<Autocomplete doc={doc} />);
return node;
},
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
}));
export const strudelAutocomplete = (context /* : CompletionContext */) => {
let word = context.matchBefore(/\w*/);
if (word.from == word.to && !context.explicit) return null;
return {
from: word.from,
options: jsdocCompletions,
/* options: [
{ label: 'match', type: 'keyword' },
{ label: 'hello', type: 'variable', info: '(World)' },
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
], */
};
};
export function isAutoCompletionEnabled(on) {
return on
? [
autocompletion({ override: [strudelAutocomplete] }),
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
]
: []; // autocompletion({ override: [] })
}
+172 -129
View File
@@ -1,35 +1,78 @@
import { EditorState } from '@codemirror/state';
import { EditorView, keymap, Decoration, lineNumbers, highlightActiveLineGutter } from '@codemirror/view';
import { defaultKeymap } from '@codemirror/commands';
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
import { closeBrackets } from '@codemirror/autocomplete';
// import { search, highlightSelectionMatches } from '@codemirror/search';
import { history } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { StateField, StateEffect } from '@codemirror/state';
import { oneDark } from './themes/one-dark';
import { repl, Drawer } from '@strudel.cycles/core';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { Compartment, EditorState } from '@codemirror/state';
import { EditorView, highlightActiveLineGutter, highlightActiveLine, keymap, lineNumbers } from '@codemirror/view';
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel.cycles/core';
import { isAutoCompletionEnabled } from './Autocomplete';
import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs';
import { theme } from './themes.mjs';
import { updateWidgets, sliderPlugin } from './slider.mjs';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
theme,
isAutoCompletionEnabled,
isPatternHighlightingEnabled,
isFlashEnabled,
keybindings,
};
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, settings, root }) {
const initialSettings = Object.keys(compartments).map((key) =>
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
);
let state = EditorState.create({
doc: initialCode,
extensions: [
theme,
/* search(),
highlightSelectionMatches(), */
...initialSettings,
javascript(),
lineNumbers(),
highlightField,
sliderPlugin,
// indentOnInput(), // works without. already brought with javascript extension?
// bracketMatching(), // does not do anything
closeBrackets(),
highlightActiveLineGutter(),
highlightActiveLine(),
syntaxHighlighting(defaultHighlightStyle),
keymap.of(defaultKeymap),
flashField,
history(),
EditorView.updateListener.of((v) => onChange(v)),
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate(),
run: () => onEvaluate?.(),
},
{
key: 'Alt-Enter',
run: () => onEvaluate?.(),
},
{
key: 'Ctrl-.',
run: () => onStop(),
run: () => onStop?.(),
},
{
key: 'Alt-.',
run: (_, e) => {
e.preventDefault();
onStop?.();
},
},
/* {
key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()),
},
{
key: 'Ctrl-Shift-Enter',
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
}, */
]),
],
});
@@ -40,160 +83,160 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, the
});
}
//
// highlighting
//
export const setHighlights = StateEffect.define();
export const highlightField = StateField.define({
create() {
return Decoration.none;
},
update(highlights, tr) {
try {
for (let e of tr.effects) {
if (e.is(setHighlights)) {
const { haps } = e.value;
const marks =
haps
.map((hap) =>
(hap.context.locations || []).map(({ start, end }) => {
// const color = hap.context.color || e.value.color || '#FFCA28';
let from = tr.newDoc.line(start.line).from + start.column;
let to = tr.newDoc.line(end.line).from + end.column;
const l = tr.newDoc.length;
if (from > l || to > l) {
return; // dont mark outside of range, as it will throw an error
}
const mark = Decoration.mark({
attributes: { style: `outline: 2px solid #FFCA28;` },
});
return mark.range(from, to);
}),
)
.flat()
.filter(Boolean) || [];
highlights = Decoration.set(marks, true);
}
}
return highlights;
} catch (err) {
// console.warn('highlighting error', err);
return Decoration.set([]);
}
},
provide: (f) => EditorView.decorations.from(f),
});
// helper to simply trigger highlighting for given haps
export function highlightHaps(view, haps) {
view.dispatch({ effects: setHighlights.of({ haps }) });
}
//
// flash
//
export const setFlash = StateEffect.define();
const flashField = StateField.define({
create() {
return Decoration.none;
},
update(flash, tr) {
try {
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash = Decoration.set([]);
}
}
}
return flash;
} catch (err) {
console.warn('flash error', err);
return flash;
}
},
provide: (f) => EditorView.decorations.from(f),
});
export const flash = (view, ms = 200) => {
view.dispatch({ effects: setFlash.of(true) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, ms);
};
export class StrudelMirror {
constructor(options) {
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, settings, ...replOptions } = options;
this.code = initialCode;
this.root = root;
this.miniLocations = [];
this.widgets = [];
this.painters = [];
this.onDraw = onDraw;
const self = this;
this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
this.highlight(currentFrame);
onDraw?.(haps, time, currentFrame);
this.highlight(currentFrame, time);
this.onDraw?.(haps, time, currentFrame, this.painters);
}, drawTime);
const prebaked = prebake();
prebaked.then(async () => {
if (!onDraw) {
return;
}
const { scheduler, evaluate } = await this.repl;
// draw first frame instantly
prebaked.then(async () => {
await evaluate(this.code, false);
this.drawer.invalidate(scheduler);
onDraw?.(this.drawer.visibleHaps, 0, []);
});
});
// this approach might not work with multiple repls on screen..
Pattern.prototype.onPaint = function (onPaint) {
self.painters.push(onPaint);
return this;
};
this.prebaked = prebake();
// this.drawFirstFrame();
this.repl = repl({
...replOptions,
onToggle: async (started) => {
onToggle: (started) => {
replOptions?.onToggle?.(started);
const { scheduler } = await this.repl;
if (started) {
this.drawer.start(scheduler);
this.drawer.start(this.repl.scheduler);
} else {
this.drawer.stop();
updateMiniLocations(this.editor, []);
cleanupDraw(false);
}
},
beforeEval: async () => {
await prebaked;
cleanupDraw();
this.painters = [];
await this.prebaked;
await replOptions?.beforeEval?.();
},
afterEval: (options) => {
// remember for when highlighting is toggled on
this.miniLocations = options.meta?.miniLocations;
this.widgets = options.meta?.widgets;
updateWidgets(this.editor, this.widgets);
updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options);
this.drawer.invalidate();
},
});
this.editor = initEditor({
root,
settings,
initialCode,
onChange: (v) => {
this.code = v.state.doc.toString();
if (v.docChanged) {
this.code = v.state.doc.toString();
this.repl.setCode(this.code);
}
},
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
});
}
async drawFirstFrame() {
if (!this.onDraw) {
return;
}
// draw first frame instantly
await this.prebaked;
try {
await this.repl.evaluate(this.code, false);
this.drawer.invalidate(this.repl.scheduler);
this.onDraw?.(this.drawer.visibleHaps, 0, []);
} catch (err) {
console.warn('first frame could not be painted');
}
}
async evaluate() {
const { evaluate } = await this.repl;
this.flash();
await evaluate(this.code);
await this.repl.evaluate(this.code);
}
async stop() {
const { scheduler } = await this.repl;
scheduler.stop();
this.repl.scheduler.stop();
}
async toggle() {
if (this.repl.scheduler.started) {
this.repl.scheduler.stop();
} else {
this.evaluate();
}
}
flash(ms) {
flash(this.editor, ms);
}
highlight(haps) {
highlightHaps(this.editor, haps);
highlight(haps, time) {
highlightMiniLocations(this.editor, time, haps);
}
setFontSize(size) {
this.root.style.fontSize = size + 'px';
}
setFontFamily(family) {
this.root.style.fontFamily = family;
}
reconfigureExtension(key, value) {
if (!extensions[key]) {
console.warn(`extension ${key} is not known`);
return;
}
value = parseBooleans(value);
const newValue = extensions[key](value, this);
this.editor.dispatch({
effects: compartments[key].reconfigure(newValue),
});
}
setLineWrappingEnabled(enabled) {
this.reconfigureExtension('isLineWrappingEnabled', enabled);
}
setLineNumbersDisplayed(enabled) {
this.reconfigureExtension('isLineNumbersDisplayed', enabled);
}
setTheme(theme) {
this.reconfigureExtension('theme', theme);
}
setAutocompletionEnabled(enabled) {
this.reconfigureExtension('isAutoCompletionEnabled', enabled);
}
updateSettings(settings) {
this.setFontSize(settings.fontSize);
this.setFontFamily(settings.fontFamily);
for (let key in extensions) {
this.reconfigureExtension(key, settings[key]);
}
}
changeSetting(key, value) {
if (extensions[key]) {
this.reconfigureExtension(key, value);
return;
} else if (key === 'fontFamily') {
this.setFontFamily(value);
} else if (key === 'fontSize') {
this.setFontSize(value);
}
}
setCode(code) {
const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
this.editor.dispatch({ changes });
}
}
function parseBooleans(value) {
return { true: true, false: false }[value] ?? value;
}
+37
View File
@@ -0,0 +1,37 @@
import { StateEffect, StateField } from '@codemirror/state';
import { Decoration, EditorView } from '@codemirror/view';
export const setFlash = StateEffect.define();
export const flashField = StateField.define({
create() {
return Decoration.none;
},
update(flash, tr) {
try {
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value && tr.newDoc.length > 0) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash = Decoration.set([]);
}
}
}
return flash;
} catch (err) {
console.warn('flash error', err);
return flash;
}
},
provide: (f) => EditorView.decorations.from(f),
});
export const flash = (view, ms = 200) => {
view.dispatch({ effects: setFlash.of(true) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, ms);
};
export const isFlashEnabled = (on) => (on ? flashField : []);
+135
View File
@@ -0,0 +1,135 @@
import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
import { Decoration, EditorView } from '@codemirror/view';
export const setMiniLocations = StateEffect.define();
export const showMiniLocations = StateEffect.define();
export const updateMiniLocations = (view, locations) => {
view.dispatch({ effects: setMiniLocations.of(locations) });
};
export const highlightMiniLocations = (view, atTime, haps) => {
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
};
const miniLocations = StateField.define({
create() {
return Decoration.none;
},
update(locations, tr) {
if (tr.docChanged) {
locations = locations.map(tr.changes);
}
for (let e of tr.effects) {
if (e.is(setMiniLocations)) {
// this is called on eval, with the mini locations obtained from the transpiler
// codemirror will automatically remap the marks when the document is edited
// create a mark for each mini location, adding the range to the spec to find it later
const marks = e.value
.filter(([from]) => from < tr.newDoc.length)
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
.map(
(range) =>
Decoration.mark({
id: range.join(':'),
// this green is only to verify that the decoration moves when the document is edited
// it will be removed later, so the mark is not visible by default
attributes: { style: `background-color: #00CA2880` },
}).range(...range), // -> Decoration
);
locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<Decoration>
}
}
return locations;
},
});
const visibleMiniLocations = StateField.define({
create() {
return { atTime: 0, haps: new Map() };
},
update(visible, tr) {
for (let e of tr.effects) {
if (e.is(showMiniLocations)) {
// this is called every frame to show the locations that are currently active
// we can NOT create new marks because the context.locations haven't changed since eval time
// this is why we need to find a way to update the existing decorations, showing the ones that have an active range
const haps = new Map();
for (let hap of e.value.haps) {
for (let { start, end } of hap.context.locations) {
let id = `${start}:${end}`;
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
haps.set(id, hap);
}
}
}
visible = { atTime: e.value.atTime, haps };
}
}
return visible;
},
});
// // Derive the set of decorations from the miniLocations and visibleLocations
const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
const iterator = state.field(miniLocations).iter();
const { haps } = state.field(visibleMiniLocations);
const builder = new RangeSetBuilder();
while (iterator.value) {
const {
from,
to,
value: {
spec: { id },
},
} = iterator;
if (haps.has(id)) {
const hap = haps.get(id);
const color = hap.context.color ?? 'var(--foreground)';
// Get explicit channels for color values
/*
const swatch = document.createElement('div');
swatch.style.color = color;
document.body.appendChild(swatch);
let channels = getComputedStyle(swatch)
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
.slice(1)
.map((c) => parseFloat(c || 1));
document.body.removeChild(swatch);
// Get percentage of event
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
channels[3] *= percent;
*/
builder.add(
from,
to,
Decoration.mark({
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
attributes: { style: `outline: solid 2px ${color}` },
}),
);
}
iterator.next();
}
return builder.finish();
});
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
export const isPatternHighlightingEnabled = (on, config) => {
on &&
config &&
setTimeout(() => {
updateMiniLocations(config.editor, config.miniLocations);
}, 100);
return on ? highlightExtension : [];
};
+4
View File
@@ -0,0 +1,4 @@
export * from './codemirror.mjs';
export * from './highlight.mjs';
export * from './flash.mjs';
export * from './slider.mjs';
+31
View File
@@ -0,0 +1,31 @@
import { Prec } from '@codemirror/state';
import { keymap, ViewPlugin } from '@codemirror/view';
// import { searchKeymap } from '@codemirror/search';
import { emacs } from '@replit/codemirror-emacs';
import { vim } from '@replit/codemirror-vim';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
const vscodePlugin = ViewPlugin.fromClass(
class {
constructor() {}
},
{
provide: () => {
return Prec.highest(keymap.of([...vscodeKeymap]));
},
},
);
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
const keymaps = {
vim,
emacs,
vscode: vscodeExtension,
};
export function keybindings(name) {
const active = keymaps[name];
return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []];
// keymap.of(searchKeymap),
}
+11 -3
View File
@@ -1,8 +1,8 @@
{
"name": "@strudel/codemirror",
"version": "0.8.4",
"version": "0.9.0",
"description": "Codemirror Extensions for Strudel",
"main": "codemirror.mjs",
"main": "index.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
@@ -33,13 +33,21 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.6.0",
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/language": "^6.6.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@strudel.cycles/core": "workspace:*"
"@replit/codemirror-emacs": "^6.0.1",
"@replit/codemirror-vim": "^6.0.14",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel.cycles/core": "workspace:*",
"@uiw/codemirror-themes": "^4.19.16",
"@uiw/codemirror-themes-all": "^4.19.16",
"react-dom": "^18.2.0"
},
"devDependencies": {
"vite": "^4.3.3"
+135
View File
@@ -0,0 +1,135 @@
import { ref, pure } from '@strudel.cycles/core';
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
export let sliderValues = {};
const getSliderID = (from) => `slider_${from}`;
export class SliderWidget extends WidgetType {
constructor(value, min, max, from, to, step, view) {
super();
this.value = value;
this.min = min;
this.max = max;
this.from = from;
this.originalFrom = from;
this.to = to;
this.step = step;
this.view = view;
}
eq() {
return false;
}
toDOM() {
let wrap = document.createElement('span');
wrap.setAttribute('aria-hidden', 'true');
wrap.className = 'cm-slider'; // inline-flex items-center
let slider = wrap.appendChild(document.createElement('input'));
slider.type = 'range';
slider.min = this.min;
slider.max = this.max;
slider.step = this.step ?? (this.max - this.min) / 1000;
slider.originalValue = this.value;
// to make sure the code stays in sync, let's save the original value
// becuase .value automatically clamps values so it'll desync with the code
slider.value = slider.originalValue;
slider.from = this.from;
slider.originalFrom = this.originalFrom;
slider.to = this.to;
slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
this.slider = slider;
slider.addEventListener('input', (e) => {
const next = e.target.value;
let insert = next;
//let insert = next.toFixed(2);
const to = slider.from + slider.originalValue.length;
let change = { from: slider.from, to, insert };
slider.originalValue = insert;
slider.value = insert;
this.view.dispatch({ changes: change });
const id = getSliderID(slider.originalFrom); // matches id generated in transpiler
window.postMessage({ type: 'cm-slider', value: Number(next), id });
});
return wrap;
}
ignoreEvent(e) {
return true;
}
}
export const setWidgets = StateEffect.define();
export const updateWidgets = (view, widgets) => {
view.dispatch({ effects: setWidgets.of(widgets) });
};
function getWidgets(widgetConfigs, view) {
return widgetConfigs.map(({ from, to, value, min, max, step }) => {
return Decoration.widget({
widget: new SliderWidget(value, min, max, from, to, step, view),
side: 0,
}).range(from /* , to */);
});
}
export const sliderPlugin = ViewPlugin.fromClass(
class {
decorations; //: DecorationSet
constructor(view /* : EditorView */) {
this.decorations = Decoration.set([]);
}
update(update /* : ViewUpdate */) {
update.transactions.forEach((tr) => {
if (tr.docChanged) {
this.decorations = this.decorations.map(tr.changes);
const iterator = this.decorations.iter();
while (iterator.value) {
// when the widgets are moved, we need to tell the dom node the current position
// this is important because the updateSliderValue function has to work with the dom node
if (iterator.value?.widget?.slider) {
iterator.value.widget.slider.from = iterator.from;
iterator.value.widget.slider.to = iterator.to;
}
iterator.next();
}
}
for (let e of tr.effects) {
if (e.is(setWidgets)) {
this.decorations = Decoration.set(getWidgets(e.value, update.view));
}
}
});
}
},
{
decorations: (v) => v.decorations,
},
);
export let slider = (value) => {
console.warn('slider will only work when the transpiler is used... passing value as is');
return pure(value);
};
// function transpiled from slider = (value, min, max)
export let sliderWithID = (id, value, min, max) => {
sliderValues[id] = value; // sync state at eval time (code -> state)
return ref(() => sliderValues[id]); // use state at query time
};
// update state when sliders are moved
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (e.data.type === 'cm-slider') {
if (sliderValues[e.data.id] !== undefined) {
// update state when slider is moved
sliderValues[e.data.id] = e.data.value;
} else {
console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
}
}
});
}
+484
View File
@@ -0,0 +1,484 @@
import {
abcdef,
androidstudio,
atomone,
aura,
bespin,
darcula,
dracula,
duotoneDark,
eclipse,
githubDark,
gruvboxDark,
materialDark,
nord,
okaidia,
solarizedDark,
sublime,
tokyoNight,
tokyoNightStorm,
vscodeDark,
xcodeDark,
bbedit,
duotoneLight,
githubLight,
gruvboxLight,
materialLight,
noctisLilac,
solarizedLight,
tokyoNightDay,
xcodeLight,
} from '@uiw/codemirror-themes-all';
import strudelTheme from './themes/strudel-theme';
import bluescreen, { settings as bluescreenSettings } from './themes/bluescreen';
import blackscreen, { settings as blackscreenSettings } from './themes/blackscreen';
import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen';
import teletext, { settings as teletextSettings } from './themes/teletext';
import algoboy, { settings as algoboySettings } from './themes/algoboy';
import terminal, { settings as terminalSettings } from './themes/terminal';
export const themes = {
strudelTheme,
bluescreen,
blackscreen,
whitescreen,
teletext,
algoboy,
terminal,
abcdef,
androidstudio,
atomone,
aura,
bespin,
darcula,
dracula,
duotoneDark,
eclipse,
githubDark,
gruvboxDark,
materialDark,
nord,
okaidia,
solarizedDark,
sublime,
tokyoNight,
tokyoNightStorm,
vscodeDark,
xcodeDark,
bbedit,
duotoneLight,
githubLight,
gruvboxLight,
materialLight,
noctisLilac,
solarizedLight,
tokyoNightDay,
xcodeLight,
};
// lineBackground is background with 50% opacity, to make sure the selection below is visible
export const settings = {
strudelTheme: {
background: '#222',
lineBackground: '#22222299',
foreground: '#fff',
// foreground: '#75baff',
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
},
bluescreen: bluescreenSettings,
blackscreen: blackscreenSettings,
whitescreen: whitescreenSettings,
teletext: teletextSettings,
algoboy: algoboySettings,
terminal: terminalSettings,
abcdef: {
background: '#0f0f0f',
lineBackground: '#0f0f0f99',
foreground: '#defdef',
caret: '#00FF00',
selection: '#515151',
selectionMatch: '#515151',
gutterBackground: '#555',
gutterForeground: '#FFFFFF',
lineHighlight: '#314151',
},
androidstudio: {
background: '#282b2e',
lineBackground: '#282b2e99',
foreground: '#a9b7c6',
caret: '#00FF00',
selection: '#343739',
selectionMatch: '#343739',
lineHighlight: '#343739',
},
atomone: {
background: '#272C35',
lineBackground: '#272C3599',
foreground: '#9d9b97',
caret: '#797977',
selection: '#ffffff30',
selectionMatch: '#2B323D',
gutterBackground: '#272C35',
gutterForeground: '#465063',
gutterBorder: 'transparent',
lineHighlight: '#2B323D',
},
aura: {
background: '#21202e',
lineBackground: '#21202e99',
foreground: '#edecee',
caret: '#a277ff',
selection: '#3d375e7f',
selectionMatch: '#3d375e7f',
gutterBackground: '#21202e',
gutterForeground: '#edecee',
gutterBorder: 'transparent',
lineHighlight: '#a394f033',
},
bbedit: {
light: true,
background: '#FFFFFF',
lineBackground: '#FFFFFF99',
foreground: '#000000',
caret: '#FBAC52',
selection: '#FFD420',
selectionMatch: '#FFD420',
gutterBackground: '#f5f5f5',
gutterForeground: '#4D4D4C',
gutterBorder: 'transparent',
lineHighlight: '#00000012',
},
bespin: {
background: '#28211c',
lineBackground: '#28211c99',
foreground: '#9d9b97',
caret: '#797977',
selection: '#36312e',
selectionMatch: '#4f382b',
gutterBackground: '#28211c',
gutterForeground: '#666666',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
},
darcula: {
background: '#2B2B2B',
lineBackground: '#2B2B2B99',
foreground: '#f8f8f2',
caret: '#FFFFFF',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
gutterBackground: 'rgba(255, 255, 255, 0.1)',
gutterForeground: '#999',
gutterBorder: 'transparent',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
},
dracula: {
background: '#282a36',
lineBackground: '#282a3699',
foreground: '#f8f8f2',
caret: '#f8f8f0',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
gutterBackground: '#282a36',
gutterForeground: '#6D8A88',
gutterBorder: 'transparent',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
},
duotoneLight: {
light: true,
background: '#faf8f5',
lineBackground: '#faf8f599',
foreground: '#b29762',
caret: '#93abdc',
selection: '#e3dcce',
selectionMatch: '#e3dcce',
gutterBackground: '#faf8f5',
gutterForeground: '#cdc4b1',
gutterBorder: 'transparent',
lineHighlight: '#EFEFEF',
},
duotoneDark: {
background: '#2a2734',
lineBackground: '#2a273499',
foreground: '#6c6783',
caret: '#ffad5c',
selection: 'rgba(255, 255, 255, 0.1)',
gutterBackground: '#2a2734',
gutterForeground: '#545167',
lineHighlight: '#36334280',
},
eclipse: {
light: true,
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#000',
caret: '#FFFFFF',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
gutterBackground: '#f7f7f7',
gutterForeground: '#999',
lineHighlight: '#e8f2ff',
gutterBorder: 'transparent',
},
githubLight: {
light: true,
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#24292e',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
gutterForeground: '#6e7781',
},
githubDark: {
background: '#0d1117',
lineBackground: '#0d111799',
foreground: '#c9d1d9',
caret: '#c9d1d9',
selection: '#003d73',
selectionMatch: '#003d73',
lineHighlight: '#36334280',
},
gruvboxDark: {
background: '#282828',
lineBackground: '#28282899',
foreground: '#ebdbb2',
caret: '#ebdbb2',
selection: '#bdae93',
selectionMatch: '#bdae93',
lineHighlight: '#3c3836',
gutterBackground: '#282828',
gutterForeground: '#7c6f64',
},
gruvboxLight: {
light: true,
background: '#fbf1c7',
lineBackground: '#fbf1c799',
foreground: '#3c3836',
caret: '#af3a03',
selection: '#ebdbb2',
selectionMatch: '#bdae93',
lineHighlight: '#ebdbb2',
gutterBackground: '#ebdbb2',
gutterForeground: '#665c54',
gutterBorder: 'transparent',
},
materialDark: {
background: '#2e3235',
lineBackground: '#2e323599',
foreground: '#bdbdbd',
caret: '#a0a4ae',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
gutterBackground: '#2e3235',
gutterForeground: '#999',
gutterActiveForeground: '#4f5b66',
lineHighlight: '#545b61',
},
materialLight: {
light: true,
background: '#FAFAFA',
lineBackground: '#FAFAFA99',
foreground: '#90A4AE',
caret: '#272727',
selection: '#80CBC440',
selectionMatch: '#FAFAFA',
gutterBackground: '#FAFAFA',
gutterForeground: '#90A4AE',
gutterBorder: 'transparent',
lineHighlight: '#CCD7DA50',
},
noctisLilac: {
light: true,
background: '#f2f1f8',
lineBackground: '#f2f1f899',
foreground: '#0c006b',
caret: '#5c49e9',
selection: '#d5d1f2',
selectionMatch: '#d5d1f2',
gutterBackground: '#f2f1f8',
gutterForeground: '#0c006b70',
lineHighlight: '#e1def3',
},
nord: {
background: '#2e3440',
lineBackground: '#2e344099',
foreground: '#FFFFFF',
caret: '#FFFFFF',
selection: '#3b4252',
selectionMatch: '#e5e9f0',
gutterBackground: '#2e3440',
gutterForeground: '#4c566a',
gutterActiveForeground: '#d8dee9',
lineHighlight: '#4c566a',
},
okaidia: {
background: '#272822',
lineBackground: '#27282299',
foreground: '#FFFFFF',
caret: '#FFFFFF',
selection: '#49483E',
selectionMatch: '#49483E',
gutterBackground: '#272822',
gutterForeground: '#FFFFFF70',
lineHighlight: '#00000059',
},
solarizedLight: {
light: true,
background: '#fdf6e3',
lineBackground: '#fdf6e399',
foreground: '#657b83',
caret: '#586e75',
selection: '#dfd9c8',
selectionMatch: '#dfd9c8',
gutterBackground: '#00000010',
gutterForeground: '#657b83',
lineHighlight: '#dfd9c8',
},
solarizedDark: {
background: '#002b36',
lineBackground: '#002b3699',
foreground: '#93a1a1',
caret: '#839496',
selection: '#173541',
selectionMatch: '#aafe661a',
gutterBackground: '#00252f',
gutterForeground: '#839496',
lineHighlight: '#173541',
},
sublime: {
background: '#303841',
lineBackground: '#30384199',
foreground: '#FFFFFF',
caret: '#FBAC52',
selection: '#4C5964',
selectionMatch: '#3A546E',
gutterBackground: '#303841',
gutterForeground: '#FFFFFF70',
lineHighlight: '#00000059',
},
tokyoNightDay: {
light: true,
background: '#e1e2e7',
lineBackground: '#e1e2e799',
foreground: '#3760bf',
caret: '#3760bf',
selection: '#99a7df',
selectionMatch: '#99a7df',
gutterBackground: '#e1e2e7',
gutterForeground: '#3760bf',
gutterBorder: 'transparent',
lineHighlight: '#5f5faf11',
},
tokyoNightStorm: {
background: '#24283b',
lineBackground: '#24283b99',
foreground: '#7982a9',
caret: '#c0caf5',
selection: '#6f7bb630',
selectionMatch: '#1f2335',
gutterBackground: '#24283b',
gutterForeground: '#7982a9',
gutterBorder: 'transparent',
lineHighlight: '#292e42',
},
tokyoNight: {
background: '#1a1b26',
lineBackground: '#1a1b2699',
foreground: '#787c99',
caret: '#c0caf5',
selection: '#515c7e40',
selectionMatch: '#16161e',
gutterBackground: '#1a1b26',
gutterForeground: '#787c99',
gutterBorder: 'transparent',
lineHighlight: '#1e202e',
},
vscodeDark: {
background: '#1e1e1e',
lineBackground: '#1e1e1e99',
foreground: '#9cdcfe',
caret: '#c6c6c6',
selection: '#6199ff2f',
selectionMatch: '#72a1ff59',
lineHighlight: '#ffffff0f',
gutterBackground: '#1e1e1e',
gutterForeground: '#838383',
gutterActiveForeground: '#fff',
},
xcodeLight: {
light: true,
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#3D3D3D',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
gutterForeground: '#AFAFAF',
lineHighlight: '#EDF4FF',
},
xcodeDark: {
background: '#292A30',
lineBackground: '#292A3099',
foreground: '#CECFD0',
caret: '#fff',
selection: '#727377',
selectionMatch: '#727377',
lineHighlight: '#2F3239',
},
};
function getColors(str) {
const colorRegex = /#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})/g;
const colors = [];
let match;
while ((match = colorRegex.exec(str)) !== null) {
const color = match[0];
if (!colors.includes(color)) {
colors.push(color);
}
}
return colors;
}
// TODO: remove
export function themeColors(theme) {
return getColors(stringifySafe(theme));
}
function getCircularReplacer() {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
}
function stringifySafe(json) {
return JSON.stringify(json, getCircularReplacer());
}
export function injectStyle(rule) {
const newStyle = document.createElement('style');
document.head.appendChild(newStyle);
const styleSheet = newStyle.sheet;
const ruleIndex = styleSheet.insertRule(rule, 0);
return () => styleSheet.deleteRule(ruleIndex);
}
export const theme = (theme) => themes[theme] || themes.strudelTheme;
+41
View File
@@ -0,0 +1,41 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: '#9bbc0f',
foreground: '#0f380f', // whats that?
caret: '#0f380f',
selection: '#306230',
selectionMatch: '#ffffff26',
lineHighlight: '#8bac0f',
lineBackground: '#9bbc0f50',
//lineBackground: 'transparent',
gutterBackground: 'transparent',
gutterForeground: '#0f380f',
light: true,
customStyle: '.cm-line { line-height: 1 }',
};
export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.keyword, color: '#0f380f' },
{ tag: t.operator, color: '#0f380f' },
{ tag: t.special(t.variableName), color: '#0f380f' },
{ tag: t.typeName, color: '#0f380f' },
{ tag: t.atom, color: '#0f380f' },
{ tag: t.number, color: '#0f380f' },
{ tag: t.definition(t.variableName), color: '#0f380f' },
{ tag: t.string, color: '#0f380f' },
{ tag: t.special(t.string), color: '#0f380f' },
{ tag: t.comment, color: '#0f380f' },
{ tag: t.variableName, color: '#0f380f' },
{ tag: t.tagName, color: '#0f380f' },
{ tag: t.bracket, color: '#0f380f' },
{ tag: t.meta, color: '#0f380f' },
{ tag: t.attributeName, color: '#0f380f' },
{ tag: t.propertyName, color: '#0f380f' },
{ tag: t.className, color: '#0f380f' },
{ tag: t.invalid, color: '#0f380f' },
{ tag: [t.unit, t.punctuation], color: '#0f380f' },
],
});
+38
View File
@@ -0,0 +1,38 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'black',
foreground: 'white', // whats that?
caret: 'white',
selection: '#ffffff20',
selectionMatch: '#036dd626',
lineHighlight: '#ffffff10',
lineBackground: '#00000050',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
{ tag: t.typeName, color: 'white' },
{ tag: t.atom, color: 'white' },
{ tag: t.number, color: 'white' },
{ tag: t.definition(t.variableName), color: 'white' },
{ tag: t.string, color: 'white' },
{ tag: t.special(t.string), color: 'white' },
{ tag: t.comment, color: 'white' },
{ tag: t.variableName, color: 'white' },
{ tag: t.tagName, color: 'white' },
{ tag: t.bracket, color: 'white' },
{ tag: t.meta, color: 'white' },
{ tag: t.attributeName, color: 'white' },
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});
+41
View File
@@ -0,0 +1,41 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: '#051DB5',
lineBackground: '#051DB550',
foreground: 'white', // whats that?
caret: 'white',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
{ tag: t.typeName, color: 'white' },
{ tag: t.atom, color: 'white' },
{ tag: t.number, color: 'white' },
{ tag: t.definition(t.variableName), color: 'white' },
{ tag: t.string, color: 'white' },
{ tag: t.special(t.string), color: 'white' },
{ tag: t.comment, color: 'white' },
{ tag: t.variableName, color: 'white' },
{ tag: t.tagName, color: 'white' },
{ tag: t.bracket, color: 'white' },
{ tag: t.meta, color: 'white' },
{ tag: t.attributeName, color: 'white' },
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});
-139
View File
@@ -1,139 +0,0 @@
import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags as t } from '@lezer/highlight';
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
const chalky = '#e5c07b',
coral = '#e06c75',
cyan = '#56b6c2',
invalid = '#ffffff',
ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
malibu = '#61afef',
sage = '#98c379',
whiskey = '#d19a66',
violet = '#c678dd',
darkBackground = '#21252b',
highlightBackground = '#2c313a',
background = '#282c34',
tooltipBackground = '#353a42',
selection = '#3E4451',
cursor = '#528bff';
/// The colors used in the theme, as CSS color strings.
export const color = {
chalky,
coral,
cyan,
invalid,
ivory,
stone,
malibu,
sage,
whiskey,
violet,
darkBackground,
highlightBackground,
background,
tooltipBackground,
selection,
cursor,
};
/// The editor theme styles for One Dark.
export const oneDarkTheme = EditorView.theme(
{
'&': {
color: ivory,
backgroundColor: background,
},
'.cm-content': {
caretColor: cursor,
},
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
{ backgroundColor: selection },
'.cm-panels': { backgroundColor: darkBackground, color: ivory },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
},
'.cm-gutters': {
backgroundColor: background,
color: stone,
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: true },
);
/// The highlighting style for code in the One Dark theme.
export const oneDarkHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: violet },
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
{ tag: [t.definition(t.name), t.separator], color: ivory },
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
{ tag: [t.meta, t.comment], color: stone },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.link, color: stone, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: coral },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
{ tag: t.invalid, color: invalid },
]);
/// Extension to enable the One Dark theme (both the editor theme and
/// the highlight style).
export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
+45
View File
@@ -0,0 +1,45 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export default createTheme({
theme: 'dark',
settings: {
background: '#222',
foreground: '#75baff', // whats that?
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
},
styles: [
{ tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' },
{ tag: t.special(t.variableName), color: '#eeffff' },
// { tag: t.typeName, color: '#f07178' }, // original
{ tag: t.typeName, color: '#c3e88d' },
{ tag: t.atom, color: '#f78c6c' },
// { tag: t.number, color: '#ff5370' }, // original
{ tag: t.number, color: '#c3e88d' },
{ tag: t.definition(t.variableName), color: '#82aaff' },
{ tag: t.string, color: '#c3e88d' },
// { tag: t.special(t.string), color: '#f07178' }, // original
{ tag: t.special(t.string), color: '#c3e88d' },
{ tag: t.comment, color: '#7d8799' },
// { tag: t.variableName, color: '#f07178' }, // original
{ tag: t.variableName, color: '#c792ea' },
// { tag: t.tagName, color: '#ff5370' }, // original
{ tag: t.tagName, color: '#c3e88d' },
{ tag: t.bracket, color: '#525154' },
// { tag: t.bracket, color: '#a2a1a4' }, // original
{ tag: t.meta, color: '#ffcb6b' },
{ tag: t.attributeName, color: '#c792ea' },
{ tag: t.propertyName, color: '#c792ea' },
{ tag: t.className, color: '#decb6b' },
{ tag: t.invalid, color: '#ffffff' },
{ tag: [t.unit, t.punctuation], color: '#82aaff' },
],
});
+50
View File
@@ -0,0 +1,50 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
let colorA = '#6edee4';
//let colorB = 'magenta';
let colorB = 'white';
let colorC = 'red';
let colorD = '#f8fc55';
export const settings = {
background: '#000000',
foreground: colorA, // whats that?
caret: colorC,
selection: colorD,
selectionMatch: colorA,
lineHighlight: '#6edee440', // panel bg
lineBackground: '#00000040',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
customStyle: '.cm-line { line-height: 1 }',
};
let punctuation = colorD;
let mini = colorB;
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: colorA },
{ tag: t.operator, color: mini },
{ tag: t.special(t.variableName), color: colorA },
{ tag: t.typeName, color: colorA },
{ tag: t.atom, color: colorA },
{ tag: t.number, color: mini },
{ tag: t.definition(t.variableName), color: colorA },
{ tag: t.string, color: mini },
{ tag: t.special(t.string), color: mini },
{ tag: t.comment, color: punctuation },
{ tag: t.variableName, color: colorA },
{ tag: t.tagName, color: colorA },
{ tag: t.bracket, color: punctuation },
{ tag: t.meta, color: colorA },
{ tag: t.attributeName, color: colorA },
{ tag: t.propertyName, color: colorA }, // methods
{ tag: t.className, color: colorA },
{ tag: t.invalid, color: colorC },
{ tag: [t.unit, t.punctuation], color: punctuation },
],
});
+36
View File
@@ -0,0 +1,36 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'black',
foreground: '#41FF00', // whats that?
caret: '#41FF00',
selection: '#ffffff20',
selectionMatch: '#036dd626',
lineHighlight: '#ffffff10',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: '#41FF00' },
{ tag: t.operator, color: '#41FF00' },
{ tag: t.special(t.variableName), color: '#41FF00' },
{ tag: t.typeName, color: '#41FF00' },
{ tag: t.atom, color: '#41FF00' },
{ tag: t.number, color: '#41FF00' },
{ tag: t.definition(t.variableName), color: '#41FF00' },
{ tag: t.string, color: '#41FF00' },
{ tag: t.special(t.string), color: '#41FF00' },
{ tag: t.comment, color: '#41FF00' },
{ tag: t.variableName, color: '#41FF00' },
{ tag: t.tagName, color: '#41FF00' },
{ tag: t.bracket, color: '#41FF00' },
{ tag: t.meta, color: '#41FF00' },
{ tag: t.attributeName, color: '#41FF00' },
{ tag: t.propertyName, color: '#41FF00' },
{ tag: t.className, color: '#41FF00' },
{ tag: t.invalid, color: '#41FF00' },
],
});
+38
View File
@@ -0,0 +1,38 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'white',
foreground: 'black', // whats that?
caret: 'black',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#ffffff26',
lineHighlight: '#cccccc50',
lineBackground: '#ffffff50',
gutterBackground: 'transparent',
gutterForeground: 'black',
light: true,
};
export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.keyword, color: 'black' },
{ tag: t.operator, color: 'black' },
{ tag: t.special(t.variableName), color: 'black' },
{ tag: t.typeName, color: 'black' },
{ tag: t.atom, color: 'black' },
{ tag: t.number, color: 'black' },
{ tag: t.definition(t.variableName), color: 'black' },
{ tag: t.string, color: 'black' },
{ tag: t.special(t.string), color: 'black' },
{ tag: t.comment, color: 'black' },
{ tag: t.variableName, color: 'black' },
{ tag: t.tagName, color: 'black' },
{ tag: t.bracket, color: 'black' },
{ tag: t.meta, color: 'black' },
{ tag: t.attributeName, color: 'black' },
{ tag: t.propertyName, color: 'black' },
{ tag: t.className, color: 'black' },
{ tag: t.invalid, color: 'black' },
],
});
+1 -1
View File
@@ -7,7 +7,7 @@ export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'codemirror.mjs'),
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
+574 -20
View File
@@ -86,6 +86,16 @@ const generic_params = [
*
*/
['gain'],
/**
* Gain applied after all effects have been processed.
*
* @name postgain
* @example
* s("bd sd,hh*4")
* .compressor("-20:20:10:.002:.02").postgain(1.5)
*
*/
['postgain'],
/**
* Like {@link gain}, but linear.
*
@@ -109,6 +119,97 @@ const generic_params = [
*/
['attack', 'att'],
/**
* Sets the Frequency Modulation Harmonicity Ratio.
* Controls the timbre of the sound.
* Whole numbers and simple ratios sound more natural,
* while decimal numbers and complex ratios sound metallic.
*
* @name fmh
* @param {number | Pattern} harmonicity
* @example
* note("c e g b")
* .fm(4)
* .fmh("<1 2 1.5 1.61>")
* .scope()
*
*/
[['fmh', 'fmi'], 'fmh'],
/**
* Sets the Frequency Modulation of the synth.
* Controls the modulation index, which defines the brightness of the sound.
*
* @name fm
* @param {number | Pattern} brightness modulation index
* @synonyms fmi
* @example
* note("c e g b")
* .fm("<0 1 2 8 32>")
* .scope()
*
*/
[['fmi', 'fmh'], 'fm'],
// fm envelope
/**
* Ramp type of fm envelope. Exp might be a bit broken..
*
* @name fmenv
* @param {number | Pattern} type lin | exp
* @example
* note("c e g b")
* .fm(4)
* .fmdecay(.2)
* .fmsustain(0)
* .fmenv("<exp lin>")
* .scope()
*
*/
['fmenv'],
/**
* Attack time for the FM envelope: time it takes to reach maximum modulation
*
* @name fmattack
* @param {number | Pattern} time attack time
* @example
* note("c e g b")
* .fm(4)
* .fmattack("<0 .05 .1 .2>")
* .scope()
*
*/
['fmattack'],
/**
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
*
* @name fmdecay
* @param {number | Pattern} time decay time
* @example
* note("c e g b")
* .fm(4)
* .fmdecay("<.01 .05 .1 .2>")
* .fmsustain(.4)
* .scope()
*
*/
['fmdecay'],
/**
* Sustain level for the FM envelope: how much modulation is applied after the decay phase
*
* @name fmsustain
* @param {number | Pattern} level sustain level
* @example
* note("c e g b")
* .fm(4)
* .fmdecay(.1)
* .fmsustain("<1 .75 .5 0>")
* .scope()
*
*/
['fmsustain'],
// these are not really useful... skipping for now
['fmrelease'],
['fmvelocity'],
/**
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
*
@@ -120,6 +221,9 @@ const generic_params = [
*/
['bank'],
['analyze'], // analyser node send amount 0 - 1 (used by scope)
['fft'], // fftSize of analyser
/**
* Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level.
* Note that the decay is only audible if the sustain value is lower than 1.
@@ -205,16 +309,52 @@ const generic_params = [
*/
['end'],
/**
* Loops the sample (from `begin` to `end`) the specified number of times.
* Loops the sample.
* Note that the tempo of the loop is not synced with the cycle tempo.
* To change the loop region, use loopBegin / loopEnd.
*
* @name loop
* @param {number | Pattern} times How often the sample is looped
* @param {number | Pattern} on If 1, the sample is looped
* @example
* s("bd").loop("<1 2 3 4>").osc()
* s("casio").loop(1)
*
*/
['loop'],
/**
* Begin to loop at a specific point in the sample (inbetween `begin` and `end`).
* Note that the loop point must be inbetween `begin` and `end`, and before `loopEnd`!
* Note: Samples starting with wt_ will automatically loop! (wt = wavetable)
*
* @name loopBegin
* @param {number | Pattern} time between 0 and 1, where 1 is the length of the sample
* @synonyms loopb
* @example
* s("space").loop(1)
* .loopBegin("<0 .125 .25>").scope()
*/
['loopBegin', 'loopb'],
/**
*
* End the looping section at a specific point in the sample (inbetween `begin` and `end`).
* Note that the loop point must be inbetween `begin` and `end`, and after `loopBegin`!
*
* @name loopEnd
* @param {number | Pattern} time between 0 and 1, where 1 is the length of the sample
* @synonyms loope
* @example
* s("space").loop(1)
* .loopEnd("<1 .75 .5 .25>").scope()
*/
['loopEnd', 'loope'],
/**
* bit crusher effect.
*
* @name crush
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
* @example
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
*
*/
// TODO: currently duplicated with "native" legato
// TODO: superdirt legato will do more: https://youtu.be/dQPmE1WaD1k?t=419
/**
@@ -229,15 +369,6 @@ const generic_params = [
*/
// ['legato'],
// ['clhatdecay'],
/**
* bit crusher effect.
*
* @name crush
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
* @example
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
*
*/
['crush'],
/**
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
@@ -250,6 +381,61 @@ const generic_params = [
*/
['coarse'],
['phaserrate', 'phasr'], // superdirt only
/**
* Phaser audio effect that approximates popular guitar pedals.
*
* @name phaser
* @synonyms ph
* @param {number | Pattern} speed speed of modulation
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
* .phaser("<1 2 4 8>")
*
*/
[['phaser', 'phaserdepth', 'phasercenter', 'phasersweep'], 'ph'],
/**
* The frequency sweep range of the lfo for the phaser effect. Defaults to 2000
*
* @name phasersweep
* @synonyms phs
* @param {number | Pattern} phasersweep most useful values are between 0 and 4000
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
* .phaser(2).phasersweep("<800 2000 4000>")
*
*/
['phasersweep', 'phs'],
/**
* The center frequency of the phaser in HZ. Defaults to 1000
*
* @name phasercenter
* @synonyms phc
* @param {number | Pattern} centerfrequency in HZ
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
* .phaser(2).phasercenter("<800 2000 4000>")
*
*/
['phasercenter', 'phc'],
/**
* The amount the signal is affected by the phaser effect. Defaults to 0.75
*
* @name phaserdepth
* @synonyms phd
* @param {number | Pattern} depth number between 0 and 1
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
* .phaser(2).phaserdepth("<0 .5 .75 1>")
*
*/
['phaserdepth', 'phd', 'phasdp'], // also a superdirt control
/**
* choose the channel the pattern is sent to in superdirt
*
@@ -283,6 +469,227 @@ const generic_params = [
*
*/
[['cutoff', 'resonance'], 'ctf', 'lpf', 'lp'],
/**
* Sets the lowpass filter envelope modulation depth.
* @name lpenv
* @param {number | Pattern} modulation depth of the lowpass filter envelope between 0 and _n_
* @synonyms lpe
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpa(.5)
* .lpenv("<4 2 1 0 -1 -2 -4>/4")
*/
['lpenv', 'lpe'],
/**
* Sets the highpass filter envelope modulation depth.
* @name hpenv
* @param {number | Pattern} modulation depth of the highpass filter envelope between 0 and _n_
* @synonyms hpe
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpa(.5)
* .hpenv("<4 2 1 0 -1 -2 -4>/4")
*/
['hpenv', 'hpe'],
/**
* Sets the bandpass filter envelope modulation depth.
* @name bpenv
* @param {number | Pattern} modulation depth of the bandpass filter envelope between 0 and _n_
* @synonyms bpe
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpa(.5)
* .bpenv("<4 2 1 0 -1 -2 -4>/4")
*/
['bpenv', 'bpe'],
/**
* Sets the attack duration for the lowpass filter envelope.
* @name lpattack
* @param {number | Pattern} attack time of the filter envelope
* @synonyms lpa
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpa("<.5 .25 .1 .01>/4")
* .lpenv(4)
*/
['lpattack', 'lpa'],
/**
* Sets the attack duration for the highpass filter envelope.
* @name hpattack
* @param {number | Pattern} attack time of the highpass filter envelope
* @synonyms hpa
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpa("<.5 .25 .1 .01>/4")
* .hpenv(4)
*/
['hpattack', 'hpa'],
/**
* Sets the attack duration for the bandpass filter envelope.
* @name bpattack
* @param {number | Pattern} attack time of the bandpass filter envelope
* @synonyms bpa
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpa("<.5 .25 .1 .01>/4")
* .bpenv(4)
*/
['bpattack', 'bpa'],
/**
* Sets the decay duration for the lowpass filter envelope.
* @name lpdecay
* @param {number | Pattern} decay time of the filter envelope
* @synonyms lpd
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpd("<.5 .25 .1 0>/4")
* .lps(0.2)
* .lpenv(4)
*/
['lpdecay', 'lpd'],
/**
* Sets the decay duration for the highpass filter envelope.
* @name hpdecay
* @param {number | Pattern} decay time of the highpass filter envelope
* @synonyms hpd
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpd("<.5 .25 .1 0>/4")
* .hps(0.2)
* .hpenv(4)
*/
['hpdecay', 'hpd'],
/**
* Sets the decay duration for the bandpass filter envelope.
* @name bpdecay
* @param {number | Pattern} decay time of the bandpass filter envelope
* @synonyms bpd
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpd("<.5 .25 .1 0>/4")
* .bps(0.2)
* .bpenv(4)
*/
['bpdecay', 'bpd'],
/**
* Sets the sustain amplitude for the lowpass filter envelope.
* @name lpsustain
* @param {number | Pattern} sustain amplitude of the lowpass filter envelope
* @synonyms lps
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpd(.5)
* .lps("<0 .25 .5 1>/4")
* .lpenv(4)
*/
['lpsustain', 'lps'],
/**
* Sets the sustain amplitude for the highpass filter envelope.
* @name hpsustain
* @param {number | Pattern} sustain amplitude of the highpass filter envelope
* @synonyms hps
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpd(.5)
* .hps("<0 .25 .5 1>/4")
* .hpenv(4)
*/
['hpsustain', 'hps'],
/**
* Sets the sustain amplitude for the bandpass filter envelope.
* @name bpsustain
* @param {number | Pattern} sustain amplitude of the bandpass filter envelope
* @synonyms bps
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpd(.5)
* .bps("<0 .25 .5 1>/4")
* .bpenv(4)
*/
['bpsustain', 'bps'],
/**
* Sets the release time for the lowpass filter envelope.
* @name lprelease
* @param {number | Pattern} release time of the filter envelope
* @synonyms lpr
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .clip(.5)
* .lpf(500)
* .lpenv(4)
* .lpr("<.5 .25 .1 0>/4")
* .release(.5)
*/
['lprelease', 'lpr'],
/**
* Sets the release time for the highpass filter envelope.
* @name hprelease
* @param {number | Pattern} release time of the highpass filter envelope
* @synonyms hpr
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .clip(.5)
* .hpf(500)
* .hpenv(4)
* .hpr("<.5 .25 .1 0>/4")
* .release(.5)
*/
['hprelease', 'hpr'],
/**
* Sets the release time for the bandpass filter envelope.
* @name bprelease
* @param {number | Pattern} release time of the bandpass filter envelope
* @synonyms bpr
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .clip(.5)
* .bpf(500)
* .bpenv(4)
* .bpr("<.5 .25 .1 0>/4")
* .release(.5)
*/
['bprelease', 'bpr'],
/**
* Sets the filter type. The 24db filter is more aggressive. More types might be added in the future.
* @name ftype
* @param {number | Pattern} type 12db (default) or 24db
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .bpenv(4)
* .ftype("<12db 24db>")
*/
['ftype'],
['fanchor'],
/**
* Applies the cutoff frequency of the **h**igh-**p**ass **f**ilter.
*
@@ -299,6 +706,45 @@ const generic_params = [
*/
// currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496
// ['hpf'],
/**
* Applies a vibrato to the frequency of the oscillator.
*
* @name vib
* @synonyms vibrato, v
* @param {number | Pattern} frequency of the vibrato in hertz
* @example
* note("a")
* .vib("<.5 1 2 4 8 16>")
* @example
* // change the modulation depth with ":"
* note("a")
* .vib("<.5 1 2 4 8 16>:12")
*/
[['vib', 'vibmod'], 'vibrato', 'v'],
/**
* Adds pink noise to the mix
*
* @name noise
* @param {number | Pattern} wet wet amount
* @example
* sound("<white pink brown>/2")
*/
['noise'],
/**
* Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set
*
* @name vibmod
* @synonyms vmod
* @param {number | Pattern} depth of vibrato (in semitones)
* @example
* note("a").vib(4)
* .vibmod("<.25 .5 1 2 12>")
* @example
* // change the vibrato frequency with ":"
* note("a")
* .vibmod("<.25 .5 1 2 12>:8")
*/
[['vibmod', 'vib'], 'vmod'],
[['hcutoff', 'hresonance'], 'hpf', 'hp'],
/**
* Controls the **h**igh-**p**ass **q**-value.
@@ -477,6 +923,14 @@ const generic_params = [
*
*/
['lsize'],
/**
* Sets the displayed text for an event on the pianoroll
*
* @name label
* @param {string} label text to display
*/
['activeLabel'],
[['label', 'activeLabel']],
// ['lfo'],
// ['lfocutoffint'],
// ['lfodelay'],
@@ -509,7 +963,7 @@ const generic_params = [
* @superDirtOnly
*/
['octave'],
['offset'], // TODO: what is this? not found in tidal doc
// ['ophatdecay'],
// TODO: example
/**
@@ -573,6 +1027,15 @@ const generic_params = [
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
// ['velocity'],
['voice'], // TODO: synth param
// voicings // https://github.com/tidalcycles/strudel/issues/506
['chord'], // chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings
['dictionary', 'dict'], // which dictionary to use for the voicings
['anchor'], // the top note to align the voicing to, defaults to c5
['offset'], // how the voicing is offset from the anchored position
['octaves'], // how many octaves are voicing steps spread apart, defaults to 1
[['mode', 'anchor']], // below = anchor note will be removed from the voicing, useful for melody harmonization
/**
* Sets the level of reverb.
*
@@ -587,20 +1050,74 @@ const generic_params = [
*
*/
[['room', 'size']],
/**
* Reverb lowpass starting frequency (in hertz).
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomlp
* @synonyms rlp
* @param {number} frequency between 0 and 20000hz
* @example
* s("bd sd").room(0.5).rlp(10000)
* @example
* s("bd sd").room(0.5).rlp(5000)
*/
['roomlp', 'rlp'],
/**
* Reverb lowpass frequency at -60dB (in hertz).
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomdim
* @synonyms rdim
* @param {number} frequency between 0 and 20000hz
* @example
* s("bd sd").room(0.5).rlp(10000).rdim(8000)
* @example
* s("bd sd").room(0.5).rlp(5000).rdim(400)
*
*/
['roomdim', 'rdim'],
/**
* Reverb fade time (in seconds).
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomfade
* @synonyms rfade
* @param {number} seconds for the reverb to fade
* @example
* s("bd sd").room(0.5).rlp(10000).rfade(0.5)
* @example
* s("bd sd").room(0.5).rlp(5000).rfade(4)
*
*/
['roomfade', 'rfade'],
/**
* Sets the sample to use as an impulse response for the reverb.
* @name iresponse
* @param {string | Pattern} sample to use as an impulse response
* @synonyms ir
* @example
* s("bd sd").room(.8).ir("<shaker_large:0 shaker_large:2>")
*
*/
[['ir', 'i'], 'iresponse'],
/**
* Sets the room size of the reverb, see {@link room}.
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomsize
* @param {number | Pattern} size between 0 and 10
* @synonyms size, sz
* @synonyms rsize, sz, size
* @example
* s("bd sd").room(.8).roomsize("<0 1 2 4 8>")
* s("bd sd").room(.8).rsize(1)
* @example
* s("bd sd").room(.8).rsize(4)
*
*/
// TODO: find out why :
// s("bd sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc()
// .. does not work. Is it because room is only one effect?
['size', 'sz', 'roomsize'],
['roomsize', 'size', 'sz', 'rsize'],
// ['sagogo'],
// ['sclap'],
// ['sclaves'],
@@ -615,6 +1132,21 @@ const generic_params = [
*
*/
['shape'],
/**
* Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")`
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
*
* @name compressor
* @example
* s("bd sd,hh*4")
* .compressor("-20:20:10:.002:.02")
*
*/
[['compressor', 'compressorRatio', 'compressorKnee', 'compressorAttack', 'compressorRelease']],
['compressorKnee'],
['compressorRatio'],
['compressorAttack'],
['compressorRelease'],
/**
* Changes the speed of sample playback, i.e. a cheap way of changing pitch.
*
@@ -699,9 +1231,6 @@ const generic_params = [
*/
['tremolodepth', 'tremdp'],
['tremolorate', 'tremr'],
// TODO: doesn't seem to do anything
['phaserdepth', 'phasdp'],
['phaserrate', 'phasr'],
['fshift'],
['fshiftnote'],
@@ -761,8 +1290,22 @@ const generic_params = [
*
*/
['clip'],
];
// ZZFX
['zrand'],
['curve'],
['slide'], // superdirt duplicate
['deltaSlide'],
['pitchJump'],
['pitchJumpTime'],
['lfo', 'repeatTime'],
['znoise'], // noise on the frequency or as bubo calls it "frequency fog" :)
['zmod'],
['zcrush'], // like crush but scaled differently
['zdelay'],
['tremolo'],
['zzfx'],
];
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
controls.createParam = function (names) {
@@ -812,6 +1355,17 @@ generic_params.forEach(([names, ...aliases]) => {
controls.createParams = (...names) =>
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
/**
* ADSR envelope: Combination of Attack, Decay, Sustain, and Release.
*
* @name adsr
* @param {number | Pattern} time attack time in seconds
* @param {number | Pattern} time decay time in seconds
* @param {number | Pattern} gain sustain level (0 to 1)
* @param {number | Pattern} time release time in seconds
* @example
* note("<c3 bb2 f3 eb3>").sound("sawtooth").lpf(600).adsr(".1:.1:.5:.2")
*/
controls.adsr = register('adsr', (adsr, pat) => {
adsr = !Array.isArray(adsr) ? [adsr] : adsr;
const [attack, decay, sustain, release] = adsr;
+17 -5
View File
@@ -9,17 +9,29 @@ import { Pattern, getTime, State, TimeSpan } from './index.mjs';
export const getDrawContext = (id = 'test-canvas') => {
let canvas = document.querySelector('#' + id);
if (!canvas) {
const scale = 2; // 2 = crisp on retina screens
canvas = document.createElement('canvas');
canvas.id = id;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
document.body.prepend(canvas);
let timeout;
window.addEventListener('resize', () => {
timeout && clearTimeout(timeout);
timeout = setTimeout(() => {
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
}, 200);
});
}
return canvas.getContext('2d');
};
Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
if (typeof window === 'undefined') {
return this;
}
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
@@ -51,7 +63,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
export const cleanupDraw = (clearScreen = true) => {
const ctx = getDrawContext();
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
@@ -134,7 +146,7 @@ export class Drawer {
);
}
invalidate(scheduler = this.scheduler) {
if (!scheduler) {
if (!scheduler || !scheduler.pattern) {
return;
}
this.scheduler = scheduler;
+7 -3
View File
@@ -1,6 +1,6 @@
/*
evaluate.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/evaluate.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -37,8 +37,12 @@ function safeEval(str, options = {}) {
}
export const evaluate = async (code, transpiler) => {
let meta = {};
if (transpiler) {
code = transpiler(code); // transform syntactically correct js code to semantically usable code
// transform syntactically correct js code to semantically usable code
const transpiled = transpiler(code);
code = transpiled.output;
meta = transpiled;
}
// if no transpiler is given, we expect a single instruction (!wrapExpression)
const options = { wrapExpression: !!transpiler };
@@ -48,5 +52,5 @@ export const evaluate = async (code, transpiler) => {
const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
}
return { mode: 'javascript', pattern: evaluated };
return { mode: 'javascript', pattern: evaluated, meta };
};
@@ -3,6 +3,6 @@
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
```sh
npm i
npm run dev
pnpm i
pnpm dev
```
@@ -1,6 +1,6 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
// make it 22 edo
@@ -34,7 +34,7 @@ await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'githu
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', {
m11: ['2M 3m 4P 7m'],
@@ -70,7 +70,7 @@ export const funk42 = `// froos - how to funk in 42 lines of code
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5)
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.8.2",
"version": "0.9.0",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
@@ -29,13 +29,14 @@
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://strudel.tidalcycles.org",
"homepage": "https://strudel.cc",
"dependencies": {
"fraction.js": "^4.2.0"
"fraction.js": "^4.2.0",
"nanostores": "^0.8.1"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
"vite": "^4.3.3",
"vitest": "^0.28.0"
"vitest": "^0.33.0"
}
}
+125 -48
View File
@@ -472,15 +472,15 @@ export class Pattern {
/**
* Returns a new pattern with the given location information added to the
* context of every hap.
* @param {Number} start
* @param {Number} end
* @param {Number} start start offset
* @param {Number} end end offset
* @returns Pattern
* @noAutocomplete
*/
withLocation(start, end) {
withLoc(start, end) {
const location = {
start: { line: start[0], column: start[1], offset: start[2] },
end: { line: end[0], column: end[1], offset: end[2] },
start,
end,
};
return this.withContext((context) => {
const locations = (context.locations || []).concat([location]);
@@ -488,32 +488,6 @@ export class Pattern {
});
}
withMiniLocation(start, end) {
const offset = {
start: { line: start[0], column: start[1], offset: start[2] },
end: { line: end[0], column: end[1], offset: end[2] },
};
return this.withContext((context) => {
let locations = context.locations || [];
locations = locations.map(({ start, end }) => {
const colOffset = start.line === 1 ? offset.start.column : 0;
return {
start: {
...start,
line: start.line - 1 + (offset.start.line - 1) + 1,
column: start.column - 1 + colOffset,
},
end: {
...end,
line: end.line - 1 + (offset.start.line - 1) + 1,
column: end.column - 1 + colOffset,
},
};
});
return { ...context, locations };
});
}
/**
* Returns a new Pattern, which only returns haps that meet the given test.
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
@@ -809,13 +783,14 @@ export class Pattern {
hap.setContext({
...hap.context,
onTrigger: (...args) => {
if (!dominant && hap.context.onTrigger) {
hap.context.onTrigger(...args);
}
// run previously set trigger, if it exists
hap.context.onTrigger?.(...args);
onTrigger(...args);
},
// we need this to know later if the default trigger should still fire
dominantTrigger: dominant,
// if dominantTrigger is set to true, the default output (webaudio) will be disabled
// when using multiple triggers, you cannot flip this flag to false again!
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
dominantTrigger: hap.context.dominantTrigger || dominant,
}),
);
}
@@ -2125,23 +2100,43 @@ export const { iterBack, iterback } = register(['iterBack', 'iterback'], functio
return _iter(times, pat, true);
});
/**
* Repeats each cycle the given number of times.
* @name repeatCycles
* @memberof Pattern
* @returns Pattern
* @example
* note(irand(12).add(34)).segment(4).repeatCycles(2).s("gm_acoustic_guitar_nylon")
*/
const _repeatCycles = function (n, pat) {
return slowcat(...Array(n).fill(pat));
};
const { repeatCycles } = register('repeatCycles', _repeatCycles);
/**
* Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle).
* @name chunk
* @synonyms slowChunk, slowchunk
* @memberof Pattern
* @returns Pattern
* @example
* "0 1 2 3".chunk(4, x=>x.add(7)).scale('A minor').note()
*/
const _chunk = function (n, func, pat, back = false) {
const _chunk = function (n, func, pat, back = false, fast = false) {
const binary = Array(n - 1).fill(false);
binary.unshift(true);
const binary_pat = _iter(n, sequence(...binary), back);
// Invert the 'back' because we want to shift the pattern forwards,
// and so time backwards
const binary_pat = _iter(n, sequence(...binary), !back);
if (!fast) {
pat = pat.repeatCycles(n);
}
return pat.when(binary_pat, func);
};
export const chunk = register('chunk', function (n, func, pat) {
return _chunk(n, func, pat, false);
const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) {
return _chunk(n, func, pat, false, false);
});
/**
@@ -2157,6 +2152,21 @@ export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], fun
return _chunk(n, func, pat, true);
});
/**
* Like `chunk`, but the cycles of the source pattern aren't repeated
* for each set of chunks.
* @name fastChunk
* @synonyms fastchunk
* @memberof Pattern
* @returns Pattern
* @example
* "<0 8> 1 2 3 4 5 6 7".fastChunk(4, x => x.color('red')).slow(4).scale("C2:major").note()
.s("folkharp")
*/
const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], function (n, func, pat) {
return _chunk(n, func, pat, false, true);
});
// TODO - redefine elsewhere in terms of mask
export const bypass = register('bypass', function (on, pat) {
on = Boolean(parseInt(on));
@@ -2181,6 +2191,9 @@ export const duration = register('duration', function (value, pat) {
/**
* Sets the color of the hap in visualizations like pianoroll or highlighting.
* @name color
* @synonyms colour
* @param {string} color Hexadecimal or CSS color name
*/
// TODO: move this to controls https://github.com/tidalcycles/strudel/issues/288
export const { color, colour } = register(['color', 'colour'], function (color, pat) {
@@ -2242,6 +2255,14 @@ export const chop = register('chop', function (n, pat) {
return pat.squeezeBind(func);
});
/**
* Cuts each sample into the given number of parts, triggering progressive portions of each sample at each loop.
* @name striate
* @memberof Pattern
* @returns Pattern
* @example
* s("numbers:0 numbers:1 numbers:2").striate(6).slow(6)
*/
export const striate = register('striate', function (n, pat) {
const slices = Array.from({ length: n }, (x, i) => i);
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
@@ -2266,14 +2287,18 @@ const _loopAt = function (factor, pat, cps = 1) {
.slow(factor);
};
/*
/**
* Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers.
* Instead of a number, it also accepts a list of numbers from 0 to 1 to slice at specific points.
* @name slice
* @memberof Pattern
* @returns Pattern
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks125/2").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
*/
export const slice = register(
@@ -2284,9 +2309,9 @@ export const slice = register(
opat.outerBind((o) => {
// If it's not an object, assume it's a string and make it a 's' control parameter
o = o instanceof Object ? o : { s: o };
// Remember we must stay pure and avoid editing the object directly
const toAdd = { begin: i / n, end: (i + 1) / n, _slices: n };
return pure({ ...toAdd, ...o });
const begin = Array.isArray(n) ? n[i] : i / n;
const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n;
return pure({ begin, end, _slices: n, ...o });
}),
),
);
@@ -2294,14 +2319,14 @@ export const slice = register(
false, // turns off auto-patternification
);
/*
/**
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
* @name splice
* @memberof Pattern
* @returns Pattern
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
* s("breaks165")
* .splice(8, "0 1 [2 3 0]@2 3 0@2 7")
* .hurry(0.65)
*/
export const splice = register(
@@ -2328,6 +2353,26 @@ export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (facto
return _loopAt(factor, pat, 1);
});
// the fit function will be redefined in repl.mjs to use the correct cps value.
// It is still here to work in cases where repl.mjs is not used
/**
* Makes the sample fit its event duration. Good for rhythmical loops like drum breaks.
* Similar to loopAt.
* @name fit
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes/4").fit()
*/
export const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: 1 / hap.whole.duration,
unit: 'c',
})),
),
);
/**
* Makes the sample fit the given number of cycles and cps value, by
* changing the speed. Please note that at some point cps will be
@@ -2344,3 +2389,35 @@ export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (facto
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
return _loopAt(factor, pat, cps);
});
/** exposes a custom value at query time. basically allows mutating state without evaluation */
export const ref = (accessor) =>
pure(1)
.withValue(() => reify(accessor()))
.innerJoin();
let fadeGain = (p) => (p < 0.5 ? 1 : 1 - (p - 0.5) / 0.5);
/**
* Cross-fades between left and right from 0 to 1:
* - 0 = (full left, no right)
* - .5 = (both equal)
* - 1 = (no left, full right)
*
* @name xfade
* @example
* xfade(s("bd*2"), "<0 .25 .5 .75 1>", s("hh*8"))
*/
export let xfade = (a, pos, b) => {
pos = reify(pos);
a = reify(a);
b = reify(b);
let gaina = pos.fmap((v) => ({ gain: fadeGain(v) }));
let gainb = pos.fmap((v) => ({ gain: fadeGain(1 - v) }));
return stack(a.mul(gaina), b.mul(gainb));
};
// the prototype version is actually flipped so left/right makes sense
Pattern.prototype.xfade = function (pos, b) {
return xfade(this, pos, b);
};
+122 -171
View File
@@ -1,6 +1,6 @@
/*
pianoroll.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/pianoroll.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pianoroll.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -29,138 +29,26 @@ const getValue = (e) => {
return value;
};
Pattern.prototype.pianoroll = function ({
cycles = 4,
playhead = 0.5,
overscan = 1,
flipTime = 0,
flipValues = 0,
hideNegative = false,
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
minMidi = 10,
maxMidi = 90,
autorange = 0,
timeframe: timeframeProp,
fold = 0,
vertical = 0,
labels = 0,
} = {}) {
const ctx = getDrawContext();
const w = ctx.canvas.width;
const h = ctx.canvas.height;
Pattern.prototype.pianoroll = function (options = {}) {
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
let from = -cycles * playhead;
let to = cycles * (1 - playhead);
if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead');
from = 0;
to = timeframeProp;
}
const timeAxis = vertical ? h : w;
const valueAxis = vertical ? w : h;
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
let foldValues = [];
flipTime && timeRange.reverse();
flipValues && valueRange.reverse();
this.draw(
(ctx, events, t) => {
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
(ctx, haps, t) => {
const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
events.filter(inFrame).forEach((event) => {
const isActive = event.whole.begin <= t && event.endClipped > t;
ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valuePx = scale(
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
...valueRange,
);
let margin = 0;
const offset = scale(t / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
pianoroll({
...options,
time: t,
ctx,
haps: haps.filter(inFrame),
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
ctx.strokeStyle = playheadColor;
ctx.beginPath();
if (vertical) {
ctx.moveTo(0, playheadPosition);
ctx.lineTo(valueAxis, playheadPosition);
} else {
ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
},
{
from: from - overscan,
to: to + overscan,
onQuery: (events) => {
const { min, max, values } = events.reduce(
({ min, max, values }, e) => {
const v = getValue(e);
return {
min: v < min ? v : min,
max: v > max ? v : max,
values: values.includes(v) ? values : [...values, v],
};
},
{ min: Infinity, max: -Infinity, values: [] },
);
if (autorange) {
minMidi = min;
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
},
},
);
return this;
@@ -168,6 +56,40 @@ Pattern.prototype.pianoroll = function ({
// this function allows drawing a pianoroll without ties to Pattern.prototype
// it will probably replace the above in the future
/**
* Displays a midi-style piano roll
*
* @name pianoroll
* @param {Object} options Object containing all the optional following parameters as key value pairs:
* @param {integer} cycles number of cycles to be displayed at the same time - defaults to 4
* @param {number} playhead location of the active notes on the time axis - 0 to 1, defaults to 0.5
* @param {boolean} vertical displays the roll vertically - 0 by default
* @param {boolean} labels displays labels on individual notes (see the label function) - 0 by default
* @param {boolean} flipTime reverse the direction of the roll - 0 by default
* @param {boolean} flipValues reverse the relative location of notes on the value axis - 0 by default
* @param {number} overscan lookup X cycles outside of the cycles window to display notes in advance - 1 by default
* @param {boolean} hideNegative hide notes with negative time (before starting playing the pattern) - 0 by default
* @param {boolean} smear notes leave a solid trace - 0 by default
* @param {boolean} fold notes takes the full value axis width - 0 by default
* @param {string} active hexadecimal or CSS color of the active notes - defaults to #FFCA28
* @param {string} inactive hexadecimal or CSS color of the inactive notes - defaults to #7491D2
* @param {string} background hexadecimal or CSS color of the background - defaults to transparent
* @param {string} playheadColor hexadecimal or CSS color of the line representing the play head - defaults to white
* @param {boolean} fill notes are filled with color (otherwise only the label is displayed) - 0 by default
* @param {boolean} fillActive active notes are filled with color - 0 by default
* @param {boolean} stroke notes are shown with colored borders - 0 by default
* @param {boolean} strokeActive active notes are shown with colored borders - 0 by default
* @param {boolean} hideInactive only active notes are shown - 0 by default
* @param {boolean} colorizeInactive use note color for inactive notes - 1 by default
* @param {string} fontFamily define the font used by notes labels - defaults to 'monospace'
* @param {integer} minMidi minimum note value to display on the value axis - defaults to 10
* @param {integer} maxMidi maximum note value to display on the value axis - defaults to 90
* @param {boolean} autorange automatically calculate the minMidi and maxMidi parameters - 0 by default
*
* @example
* note("C2 A2 G2").euclid(5,8).s('piano').clip(1).color('salmon').pianoroll({vertical:1, labels:1})
*/
export function pianoroll({
time,
haps,
@@ -191,6 +113,14 @@ export function pianoroll({
fold = 0,
vertical = 0,
labels = false,
fill = 1,
fillActive = false,
strokeActive = true,
stroke,
hideInactive = 0,
colorizeInactive = 1,
fontFamily,
clear = true,
ctx,
} = {}) {
const w = ctx.canvas.width;
@@ -234,58 +164,75 @@ export function pianoroll({
// foldValues = values.sort((a, b) => a - b);
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
if (clear && !smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
/* const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
haps
// .filter(inFrame)
.forEach((event) => {
const isActive = event.whole.begin <= time && event.whole.end > time;
const color = event.value?.color || event.context?.color;
ctx.fillStyle = color || inactive;
ctx.strokeStyle = color || active;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valuePx = scale(
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
...valueRange,
);
let margin = 0;
const offset = scale(time / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
haps.forEach((event) => {
const isActive = event.whole.begin <= time && event.endClipped > time;
let strokeCurrent = stroke ?? (strokeActive && isActive);
let fillCurrent = (!isActive && fill) || (isActive && fillActive);
if (hideInactive && !isActive) {
return;
}
let color = event.value?.color || event.context?.color;
active = color || active;
inactive = colorizeInactive ? color || inactive : inactive;
color = isActive ? active : inactive;
ctx.fillStyle = fillCurrent ? color : 'transparent';
ctx.strokeStyle = color;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
const timePx = scale(timeProgress, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valueProgress = fold
? foldValues.indexOf(value) / foldValues.length
: (Number(value) - minMidi) / valueExtent;
const valuePx = scale(valueProgress, ...valueRange);
let margin = 0;
const offset = scale(time / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
/* const xFactor = Math.sin(performance.now() / 500) + 1;
coords[0] *= xFactor; */
if (strokeCurrent) {
ctx.strokeRect(...coords);
}
if (fillCurrent) {
ctx.fillRect(...coords);
}
//ctx.ellipse(...ellipseFromRect(...coords))
if (labels) {
const defaultLabel = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
const { label: inactiveLabel, activeLabel } = event.value;
const customLabel = isActive ? activeLabel || inactiveLabel : inactiveLabel;
const label = customLabel ?? defaultLabel;
let measure = vertical ? durationPx : barThickness * 0.75;
ctx.font = `${measure}px ${fontFamily || 'monospace'}`;
// font color
ctx.fillStyle = /* isActive && */ !fillCurrent ? color : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
@@ -311,11 +258,15 @@ export function getDrawOptions(drawTime, options = {}) {
}
Pattern.prototype.punchcard = function (options) {
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }),
return this.onPaint((ctx, time, haps, drawTime, paintOptions = {}) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) }),
);
};
Pattern.prototype.wordfall = function (options) {
return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options });
};
/* Pattern.prototype.pianoroll = function (options) {
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),
+47 -5
View File
@@ -4,6 +4,22 @@ import { logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register } from './pattern.mjs';
import { atom, computed } from 'nanostores';
export const $replstate = atom({
schedulerError: undefined,
evalError: undefined,
code: '// LOADING',
activeCode: '// LOADING',
pattern: undefined,
miniLocations: [],
widgets: [],
pending: true,
started: false,
});
export const $repldirty = computed($replstate, (s) => s.code !== s.activeCode);
export const setReplState = (key, value) => $replstate.set({ ...$replstate.get(), [key]: value });
export function repl({
interval,
@@ -22,8 +38,12 @@ export function repl({
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
onToggle,
onToggle: (started) => {
setReplState('started', started);
onToggle?.(started);
},
});
let playPatterns = [];
const setPattern = (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
@@ -34,22 +54,37 @@ export function repl({
throw new Error('no code to evaluate');
}
try {
setReplState('code', code);
setReplState('pending', true);
await beforeEval?.({ code });
let { pattern } = await _evaluate(code, transpiler);
playPatterns = [];
let { pattern, meta } = await _evaluate(code, transpiler);
if (playPatterns.length) {
pattern = pattern.stack(...playPatterns);
}
logger(`[eval] code updated`);
setPattern(pattern, autostart);
afterEval?.({ code, pattern });
setReplState('miniLocations', meta?.miniLocations || []);
setReplState('widgets', meta?.widgets || []);
setReplState('activeCode', code);
setReplState('pattern', pattern);
setReplState('evalError', undefined);
setReplState('schedulerError', undefined);
setReplState('pending', false);
afterEval?.({ code, pattern, meta });
return pattern;
} catch (err) {
// console.warn(`[repl] eval error: ${err.message}`);
logger(`[eval] error: ${err.message}`, 'error');
setReplState('evalError', err);
setReplState('pending', false);
onEvalError?.(err);
}
};
const stop = () => scheduler.stop();
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
@@ -58,6 +93,11 @@ export function repl({
return pat.loopAtCps(cycles, scheduler.cps);
});
const play = register('play', (pat) => {
playPatterns.push(pat);
return pat;
});
const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
@@ -71,13 +111,15 @@ export function repl({
evalScope({
loopAt,
fit,
play,
setCps,
setcps: setCps,
setCpm,
setcpm: setCpm,
});
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
const setCode = (c) => setReplState('code', c);
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle };
}
export const getTrigger =
+54 -2
View File
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { Hap } from './hap.mjs';
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
import Fraction from './fraction.mjs';
import { id } from './util.mjs';
import { id, _mod, clamp } from './util.mjs';
export function steady(value) {
// A continuous value
@@ -155,12 +155,62 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
*/
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
/**
* pick from the list of values (or patterns of values) via the index using the given
* pattern of integers
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note(pick("<0 1 [2!2] 3>", ["g a", "e f", "f g f g" , "g a c d"]))
*/
export const pick = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat
.fmap((i) => {
const key = clamp(Math.round(i), 0, xs.length - 1);
return xs[key];
})
.innerJoin();
};
/**
* pick from the list of values (or patterns of values) via the index using the given
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
*/
export const squeeze = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat
.fmap((i) => {
const key = _mod(Math.round(i), xs.length);
return xs[key];
})
.squeezeJoin();
};
export const __chooseWith = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat.range(0, xs.length).fmap((i) => xs[Math.floor(i)]);
return pat.range(0, xs.length).fmap((i) => {
const key = Math.min(Math.max(Math.floor(i), 0), xs.length - 1);
return xs[key];
});
};
/**
* Choose from the list of values (or patterns of values) using the given
@@ -168,6 +218,8 @@ export const __chooseWith = (pat, xs) => {
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note("c2 g2!2 d2 f1").s(chooseWith(sine.fast(2), ["sawtooth", "triangle", "bd:6"]))
*/
export const chooseWith = (pat, xs) => {
return __chooseWith(pat, xs).outerJoin();
+5 -4
View File
@@ -67,9 +67,10 @@ Pattern.prototype.spiral = function (options = {}) {
// logSpiral = true,
} = options;
function spiral({ ctx, time, haps, drawTime }) {
ctx.clearRect(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight);
const [cx, cy] = [ctx.canvas.width / 2, ctx.canvas.height / 2];
function spiral({ ctx, time, haps, drawTime, clear }) {
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
clear && ctx.clearRect(0, 0, w * 2, h * 2);
const [cx, cy] = [w / 2, h / 2];
const settings = {
margin: size / stretch,
cx,
@@ -113,5 +114,5 @@ Pattern.prototype.spiral = function (options = {}) {
});
}
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
return this.onPaint((ctx, time, haps, drawTime, options) => spiral({ ctx, time, haps, drawTime, ...options }));
};
+19
View File
@@ -1003,4 +1003,23 @@ describe('Pattern', () => {
);
});
});
describe('chunk', () => {
it('Processes each cycle of the source pattern multiple times, once for each chunk', () => {
expect(sequence(0, 1, 2, 3).slow(2).chunk(2, add(10)).fast(4).firstCycleValues).toStrictEqual([
10, 1, 0, 11, 12, 3, 2, 13,
]);
});
});
describe('fastChunk', () => {
it('Unlike chunk, cycles of the source pattern proceed cycle-by-cycle', () => {
expect(sequence(0, 1, 2, 3).slow(2).fastChunk(2, add(10)).fast(4).firstCycleValues).toStrictEqual([
10, 1, 2, 13, 10, 1, 2, 13,
]);
});
});
describe('repeatCycles', () => {
it('Repeats each cycle of the source pattern the given number of times', () => {
expect(slowcat(0, 1).repeatCycles(2).fast(6).firstCycleValues).toStrictEqual([0, 0, 1, 1, 0, 0]);
});
});
});
+1 -3
View File
@@ -1,6 +1,6 @@
/*
ui.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/ui.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/ui.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -46,7 +46,5 @@ export const cleanupUi = () => {
const container = document.getElementById('code');
if (container) {
container.style = '';
// TODO: find a way to remove that duplication..
container.className = 'grow flex text-gray-100 relative overflow-auto cursor-text pb-0'; // has to match App.tsx
}
};
+8 -5
View File
@@ -11,21 +11,24 @@ export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9])?$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
if (!pc) {
return [];
}
return [pc, acc, oct ? Number(oct) : undefined];
};
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
const accs = { '#': 1, b: -1, s: 1, f: -1 };
// turns the given note into its midi number representation
export const noteToMidi = (note) => {
const [pc, acc, oct = 3] = tokenizeNote(note);
export const noteToMidi = (note, defaultOctave = 3) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
}
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1, f: -1 }[char], 0) || 0;
const chroma = chromas[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const midiToFreq = (n) => {
+1 -1
View File
@@ -137,7 +137,7 @@ export async function loadOrc(url) {
export const csoundm = register('csoundm', (instrument, pat) => {
let p1 = instrument;
if (typeof instrument === 'string') {
p1 = `"{instrument}"`;
p1 = `"${instrument}"`;
}
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
return pat.onTrigger((tidal_time, hap) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/csound",
"version": "0.8.0",
"version": "0.9.0",
"description": "csound bindings for strudel",
"main": "index.mjs",
"publishConfig": {
+3
View File
@@ -0,0 +1,3 @@
# @strudel/desktopbridge
This package contains utilities used to communicate with the Tauri backend
@@ -1,12 +1,9 @@
/*
evaluate.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.mjs>
index.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/desktopbridge/index.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { evaluate as _evaluate } from '@strudel.cycles/core';
import shapeshifter from './shapeshifter.mjs';
export const evaluate = async (code) => {
return _evaluate(code, shapeshifter);
};
export * from './midibridge.mjs';
export * from './utils.mjs';
export * from './oscbridge.mjs';
+11
View File
@@ -0,0 +1,11 @@
import { listen } from '@tauri-apps/api/event';
import { logger } from '../core/logger.mjs';
// listen for log events from the Tauri backend and log in the UI
await listen('log-event', (e) => {
if (e.payload == null) {
return;
}
const { message, message_type } = e.payload;
logger(message, message_type);
});
+52
View File
@@ -0,0 +1,52 @@
import { Invoke } from './utils.mjs';
import { Pattern, noteToMidi } from '@strudel.cycles/core';
const ON_MESSAGE = 0x90;
const OFF_MESSAGE = 0x80;
const CC_MESSAGE = 0xb0;
Pattern.prototype.midi = function (output) {
return this.onTrigger((time, hap, currentTime) => {
const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
const offset = (time - currentTime) * 1000;
const velocity = Math.floor((hap.context?.velocity ?? 0.9) * 100); // TODO: refactor velocity
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
const roundedOffset = Math.round(offset);
const midichan = (hap.value.midichan ?? 1) - 1;
const requestedport = output ?? 'IAC';
const messagesfromjs = [];
if (note != null) {
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
messagesfromjs.push({
requestedport,
message: [ON_MESSAGE + midichan, midiNumber, velocity],
offset: roundedOffset,
});
messagesfromjs.push({
requestedport,
message: [OFF_MESSAGE + midichan, midiNumber, velocity],
offset: roundedOffset + duration,
});
}
if (ccv && ccn) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1');
}
if (!['string', 'number'].includes(typeof ccn)) {
throw new Error('expected ccn to be a number or a string');
}
const scaled = Math.round(ccv * 127);
messagesfromjs.push({
requestedport,
message: [CC_MESSAGE + midichan, ccn, scaled],
offset: roundedOffset,
});
}
// invoke is temporarily blocking, run in an async process
if (messagesfromjs.length) {
setTimeout(() => {
Invoke('sendmidi', { messagesfromjs });
});
}
});
};
+43
View File
@@ -0,0 +1,43 @@
import { parseNumeral, Pattern } from '@strudel.cycles/core';
import { Invoke } from './utils.mjs';
Pattern.prototype.osc = function () {
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
hap.ensureObjectValue();
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
// make sure n and note are numbers
controls.n && (controls.n = parseNumeral(controls.n));
controls.note && (controls.note = parseNumeral(controls.note));
const params = [];
const timestamp = Math.round(Date.now() + (time - currentTime) * 1000);
Object.keys(controls).forEach((key) => {
const val = controls[key];
const value = typeof val === 'number' ? val.toString() : val;
if (value == null) {
return;
}
params.push({
name: key,
value,
valueisnumber: typeof val === 'number',
});
});
const messagesfromjs = [];
if (params.length) {
messagesfromjs.push({ target: '/dirt/play', timestamp, params });
}
if (messagesfromjs.length) {
setTimeout(() => {
Invoke('sendosc', { messagesfromjs });
});
}
});
};
+29
View File
@@ -0,0 +1,29 @@
{
"name": "@strudel/desktopbridge",
"version": "0.1.0",
"private": true,
"description": "tools/shims for communicating between the JS and Tauri (Rust) sides of the Studel desktop app",
"main": "index.mjs",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Jade Rowland <jaderowlanddev@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@tauri-apps/api": "^1.4.0"
},
"homepage": "https://github.com/tidalcycles/strudel#readme"
}
+4
View File
@@ -0,0 +1,4 @@
import { invoke } from '@tauri-apps/api/tauri';
export const Invoke = invoke;
export const isTauri = () => window.__TAURI_IPC__ != null;
+1 -1
View File
@@ -6,7 +6,7 @@ class Strudel extends HTMLElement {
setTimeout(() => {
const code = (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
const iframe = document.createElement('iframe');
const src = `https://strudel.tidalcycles.org/#${encodeURIComponent(btoa(code))}`;
const src = `https://strudel.cc/#${encodeURIComponent(btoa(code))}`;
// const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`;
iframe.setAttribute('src', src);
iframe.setAttribute('width', '600');
-3
View File
@@ -1,3 +0,0 @@
shift-parser
shift-reducer
!shift-traverser
-50
View File
@@ -1,50 +0,0 @@
# @strudel.cycles/eval
This package contains the strudel code transformer and evaluator.
It allows creating strudel patterns from input code that is optimized for minimal keystrokes and human readability.
## Deprecation Note
This package will not be developed further. Consider using `@strudel.cycles/transpiler` as a replacement.
## Install
```sh
npm i @strudel.cycles/eval --save
```
## Example
```js
import { evalScope } from '@strudel.cycles/core';
import { evaluate } from '@strudel.cycles/eval';
evalScope(
import('@strudel.cycles/core'),
// import other strudel packages here
); // add strudel to eval scope
async function run(code) {
const { pattern } = await evaluate(code);
const events = pattern.firstCycle();
console.log(events.map((e) => e.show()).join('\n'));
}
run('sequence([a3, [b3, c4]])');
```
yields:
```js
(0/1 -> 1/2, 0/1 -> 1/2, a3)
(1/2 -> 3/4, 1/2 -> 3/4, b3)
(3/4 -> 1/1, 3/4 -> 1/1, c4)
```
[play with @strudel.cycles/eval on codesandbox](https://codesandbox.io/s/strudel-eval-example-ndz1d8?file=/src/index.js)
## Dev Notes
shift-traverser is currently monkey patched because its package.json uses estraverse@^4.2.0,
which does not support the spread operator (Error: Unknown node type SpreadProperty.).
By monkey patched, I mean I copied the source of shift-traverser to a subfolder and installed the dependencies (shift-spec + estraverse@^5.3.0)
-1
View File
@@ -1 +0,0 @@
export * from './evaluate.mjs';
-50
View File
@@ -1,50 +0,0 @@
{
"name": "@strudel.cycles/eval",
"version": "0.8.0",
"description": "Code evaluator for strudel",
"main": "index.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"test": "vitest run",
"prepublishOnly": "npm run build"
},
"type": "module",
"directories": {
"test": "test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"estraverse": "^5.3.0",
"shift-ast": "^7.0.0",
"shift-codegen": "^8.1.0",
"shift-parser": "^8.0.0",
"shift-spec": "^2019.0.0",
"shift-traverser": "^1.0.0"
},
"devDependencies": {
"@strudel.cycles/mini": "workspace:*",
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
-296
View File
@@ -1,296 +0,0 @@
/*
shapeshifter.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/shapeshifter.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* import { parseScriptWithLocation } from './shift-parser/index.js'; // npm module does not work in the browser
import traverser from './shift-traverser/index.js'; // npm module does not work in the browser */
import { parseScriptWithLocation } from 'shift-parser';
import traverser from './shift-traverser/index.js';
const { replace } = traverser;
import {
LiteralStringExpression,
IdentifierExpression,
CallExpression,
StaticMemberExpression,
ReturnStatement,
ArrayExpression,
LiteralNumericExpression,
} from 'shift-ast';
import shiftCodegen from 'shift-codegen';
const codegen = shiftCodegen.default || shiftCodegen; // parcel module resolution fuckup
import * as strudel from '@strudel.cycles/core';
const { Pattern } = strudel;
const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
const addLocations = true;
export const addMiniLocations = true;
export const minifyStrings = true;
export const wrappedAsync = false; // this is now handled by core evaluate by default
export const shouldAddReturn = true;
export default (_code) => {
const { code, addReturn } = wrapAsync(_code);
const ast = parseScriptWithLocation(disguiseImports(code));
const artificialNodes = [];
const parents = [];
const shifted = replace(ast.tree, {
enter(node, parent) {
parents.push(parent);
const isSynthetic = parents.some((p) => artificialNodes.includes(p));
if (isSynthetic) {
return node;
}
// replace template string `xxx` with mini(`xxx`)
if (minifyStrings && isBackTickString(node)) {
return minifyWithLocation(node, node, ast.locations, artificialNodes);
}
// allows to use top level strings, which are normally directives... but we don't need directives
if (minifyStrings && node.directives?.length === 1 && !node.statements?.length) {
const str = new LiteralStringExpression({ value: node.directives[0].rawValue });
const wrapped = minifyWithLocation(str, node.directives[0], ast.locations, artificialNodes);
return { ...node, directives: [], statements: [wrapped] };
}
// replace double quote string "xxx" with mini('xxx')
if (minifyStrings && isStringWithDoubleQuotes(node, ast.locations, code)) {
return minifyWithLocation(node, node, ast.locations, artificialNodes);
}
// operator overloading => still not done
const operators = {
'*': 'fast',
'/': 'slow',
'&': 'stack',
'&&': 'append',
};
if (
node.type === 'BinaryExpression' &&
operators[node.operator] &&
['LiteralNumericExpression', 'LiteralStringExpression', 'IdentifierExpression'].includes(node.right?.type) &&
canBeOverloaded(node.left)
) {
let arg = node.left;
if (node.left.type === 'IdentifierExpression') {
arg = wrapFunction('reify', node.left);
}
return new CallExpression({
callee: new StaticMemberExpression({
property: operators[node.operator],
object: wrapFunction('reify', arg),
}),
arguments: [node.right],
});
}
const isMarkable = isPatternArg(parents) || hasModifierCall(parent);
// add to location to pure(x) calls
if (node.type === 'CallExpression' && node.callee.name === 'pure') {
const literal = node.arguments[0];
// const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
return reifyWithLocation(literal, node.arguments[0], ast.locations, artificialNodes);
}
// replace pseudo note variables
if (node.type === 'IdentifierExpression') {
if (isNote(node.name)) {
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
if (addLocations && isMarkable) {
return reifyWithLocation(new LiteralStringExpression({ value }), node, ast.locations, artificialNodes);
}
return new LiteralStringExpression({ value });
}
if (node.name === 'r') {
return new IdentifierExpression({ name: 'silence' });
}
}
if (
addLocations &&
['LiteralStringExpression' /* , 'LiteralNumericExpression' */].includes(node.type) &&
isMarkable
) {
// TODO: to make LiteralNumericExpression work, we need to make sure we're not inside timeCat...
return reifyWithLocation(node, node, ast.locations, artificialNodes);
}
if (addMiniLocations) {
return addMiniNotationLocations(node, ast.locations, artificialNodes);
}
return node;
},
leave() {
parents.pop();
},
});
// add return to last statement (because it's wrapped in an async function artificially)
if (shouldAddReturn) {
addReturn(shifted);
}
const generated = undisguiseImports(codegen(shifted));
return generated;
};
// renames all import statements to "_mport" as Shift doesn't support dynamic import.
// there shouldn't be any side-effects from this as this change does not affect
// the syntax & will be undone by the equivalent replace in "undisguiseImports".
function disguiseImports(code) {
return code.replaceAll('import', '_mport'); // Must be the same length!
}
// Rename the renamed import statements back to "import"
function undisguiseImports(code) {
return code.replaceAll('_mport', 'import');
}
function wrapAsync(code) {
// wrap code in async to make await work on top level => this will create 1 line offset to locations
// this is why line offset is -1 in getLocationObject calls below
if (wrappedAsync) {
code = `(async () => {
${code}
})()`;
}
const addReturn = (ast) => {
const body = wrappedAsync ? ast.statements[0].expression.callee.body : ast;
body.statements = body.statements
.slice(0, -1)
.concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]);
};
return {
code,
addReturn,
};
}
function addMiniNotationLocations(node, locations, artificialNodes) {
const miniFunctions = ['mini', 'm'];
// const isAlreadyWrapped = parent?.type === 'CallExpression' && parent.callee.name === 'withLocationOffset';
if (node.type === 'CallExpression' && miniFunctions.includes(node.callee.name)) {
// mini('c3')
if (node.arguments.length > 1) {
// TODO: transform mini(...args) to cat(...args.map(mini)) ?
console.warn('multi arg mini locations not supported yet...');
return node;
}
const str = node.arguments[0];
return minifyWithLocation(str, str, locations, artificialNodes);
}
if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property)) {
// 'c3'.mini or 'c3'.m
return minifyWithLocation(node.object, node, locations, artificialNodes);
}
return node;
}
function wrapFunction(name, ...args) {
return new CallExpression({
callee: new IdentifierExpression({ name }),
arguments: args,
});
}
function isBackTickString(node) {
return node.type === 'TemplateExpression' && node.elements.length === 1;
}
function isStringWithDoubleQuotes(node, locations, code) {
if (node.type !== 'LiteralStringExpression') {
return false;
}
const loc = locations.get(node);
const snippet = code.slice(loc.start.offset, loc.end.offset);
return snippet[0] === '"'; // we can trust the end is also ", as the parsing did not fail
}
// returns true if the given parents belong to a pattern argument node
// this is used to check if a node should receive a location for highlighting
function isPatternArg(parents) {
if (!parents.length) {
return false;
}
const ancestors = parents.slice(0, -1);
const parent = parents[parents.length - 1];
if (isPatternFactory(parent)) {
return true;
}
if (parent?.type === 'ArrayExpression') {
return isPatternArg(ancestors);
}
return false;
}
function hasModifierCall(parent) {
// TODO: modifiers are more than composables, for example every is not composable but should be seen as modifier..
// need all prototypes of Pattern
return parent?.type === 'StaticMemberExpression';
// && Object.keys(Pattern.prototype.composable).includes(parent.property)
}
const factories = Object.keys(Pattern.prototype.factories).concat(['mini']);
function isPatternFactory(node) {
return node?.type === 'CallExpression' && factories.includes(node.callee.name);
}
function canBeOverloaded(node) {
return (node.type === 'IdentifierExpression' && isNote(node.name)) || isPatternFactory(node);
// TODO: support sequence(c3).transpose(3).x.y.z
}
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
function reifyWithLocation(literalNode, node, locations, artificialNodes) {
const args = getLocationArguments(node, locations);
const withLocation = new CallExpression({
callee: new StaticMemberExpression({
object: wrapFunction('reify', literalNode),
property: 'withLocation',
}),
arguments: args,
});
artificialNodes.push(withLocation);
return withLocation;
}
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
function minifyWithLocation(literalNode, node, locations, artificialNodes) {
const args = getLocationArguments(node, locations);
const wrapped = wrapFunction('mini', literalNode);
if (!addMiniLocations) {
artificialNodes.push(wrapped);
return wrapped;
}
const withLocation = new CallExpression({
callee: new StaticMemberExpression({
object: wrapped,
property: 'withMiniLocation',
}),
arguments: args,
});
artificialNodes.push(withLocation);
return withLocation;
}
function getLocationArguments(node, locations) {
const loc = locations.get(node);
const lineOffset = wrappedAsync ? -1 : 0;
return [
new ArrayExpression({
elements: [
new LiteralNumericExpression({ value: loc.start.line + lineOffset }), // the minus 1 assumes the code has been wrapped in async iife
new LiteralNumericExpression({ value: loc.start.column }),
new LiteralNumericExpression({ value: loc.start.offset }),
],
}),
new ArrayExpression({
elements: [
new LiteralNumericExpression({ value: loc.end.line + lineOffset }), // the minus 1 assumes the code has been wrapped in async iife
new LiteralNumericExpression({ value: loc.end.column }),
new LiteralNumericExpression({ value: loc.end.offset }),
],
}),
];
}
-68
View File
@@ -1,68 +0,0 @@
/*
index.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/shift-traverser/index.js>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
Copyright (C) 2014 Yusuke Suzuki <utatane.tea@gmail.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import _Spec from 'shift-spec';
const Spec = _Spec.default || _Spec; // parcel module resolution fuckup
// import { version } from '../package.json'
// Loading uncached estraverse for changing estraverse.Syntax.
import _estraverse from 'estraverse';
const estraverse = _estraverse.cloneEnvironment();
// Adjust estraverse members.
Object.keys(estraverse.Syntax)
.filter((key) => key !== 'Property')
.forEach((key) => {
delete estraverse.Syntax[key];
delete estraverse.VisitorKeys[key];
});
Object.assign(
estraverse.Syntax,
Object.keys(Spec).reduce((result, key) => {
result[key] = key;
return result;
}, {}),
);
Object.assign(
estraverse.VisitorKeys,
Object.keys(Spec).reduce((result, key) => {
result[key] = Spec[key].fields.map((field) => field.name);
return result;
}, {}),
);
// estraverse.version = version;
export default estraverse;
/* vim: set sw=4 ts=4 et tw=80 : */
-32
View File
@@ -1,32 +0,0 @@
/*
evaluate.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/test/evaluate.test.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { expect, describe, it } from 'vitest';
import { evaluate } from '../evaluate.mjs';
import { mini } from '@strudel.cycles/mini';
import * as strudel from '@strudel.cycles/core';
const { fastcat, evalScope } = strudel;
describe('evaluate', async () => {
await evalScope({ mini }, strudel);
const ev = async (code) => (await evaluate(code)).pattern.firstCycleValues;
it('Should evaluate strudel functions', async () => {
expect(await ev('pure("c3")')).toEqual(['c3']);
expect(await ev('cat("c3")')).toEqual(['c3']);
expect(await ev('fastcat("c3", "d3")')).toEqual(['c3', 'd3']);
expect(await ev('slowcat("c3", "d3")')).toEqual(['c3']);
});
it('Scope should be extendable', async () => {
await evalScope({ myFunction: (...x) => fastcat(...x) });
expect(await ev('myFunction("c3", "d3")')).toEqual(['c3', 'd3']);
});
it('Should evaluate simple double quoted mini notation', async () => {
expect(await ev('"c3"')).toEqual(['c3']);
expect(await ev('"c3 d3"')).toEqual(['c3', 'd3']);
expect(await ev('"<c3 d3>"')).toEqual(['c3']);
});
});
-25
View File
@@ -1,25 +0,0 @@
/*
shapeshifter.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/test/shapeshifter.test.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { describe, it, expect } from 'vitest';
import shapeshifter, { wrappedAsync } from '../shapeshifter.mjs';
describe('shapeshifter', () => {
it('Should shift simple double quote string', () => {
if (wrappedAsync) {
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
} else {
expect(shapeshifter('"c3"')).toEqual('return mini("c3").withMiniLocation([1,0,0],[1,4,4])');
}
});
if (wrappedAsync) {
it('Should handle dynamic imports', () => {
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
'const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])',
);
});
}
});
+29
View File
@@ -0,0 +1,29 @@
# @strudel/hydra
This package integrates [hydra-synth](https://www.npmjs.com/package/hydra-synth) into strudel.
## Usage in Strudel
This package is imported into strudel by default. To activate Hydra, place this code at the top of your code:
```js
await initHydra();
```
Then you can use hydra below!
## Usage via npm
```sh
npm i @strudel/hydra
```
Then add the import to your evalScope:
```js
import { evalScope } from '@strudel.cycles/core';
evalScope(
import('@strudel/hydra')
)
```
+15
View File
@@ -0,0 +1,15 @@
import { getDrawContext } from '@strudel.cycles/core';
export async function initHydra() {
if (!document.getElementById('hydra-canvas')) {
const { canvas: testCanvas } = getDrawContext();
await import('https://unpkg.com/hydra-synth');
const hydraCanvas = testCanvas.cloneNode(true);
hydraCanvas.id = 'hydra-canvas';
testCanvas.after(hydraCanvas);
new Hydra({ canvas: hydraCanvas, detectAudio: false });
s0.init({ src: testCanvas });
}
}
export const H = (p) => () => p.queryArc(getTime(), getTime())[0].value;
@@ -1,14 +1,17 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.8.0",
"description": "WebDirt integration for Strudel",
"main": "index.mjs",
"type": "module",
"name": "@strudel/hydra",
"version": "0.9.0",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"server": "node server.js",
"tidal-sniffer": "node tidal-sniffer.js",
"client": "npx serve -p 4321",
"build-bin": "npx pkg server.js --targets node16-macos-x64,node16-win-x64,node16-linux-x64 --out-path bin",
"build": "vite build",
"prepublishOnly": "npm run build"
},
@@ -31,11 +34,10 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"WebDirt": "github:dktr0/WebDirt"
"hydra-synth": "^1.3.29"
},
"devDependencies": {
"vite": "^4.3.3",
"vitest": "^0.28.0"
"pkg": "^5.8.1",
"vite": "^4.3.3"
}
}
@@ -7,7 +7,7 @@ export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
entry: resolve(__dirname, 'hydra.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
+122 -65
View File
@@ -5,10 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { Pattern, isPattern, logger, ref } from '@strudel.cycles/core';
import { noteToMidi } from '@strudel.cycles/core';
import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -16,12 +15,28 @@ function supportsMidi() {
return typeof navigator.requestMIDIAccess === 'function';
}
export function enableWebMidi(options = {}) {
const { onReady, onConnected, onDisconnected } = options;
function getMidiDeviceNamesString(devices) {
return devices.map((o) => `'${o.name}'`).join(' | ');
}
export function enableWebMidi(options = {}) {
const { onReady, onConnected, onDisconnected, onEnabled } = options;
if (WebMidi.enabled) {
return;
}
if (!supportsMidi()) {
throw new Error('Your Browser does not support WebMIDI.');
}
WebMidi.addListener('connected', () => {
onConnected?.(WebMidi);
});
WebMidi.addListener('enabled', () => {
onEnabled?.(WebMidi);
});
// Reacting when a device becomes unavailable
WebMidi.addListener('disconnected', (e) => {
onDisconnected?.(WebMidi, e);
});
return new Promise((resolve, reject) => {
if (WebMidi.enabled) {
// if already enabled, just resolve WebMidi
@@ -32,57 +47,49 @@ export function enableWebMidi(options = {}) {
if (err) {
reject(err);
}
WebMidi.addListener('connected', (e) => {
onConnected?.(WebMidi);
});
// Reacting when a device becomes unavailable
WebMidi.addListener('disconnected', (e) => {
onDisconnected?.(WebMidi, e);
});
onReady?.(WebMidi);
resolve(WebMidi);
});
});
}
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
const outputByName = (name) => WebMidi.getOutputByName(name);
let midiReady;
// output?: string | number, outputs: typeof WebMidi.outputs
function getDevice(output, outputs) {
if (!outputs.length) {
function getDevice(indexOrName, devices) {
if (!devices.length) {
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
}
if (typeof output === 'number') {
return outputs[output];
if (typeof indexOrName === 'number') {
return devices[indexOrName];
}
if (typeof output === 'string') {
return outputByName(output);
const byName = (name) => devices.find((output) => output.name.includes(name));
if (typeof indexOrName === 'string') {
return byName(indexOrName);
}
return outputs[0];
// attempt to default to first IAC device if none is specified
const IACOutput = byName('IAC');
const device = IACOutput ?? devices[0];
if (!device) {
throw new Error(
`🔌 MIDI device '${device ? device : ''}' not found. Use one of ${getMidiDeviceNamesString(devices)}`,
);
}
return IACOutput ?? devices[0];
}
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.midi = function (output) {
if (!supportsMidi()) {
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
}
/* await */ enableWebMidi({
onConnected: ({ outputs }) =>
logger(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
onDisconnected: ({ outputs }) =>
logger(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
onReady: ({ outputs }) => {
const device = getDevice(output, outputs);
const otherOutputs = outputs
.filter((o) => o.name !== device.name)
.map((o) => `'${o.name}'`)
.join(' | ');
midiReady = true;
logger(`Midi connected! Using "${device.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
},
// send start/stop messages to outputs when repl starts/stops
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (!WebMidi?.enabled) {
return;
}
if (e.data === 'strudel-stop') {
WebMidi.outputs.forEach((output) => output.sendStop());
}
// cannot start here, since we have no timing info, see sendStart below
});
}
Pattern.prototype.midi = function (output) {
if (isPattern(output)) {
throw new Error(
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
@@ -90,38 +97,47 @@ Pattern.prototype.midi = function (output) {
}')`,
);
}
return this.onTrigger((time, hap) => {
if (!midiReady) {
enableWebMidi({
onEnabled: ({ outputs }) => {
const device = getDevice(output, outputs);
const otherOutputs = outputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
}`,
);
},
onDisconnected: ({ outputs }) =>
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});
return this.onTrigger((time, hap, currentTime, cps) => {
if (!WebMidi.enabled) {
console.log('not enabled');
return;
}
const device = getDevice(output, WebMidi.outputs);
if (!device) {
throw new Error(
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
.map((o) => `'${o.name}'`)
.join(' | ')}`,
);
}
hap.ensureObjectValue();
// calculate time
const timingOffset = WebMidi.time - getAudioContext().getOutputTimestamp().contextTime * 1000;
time = time * 1000 + timingOffset;
const offset = (time - currentTime) * 1000;
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
const timeOffsetString = `+${offset}`;
// destructure value
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd } = hap.value;
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
const duration = hap.duration.valueOf() * 1000 - 5;
if (note) {
const midiNumber = noteToMidi(note);
device.playNote(midiNumber, midichan, {
time,
duration,
attack: velocity,
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
if (note != null) {
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
const midiNote = new Note(midiNumber, { attack: velocity, duration });
device.playNote(midiNote, midichan, {
time: timeOffsetString,
});
}
if (ccv && ccn) {
if (ccv !== undefined && ccn !== undefined) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1');
}
@@ -129,7 +145,48 @@ Pattern.prototype.midi = function (output) {
throw new Error('expected ccn to be a number or a string');
}
const scaled = Math.round(ccv * 127);
device.sendControlChange(ccn, scaled, midichan, { time });
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
}
if (hap.whole.begin + 0 === 0) {
// we need to start here because we have the timing info
device.sendStart({ time: timeOffsetString });
}
if (['clock', 'midiClock'].includes(midicmd)) {
device.sendClock({ time: timeOffsetString });
} else if (['start'].includes(midicmd)) {
device.sendStart({ time: timeOffsetString });
} else if (['stop'].includes(midicmd)) {
device.sendStop({ time: timeOffsetString });
} else if (['continue'].includes(midicmd)) {
device.sendContinue({ time: timeOffsetString });
}
});
};
let listeners = {};
const refs = {};
export async function midin(input) {
const initial = await enableWebMidi(); // only returns on first init
const device = getDevice(input, WebMidi.inputs);
if (initial) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`,
);
refs[input] = {};
}
const cc = (cc) => ref(() => refs[input][cc] || 0);
listeners[input] && device.removeListener('midimessage', listeners[input]);
listeners[input] = (e) => {
const cc = e.dataBytes[0];
const v = e.dataBytes[1];
refs[input] && (refs[input][cc] = v / 127);
};
device.addListener('midimessage', listeners[input]);
return cc;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.8.0",
"version": "0.9.0",
"description": "Midi API for strudel",
"main": "index.mjs",
"publishConfig": {
+2 -2
View File
@@ -32,7 +32,7 @@ yields:
## Mini Notation API
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/learn/mini-notation)
See "Mini Notation" in the [Strudel Tutorial](https://strudel.cc/learn/mini-notation)
## Building the Parser
@@ -40,5 +40,5 @@ The parser [krill-parser.js] is generated from [krill.pegjs](./krill.pegjs) usin
To generate the parser, run
```js
npm build:parser
npm run build:parser
```
File diff suppressed because one or more lines are too long
+31 -2
View File
@@ -79,6 +79,9 @@ frac
int
= zero / (digit1_9 DIGIT*)
intneg
= minus? int { return parseInt(text()); }
minus
= "-"
@@ -100,7 +103,8 @@ quote = '"' / "'"
// ------------------ steps and cycles ---------------------------
// single step definition (e.g bd)
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_"
step_char "a letter, a number, \"-\", \"#\", \".\", \"^\", \"_\"" =
unicode_letter / [0-9~] / "-" / "#" / "." / "^" / "_"
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
// define a sub cycle e.g. [1 2, 3 [4]]
@@ -123,7 +127,7 @@ slice = step / sub_cycle / polymeter / slow_sequence
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
// at this point, we assume we can represent them as regular sequence operators
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail / op_range
op_weight = "@" a:number
{ return x => x.options_['weight'] = a }
@@ -146,6 +150,9 @@ op_degrade = "?"a:number?
op_tail = ":" s:slice
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
op_range = ".." s:slice
{ return x => x.options_['ops'].push({ type_: "range", arguments_ :{ element:s } }) }
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
slice_with_ops = s:slice ops:slice_op*
{ const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
@@ -259,3 +266,25 @@ hush = "hush"
// ---------------------- statements ----------------------------
statement = mini_definition / command
// ---------------------- unicode ----------------------------
unicode_letter = Lu / Ll / Lt / Lm / Lo / Nl
// Letter, Lowercase
Ll = [\u0061-\u007A\u00B5\u00DF-\u00F6\u00F8-\u00FF\u0101\u0103\u0105\u0107\u0109\u010B\u010D\u010F\u0111\u0113\u0115\u0117\u0119\u011B\u011D\u011F\u0121\u0123\u0125\u0127\u0129\u012B\u012D\u012F\u0131\u0133\u0135\u0137-\u0138\u013A\u013C\u013E\u0140\u0142\u0144\u0146\u0148-\u0149\u014B\u014D\u014F\u0151\u0153\u0155\u0157\u0159\u015B\u015D\u015F\u0161\u0163\u0165\u0167\u0169\u016B\u016D\u016F\u0171\u0173\u0175\u0177\u017A\u017C\u017E-\u0180\u0183\u0185\u0188\u018C-\u018D\u0192\u0195\u0199-\u019B\u019E\u01A1\u01A3\u01A5\u01A8\u01AA-\u01AB\u01AD\u01B0\u01B4\u01B6\u01B9-\u01BA\u01BD-\u01BF\u01C6\u01C9\u01CC\u01CE\u01D0\u01D2\u01D4\u01D6\u01D8\u01DA\u01DC-\u01DD\u01DF\u01E1\u01E3\u01E5\u01E7\u01E9\u01EB\u01ED\u01EF-\u01F0\u01F3\u01F5\u01F9\u01FB\u01FD\u01FF\u0201\u0203\u0205\u0207\u0209\u020B\u020D\u020F\u0211\u0213\u0215\u0217\u0219\u021B\u021D\u021F\u0221\u0223\u0225\u0227\u0229\u022B\u022D\u022F\u0231\u0233-\u0239\u023C\u023F-\u0240\u0242\u0247\u0249\u024B\u024D\u024F-\u0293\u0295-\u02AF\u0371\u0373\u0377\u037B-\u037D\u0390\u03AC-\u03CE\u03D0-\u03D1\u03D5-\u03D7\u03D9\u03DB\u03DD\u03DF\u03E1\u03E3\u03E5\u03E7\u03E9\u03EB\u03ED\u03EF-\u03F3\u03F5\u03F8\u03FB-\u03FC\u0430-\u045F\u0461\u0463\u0465\u0467\u0469\u046B\u046D\u046F\u0471\u0473\u0475\u0477\u0479\u047B\u047D\u047F\u0481\u048B\u048D\u048F\u0491\u0493\u0495\u0497\u0499\u049B\u049D\u049F\u04A1\u04A3\u04A5\u04A7\u04A9\u04AB\u04AD\u04AF\u04B1\u04B3\u04B5\u04B7\u04B9\u04BB\u04BD\u04BF\u04C2\u04C4\u04C6\u04C8\u04CA\u04CC\u04CE-\u04CF\u04D1\u04D3\u04D5\u04D7\u04D9\u04DB\u04DD\u04DF\u04E1\u04E3\u04E5\u04E7\u04E9\u04EB\u04ED\u04EF\u04F1\u04F3\u04F5\u04F7\u04F9\u04FB\u04FD\u04FF\u0501\u0503\u0505\u0507\u0509\u050B\u050D\u050F\u0511\u0513\u0515\u0517\u0519\u051B\u051D\u051F\u0521\u0523\u0525\u0527\u0529\u052B\u052D\u052F\u0560-\u0588\u10D0-\u10FA\u10FD-\u10FF\u13F8-\u13FD\u1C80-\u1C88\u1D00-\u1D2B\u1D6B-\u1D77\u1D79-\u1D9A\u1E01\u1E03\u1E05\u1E07\u1E09\u1E0B\u1E0D\u1E0F\u1E11\u1E13\u1E15\u1E17\u1E19\u1E1B\u1E1D\u1E1F\u1E21\u1E23\u1E25\u1E27\u1E29\u1E2B\u1E2D\u1E2F\u1E31\u1E33\u1E35\u1E37\u1E39\u1E3B\u1E3D\u1E3F\u1E41\u1E43\u1E45\u1E47\u1E49\u1E4B\u1E4D\u1E4F\u1E51\u1E53\u1E55\u1E57\u1E59\u1E5B\u1E5D\u1E5F\u1E61\u1E63\u1E65\u1E67\u1E69\u1E6B\u1E6D\u1E6F\u1E71\u1E73\u1E75\u1E77\u1E79\u1E7B\u1E7D\u1E7F\u1E81\u1E83\u1E85\u1E87\u1E89\u1E8B\u1E8D\u1E8F\u1E91\u1E93\u1E95-\u1E9D\u1E9F\u1EA1\u1EA3\u1EA5\u1EA7\u1EA9\u1EAB\u1EAD\u1EAF\u1EB1\u1EB3\u1EB5\u1EB7\u1EB9\u1EBB\u1EBD\u1EBF\u1EC1\u1EC3\u1EC5\u1EC7\u1EC9\u1ECB\u1ECD\u1ECF\u1ED1\u1ED3\u1ED5\u1ED7\u1ED9\u1EDB\u1EDD\u1EDF\u1EE1\u1EE3\u1EE5\u1EE7\u1EE9\u1EEB\u1EED\u1EEF\u1EF1\u1EF3\u1EF5\u1EF7\u1EF9\u1EFB\u1EFD\u1EFF-\u1F07\u1F10-\u1F15\u1F20-\u1F27\u1F30-\u1F37\u1F40-\u1F45\u1F50-\u1F57\u1F60-\u1F67\u1F70-\u1F7D\u1F80-\u1F87\u1F90-\u1F97\u1FA0-\u1FA7\u1FB0-\u1FB4\u1FB6-\u1FB7\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FC7\u1FD0-\u1FD3\u1FD6-\u1FD7\u1FE0-\u1FE7\u1FF2-\u1FF4\u1FF6-\u1FF7\u210A\u210E-\u210F\u2113\u212F\u2134\u2139\u213C-\u213D\u2146-\u2149\u214E\u2184\u2C30-\u2C5E\u2C61\u2C65-\u2C66\u2C68\u2C6A\u2C6C\u2C71\u2C73-\u2C74\u2C76-\u2C7B\u2C81\u2C83\u2C85\u2C87\u2C89\u2C8B\u2C8D\u2C8F\u2C91\u2C93\u2C95\u2C97\u2C99\u2C9B\u2C9D\u2C9F\u2CA1\u2CA3\u2CA5\u2CA7\u2CA9\u2CAB\u2CAD\u2CAF\u2CB1\u2CB3\u2CB5\u2CB7\u2CB9\u2CBB\u2CBD\u2CBF\u2CC1\u2CC3\u2CC5\u2CC7\u2CC9\u2CCB\u2CCD\u2CCF\u2CD1\u2CD3\u2CD5\u2CD7\u2CD9\u2CDB\u2CDD\u2CDF\u2CE1\u2CE3-\u2CE4\u2CEC\u2CEE\u2CF3\u2D00-\u2D25\u2D27\u2D2D\uA641\uA643\uA645\uA647\uA649\uA64B\uA64D\uA64F\uA651\uA653\uA655\uA657\uA659\uA65B\uA65D\uA65F\uA661\uA663\uA665\uA667\uA669\uA66B\uA66D\uA681\uA683\uA685\uA687\uA689\uA68B\uA68D\uA68F\uA691\uA693\uA695\uA697\uA699\uA69B\uA723\uA725\uA727\uA729\uA72B\uA72D\uA72F-\uA731\uA733\uA735\uA737\uA739\uA73B\uA73D\uA73F\uA741\uA743\uA745\uA747\uA749\uA74B\uA74D\uA74F\uA751\uA753\uA755\uA757\uA759\uA75B\uA75D\uA75F\uA761\uA763\uA765\uA767\uA769\uA76B\uA76D\uA76F\uA771-\uA778\uA77A\uA77C\uA77F\uA781\uA783\uA785\uA787\uA78C\uA78E\uA791\uA793-\uA795\uA797\uA799\uA79B\uA79D\uA79F\uA7A1\uA7A3\uA7A5\uA7A7\uA7A9\uA7AF\uA7B5\uA7B7\uA7B9\uA7FA\uAB30-\uAB5A\uAB60-\uAB65\uAB70-\uABBF\uFB00-\uFB06\uFB13-\uFB17\uFF41-\uFF5A]
// Letter, Modifier
Lm = [\u02B0-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0374\u037A\u0559\u0640\u06E5-\u06E6\u07F4-\u07F5\u07FA\u081A\u0824\u0828\u0971\u0E46\u0EC6\u10FC\u17D7\u1843\u1AA7\u1C78-\u1C7D\u1D2C-\u1D6A\u1D78\u1D9B-\u1DBF\u2071\u207F\u2090-\u209C\u2C7C-\u2C7D\u2D6F\u2E2F\u3005\u3031-\u3035\u303B\u309D-\u309E\u30FC-\u30FE\uA015\uA4F8-\uA4FD\uA60C\uA67F\uA69C-\uA69D\uA717-\uA71F\uA770\uA788\uA7F8-\uA7F9\uA9CF\uA9E6\uAA70\uAADD\uAAF3-\uAAF4\uAB5C-\uAB5F\uFF70\uFF9E-\uFF9F]
// Letter, Other
Lo = [\u00AA\u00BA\u01BB\u01C0-\u01C3\u0294\u05D0-\u05EA\u05EF-\u05F2\u0620-\u063F\u0641-\u064A\u066E-\u066F\u0671-\u06D3\u06D5\u06EE-\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u0800-\u0815\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0972-\u0980\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC-\u09DD\u09DF-\u09E1\u09F0-\u09F1\u09FC\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33\u0A35-\u0A36\u0A38-\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0-\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32-\u0B33\u0B35-\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60-\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0-\u0CE1\u0CF1-\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32-\u0E33\u0E40-\u0E45\u0E81-\u0E82\u0E84\u0E87-\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA-\u0EAB\u0EAD-\u0EB0\u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065-\u1066\u106E-\u1070\u1075-\u1081\u108E\u1100-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17DC\u1820-\u1842\u1844-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE-\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C77\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5-\u1CF6\u2135-\u2138\u2D30-\u2D67\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3006\u303C\u3041-\u3096\u309F\u30A1-\u30FA\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEF\uA000-\uA014\uA016-\uA48C\uA4D0-\uA4F7\uA500-\uA60B\uA610-\uA61F\uA62A-\uA62B\uA66E\uA6A0-\uA6E5\uA78F\uA7F7\uA7FB-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD-\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9E0-\uA9E4\uA9E7-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA6F\uAA71-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5-\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADC\uAAE0-\uAAEA\uAAF2\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF66-\uFF6F\uFF71-\uFF9D\uFFA0-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]
// Letter, Titlecase
Lt = [\u01C5\u01C8\u01CB\u01F2\u1F88-\u1F8F\u1F98-\u1F9F\u1FA8-\u1FAF\u1FBC\u1FCC\u1FFC]
// Letter, Uppercase
Lu = [\u0041-\u005A\u00C0-\u00D6\u00D8-\u00DE\u0100\u0102\u0104\u0106\u0108\u010A\u010C\u010E\u0110\u0112\u0114\u0116\u0118\u011A\u011C\u011E\u0120\u0122\u0124\u0126\u0128\u012A\u012C\u012E\u0130\u0132\u0134\u0136\u0139\u013B\u013D\u013F\u0141\u0143\u0145\u0147\u014A\u014C\u014E\u0150\u0152\u0154\u0156\u0158\u015A\u015C\u015E\u0160\u0162\u0164\u0166\u0168\u016A\u016C\u016E\u0170\u0172\u0174\u0176\u0178-\u0179\u017B\u017D\u0181-\u0182\u0184\u0186-\u0187\u0189-\u018B\u018E-\u0191\u0193-\u0194\u0196-\u0198\u019C-\u019D\u019F-\u01A0\u01A2\u01A4\u01A6-\u01A7\u01A9\u01AC\u01AE-\u01AF\u01B1-\u01B3\u01B5\u01B7-\u01B8\u01BC\u01C4\u01C7\u01CA\u01CD\u01CF\u01D1\u01D3\u01D5\u01D7\u01D9\u01DB\u01DE\u01E0\u01E2\u01E4\u01E6\u01E8\u01EA\u01EC\u01EE\u01F1\u01F4\u01F6-\u01F8\u01FA\u01FC\u01FE\u0200\u0202\u0204\u0206\u0208\u020A\u020C\u020E\u0210\u0212\u0214\u0216\u0218\u021A\u021C\u021E\u0220\u0222\u0224\u0226\u0228\u022A\u022C\u022E\u0230\u0232\u023A-\u023B\u023D-\u023E\u0241\u0243-\u0246\u0248\u024A\u024C\u024E\u0370\u0372\u0376\u037F\u0386\u0388-\u038A\u038C\u038E-\u038F\u0391-\u03A1\u03A3-\u03AB\u03CF\u03D2-\u03D4\u03D8\u03DA\u03DC\u03DE\u03E0\u03E2\u03E4\u03E6\u03E8\u03EA\u03EC\u03EE\u03F4\u03F7\u03F9-\u03FA\u03FD-\u042F\u0460\u0462\u0464\u0466\u0468\u046A\u046C\u046E\u0470\u0472\u0474\u0476\u0478\u047A\u047C\u047E\u0480\u048A\u048C\u048E\u0490\u0492\u0494\u0496\u0498\u049A\u049C\u049E\u04A0\u04A2\u04A4\u04A6\u04A8\u04AA\u04AC\u04AE\u04B0\u04B2\u04B4\u04B6\u04B8\u04BA\u04BC\u04BE\u04C0-\u04C1\u04C3\u04C5\u04C7\u04C9\u04CB\u04CD\u04D0\u04D2\u04D4\u04D6\u04D8\u04DA\u04DC\u04DE\u04E0\u04E2\u04E4\u04E6\u04E8\u04EA\u04EC\u04EE\u04F0\u04F2\u04F4\u04F6\u04F8\u04FA\u04FC\u04FE\u0500\u0502\u0504\u0506\u0508\u050A\u050C\u050E\u0510\u0512\u0514\u0516\u0518\u051A\u051C\u051E\u0520\u0522\u0524\u0526\u0528\u052A\u052C\u052E\u0531-\u0556\u10A0-\u10C5\u10C7\u10CD\u13A0-\u13F5\u1C90-\u1CBA\u1CBD-\u1CBF\u1E00\u1E02\u1E04\u1E06\u1E08\u1E0A\u1E0C\u1E0E\u1E10\u1E12\u1E14\u1E16\u1E18\u1E1A\u1E1C\u1E1E\u1E20\u1E22\u1E24\u1E26\u1E28\u1E2A\u1E2C\u1E2E\u1E30\u1E32\u1E34\u1E36\u1E38\u1E3A\u1E3C\u1E3E\u1E40\u1E42\u1E44\u1E46\u1E48\u1E4A\u1E4C\u1E4E\u1E50\u1E52\u1E54\u1E56\u1E58\u1E5A\u1E5C\u1E5E\u1E60\u1E62\u1E64\u1E66\u1E68\u1E6A\u1E6C\u1E6E\u1E70\u1E72\u1E74\u1E76\u1E78\u1E7A\u1E7C\u1E7E\u1E80\u1E82\u1E84\u1E86\u1E88\u1E8A\u1E8C\u1E8E\u1E90\u1E92\u1E94\u1E9E\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAA\u1EAC\u1EAE\u1EB0\u1EB2\u1EB4\u1EB6\u1EB8\u1EBA\u1EBC\u1EBE\u1EC0\u1EC2\u1EC4\u1EC6\u1EC8\u1ECA\u1ECC\u1ECE\u1ED0\u1ED2\u1ED4\u1ED6\u1ED8\u1EDA\u1EDC\u1EDE\u1EE0\u1EE2\u1EE4\u1EE6\u1EE8\u1EEA\u1EEC\u1EEE\u1EF0\u1EF2\u1EF4\u1EF6\u1EF8\u1EFA\u1EFC\u1EFE\u1F08-\u1F0F\u1F18-\u1F1D\u1F28-\u1F2F\u1F38-\u1F3F\u1F48-\u1F4D\u1F59\u1F5B\u1F5D\u1F5F\u1F68-\u1F6F\u1FB8-\u1FBB\u1FC8-\u1FCB\u1FD8-\u1FDB\u1FE8-\u1FEC\u1FF8-\u1FFB\u2102\u2107\u210B-\u210D\u2110-\u2112\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u2130-\u2133\u213E-\u213F\u2145\u2183\u2C00-\u2C2E\u2C60\u2C62-\u2C64\u2C67\u2C69\u2C6B\u2C6D-\u2C70\u2C72\u2C75\u2C7E-\u2C80\u2C82\u2C84\u2C86\u2C88\u2C8A\u2C8C\u2C8E\u2C90\u2C92\u2C94\u2C96\u2C98\u2C9A\u2C9C\u2C9E\u2CA0\u2CA2\u2CA4\u2CA6\u2CA8\u2CAA\u2CAC\u2CAE\u2CB0\u2CB2\u2CB4\u2CB6\u2CB8\u2CBA\u2CBC\u2CBE\u2CC0\u2CC2\u2CC4\u2CC6\u2CC8\u2CCA\u2CCC\u2CCE\u2CD0\u2CD2\u2CD4\u2CD6\u2CD8\u2CDA\u2CDC\u2CDE\u2CE0\u2CE2\u2CEB\u2CED\u2CF2\uA640\uA642\uA644\uA646\uA648\uA64A\uA64C\uA64E\uA650\uA652\uA654\uA656\uA658\uA65A\uA65C\uA65E\uA660\uA662\uA664\uA666\uA668\uA66A\uA66C\uA680\uA682\uA684\uA686\uA688\uA68A\uA68C\uA68E\uA690\uA692\uA694\uA696\uA698\uA69A\uA722\uA724\uA726\uA728\uA72A\uA72C\uA72E\uA732\uA734\uA736\uA738\uA73A\uA73C\uA73E\uA740\uA742\uA744\uA746\uA748\uA74A\uA74C\uA74E\uA750\uA752\uA754\uA756\uA758\uA75A\uA75C\uA75E\uA760\uA762\uA764\uA766\uA768\uA76A\uA76C\uA76E\uA779\uA77B\uA77D-\uA77E\uA780\uA782\uA784\uA786\uA78B\uA78D\uA790\uA792\uA796\uA798\uA79A\uA79C\uA79E\uA7A0\uA7A2\uA7A4\uA7A6\uA7A8\uA7AA-\uA7AE\uA7B0-\uA7B4\uA7B6\uA7B8\uFF21-\uFF3A]
// Number, Letter
Nl = [\u16EE-\u16F0\u2160-\u2182\u2185-\u2188\u3007\u3021-\u3029\u3038-\u303A\uA6E6-\uA6EF]
+79 -53
View File
@@ -9,7 +9,7 @@ import * as strudel from '@strudel.cycles/core';
const randOffset = 0.0003;
const applyOptions = (parent, code) => (pat, i) => {
const applyOptions = (parent, enter) => (pat, i) => {
const ast = parent.source_[i];
const options = ast.options_;
const ops = options?.ops;
@@ -23,18 +23,14 @@ const applyOptions = (parent, code) => (pat, i) => {
if (!legalTypes.includes(type)) {
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
}
pat = strudel.reify(pat)[type](patternifyAST(amount, code));
pat = strudel.reify(pat)[type](enter(amount));
break;
}
case 'bjorklund': {
if (op.arguments_.rotation) {
pat = pat.euclidRot(
patternifyAST(op.arguments_.pulse, code),
patternifyAST(op.arguments_.step, code),
patternifyAST(op.arguments_.rotation, code),
);
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
} else {
pat = pat.euclid(patternifyAST(op.arguments_.pulse, code), patternifyAST(op.arguments_.step, code));
pat = pat.euclid(enter(op.arguments_.pulse), enter(op.arguments_.step));
}
break;
}
@@ -45,10 +41,21 @@ const applyOptions = (parent, code) => (pat, i) => {
break;
}
case 'tail': {
const friend = patternifyAST(op.arguments_.element, code);
const friend = enter(op.arguments_.element);
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
break;
}
case 'range': {
const friend = enter(op.arguments_.element);
pat = strudel.reify(pat);
const arrayRange = (start, stop, step = 1) =>
Array.from({ length: Math.abs(stop - start) / step + 1 }, (value, index) =>
start < stop ? start + index * step : start - index * step,
);
let range = (apat, bpat) => apat.squeezeBind((a) => bpat.bind((b) => strudel.fastcat(...arrayRange(a, b))));
pat = range(pat, friend);
break;
}
default: {
console.warn(`operator "${op.type_}" not implemented`);
}
@@ -72,11 +79,14 @@ function resolveReplications(ast) {
);
}
export function patternifyAST(ast, code) {
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
export function patternifyAST(ast, code, onEnter, offset = 0) {
onEnter?.(ast);
const enter = (node) => patternifyAST(node, code, onEnter, offset);
switch (ast.type_) {
case 'pattern': {
resolveReplications(ast);
const children = ast.source_.map((child) => patternifyAST(child, code)).map(applyOptions(ast, code));
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
const alignment = ast.arguments_.alignment;
if (alignment === 'stack') {
return strudel.stack(...children);
@@ -84,7 +94,7 @@ export function patternifyAST(ast, code) {
if (alignment === 'polymeter') {
// polymeter
const stepsPerCycle = ast.arguments_.stepsPerCycle
? patternifyAST(ast.arguments_.stepsPerCycle, code).fmap((x) => strudel.Fraction(x))
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
@@ -111,7 +121,7 @@ export function patternifyAST(ast, code) {
return pat;
}
case 'element': {
return patternifyAST(ast.source_, code);
return enter(ast.source_);
}
case 'atom': {
if (ast.source_ === '~') {
@@ -121,66 +131,82 @@ export function patternifyAST(ast, code) {
console.warn('no location for', ast);
return ast.source_;
}
const { start, end } = ast.location_;
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
// the following line expects the shapeshifter append .withMiniLocation
// because location_ is only relative to the mini string, but we need it relative to whole code
// make sure whitespaces are not part of the highlight:
const actual = code?.split('').slice(start.offset, end.offset).join('');
const [offsetStart = 0, offsetEnd = 0] = actual
? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length)
: [];
return strudel
.pure(value)
.withLocation(
[start.line, start.column + offsetStart, start.offset + offsetStart],
[start.line, end.column - offsetEnd, end.offset - offsetEnd],
);
if (offset === -1) {
// skip location handling (used when getting leaves to avoid confusion)
return strudel.pure(value);
}
const [from, to] = getLeafLocation(code, ast, offset);
return strudel.pure(value).withLoc(from, to);
}
case 'stretch':
return patternifyAST(ast.source_, code).slow(patternifyAST(ast.arguments_.amount, code));
/* case 'scale':
let [tonic, scale] = Scale.tokenize(ast.arguments_.scale);
const intervals = Scale.get(scale).intervals;
const pattern = patternifyAST(ast.source_);
tonic = tonic || 'C4';
// console.log('scale', ast, pattern, tonic, scale);
console.log('tonic', tonic);
return pattern.fmap((step: any) => {
step = Number(step);
if (isNaN(step)) {
console.warn(`scale step "${step}" not a number`);
return step;
}
const octaves = Math.floor(step / intervals.length);
const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
const index = mod(step, intervals.length); // % with negative numbers. e.g. -1 % 3 = 2
const interval = Interval.add(intervals[index], Interval.fromSemitones(octaves * 12));
return Note.transpose(tonic, interval || '1P');
}); */
/* case 'struct':
// TODO:
return strudel.silence; */
return enter(ast.source_).slow(enter(ast.arguments_.amount));
default:
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
return strudel.silence;
}
}
// takes quoted mini string + leaf node within, returns source location of node (whitespace corrected)
export const getLeafLocation = (code, leaf, globalOffset = 0) => {
// value is expected without quotes!
const { start, end } = leaf.location_;
const actual = code?.split('').slice(start.offset, end.offset).join('');
// make sure whitespaces are not part of the highlight
const [offsetStart = 0, offsetEnd = 0] = actual
? actual.split(leaf.source_).map((p) => p.split('').filter((c) => c === ' ').length)
: [];
return [start.offset + offsetStart + globalOffset, end.offset - offsetEnd + globalOffset];
};
// takes quoted mini string, returns ast
export const mini2ast = (code) => krill.parse(code);
// takes quoted mini string, returns all nodes that are leaves
export const getLeaves = (code) => {
const ast = mini2ast(code);
let leaves = [];
patternifyAST(
ast,
code,
(node) => {
if (node.type_ === 'atom') {
leaves.push(node);
}
},
-1,
);
return leaves;
};
// takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes
export const getLeafLocations = (code, offset = 0) => {
return getLeaves(code).map((l) => getLeafLocation(code, l, offset));
};
// mini notation only (wraps in "")
export const mini = (...strings) => {
const pats = strings.map((str) => {
const code = `"${str}"`;
const ast = krill.parse(code);
const ast = mini2ast(code);
return patternifyAST(ast, code);
});
return strudel.sequence(...pats);
};
// turns str mini string (without quotes) into pattern
// offset is the position of the mini string in the JS code
// each leaf node will get .withLoc added
// this function is used by the transpiler for double quoted strings
export const m = (str, offset) => {
const code = `"${str}"`;
const ast = mini2ast(code);
return patternifyAST(ast, code, null, offset);
};
// includes haskell style (raw krill parsing)
export const h = (string) => {
const ast = krill.parse(string);
// console.log('ast', ast);
const ast = mini2ast(string);
return patternifyAST(ast, string);
};
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.8.2",
"version": "0.9.0",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
@@ -37,6 +37,6 @@
"devDependencies": {
"peggy": "^3.0.2",
"vite": "^4.3.3",
"vitest": "^0.28.0"
"vitest": "^0.33.0"
}
}
+40 -1
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { mini } from '../mini.mjs';
import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs';
import '@strudel.cycles/core/euclid.mjs';
import { describe, expect, it } from 'vitest';
@@ -184,4 +184,43 @@ describe('mini', () => {
it('supports lists', () => {
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
});
it('supports ranges', () => {
expect(minV('0 .. 4')).toEqual([0, 1, 2, 3, 4]);
});
it('supports patterned ranges', () => {
expect(minS('[<0 1> .. <2 4>]*2')).toEqual(minS('[0 1 2] [1 2 3 4]'));
});
});
describe('getLeafLocation', () => {
it('gets location of leaf nodes', () => {
const code = '"bd sd"';
const ast = mini2ast(code);
const bd = ast.source_[0].source_;
expect(getLeafLocation(code, bd)).toEqual([1, 3]);
const sd = ast.source_[1].source_;
expect(getLeafLocation(code, sd)).toEqual([4, 6]);
});
});
describe('getLeafLocations', () => {
it('gets locations of leaf nodes', () => {
expect(getLeafLocations('"bd sd"')).toEqual([
[1, 3], // bd columns
[4, 6], // sd columns
]);
expect(getLeafLocations('"bd*2 [sd cp]"')).toEqual([
[1, 3], // bd columns
[7, 9], // sd columns
[10, 12], // cp columns
[4, 5], // "2" columns
]);
expect(getLeafLocations('"bd*<2 3>"')).toEqual([
[1, 3], // bd columns
[5, 6], // "2" columns
[7, 8], // "3" columns
]);
});
});
+2 -2
View File
@@ -34,6 +34,6 @@ Now open the REPL and type:
s("<bd sd> hh").osc()
```
or just [click here](https://strudel.tidalcycles.org/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
or just [click here](https://strudel.cc/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.tidalcycles.org/learn/input-output/#superdirt-api)
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
+7 -10
View File
@@ -5,6 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import OSC from 'osc-js';
import { logger, parseNumeral, Pattern } from '@strudel.cycles/core';
let connection; // Promise<OSC>
@@ -33,13 +34,10 @@ function connect() {
return connection;
}
const latency = 0.1;
let startedAt = -1;
/**
*
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
* For more info, read [MIDI & OSC in the docs](https://strudel.tidalcycles.org/learn/input-output)
* For more info, read [MIDI & OSC in the docs](https://strudel.cc/learn/input-output)
*
* @name osc
* @memberof Pattern
@@ -51,17 +49,16 @@ Pattern.prototype.osc = function () {
const osc = await connect();
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
// time should be audio time of onset
// currentTime should be current time of audio context (slightly before time)
if (startedAt < 0) {
startedAt = Date.now() - currentTime * 1000;
}
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
// make sure n and note are numbers
controls.n && (controls.n = parseNumeral(controls.n));
controls.note && (controls.note = parseNumeral(controls.note));
const keyvals = Object.entries(controls).flat();
const ts = Math.floor(startedAt + (time + latency) * 1000);
// time should be audio time of onset
// currentTime should be current time of audio context (slightly before time)
const offset = (time - currentTime) * 1000;
// timestamp in milliseconds used to trigger the osc bundle at a precise moment
const ts = Math.floor(Date.now() + offset);
const message = new OSC.Message('/dirt/play', ...keyvals);
const bundle = new OSC.Bundle([message], ts);
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.8.0",
"version": "0.9.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"publishConfig": {
+3 -3
View File
@@ -21,12 +21,12 @@ import { samples, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
async function prebake() {
await samples(
'https://strudel.tidalcycles.org/tidal-drum-machines.json',
'https://strudel.cc/tidal-drum-machines.json',
'github:ritchse/tidal-drum-machines/main/machines/'
);
await samples(
'https://strudel.tidalcycles.org/EmuSP12.json',
'https://strudel.tidalcycles.org/EmuSP12/'
'https://strudel.cc/EmuSP12.json',
'https://strudel.cc/EmuSP12/'
);
}
+25 -29
View File
@@ -1,5 +1,5 @@
import { controls, evalScope } from '@strudel.cycles/core';
import { CodeMirror, useHighlighting, useKeydown, useStrudel, flash } from '@strudel.cycles/react';
import { CodeMirror, useHighlighting, useStrudel, flash } from '@strudel.cycles/react';
import {
getAudioContext,
initAudioOnFirstClick,
@@ -82,9 +82,10 @@ function App() {
code,
defaultOutput: webaudioOutput,
getTime,
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
});
useHighlighting({
const { setMiniLocations } = useHighlighting({
view,
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
@@ -92,32 +93,6 @@ function App() {
});
const error = evalError || schedulerError;
useKeydown(
useCallback(
async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await evaluate(code);
if (e.shiftKey) {
panic();
scheduler.stop();
scheduler.start();
}
if (!scheduler.started) {
scheduler.start();
}
} else if (e.code === 'Period') {
scheduler.stop();
panic();
e.preventDefault();
}
}
},
[scheduler, evaluate, view, code],
),
);
return (
<div>
<nav className="z-[12] w-full flex justify-center fixed bottom-0">
@@ -135,7 +110,28 @@ function App() {
</div>
{error && <p>error {error.message}</p>}
</nav>
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
<CodeMirror
value={code}
onChange={setCode}
onViewChanged={setView}
onEvaluate={async () => {
flash(view);
await evaluate(code);
if (!scheduler.started) {
scheduler.start();
}
}}
onReEvaluate={async () => {
await evaluate(code);
panic();
scheduler.stop();
scheduler.start();
}}
onStop={() => {
scheduler.stop();
panic();
}}
/>
</div>
);
}
+7 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/react",
"version": "0.8.0",
"version": "0.9.0",
"description": "React components for strudel",
"main": "src/index.js",
"publishConfig": {
@@ -33,15 +33,21 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.6.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@replit/codemirror-emacs": "^6.0.1",
"@replit/codemirror-vim": "^6.0.14",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel/codemirror": "workspace:*",
"@uiw/codemirror-themes": "^4.19.16",
"@uiw/react-codemirror": "^4.19.16",
"react-hook-inview": "^4.5.0"
@@ -26,7 +26,6 @@ export function Autocomplete({ doc }) {
<pre
className="cursor-pointer"
onMouseDown={(e) => {
console.log('ola!');
navigator.clipboard.writeText(example);
e.stopPropagation();
}}
+93 -191
View File
@@ -1,109 +1,47 @@
import React, { useMemo } from 'react';
import _CodeMirror from '@uiw/react-codemirror';
import { EditorView, Decoration } from '@codemirror/view';
import { StateField, StateEffect } from '@codemirror/state';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import strudelTheme from '../themes/strudel-theme';
import './style.css';
import { useCallback } from 'react';
import { autocompletion } from '@codemirror/autocomplete';
import { strudelAutocomplete } from './Autocomplete';
import { vim } from '@replit/codemirror-vim';
import { Prec } from '@codemirror/state';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { ViewPlugin, EditorView, keymap } from '@codemirror/view';
import { emacs } from '@replit/codemirror-emacs';
import { vim } from '@replit/codemirror-vim';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import _CodeMirror from '@uiw/react-codemirror';
import React, { useCallback, useMemo } from 'react';
import strudelTheme from '../themes/strudel-theme';
import { strudelAutocomplete } from './Autocomplete';
import { strudelTooltip } from './Tooltip';
import {
highlightExtension,
flashField,
flash,
highlightMiniLocations,
updateMiniLocations,
} from '@strudel/codemirror';
import './style.css';
import { sliderPlugin } from '@strudel/codemirror/slider.mjs';
export const setFlash = StateEffect.define();
const flashField = StateField.define({
create() {
return Decoration.none;
},
update(flash, tr) {
try {
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash = Decoration.set([]);
}
}
}
return flash;
} catch (err) {
console.warn('flash error', err);
return flash;
}
},
provide: (f) => EditorView.decorations.from(f),
});
export { flash, highlightMiniLocations, updateMiniLocations };
export const flash = (view) => {
view.dispatch({ effects: setFlash.of(true) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, 200);
};
export const setHighlights = StateEffect.define();
const highlightField = StateField.define({
create() {
return Decoration.none;
},
update(highlights, tr) {
try {
for (let e of tr.effects) {
if (e.is(setHighlights)) {
const { haps } = e.value;
const marks =
haps
.map((hap) =>
(hap.context.locations || []).map(({ start, end }) => {
const color = hap.context.color || e.value.color;
let from = tr.newDoc.line(start.line).from + start.column;
let to = tr.newDoc.line(end.line).from + end.column;
const l = tr.newDoc.length;
if (from > l || to > l) {
return; // dont mark outside of range, as it will throw an error
}
let mark;
if (color) {
mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
} else {
mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
}
return mark.range(from, to);
}),
)
.flat()
.filter(Boolean) || [];
highlights = Decoration.set(marks, true);
}
}
return highlights;
} catch (err) {
// console.warn('highlighting error', err);
return Decoration.set([]);
}
},
provide: (f) => EditorView.decorations.from(f),
});
const staticExtensions = [javascript(), highlightField, flashField];
const staticExtensions = [javascript(), flashField, highlightExtension, sliderPlugin];
export default function CodeMirror({
value,
onChange,
onViewChanged,
onSelectionChange,
onDocChange,
onEvaluate,
onReEvaluate,
onPanic,
onStop,
theme,
keybindings,
isLineNumbersDisplayed,
isAutoCompletionEnabled,
isTooltipEnabled,
isLineWrappingEnabled,
fontSize = 18,
fontFamily = 'monospace',
options,
editorDidMount,
}) {
const handleOnChange = useCallback(
(value) => {
@@ -121,6 +59,9 @@ export default function CodeMirror({
const handleOnUpdate = useCallback(
(viewUpdate) => {
if (viewUpdate.docChanged && onDocChange) {
onDocChange?.(viewUpdate);
}
if (viewUpdate.selectionSet && onSelectionChange) {
onSelectionChange?.(viewUpdate.state.selection);
}
@@ -128,11 +69,25 @@ export default function CodeMirror({
[onSelectionChange],
);
const vscodePlugin = ViewPlugin.fromClass(
class {
constructor(view) {}
},
{
provide: (plugin) => {
return Prec.highest(keymap.of([...vscodeKeymap]));
},
},
);
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
const extensions = useMemo(() => {
let _extensions = [...staticExtensions];
let bindings = {
vim,
emacs,
vscode: vscodeExtension,
};
if (bindings[keybindings]) {
@@ -144,13 +99,60 @@ export default function CodeMirror({
} else {
_extensions.push(autocompletion({ override: [] }));
}
if (isTooltipEnabled) {
_extensions.push(strudelTooltip);
}
//_extensions.push([keymap.of({})]);
if (isLineWrappingEnabled) {
_extensions.push(EditorView.lineWrapping);
}
_extensions.push(
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate?.(),
},
{
key: 'Alt-Enter',
run: () => onEvaluate?.(),
},
{
key: 'Ctrl-.',
run: () => onStop?.(),
},
{
key: 'Alt-.',
run: (_, e) => {
e.preventDefault();
onStop?.();
},
},
{
key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()),
},
{
key: 'Ctrl-Shift-Enter',
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
},
]),
);
return _extensions;
}, [keybindings, isAutoCompletionEnabled, isLineWrappingEnabled]);
}, [
keybindings,
isAutoCompletionEnabled,
isTooltipEnabled,
isLineWrappingEnabled,
onEvaluate,
onReEvaluate,
onStop,
onPanic,
]);
const basicSetup = useMemo(() => ({ lineNumbers: isLineNumbersDisplayed }), [isLineNumbersDisplayed]);
return (
<div style={{ fontSize, fontFamily }} className="w-full">
@@ -161,108 +163,8 @@ export default function CodeMirror({
onCreateEditor={handleOnCreateEditor}
onUpdate={handleOnUpdate}
extensions={extensions}
basicSetup={{ lineNumbers: isLineNumbersDisplayed }}
basicSetup={basicSetup}
/>
</div>
);
}
let parenMark;
export const markParens = (editor, data) => {
const v = editor.getDoc().getValue();
const marked = getCurrentParenArea(v, data);
parenMark?.clear();
parenMark = editor.getDoc().markText(...marked, { css: 'background-color: #00007720' }); //
};
// returns { line, ch } from absolute character offset
export function offsetToPosition(offset, code) {
const lines = code.split('\n');
let line = 0;
let ch = 0;
for (let i = 0; i < offset; i++) {
if (ch === lines[line].length) {
line++;
ch = 0;
} else {
ch++;
}
}
return { line, ch };
}
// returns absolute character offset from { line, ch }
export function positionToOffset(position, code) {
const lines = code.split('\n');
if (position.line > lines.length) {
// throw new Error('positionToOffset: position.line > lines.length');
return 0;
}
let offset = 0;
for (let i = 0; i < position.line; i++) {
offset += lines[i].length + 1;
}
offset += position.ch;
return offset;
}
// given code and caret position, the functions returns the indices of the parens we are in
export function getCurrentParenArea(code, caretPosition) {
const caret = positionToOffset(caretPosition, code);
let open, i, begin, end;
// walk left
i = caret;
open = 0;
while (i > 0) {
if (code[i - 1] === '(') {
open--;
} else if (code[i - 1] === ')') {
open++;
}
if (open === -1) {
break;
}
i--;
}
begin = i;
// walk right
i = caret;
open = 0;
while (i < code.length) {
if (code[i] === '(') {
open--;
} else if (code[i] === ')') {
open++;
}
if (open === 1) {
break;
}
i++;
}
end = i;
return [begin, end].map((o) => offsetToPosition(o, code));
}
/*
export const markEvent = (editor) => (time, event) => {
const locs = event.context.locations;
if (!locs || !editor) {
return;
}
const col = event.context?.color || '#FFCA28';
// mark active event
const marks = locs.map(({ start, end }) =>
editor.getDoc().markText(
{ line: start.line - 1, ch: start.column },
{ line: end.line - 1, ch: end.column },
//{ css: 'background-color: #FFCA28; color: black' } // background-color is now used by parent marking
{ css: 'outline: 1px solid ' + col + '; box-sizing:border-box' },
//{ css: `background-color: ${col};border-radius:5px` },
),
);
//Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler...
setTimeout(() => {
marks.forEach((mark) => mark.clear());
// }, '+' + event.duration * 0.5);
}, event.duration * 1000);
}; */
+7 -23
View File
@@ -10,7 +10,6 @@ import { Icon } from './Icon';
import './style.css';
import { logger } from '@strudel.cycles/core';
import useEvent from '../hooks/useEvent.mjs';
import useKeydown from '../hooks/useKeydown.mjs';
const getTime = () => getAudioContext().currentTime;
@@ -71,6 +70,7 @@ export function MiniRepl({
evalOnMount,
drawContext,
drawTime,
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
});
const [view, setView] = useState();
@@ -84,34 +84,13 @@ export function MiniRepl({
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
useHighlighting({
const { setMiniLocations } = useHighlighting({
view,
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.now(),
});
// keyboard shortcuts
useKeydown(
useCallback(
async (e) => {
if (view?.hasFocus) {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.key === '.' || e.code === 'Period') {
stop();
e.preventDefault();
}
}
}
},
[activateCode, stop, view],
),
);
const [log, setLog] = useState([]);
useLogger(
useCallback((e) => {
@@ -163,6 +142,11 @@ export function MiniRepl({
fontSize={fontSize}
keybindings={keybindings}
isLineNumbersDisplayed={isLineNumbersDisplayed}
onEvaluate={() => {
flash(view);
activateCode();
}}
onStop={() => stop()}
/>
)}
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
+69
View File
@@ -0,0 +1,69 @@
import { createRoot } from 'react-dom/client';
import { hoverTooltip } from '@codemirror/view';
import jsdoc from '../../../../doc.json';
import { Autocomplete } from './Autocomplete';
const getDocLabel = (doc) => doc.name || doc.longname;
let ctrlDown = false;
// Record Control key event to trigger or block the tooltip depending on the state
window.addEventListener(
'keyup',
function (e) {
if (e.key == 'Control') {
ctrlDown = false;
}
},
true,
);
window.addEventListener(
'keydown',
function (e) {
if (e.key == 'Control') {
ctrlDown = true;
}
},
true,
);
export const strudelTooltip = hoverTooltip(
(view, pos, side) => {
// Word selection from CodeMirror Hover Tooltip example https://codemirror.net/examples/tooltip/#hover-tooltips
let { from, to, text } = view.state.doc.lineAt(pos);
let start = pos,
end = pos;
while (start > from && /\w/.test(text[start - from - 1])) {
start--;
}
while (end < to && /\w/.test(text[end - from])) {
end++;
}
if ((start == pos && side < 0) || (end == pos && side > 0)) {
return null;
}
let word = text.slice(start - from, end - from);
// Get entry from Strudel documentation
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
if (!entry) {
return null;
}
if (!ctrlDown) {
return null;
}
return {
pos: start,
end,
above: false,
arrow: true,
create(view) {
let dom = document.createElement('div');
dom.className = 'strudel-tooltip';
createRoot(dom).render(<Autocomplete doc={entry} />);
return { dom };
},
};
},
{ hoverTime: 10 },
);
+4
View File
@@ -28,3 +28,7 @@
footer {
z-index: 0 !important;
}
.strudel-tooltip {
padding: 5px;
}
+15 -5
View File
@@ -1,10 +1,18 @@
import { useEffect, useRef } from 'react';
import { setHighlights } from '../components/CodeMirror6';
import { useEffect, useRef, useState } from 'react';
import { highlightMiniLocations, updateMiniLocations } from '../components/CodeMirror6';
const round = (x) => Math.round(x * 1000) / 1000;
function useHighlighting({ view, pattern, active, getTime }) {
const highlights = useRef([]);
const lastEnd = useRef(0);
const [miniLocations, setMiniLocations] = useState([]);
useEffect(() => {
if (view) {
updateMiniLocations(view, miniLocations);
}
}, [view, miniLocations]);
useEffect(() => {
if (view) {
if (pattern && active) {
@@ -20,9 +28,9 @@ function useHighlighting({ view, pattern, active, getTime }) {
highlights.current = highlights.current.filter((hap) => hap.endClipped > audioTime); // keep only highlights that are still active
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
highlights.current = highlights.current.concat(haps); // add potential new onsets
view.dispatch({ effects: setHighlights.of({ haps: highlights.current }) }); // highlight all still active + new active haps
highlightMiniLocations(view, begin, highlights.current);
} catch (err) {
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
highlightMiniLocations(view, 0, []);
}
frame = requestAnimationFrame(updateHighlights);
});
@@ -31,10 +39,12 @@ function useHighlighting({ view, pattern, active, getTime }) {
};
} else {
highlights.current = [];
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
highlightMiniLocations(view, 0, highlights.current);
}
}
}, [pattern, active, view]);
return { setMiniLocations };
}
export default useHighlighting;
+2 -2
View File
@@ -25,10 +25,10 @@ function usePatternFrame({ pattern, started, getTime, onDraw, drawTime = [-2, 2]
const haps = pattern.queryArc(Math.max(lastFrame.current, phase - 1 / 10), phase);
lastFrame.current = phase;
visibleHaps.current = (visibleHaps.current || [])
.filter((h) => h.whole.end >= phase - lookbehind - lookahead) // in frame
.filter((h) => h.endClipped >= phase - lookbehind - lookahead) // in frame
.concat(haps.filter((h) => h.hasOnset()));
onDraw(pattern, phase - lookahead, visibleHaps.current, drawTime);
}, [pattern]),
}, [pattern, onDraw]),
);
useEffect(() => {
if (started) {
+3 -2
View File
@@ -18,6 +18,7 @@ function useStrudel({
canvasId,
drawContext,
drawTime = [-2, 2],
paintOptions = {},
}) {
const id = useMemo(() => s4(), []);
canvasId = canvasId || `canvas-${id}`;
@@ -85,9 +86,9 @@ function useStrudel({
(pattern, time, haps, drawTime) => {
const { onPaint } = pattern.context || {};
const ctx = typeof drawContext === 'function' ? drawContext(canvasId) : drawContext;
onPaint?.(ctx, time, haps, drawTime);
onPaint?.(ctx, time, haps, drawTime, paintOptions);
},
[drawContext, canvasId],
[drawContext, canvasId, paintOptions],
);
const drawFirstFrame = useCallback(
+13
View File
@@ -0,0 +1,13 @@
import { useEffect, useState } from 'react';
import { updateWidgets } from '@strudel/codemirror';
// i know this is ugly.. in the future, repl needs to run without react
export function useWidgets(view) {
const [widgets, setWidgets] = useState([]);
useEffect(() => {
if (view) {
updateWidgets(view, widgets);
}
}, [view, widgets]);
return { widgets, setWidgets };
}
+2 -1
View File
@@ -1,6 +1,6 @@
// import 'tailwindcss/tailwind.css';
export { default as CodeMirror, flash } from './components/CodeMirror6'; // !SSR
export { default as CodeMirror, flash, updateMiniLocations, highlightMiniLocations } from './components/CodeMirror6'; // !SSR
export * from './components/MiniRepl'; // !SSR
export { default as useHighlighting } from './hooks/useHighlighting'; // !SSR
export { default as useStrudel } from './hooks/useStrudel'; // !SSR
@@ -8,4 +8,5 @@ export { default as usePostMessage } from './hooks/usePostMessage';
export { default as useKeydown } from './hooks/useKeydown';
export { default as useEvent } from './hooks/useEvent';
export { default as strudelTheme } from './themes/strudel-theme';
export { default as teletext } from './themes/teletext';
export { default as cx } from './cx';
+2
View File
@@ -12,6 +12,7 @@ export const settings = {
gutterBackground: 'transparent',
gutterForeground: '#0f380f',
light: true,
customStyle: '.cm-line { line-height: 1 }',
};
export default createTheme({
theme: 'light',
@@ -35,5 +36,6 @@ export default createTheme({
{ tag: t.propertyName, color: '#0f380f' },
{ tag: t.className, color: '#0f380f' },
{ tag: t.invalid, color: '#0f380f' },
{ tag: [t.unit, t.punctuation], color: '#0f380f' },
],
});
+1
View File
@@ -33,5 +33,6 @@ export default createTheme({
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});
+1
View File
@@ -36,5 +36,6 @@ export default createTheme({
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});
@@ -40,5 +40,6 @@ export default createTheme({
{ tag: t.className, color: '#decb6b' },
{ tag: t.invalid, color: '#ffffff' },
{ tag: [t.unit, t.punctuation], color: '#82aaff' },
],
});
+50
View File
@@ -0,0 +1,50 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
let colorA = '#6edee4';
//let colorB = 'magenta';
let colorB = 'white';
let colorC = 'red';
let colorD = '#f8fc55';
export const settings = {
background: '#000000',
foreground: colorA, // whats that?
caret: colorC,
selection: colorD,
selectionMatch: colorA,
lineHighlight: '#6edee440', // panel bg
lineBackground: '#00000040',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
customStyle: '.cm-line { line-height: 1 }',
};
let punctuation = colorD;
let mini = colorB;
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: colorA },
{ tag: t.operator, color: mini },
{ tag: t.special(t.variableName), color: colorA },
{ tag: t.typeName, color: colorA },
{ tag: t.atom, color: colorA },
{ tag: t.number, color: mini },
{ tag: t.definition(t.variableName), color: colorA },
{ tag: t.string, color: mini },
{ tag: t.special(t.string), color: mini },
{ tag: t.comment, color: punctuation },
{ tag: t.variableName, color: colorA },
{ tag: t.tagName, color: colorA },
{ tag: t.bracket, color: punctuation },
{ tag: t.meta, color: colorA },
{ tag: t.attributeName, color: colorA },
{ tag: t.propertyName, color: colorA }, // methods
{ tag: t.className, color: colorA },
{ tag: t.invalid, color: colorC },
{ tag: [t.unit, t.punctuation], color: punctuation },
],
});
-2
View File
@@ -22,8 +22,6 @@ export default defineConfig({
...Object.keys(peerDependencies),
...Object.keys(dependencies),
// TODO: find out which of below names are obsolete now
'@strudel.cycles/tone',
'@strudel.cycles/eval',
'@strudel.cycles/transpiler',
'acorn',
'@strudel.cycles/core',
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/serial",
"version": "0.8.0",
"version": "0.9.0",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"publishConfig": {
+9 -9
View File
@@ -6,23 +6,23 @@ This program is free software: you can redistribute it and/or modify it under th
import { Pattern, isPattern } from '@strudel.cycles/core';
var writeMessage;
var writeMessagers = {};
var choosing = false;
export async function getWriter(br = 38400) {
export async function getWriter(name, br) {
if (choosing) {
return;
}
choosing = true;
if (writeMessage) {
return writeMessage;
if (name in writeMessagers) {
return writeMessagers[name];
}
if ('serial' in navigator) {
const port = await navigator.serial.requestPort();
await port.open({ baudRate: br });
const encoder = new TextEncoder();
const writer = port.writable.getWriter();
writeMessage = function (message, chk) {
writeMessagers[name] = function (message, chk) {
const encoded = encoder.encode(message);
if (!chk) {
writer.write(encoded);
@@ -63,10 +63,10 @@ function crc16(data) {
return crc & 0xffff;
}
Pattern.prototype.serial = function (br = 38400, sendcrc = false, singlecharids = false) {
Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids = false, name = 'default') {
return this.withHap((hap) => {
if (!writeMessage) {
getWriter(br);
if (!(name in writeMessagers)) {
getWriter(name, br);
}
const onTrigger = (time, hap, currentTime) => {
var message = '';
@@ -108,7 +108,7 @@ Pattern.prototype.serial = function (br = 38400, sendcrc = false, singlecharids
const offset = (time - currentTime + latency) * 1000;
window.setTimeout(function () {
writeMessage(message, chk);
writeMessagers[name](message, chk);
}, offset);
};
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.8.2",
"version": "0.9.0",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
+170
View File
@@ -0,0 +1,170 @@
# superdough
superdough is a simple web audio sampler and synth, intended for live coding.
It is the default output of [strudel](https://strudel.cc/).
This package has no ties to strudel and can be used to quickly bake your own music system on the web.
## Install
via npm:
```js
npm i superdough --save
```
## Use
```js
import { superdough, samples, initAudioOnFirstClick, registerSynthSounds } from 'superdough';
const init = Promise.all([
initAudioOnFirstClick(),
samples('github:tidalcycles/Dirt-Samples/master'),
registerSynthSounds(),
]);
const loop = (t = 0) => {
// superdough(value, time, duration)
superdough({ s: 'bd', delay: 0.5 }, t);
superdough({ note: 'g1', s: 'sawtooth', cutoff: 600, resonance: 8 }, t, 0.125);
superdough({ note: 'g2', s: 'sawtooth', cutoff: 600, resonance: 8 }, t + 0.25, 0.125);
superdough({ s: 'hh' }, t + 0.25);
superdough({ s: 'sd', room: 0.5 }, t + 0.5);
superdough({ s: 'hh' }, t + 0.75);
};
document.getElementById('play').addEventListener('click', async () => {
await init;
let t = 0.1;
while (t < 16) {
loop(t++);
}
});
```
[Open this in Codesandbox](https://codesandbox.io/s/superdough-demo-forked-sf8djh?file=/src/index.js)
## API
### superdough(value, deadline, duration)
```js
superdough({ s: 'bd', delay: 0.5 }, 0, 1);
```
- `value`: the sound properties:
- `s`: the name of the sound as loaded via `samples` or `registerSound`
- `n`: selects sample with given index
- `bank`: prefix_ that is attached to the sound, e.g. `{ s: 'bd', bank: 'RolandTR909' }` = `{ s: 'RolandTR909_bd' }`
- `gain`: gain from 0 to 1 (higher values also work but might clip)
- `velocity`: additional gain multiplier
- `cutoff`: low pass filter cutoff
- `resonance`: low pass filter resonance
- `hcutoff`: high pass filter cutoff
- `hresonance`: high pass filter resonance
- `bandf`: band pass filter cutoff
- `bandq`: band pass filter resonance
- `crush`: amplitude bit crusher using given number of bits
- `shape`: distortion effect from 0 (none) to 1 (full). might get loud!
- `pan`: stereo panning from 0 (left) to 1 (right)
- `phaser`: sets the speed of the modulation
- `phaserdepth`: the amount the signal is affected by the phaser effect.
- `phasersweep`: the frequency sweep range of the lfo for the phaser effect.
- `phasercenter`: the amount the signal is affected by the phaser effect.
- `vowel`: vowel filter. possible values: "a", "e", "i", "o", "u"
- `delay`: delay mix
- `delayfeedback`: delay feedback
- `delaytime`: delay time
- `room`: reverb mix
- `size`: reverb room size
- `orbit`: bus name for global effects `delay` and `room`. same orbits will get the same effects
- `freq`: repitches sound to given frequency in Hz
- `note`: repitches sound to given note or midi number
- `cut`: sets cut group. Sounds of same group will cut each other off
- `clip`: multiplies duration with given number
- `speed`: repitches sound by given factor
- `begin`: moves beginning of sample to given factor (between 0 and 1)
- `end`: moves end of sample to given factor (between 0 and 1)
- `attack`: seconds of attack phase
- `decay`: seconds of decay phase
- `sustain`: gain of sustain phase
- `release`: seconds of release phase
- `deadline`: seconds until the sound should play (0 = immediate)
- `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds
### registerSynthSounds()
Loads the default waveforms `sawtooth`, `square`, `triangle` and `sine`. Use them like this:
```js
superdough({ s:'sawtooth' }, 0, 1)
```
The duration needs to be set for these sounds!
### samples(sampleMap)
allows you to load samples from URLs. There are 3 ways to load samples
1. sample map object
2. url of sample map json file
3. github repo
#### sample map object
You can pass a sample map like this:
```js
samples({
'_base': 'https://raw.githubusercontent.com/felixroos/samples/main/',
'bd': 'president/president_bd.mp3',
'sd': ['president/president_sd.mp3', 'president/president_sd2.mp3'],
'hh': ['president/president_hh.mp3'],
})
```
The `_base` property defines the root url while the others declare one or more sample paths for each sound.
For example the full URL for `bd` would then be `https://raw.githubusercontent.com/felixroos/samples/main/president/president_bd.mp3`
A loaded sound can then be played with `superdough({ s: 'bd' }, 0)`.
If you declare multiple sounds, you can select them with `n`: `superdough({ s: 'sd', n: 1 }, 0)`
The duration property is not needed for samples.
#### loading samples from a json file
Instead of passing an object as a sample map, you can also pass a URL to a json that contains a sample map:
```js
samples('https://raw.githubusercontent.com/felixroos/samples/main/strudel.json')
```
The json file is expected to have the same format as described above.
#### loading samples from a github repo
Because it is common to use github for samples, there is a short way to load a sample map from github:
```js
samples('github:tidalcycles/Dirt-Samples/master')
```
The format is `github:<user>/<repo>/<branch>`.
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
The format is also expected to be the same as explained above.
### initAudioOnFirstClick()
Initializes audio and makes sure it is playable after the first click in the document. A click is needed because of the [Autoplay Policy](https://www.w3.org/TR/autoplay-detection/).
You can call this function when the document loads.
Then just make sure your first call of `superdough` happens after a click of something.
## Credits
- [ZZFX](https://github.com/KilledByAPixel/ZzFX) used for synths starting with z
- [SuperDirt](https://github.com/musikinformatik/SuperDirt)
- [WebDirt](https://github.com/dktr0/WebDirt)
+79
View File
@@ -0,0 +1,79 @@
import { getAudioContext } from './superdough.mjs';
let worklet;
export async function dspWorklet(ac, code) {
const name = `dsp-worklet-${Date.now()}`;
const workletCode = `${code}
let __q = []; // trigger queue
class MyProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.t = 0;
this.stopped = false;
this.port.onmessage = (e) => {
if(e.data==='stop') {
this.stopped = true;
} else if(e.data?.dough) {
__q.push(e.data)
} else {
msg?.(e.data)
}
};
}
process(inputs, outputs, parameters) {
const output = outputs[0];
if(__q.length) {
for(let i=0;i<__q.length;++i) {
const deadline = __q[i].time-currentTime;
if(deadline<=0) {
trigger(__q[i].dough)
__q.splice(i,1)
}
}
}
for (let i = 0; i < output[0].length; i++) {
const out = dsp(this.t / sampleRate);
output.forEach((channel) => {
channel[i] = out;
});
this.t++;
}
return !this.stopped;
}
}
registerProcessor('${name}', MyProcessor);
`;
const base64String = btoa(workletCode);
const dataURL = `data:text/javascript;base64,${base64String}`;
await ac.audioWorklet.addModule(dataURL);
const node = new AudioWorkletNode(ac, name);
const stop = () => node.port.postMessage('stop');
return { node, stop };
}
const stop = () => {
if (worklet) {
worklet?.stop();
worklet?.node?.disconnect();
}
};
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (e.data === 'strudel-stop') {
stop();
} else if (e.data?.dough) {
worklet?.node.port.postMessage(e.data);
}
});
}
export const dough = async (code) => {
const ac = getAudioContext();
stop();
worklet = await dspWorklet(ac, code);
worklet.node.connect(ac.destination);
};
export function doughTrigger(t, hap, currentTime, duration, cps) {
window.postMessage({ time: t, dough: hap.value, currentTime, duration, cps });
}
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>Superdough Example</title>
<meta charset="UTF-8" />
</head>
<body>
<button id="play">PLAAAAAAAY</button>
<script src="/main.js" type="module"></script>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
import { superdough, samples, initAudioOnFirstClick, registerSynthSounds } from 'superdough';
const init = Promise.all([
initAudioOnFirstClick(),
samples('github:tidalcycles/Dirt-Samples/master'),
registerSynthSounds(),
]);
const loop = (t = 0) => {
// superdough(value, time, duration)
superdough({ s: 'bd', delay: 0.5 }, t);
superdough({ note: 'g1', s: 'sawtooth', cutoff: 600, resonance: 8 }, t, 0.125);
superdough({ note: 'g2', s: 'sawtooth', cutoff: 600, resonance: 8 }, t + 0.25, 0.125);
superdough({ s: 'hh' }, t + 0.25);
superdough({ s: 'sd', room: 0.5 }, t + 0.5);
superdough({ s: 'hh' }, t + 0.75);
};
document.getElementById('play').addEventListener('click', async () => {
await init;
let t = 0.1;
while (t < 16) {
loop(t++);
}
});
+17
View File
@@ -0,0 +1,17 @@
{
"name": "superdough-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"superdough": "workspace:*"
},
"devDependencies": {
"vite": "^4.4.5"
}
}

Some files were not shown because too many files have changed in this diff Show More