Compare commits

...

283 Commits

Author SHA1 Message Date
Felix Roos a2f3b85f38 remove notes 2023-06-11 11:20:28 +02:00
Felix Roos 8e6846a3aa add workshop file 2023-06-10 12:14:15 +02:00
Felix Roos 34ee2081d3 fix: typo 2023-06-09 23:02:48 +02:00
Felix Roos 4d8308a763 fix: link goes to wrong language 2023-06-09 21:33:32 +02:00
Felix Roos e10104d401 bring de improvements to en workshop 2023-06-09 20:59:40 +02:00
Felix Roos 576b88221d Merge remote-tracking branch 'origin/main' into workshop-new 2023-06-09 18:00:16 +02:00
Felix Roos 7dc2a0091b more detail 2023-06-09 17:59:08 +02:00
Felix Roos 89a4a85589 more friendly heading 2023-06-09 17:58:05 +02:00
Felix Roos a3baf07a5c Merge pull request #580 from roipoussiere/music_metadata
Music metadata
2023-06-09 17:31:58 +02:00
Felix Roos ae868c6372 hide gutter in mini repl 2023-06-09 00:46:04 +02:00
Felix Roos c287ff554c optimize sounds chapter after teach test 2023-06-09 00:26:40 +02:00
Felix Roos 3ccbd35fad add label support to pianoroll 2023-06-09 00:25:54 +02:00
Felix Roos 51cc69208f fix: repl link to docs was old 2023-06-09 00:25:14 +02:00
Felix Roos 1fec2f7a46 fix: build 2023-06-08 21:31:32 +02:00
Felix Roos ffffdd2d7d fix: format 2023-06-08 20:55:26 +02:00
Felix Roos f8c3e3d5aa add missing sounds by hand 2023-06-08 20:43:45 +02:00
Felix Roos eccce6fa84 remove dirt samples
+ load a small selection for workshop instead
2023-06-08 17:48:54 +02:00
Felix Roos 44be6cee2c Merge pull request #585 from srenatus/typo
learn/tonal: fix typo in "scaleTran[s]pose"
2023-06-08 09:33:34 +02:00
Stephan Renatus 799afd511a learn/tonal: fix typo in "scaleTran[s]pose" 2023-06-08 08:17:38 +02:00
Felix Roos 37c4541468 translate link 2023-06-07 21:50:26 +02:00
Felix Roos 119a66acd0 - translate getting-started
- fix de links
- link english docs / german workshop
2023-06-07 21:49:57 +02:00
Felix Roos 105193a452 replace tutorial with workshop
+ move parts of tutorial to more
+ move more before Pattern Functions
2023-06-07 21:22:01 +02:00
Felix Roos 02d793e9e0 Merge remote-tracking branch 'origin/main' into workshop-new 2023-06-07 20:19:34 +02:00
Felix Roos 90647a8ba8 Merge pull request #582 from roipoussiere/line_numbers
repl: add option to display line numbers
2023-06-07 20:19:11 +02:00
Felix Roos 06950a6bd8 translate rest of workshop to german 2023-06-07 16:50:54 +02:00
Roipoussiere 0c2147d9c5 repl: add option to display line numbers 2023-06-07 15:34:57 +02:00
Roipoussiere 51e8174689 metadata: add quotes syntax for titles 2023-06-07 12:26:23 +02:00
Roipoussiere 185318a70c metadata: fix typos 2023-06-07 11:02:38 +02:00
Felix Roos a88759ba94 translate notes chapter 2023-06-06 22:43:51 +02:00
Roipoussiere 96ac054392 metadata: improve doc 2023-06-06 21:34:53 +02:00
Roipoussiere bd7f4dd73f metadata: add tests 2023-06-06 21:34:53 +02:00
Roipoussiere 2e22152089 metadata: allow several values and one-liners 2023-06-06 21:34:53 +02:00
Roipoussiere 98a89aea11 lint: metadata parsing 2023-06-06 21:34:53 +02:00
Roipoussiere 372e33c066 doc: add section about metadata 2023-06-06 21:34:53 +02:00
Roipoussiere 52641db590 load titles from metadata in examples page 2023-06-06 21:34:53 +02:00
Roipoussiere f9ad5f0d01 add tunes metadata 2023-06-06 21:34:53 +02:00
Roipoussiere 3db1c7a521 swatch: look for song titles in code metadata 2023-06-06 21:34:53 +02:00
Felix Roos 61a9b01062 started german translation of workshop 2023-06-05 23:48:04 +02:00
Felix Roos f0faf5b42e Merge remote-tracking branch 'origin/main' into workshop-new 2023-06-05 22:48:50 +02:00
Felix Roos 11733d5e05 Merge pull request #563 from tidalcycles/vanilla-highlighting
Vanilla JS Refactoring
2023-06-05 21:35:49 +02:00
Felix Roos c4a38d9008 + pattern effects chapter
+ recap page
+ only load mini repls when visible
2023-05-30 06:16:02 +02:00
Felix Roos 0edd7e14b7 add compound adsr + ds controls 2023-05-29 12:39:36 +02:00
Felix Roos 8b7bb7b6ae add adsr section to effects chapter 2023-05-29 12:39:17 +02:00
Felix Roos 39d5955e58 add function recap to sounds chapter 2023-05-29 12:39:05 +02:00
Felix Roos 536327f403 effects chapter mostly finished 2023-05-29 02:18:36 +02:00
Felix Roos 8679dc63be pianoroll: also reflect gain in transparency 2023-05-29 02:18:27 +02:00
Felix Roos 1e9979ae18 start fleshing out effects chapter 2023-05-28 12:42:24 +02:00
Felix Roos d2dffe3186 MiniRepl: consume font settings 2023-05-28 12:42:15 +02:00
Felix Roos 9971867e2f clamp function 2023-05-28 12:41:53 +02:00
Felix Roos 8c93e578a0 clamp delayfeedback 2023-05-28 12:41:46 +02:00
Felix Roos 1ba5d2e1ca finish notes chapter 2023-05-27 21:52:44 +02:00
Felix Roos ed792fc0d4 continue notes chapter 2023-05-27 16:29:05 +02:00
Felix Roos 4e575c44b3 begin first notes page 2023-05-27 13:31:18 +02:00
Felix Roos fc06181217 - add claviature flag to minirepl
- bring back option+dot on macos
- consume more editor settings in minirepl
2023-05-27 13:30:57 +02:00
Felix Roos 0d6fcf78d8 hide mini repl headers + improve workshop 2023-05-26 16:05:53 +02:00
Felix Roos 82225f0b81 started workshop pages 2023-05-26 14:12:53 +02:00
Felix Roos 2177e13b49 Merge branch 'vanilla-highlighting' into workshop 2023-05-25 21:38:34 +02:00
Felix Roos d3240a75d0 fix: dupe style.css to Repl.css
for some reason they were missing after update
+ darken line backgrounds
+ change preview port to keep workbox out of dev server
2023-05-12 11:56:50 +02:00
Felix Roos d93e396ce5 fix: nano-repl 2023-05-12 10:21:05 +02:00
Felix Roos d74cc234c4 fix: thinner outline for highlights with .color 2023-05-12 10:06:55 +02:00
Felix Roos 0fda83d8de bump soundfonts 2023-05-12 10:00:55 +02:00
Felix Roos a1ec8545fb format 2023-05-10 13:03:06 +02:00
Felix Roos 087f574355 update web readme + add more html examples 2023-05-10 12:49:45 +02:00
Felix Roos 1de784b817 add headless-serverless-buildless example 2023-05-10 12:20:27 +02:00
Felix Roos db79c05d9a bump web pkg dependencies 2023-05-10 12:11:51 +02:00
Felix Roos 1fdb1f0da6 bump webaudio package 2023-05-10 12:09:20 +02:00
Felix Roos e2bfaa6233 remove soundfonts from web package for now 2023-05-10 12:06:16 +02:00
Felix Roos 6089849d35 rename init to initStrudel + add to window 2023-05-10 11:53:26 +02:00
Felix Roos 6e02bf59e9 Revert "refactor: remove old draw logic"
This reverts commit 95719654f3.
2023-05-10 11:44:55 +02:00
Felix Roos 12228c56d9 improve api for web package 2023-05-07 22:36:26 +02:00
Felix Roos 3b631cb6af add @strudel/web umbrella package + example 2023-05-07 11:56:35 +02:00
Felix Roos b27a58df7e bump more packages to 0.8.0 2023-05-05 23:58:47 +02:00
Felix Roos 308f139193 bump codemirror again to get new core version 2023-05-05 23:52:55 +02:00
Felix Roos 692fc4e11d revert rename core package 2023-05-05 23:52:04 +02:00
Felix Roos b52f65c4d7 rename core package + bump to 0.8.0 2023-05-05 23:50:06 +02:00
Felix Roos 299275e368 lockfile 2023-05-05 23:47:15 +02:00
Felix Roos bb71c519db bump codemirror package 2023-05-05 23:46:42 +02:00
Felix Roos 9ec7109dc8 + simplify vanilla setup drastically
+ move repl + drawer boilerplate inside StrudelMirror
2023-05-05 23:43:07 +02:00
Felix Roos 5b67fccb1b vanilla-repl-cm6: draw first frame 2023-05-05 16:17:22 +02:00
Felix Roos 9807c4b7a1 move Framer + Drawer to core
... from vite-vanilla-repl-cm6
2023-05-05 16:06:11 +02:00
Felix Roos 9039600f6d bump codemirror to 0.8.1 2023-05-05 15:31:19 +02:00
Felix Roos b5b0156e08 fix: codemirror reeval 2023-05-05 15:31:04 +02:00
Felix Roos 352e647d37 add build script for codemirror package 2023-05-05 15:15:45 +02:00
Felix Roos e6b66f2525 lockfile 2023-05-05 15:14:46 +02:00
Felix Roos f5075906e2 add codemirror package
+ use it in vite-vanilla-repl-cm6
2023-05-05 15:13:17 +02:00
Felix Roos a6f57bced8 prettier 2023-05-05 12:00:13 +02:00
Felix Roos d35bf9591c vanilla-repl-cm6: better theme 2023-05-05 11:54:09 +02:00
Felix Roos d96f3f9dd4 vanilla-repl-cm6: style + codesplitting 2023-05-05 11:09:44 +02:00
Felix Roos 1f4c2f8c5a draw scheduler in vanilla js 2023-05-05 10:52:21 +02:00
Felix Roos 95719654f3 refactor: remove old draw logic
+ pianoroll now uses .onPaint
2023-05-05 09:52:07 +02:00
Felix Roos 4b921c47f5 vanilla-repl-cm6: add flash effect 2023-05-05 09:11:40 +02:00
Felix Roos f069f53fae vanilla-repl-cm6: add keybindings 2023-05-05 09:06:27 +02:00
Felix Roos 1494cc38fc add vite-vanilla-repl-cm6 2023-05-05 08:55:43 +02:00
Felix Roos a035412956 Merge pull request #559 from tidalcycles/patchday
Patchday
2023-05-05 08:34:36 +02:00
Felix Roos 08662ea831 Merge pull request #557 from tidalcycles/some-voicings
add basic triads and guidetone voicings
2023-05-02 21:48:32 +02:00
Felix Roos d97103268b add basic triads and guidetone voicings 2023-05-02 21:40:20 +02:00
Felix Roos a959e2e34d update remaining packages 2023-04-30 08:30:29 +02:00
Felix Roos 602bfbdd79 update a bunch of minor versions 2023-04-30 08:20:51 +02:00
Felix Roos 0785bdd37a update lerna 2023-04-30 08:04:56 +02:00
Felix Roos 8b62b6e1b1 update codemirror + tailwindcss 2023-04-30 08:02:06 +02:00
Felix Roos d1c8ebd15b remove c8 2023-04-30 07:55:23 +02:00
Felix Roos 13088e4001 - remove gh-pages
- update peggy
- rebuild parser
2023-04-30 07:48:08 +02:00
Felix Roos 5c6fa1c82b update vite to v4 2023-04-29 08:10:12 +02:00
Felix Roos fd81867e7b fix: controls import broke on ssr 2023-04-29 07:45:21 +02:00
Felix Roos 78293e64dd lockfile 2023-04-29 07:45:09 +02:00
Felix Roos 930fc10aec bump some minor versions 2023-04-29 07:45:04 +02:00
Felix Roos 0b2a4fda44 update supabase-js 2023-04-29 07:24:03 +02:00
Felix Roos efdce6d4cd update astro 2023-04-29 06:55:34 +02:00
Felix Roos c0b0ea4493 remove preact 2023-04-29 06:25:19 +02:00
Felix Roos 0fcec0a0ca upgrade jsdoc 2023-04-28 13:05:50 +02:00
Felix Roos c0701d8a35 Merge remote-tracking branch 'origin/main' into patchday 2023-04-28 12:50:20 +02:00
Felix Roos ae51092044 Merge pull request #556 from tidalcycles/fix-prebake
fix: make soundfonts import dynamic
2023-04-28 12:46:56 +02:00
Felix Roos b7fcccb8db fix: make soundfonts import dynamic
- it broke the whole docs page
2023-04-28 12:42:07 +02:00
Felix Roos b21a52f5f3 update @vite-pwa/astro 2023-04-28 12:23:10 +02:00
Felix Roos 2bdcfe13e6 update @vitest/ui 2023-04-28 12:20:35 +02:00
Felix Roos 12a97ec429 update nanostores 2023-04-28 12:18:45 +02:00
Felix Roos 99298ce9f2 update vite-plugin-pwa 2023-04-28 12:14:07 +02:00
Felix Roos a0ed25d373 use vitests@^0.28.0 in packages 2023-04-28 12:12:05 +02:00
Felix Roos dc6c29c668 update vitest to ^0.28.0 2023-04-28 12:08:48 +02:00
Felix Roos 3a85f28b5c install with pnpm 8.3.1 2023-04-28 10:33:47 +02:00
Felix Roos 95c44b1ce2 Merge pull request #553 from tidalcycles/highlighting-colors
fix: colorable highlighting
2023-04-22 15:43:22 +02:00
Felix Roos b88e0ecb78 fix: colorable highlighting 2023-04-22 15:25:00 +02:00
Felix Roos c26f6b72d9 Merge pull request #550 from tidalcycles/fix-soundfont-loading
fix: load soundfonts in prebake
2023-04-06 00:11:02 +02:00
Felix Roos 6cd3cc96a3 fix: do not prebake in tests
+ pull piano function to separate file for tests
2023-04-06 00:08:23 +02:00
Felix Roos c138cf0b76 fix: load soundfonts in prebake 2023-04-06 00:00:49 +02:00
Felix Roos 8edb086178 Merge pull request #548 from tidalcycles/stop-pause
fix: reset time on stop
2023-03-29 22:23:07 +02:00
Felix Roos 0f74b5a4b1 Merge pull request #547 from tidalcycles/fix-mini-whitespace
fix: allow whitespace at the end of a mini pattern
2023-03-29 22:16:37 +02:00
Felix Roos 6de2feff05 fix: reset time on stop 2023-03-29 22:05:13 +02:00
Felix Roos a9811ee955 fix: allow whitespace at the end of a mini pattern
- fixes #546
2023-03-29 21:57:55 +02:00
Felix Roos 98b6c11a82 Merge pull request #544 from tidalcycles/fonts
add firacode font
2023-03-24 22:10:55 +01:00
Felix Roos 3df2929d12 dummy commit (github cert error :-/) 2023-03-24 22:06:40 +01:00
Felix Roos 30d1f423cb fix: remove loading state on eval error 2023-03-24 21:50:50 +01:00
Felix Roos e0c5566f35 add 2 FiraCode fonts 2023-03-24 21:23:37 +01:00
Felix Roos c5a2ae47d7 Merge pull request #542 from tidalcycles/loading-animation
feat: add loader bar to animate loading state
2023-03-24 12:54:20 +01:00
Felix Roos 17654b885b feat: add loader bar to animate loading state 2023-03-24 12:41:00 +01:00
Felix Roos acaf0ca0af Merge pull request #539 from tidalcycles/setcps-reset-only-on-shuffle
do not reset cps before eval #517
2023-03-23 22:39:26 +01:00
Felix Roos 6a201be1fd Merge remote-tracking branch 'origin/main' into setcps-reset-only-on-shuffle 2023-03-23 22:35:57 +01:00
Felix Roos 8617b9164e do not reset cps before eval #517 2023-03-23 22:34:25 +01:00
Felix Roos 991e5f1b3c Merge pull request #538 from tidalcycles/improve-loading
improve initial loading + wait before eval
2023-03-23 22:27:31 +01:00
Felix Roos 55c533c947 improve initial loading + wait before eval 2023-03-23 21:56:20 +01:00
Felix Roos d17543d5d9 Merge pull request #537 from tidalcycles/fix-keypress-period
fix period key for dvorak + remove duplicated code
2023-03-23 21:40:19 +01:00
Felix Roos 73169563c4 fix period key for dvorak + remove duplicated code 2023-03-23 21:37:38 +01:00
Felix Roos d351cb9f7f Merge pull request #535 from tidalcycles/update-lerna
Update lerna
2023-03-23 11:44:56 +01:00
Felix Roos 251be60630 update package publishing guide 2023-03-23 11:39:33 +01:00
Felix Roos 18d7222804 Publish
- @strudel.cycles/core@0.7.2
 - @strudel.cycles/csound@0.7.1
 - @strudel.cycles/eval@0.7.1
 - @strudel.cycles/midi@0.7.1
 - @strudel.cycles/mini@0.7.2
 - @strudel.cycles/osc@0.7.1
 - @strudel.cycles/react@0.7.1
 - @strudel.cycles/serial@0.7.1
 - @strudel.cycles/soundfonts@0.7.1
 - @strudel.cycles/tonal@0.7.1
 - @strudel.cycles/tone@0.7.1
 - @strudel.cycles/transpiler@0.7.1
 - @strudel.cycles/webaudio@0.7.1
 - @strudel.cycles/webdirt@0.7.1
 - @strudel.cycles/xen@0.7.1
2023-03-23 11:34:07 +01:00
Felix Roos ff1c37d361 bump core + mini to 0.7.1 2023-03-23 11:26:23 +01:00
Felix Roos 3abe6db3de Publish
- @strudel.cycles/core@0.7.0
 - @strudel.cycles/csound@0.7.0
 - @strudel.cycles/eval@0.7.0
 - @strudel.cycles/midi@0.7.0
 - @strudel.cycles/mini@0.7.0
 - @strudel.cycles/osc@0.7.0
 - @strudel.cycles/react@0.7.0
 - @strudel.cycles/serial@0.7.0
 - @strudel.cycles/soundfonts@0.7.0
 - @strudel.cycles/tonal@0.7.0
 - @strudel.cycles/tone@0.7.0
 - @strudel.cycles/transpiler@0.7.0
 - @strudel.cycles/webaudio@0.7.0
 - @strudel.cycles/webdirt@0.7.0
 - @strudel.cycles/xen@0.7.0
2023-03-23 11:21:02 +01:00
Felix Roos b9ca9d3b06 update lerna 2023-03-23 11:17:21 +01:00
Felix Roos 9bbc04843d Merge pull request #534 from tidalcycles/soundfont-freq-support
feat: add freq support to gm soundfonts
2023-03-23 10:21:55 +01:00
Felix Roos ddf61a6438 Merge pull request #531 from ijc8/main
Maintain random seed state in parser, not globally
2023-03-23 10:18:58 +01:00
Felix Roos ba35a81e9b - feat: add freq support to gm soundfonts
- refactor: toMidi -> noteToMidi
- refactor: fromMidi -> midiToFreq
2023-03-23 10:18:24 +01:00
Felix Roos 13133583ca Merge pull request #527 from bwagner/main
FIXES: alias pm for polymeter
2023-03-21 22:35:17 +01:00
Felix Roos e91b981740 Merge pull request #529 from revolunet/patch-1
fix(footer): fix link to tidalcycles
2023-03-21 22:29:06 +01:00
Bernhard Wagner 1458ee64ec FIXES: alias pm for polymeter 2023-03-21 21:20:57 +01:00
Ian Clester 4e2a5e1663 Add pairwise independence test, tweak randOffset 2023-03-20 22:27:35 -04:00
Ian Clester c98b0e9687 Format code with prettier 2023-03-20 19:19:42 -04:00
Ian Clester 4e25990de9 Uncomment test for random choice operator (|) 2023-03-20 17:46:57 -04:00
Ian Clester 4b51f1a3fc Sync up ? with degrade() 2023-03-20 15:26:27 -04:00
Ian Clester 5490b01004 Maintain random seed state in parser, not globally 2023-03-20 14:59:43 -04:00
Julien Bouquillon 43176e1f1d fix(footer): fix link to tidalcycles 2023-03-20 01:38:21 +01:00
Bernhard Wagner fffd3c07ca FIXES: alias pm for polymeter
(return statement was missing)
2023-03-18 22:12:12 +01:00
Felix Roos 952bd38e70 Merge pull request #525 from bwagner/patch-4
Update intro.mdx
2023-03-18 20:25:21 +01:00
Felix Roos 8589f5cc89 Merge pull request #524 from bwagner/patch-3
Update samples.mdx
2023-03-18 20:24:38 +01:00
Bernhard Wagner ec594d52f8 Update intro.mdx
shortly -> concisely
2023-03-18 19:31:57 +01:00
Bernhard Wagner 848cbf9c60 Update samples.mdx
fixes few typos and the former `samples` tab is now `sounds`
2023-03-18 15:37:59 +01:00
Felix Roos e02be7dd9a Merge pull request #521 from tidalcycles/fix-envelope
fix: envelopes in chrome
2023-03-17 09:03:10 +01:00
Felix Roos 33d35e47a6 fix: envelope in chrome 2023-03-17 08:56:05 +01:00
Felix Roos 18d62b32c2 Merge pull request #516 from tidalcycles/source-nodes
registerSound API + improved sounds tab + regroup soundfonts
2023-03-16 16:13:30 +01:00
Felix Roos 58ed51b49c snapshot 2023-03-16 15:49:56 +01:00
Felix Roos 9f9fe529e4 fix only tune that uses soundfonts 2023-03-16 15:49:08 +01:00
Felix Roos aad721890a pass duration to source callback 2023-03-16 15:48:59 +01:00
Felix Roos e63a5d10c8 regroup sounds
- extra tab for drum-machines
- combine synths and soundfonts
2023-03-16 15:21:16 +01:00
Felix Roos 15681262de use only _ for easier copying via double click 2023-03-16 15:19:38 +01:00
Felix Roos b5d3aa81f9 format 2023-03-16 12:25:08 +01:00
Felix Roos a688061616 group soundfonts into readable names 2023-03-16 12:23:58 +01:00
Felix Roos f788d347ec add envelope to soundfonts 2023-03-15 23:02:05 +01:00
Felix Roos a33ce67dd4 fix: endless preview 2023-03-15 23:01:56 +01:00
Felix Roos 8a22c5627a fix: cut glitches due to envelope release trouble 2023-03-15 22:42:03 +01:00
Felix Roos 7b529a30f4 style sounds tab 2023-03-14 22:54:12 +01:00
Felix Roos 4910b34f26 Merge pull request #518 from tidalcycles/fonts
add 2 illegible fonts
2023-03-14 21:54:52 +01:00
Felix Roos 6a73b43e3a add 2 illegible fonts 2023-03-12 21:16:46 +01:00
Felix Roos b59b26460d soundfont list curation part 2 2023-03-12 20:48:29 +01:00
Felix Roos aa324ae25d start curating the soundfont list 2023-03-10 11:33:55 +01:00
Felix Roos ff412f6396 level down soundfonts
+ do not load in prebake (breaks static rendering)
+ instead load on import
2023-03-09 23:19:19 +01:00
Felix Roos ac148b2f32 register soundfonts as sounds too 2023-03-09 22:48:40 +01:00
Felix Roos 6f5d096e6d rename setSound to registerSound 2023-03-09 21:32:40 +01:00
Felix Roos ff1e6714c6 document setSound api 2023-03-09 21:27:24 +01:00
Felix Roos 41d82eaadf add cleanup to sound tab 2023-03-09 10:53:35 +01:00
Felix Roos be6b011204 proper audio node cleanup
+ cleanup onTriggerSample
2023-03-09 10:46:45 +01:00
Felix Roos 65e48c05f0 sounds onset and offset can be triggered independently
+ sounds tab now supports mousedown / mouseup to listen
2023-03-09 08:35:20 +01:00
Felix Roos cee08ea67d sounds list can now filter out defaults 2023-03-08 00:33:21 +01:00
Felix Roos 35ef26c013 can now pass meta data to setSound
+ added types for synth and sample
+ list sample count for samples in sounds tab (as before)
2023-03-07 21:43:01 +01:00
Felix Roos b08a0b8102 use nanostore for soundmap
+ rename tab samples to sounds
+ listed sounds are now reactive
2023-03-06 23:11:09 +01:00
Felix Roos 220342154b Merge pull request #474 from bwagner/main
Update README.md
2023-03-06 22:55:00 +01:00
Bernhard Wagner ef7182cd76 Merge branch 'tidalcycles:main' into main 2023-03-06 20:15:16 +01:00
Felix Roos 6059c69995 first draft of soundMap to register keys for s
+ refactor sampler to use it
+ refactor synth to use it
+ add 'source' control
+ wip: samples tab
+ wip: webadirt ?
+ wip: soundfonts
2023-03-05 23:27:37 +01:00
Felix Roos 892d8ba152 Merge pull request #508 from tidalcycles/arrange
add arrange function
2023-03-05 14:52:30 +01:00
Felix Roos da11069f75 Merge pull request #514 from tidalcycles/react-update
update react to 18
2023-03-05 14:29:08 +01:00
Alex McLean bf72908dc9 Support list syntax in mininotation (#512)
fixes #504

* support list syntax in mininotation
* support compound controls
* remove redundant : splitting of s/n and note/n from webaudio
* patternable scale names
2023-03-04 18:06:18 +00:00
Felix Roos 81ab5c78e2 update react to 18 2023-03-03 20:38:20 +01:00
Felix Roos de503c7d63 add snapshot 2023-03-03 20:30:35 +01:00
Felix Roos 487ccb90a1 add arrange function 2023-03-03 20:17:23 +01:00
Felix Roos 4bfeaa47bf Merge pull request #502 from tidalcycles/scale-space-alternative
can now use : as a replacement for space in scales
2023-03-03 12:40:23 +01:00
Felix Roos 9a4d8036a6 add test 2023-03-03 12:38:11 +01:00
Felix Roos a50bd6375d use : instead of _ 2023-03-03 12:37:25 +01:00
Felix Roos 14508e91a9 can now use _ as a replacement for space in scales 2023-03-02 20:39:06 +01:00
Alex McLean 2d5064bd5b Reinstate slice and splice (#500)
Reinstate slice/splice, fixing up "unit" param using new global cps
2023-03-02 14:44:41 +00:00
Felix Roos fdc4819e39 Merge pull request #501 from tidalcycles/nano-repl-highlighting-fix
fix: nano-repl highlighting
2023-03-02 14:49:30 +01:00
Felix Roos efddbb616b fix: nano-repl highlighting 2023-03-02 14:47:15 +01:00
Alex McLean db4d0f26a8 Add control aliases (#497)
* add and simplify control aliases, fixes #496
* roomsize now alias of room
* simplify webaudio param handling
---------

Co-authored-by: Felix Roos <flix91@gmail.com>
2023-03-02 13:17:12 +00:00
Felix Roos 06cffbc31b add readme to nano repl 2023-03-01 12:57:33 +01:00
Felix Roos 3cfb0f4c76 Merge pull request #493 from tidalcycles/cps
implement cps in scheduler
2023-03-01 09:27:26 +01:00
Felix Roos 436ba067bc reset cps before eval 2023-03-01 09:23:01 +01:00
Felix Roos f8724bf47e comment out cps ui 2023-03-01 00:05:23 +01:00
Felix Roos 1f7e293204 add setcps to global scope 2023-02-28 23:56:11 +01:00
Felix Roos 86d2652258 pass cps to hap trigger 2023-02-28 23:54:56 +01:00
Felix Roos f5c0cc2e2d no need to warn if evalScope is called more often 2023-02-28 23:54:40 +01:00
Felix Roos 4463d4a417 Update README.md 2023-02-28 23:25:40 +01:00
Felix Roos df35a5c9b1 Merge pull request #491 from tidalcycles/refactor-react
react style fixes
2023-02-28 23:06:29 +01:00
Felix Roos dda93b3c13 Update @strudel.cycles/react to 0.6.4 2023-02-28 23:02:52 +01:00
Felix Roos 5de40b6bf5 fix disabled button take 2 2023-02-28 23:02:28 +01:00
Felix Roos dcbe799d25 Merge pull request #490 from tidalcycles/refactor-react
refactor react package
2023-02-28 22:57:19 +01:00
Felix Roos a5641aef03 Update @strudel.cycles/react to 0.6.3 2023-02-28 22:52:57 +01:00
Felix Roos 668f97a4c8 refactor react package for publishing 2023-02-28 22:48:50 +01:00
Felix Roos e9d577496b fix disabled button 2023-02-28 22:48:06 +01:00
Felix Roos 66fba947ac Update @strudel.cycles/react to 0.6.2 2023-02-28 22:40:52 +01:00
Felix Roos 18c7e02609 fix: minirepl styles for package 2023-02-28 22:40:18 +01:00
Felix Roos 7e1351507a add variables to react tailwind config 2023-02-28 22:17:24 +01:00
Felix Roos 032acbd27c comment out autocomplete 2023-02-28 22:11:17 +01:00
Felix Roos 8b51f0fbd2 Update @strudel.cycles/react to 0.6.1 2023-02-28 21:57:03 +01:00
Felix Roos ead5942ef0 implement cps + add baasic tempo control ui 2023-02-28 13:00:35 +01:00
Felix Roos cfe369d9fe Merge pull request #488 from tidalcycles/algolia
add algolia creds + optimize sidebar for crawling
2023-02-27 23:47:34 +01:00
Felix Roos 2750a3c130 fix pseudo relative links 2023-02-27 22:59:50 +01:00
Felix Roos a7f6d6ecac add algolia config 2023-02-27 22:59:38 +01:00
Felix Roos 3680282450 Merge pull request #485 from tidalcycles/app-height-fix
fix app height
2023-02-27 19:28:09 +01:00
Felix Roos 3b3435c999 change 1px 2023-02-27 19:24:49 +01:00
Felix Roos e2ae6278c2 Merge pull request #484 from tidalcycles/revert-wip-390
Revert "Another attempt at composable functions - WIP (#390)"
2023-02-27 16:04:20 +01:00
Alex McLean 37d6723e3c Update mini-notation.mdx (#365)
`x!n` is no longer an alias for `[x*n]@n` and the behaviour is a bit different. Probably best to remove this line in case it confuses someone?
2023-02-27 14:45:19 +00:00
Felix Roos 961bbf6f55 Revert "Another attempt at composable functions - WIP (#390)"
This reverts commit cbae355896.
2023-02-27 15:20:49 +01:00
Felix Roos fc71d349ed Merge pull request #482 from tidalcycles/docs
docs: packages + offline
2023-02-27 12:52:09 +01:00
Felix Roos 3015e7b203 Merge pull request #480 from tidalcycles/fix-array-args
Fix array args
2023-02-25 14:26:49 +01:00
Felix Roos 7ac0cdc0f9 fix: lint error 2023-02-25 14:18:23 +01:00
Felix Roos 49234183a3 Merge remote-tracking branch 'origin/main' into fix-array-args 2023-02-25 14:13:55 +01:00
Felix Roos ed7763df92 fix: reify after sequence to preserve array
ref https://github.com/tidalcycles/strudel/issues/383#issuecomment-1399514090
2023-02-25 14:13:39 +01:00
Felix Roos b818a02f76 Merge pull request #478 from tidalcycles/midi-fleshout
midi cc support
2023-02-25 12:33:22 +01:00
Felix Roos 886f8449fd add midichan + docs 2023-02-25 12:23:01 +01:00
Felix Roos 5de6643604 midi: support ccn and ccv 2023-02-24 10:15:21 +01:00
Felix Roos 6283273d81 add helper hap.ensureObjectValue 2023-02-24 10:15:06 +01:00
Bernhard Wagner 5506d18bc5 Update README.md
npm -> pnpm
2023-02-23 18:28:11 +01:00
Felix Roos c8f8f02a46 Merge pull request #473 from tidalcycles/fix-hashes
fix: hash links
2023-02-23 00:11:04 +01:00
Felix Roos 4c5d6f1d6c fix: hash links 2023-02-23 00:04:53 +01:00
Felix Roos f349e36345 Merge pull request #467 from tidalcycles/vim-mode
settings tab with vim / emacs modes + additional themes and fonts
2023-02-22 22:54:39 +01:00
Felix Roos 1574c4ab13 Merge pull request #471 from bwagner/patch-1
Update input-output.mdx
2023-02-22 22:53:22 +01:00
Felix Roos 0dc9bc3ab6 Merge pull request #464 from bwagner/main
FIXES: freqs instead of pitches
2023-02-22 22:52:49 +01:00
Felix Roos 0006d57a4a move isZen to nanostore
+ correctly parse non strings in store
2023-02-22 22:47:33 +01:00
Felix Roos 70f7e73b9a store footer tab + latest code in nanostore 2023-02-22 22:23:54 +01:00
Felix Roos b67b049802 refactor settings to nanostores 2023-02-22 22:04:39 +01:00
Felix Roos ff99dbcd22 customizable font family 2023-02-22 20:28:22 +01:00
Felix Roos 0198697737 Merge pull request #472 from tidalcycles/osc-fix
fix: osc should not return a promise
2023-02-22 20:01:00 +01:00
Felix Roos a742a71f67 fix: osc should not return a promise 2023-02-22 19:50:37 +01:00
Felix Roos 8f6c1531fa more contrast for blackscreen theme 2023-02-22 19:45:06 +01:00
Bernhard Wagner d9f56f11cb Merge pull request #1 from tidalcycles/main
a
2023-02-22 17:15:00 +01:00
Bernhard Wagner 7ff877464f Update input-output.mdx
npm -> pnpm
2023-02-22 17:06:15 +01:00
Alex McLean 7c367eb1e8 slice and splice (#466)
Implements `slice` and `splice` from tidal, intended for beat slicing
2023-02-22 11:51:31 +00:00
Felix Roos 20848aac09 fix store import 2023-02-19 23:28:46 +01:00
Felix Roos 7716574076 use terminal font for console + samples 2023-02-19 23:28:33 +01:00
Felix Roos 89cd0c769b fix bluescreen line bg 2023-02-19 23:27:56 +01:00
Felix Roos a21b3d788f add settings reset button 2023-02-19 22:32:28 +01:00
Felix Roos 7994ba8b38 get rid of all hard coded colors 2023-02-19 22:18:32 +01:00
Felix Roos 81b4cb2f16 more themes + fonts 2023-02-19 22:18:12 +01:00
Felix Roos 14af1df6a3 define font + fix app height 2023-02-19 20:04:30 +01:00
Felix Roos ac3bd7d7db rename algoboy theme + add font 2023-02-19 20:04:19 +01:00
Felix Roos 632e8e9634 use css var for highlighting color 2023-02-19 20:03:12 +01:00
Felix Roos 1e3f09f69b add some custom themes 2023-02-19 18:53:35 +01:00
Felix Roos f3f18ffca7 fix app height for ios 2023-02-19 13:50:08 +01:00
Felix Roos ceb3aa0627 add emacs mode + fontSize slider 2023-02-19 13:49:55 +01:00
Felix Roos 014555fe5d add vim toggle to settings
+ added persistent global state store
+ refactored themes to use the new store
2023-02-19 01:51:31 +01:00
Alex McLean fdb76867a7 weave and weaveWith (#465)
Added weave and weaveWith from tidal
2023-02-18 00:00:18 +00:00
Felix Roos 4a3540cf2b encapsulate footer components 2023-02-17 21:48:45 +01:00
Felix Roos 540bd938f2 remove unused Footer
+ fix AvatarList (still unused)
2023-02-17 21:48:34 +01:00
Bernhard Wagner c6fbebd996 FIXES: freqs instead of pitches 2023-02-17 19:05:09 +01:00
Felix Roos 18d3a7e23a add vim extension (hard coded) 2023-02-17 10:41:49 +01:00
Felix Roos 70d1eb17a7 Merge branch 'main' into vim-mode 2023-02-16 11:41:01 +01:00
188 changed files with 16478 additions and 8063 deletions
+4 -1
View File
@@ -39,4 +39,7 @@ server/samples/old
repl/stats.html
coverage
public/icons/apple-splash-*
dev-dist
dev-dist
Dirt-Samples
tidal-drum-machines
webaudiofontdata
+8 -1
View File
@@ -123,7 +123,14 @@ To publish all packages that have been changed since the last release, run:
```sh
npm login
npx lerna publish
# this will increment all the versions in package.json files of non private packages to selected versions
npx lerna version --no-private
# publish all packages inside /packages using pnpm! don't use lerna to publish!!
pnpm --filter "./packages/**" publish --dry-run
# the last command was only a dry-run, make sure everything looks ok, if yes, run the same command without flag
```
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
+2 -2
View File
@@ -14,8 +14,8 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
After cloning the project, you can run the REPL locally:
```bash
npm run setup
npm run repl
pnpm run setup
pnpm run repl
```
## Using Strudel In Your Project
+11 -13
View File
@@ -18,7 +18,6 @@
"build": "npm run prebuild && cd website && npm run build",
"preview": "cd website && npm run preview",
"osc": "cd packages/osc && npm run server",
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
"lint": "eslint . --ext mjs,js --quiet",
@@ -46,28 +45,27 @@
},
"homepage": "https://strudel.tidalcycles.org",
"dependencies": {
"dependency-tree": "^9.0.0",
"vitest": "^0.25.7",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/xen": "workspace:*"
"@strudel.cycles/xen": "workspace:*",
"acorn": "^8.8.1",
"dependency-tree": "^9.0.0",
"vitest": "^0.28.0"
},
"devDependencies": {
"@vitest/ui": "^0.25.7",
"c8": "^7.12.0",
"canvas": "^2.11.0",
"eslint": "^8.28.0",
"@vitest/ui": "^0.28.0",
"canvas": "^2.11.2",
"eslint": "^8.39.0",
"eslint-plugin-import": "^2.27.5",
"events": "^3.3.0",
"gh-pages": "^4.0.0",
"jsdoc": "^3.6.10",
"jsdoc": "^4.0.2",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^7.1.1",
"lerna": "^4.0.0",
"prettier": "^2.8.1",
"jsdoc-to-markdown": "^8.0.0",
"lerna": "^6.6.1",
"prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.8.1"
}
}
+3
View File
@@ -0,0 +1,3 @@
# @strudel/codemirror
This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it.
+199
View File
@@ -0,0 +1,199 @@
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 { javascript } from '@codemirror/lang-javascript';
import { StateField, StateEffect } from '@codemirror/state';
import { oneDark } from './themes/one-dark';
import { repl, Drawer } from '@strudel.cycles/core';
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
let state = EditorState.create({
doc: initialCode,
extensions: [
theme,
javascript(),
lineNumbers(),
highlightField,
highlightActiveLineGutter(),
syntaxHighlighting(defaultHighlightStyle),
keymap.of(defaultKeymap),
flashField,
EditorView.updateListener.of((v) => onChange(v)),
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate(),
},
{
key: 'Ctrl-.',
run: () => onStop(),
},
]),
],
});
return new EditorView({
state,
parent: root,
});
}
//
// 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;
this.code = initialCode;
this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.whole.end);
this.highlight(currentFrame);
onDraw?.(haps, time, currentFrame);
}, drawTime);
const prebaked = prebake();
prebaked.then(async () => {
if (!onDraw) {
return;
}
const { scheduler, evaluate } = await this.repl;
// draw first frame instantly
prebaked.then(async () => {
await evaluate(this.code, false);
this.drawer.invalidate(scheduler);
onDraw?.(this.drawer.visibleHaps, 0, []);
});
});
this.repl = repl({
...replOptions,
onToggle: async (started) => {
replOptions?.onToggle?.(started);
const { scheduler } = await this.repl;
if (started) {
this.drawer.start(scheduler);
} else {
this.drawer.stop();
}
},
beforeEval: async () => {
await prebaked;
},
afterEval: (options) => {
replOptions?.afterEval?.(options);
this.drawer.invalidate();
},
});
this.editor = initEditor({
root,
initialCode,
onChange: (v) => {
this.code = v.state.doc.toString();
},
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
});
}
async evaluate() {
const { evaluate } = await this.repl;
this.flash();
await evaluate(this.code);
}
async stop() {
const { scheduler } = await this.repl;
scheduler.stop();
}
flash(ms) {
flash(this.editor, ms);
}
highlight(haps) {
highlightHaps(this.editor, haps);
}
}
+47
View File
@@ -0,0 +1,47 @@
{
"name": "@strudel/codemirror",
"version": "0.8.3",
"description": "Codemirror Extensions for Strudel",
"main": "codemirror.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": [
"Alex McLean <alex@slab.org>"
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/language": "^6.6.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^4.3.3"
}
}
+139
View File
@@ -0,0 +1,139 @@
import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags as t } from '@lezer/highlight';
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
const chalky = '#e5c07b',
coral = '#e06c75',
cyan = '#56b6c2',
invalid = '#ffffff',
ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
malibu = '#61afef',
sage = '#98c379',
whiskey = '#d19a66',
violet = '#c678dd',
darkBackground = '#21252b',
highlightBackground = '#2c313a',
background = '#282c34',
tooltipBackground = '#353a42',
selection = '#3E4451',
cursor = '#528bff';
/// The colors used in the theme, as CSS color strings.
export const color = {
chalky,
coral,
cyan,
invalid,
ivory,
stone,
malibu,
sage,
whiskey,
violet,
darkBackground,
highlightBackground,
background,
tooltipBackground,
selection,
cursor,
};
/// The editor theme styles for One Dark.
export const oneDarkTheme = EditorView.theme(
{
'&': {
color: ivory,
backgroundColor: background,
},
'.cm-content': {
caretColor: cursor,
},
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
{ backgroundColor: selection },
'.cm-panels': { backgroundColor: darkBackground, color: ivory },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
},
'.cm-gutters': {
backgroundColor: background,
color: stone,
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: true },
);
/// The highlighting style for code in the One Dark theme.
export const oneDarkHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: violet },
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
{ tag: [t.definition(t.name), t.separator], color: ivory },
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
{ tag: [t.meta, t.comment], color: stone },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.link, color: stone, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: coral },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
{ tag: t.invalid, color: invalid },
]);
/// Extension to enable the One Dark theme (both the editor theme and
/// the highlight style).
export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'codemirror.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+2 -1
View File
@@ -1,4 +1,5 @@
import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
import { Pattern, getDrawContext, silence, register, pure } from './index.mjs';
import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
const { createParams } = controls;
let clearColor = '#22222210';
+259 -291
View File
@@ -4,20 +4,34 @@ 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 { Pattern, sequence, registerControl } from './pattern.mjs';
import { Pattern, register, sequence } from './pattern.mjs';
import { zipWith } from './util.mjs';
const controls = {};
const generic_params = [
/**
* Select a sound / sample by name.
* Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters
* separated by ':'.
*
* @name s
* @param {string | Pattern} sound The sound / pattern of sounds to pick
* @synonyms sound
* @example
* s("bd hh")
* @example
* s("bd:0 bd:1 bd:0:0.3 bd:1:1.4")
*
*/
['s', 's', 'sound'],
[['s', 'n', 'gain'], 'sound'],
/**
* Define a custom webaudio node to use as a sound source.
*
* @name source
* @param {function} getSource
* @synonyms src
*
*/
['source', 'src'],
/**
* Selects the given index from the sample map.
* Numbers too high will wrap around.
@@ -29,7 +43,7 @@ const generic_params = [
* s("bd sd,hh*3").n("<0 1>")
*/
// also see https://github.com/tidalcycles/strudel/pull/63
['f', 'n', 'The sample number to choose for a synth or sampleset'],
['n'],
/**
* Plays the given note name or midi number. A note name consists of
*
@@ -49,10 +63,8 @@ const generic_params = [
* @example
* note("60 69 65 64")
*/
['f', 'note', 'The note or pitch to play a sound or synth with'],
//['s', 'toArg', 'for internal sound routing'],
// ["f", "from", "for internal sound routing"),
//['f', 'to', 'for internal sound routing'],
[['note', 'n']],
/**
* A pattern of numbers that speed up (or slow down) samples while they play. Currently only supported by osc / superdirt.
*
@@ -63,7 +75,7 @@ const generic_params = [
* s("sax").accelerate("<0 1 2 4 8 16>").slow(2).osc()
*
*/
['f', 'accelerate', 'a pattern of numbers that speed up (or slow down) samples while they play.'],
['accelerate'],
/**
* Controls the gain by an exponential amount.
*
@@ -73,11 +85,7 @@ const generic_params = [
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1")
*
*/
[
'f',
'gain',
'a pattern of numbers that specify volume. Values less than 1 make the sound quieter. Values greater than 1 make the sound louder. For the linear equivalent, see @amp@.',
],
['gain'],
/**
* Like {@link gain}, but linear.
*
@@ -88,17 +96,18 @@ const generic_params = [
* s("bd*8").amp(".1*2 .5 .1*2 .5 .1 .5").osc()
*
*/
['f', 'amp', 'like @gain@, but linear.'],
['amp'],
/**
* Amplitude envelope attack time: Specifies how long it takes for the sound to reach its peak value, relative to the onset.
*
* @name attack
* @param {number | Pattern} attack time in seconds.
* @synonyms att
* @example
* note("c3 e3").attack("<0 .1 .5>")
*
*/
['f', 'attack'],
['attack', 'att'],
/**
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
@@ -109,7 +118,7 @@ const generic_params = [
* s("bd sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
*
*/
['f', 'bank', 'selects sound bank to use'],
['bank'],
/**
* Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level.
@@ -121,52 +130,46 @@ const generic_params = [
* note("c3 e3").decay("<.1 .2 .3 .4>").sustain(0)
*
*/
['f', 'decay', ''],
['decay'],
/**
* Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset.
*
* @name sustain
* @param {number | Pattern} gain sustain level between 0 and 1
* @synonyms sus
* @example
* note("c3 e3").decay(.2).sustain("<0 .1 .4 .6 1>")
*
*/
['f', 'sustain', ''],
['sustain', 'sus'],
/**
* Amplitude envelope release time: The time it takes after the offset to go from sustain level to zero.
*
* @name release
* @param {number | Pattern} time release time in seconds
* @synonyms rel
* @example
* note("c3 e3 g3 c4").release("<0 .1 .4 .6 1>/2")
*
*/
[
'f',
'release',
'a pattern of numbers to specify the release time (in seconds) of an envelope applied to each sample.',
],
[
'f',
'hold',
'a pattern of numbers to specify the hold time (in seconds) of an envelope applied to each sample. Only takes effect if `attack` and `release` are also specified.',
],
['release', 'rel'],
['hold'],
// TODO: in tidal, it seems to be normalized
/**
* Sets the center frequency of the **b**and-**p**ass **f**ilter.
* Sets the center frequency of the **b**and-**p**ass **f**ilter. When using mininotation, you
* can also optionally supply the 'bpq' parameter separated by ':'.
*
* @name bpf
* @param {number | Pattern} frequency center frequency
* @synonyms bandf
* @synonyms bandf, bp
* @example
* s("bd sd,hh*3").bpf("<1000 2000 4000 8000>")
*
*/
['f', 'bpf', ''],
['f', 'bandf', 'A pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
[['bandf', 'bandq'], 'bpf', 'bp'],
// TODO: in tidal, it seems to be normalized
/**
* Sets the **b**and-**p**ass **q**-factor (resonance)
* Sets the **b**and-**p**ass **q**-factor (resonance).
*
* @name bpq
* @param {number | Pattern} q q factor
@@ -175,8 +178,9 @@ const generic_params = [
* s("bd sd").bpf(500).bpq("<0 1 2 3>")
*
*/
['f', 'bpq', ''],
['f', 'bandq', 'a pattern of anumbers from 0 to 1. Sets the q-factor of the band-pass filter.'],
// currently an alias of 'bandq' https://github.com/tidalcycles/strudel/issues/496
// ['bpq'],
['bandq', 'bpq'],
/**
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
*
@@ -188,11 +192,7 @@ const generic_params = [
* s("rave").begin("<0 .25 .5 .75>")
*
*/
[
'f',
'begin',
'a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.',
],
['begin'],
/**
* The same as .begin, but cuts off the end off each sample.
*
@@ -203,11 +203,7 @@ const generic_params = [
* s("bd*2,oh*4").end("<.1 .2 .5 1>")
*
*/
[
'f',
'end',
'the same as `begin`, but cuts the end off samples, shortening them; e.g. `0.75` to cut off the last quarter of each sample.',
],
['end'],
/**
* Loops the sample (from `begin` to `end`) the specified number of times.
* Note that the tempo of the loop is not synced with the cycle tempo.
@@ -218,7 +214,7 @@ const generic_params = [
* s("bd").loop("<1 2 3 4>").osc()
*
*/
['f', 'loop', 'loops the sample (from `begin` to `end`) the specified number of times.'],
['loop'],
// TODO: currently duplicated with "native" legato
// TODO: superdirt legato will do more: https://youtu.be/dQPmE1WaD1k?t=419
/**
@@ -230,8 +226,8 @@ const generic_params = [
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
*
*/
// ['f', 'legato', 'controls the amount of overlap between two adjacent sounds'],
// ['f', 'clhatdecay', ''],
// ['legato'],
// ['clhatdecay'],
/**
* bit crusher effect.
*
@@ -241,11 +237,7 @@ const generic_params = [
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
*
*/
[
'f',
'crush',
'bit crushing, a pattern of numbers from 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).',
],
['crush'],
/**
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
*
@@ -255,11 +247,7 @@ const generic_params = [
* s("bd sd,hh*4").coarse("<1 4 8 16 32>")
*
*/
[
'f',
'coarse',
'fake-resampling, a pattern of numbers for lowering the sample rate, i.e. 1 for original 2 for half, 3 for a third and so on.',
],
['coarse'],
/**
* choose the channel the pattern is sent to in superdirt
@@ -268,7 +256,7 @@ const generic_params = [
* @param {number | Pattern} channel channel number
*
*/
['i', 'channel', 'choose the channel the pattern is sent to in superdirt'],
['channel'],
/**
* In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.
*
@@ -278,35 +266,39 @@ const generic_params = [
* s("rd*4").cut(1)
*
*/
[
'i',
'cut',
'In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.',
],
['cut'],
/**
* Applies the cutoff frequency of the **l**ow-**p**ass **f**ilter.
*
* When using mininotation, you can also optionally add the 'lpq' parameter, separated by ':'.
*
* @name lpf
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms cutoff
* @synonyms cutoff, ctf, lp
* @example
* s("bd sd,hh*3").lpf("<4000 2000 1000 500 200 100>")
* @example
* s("bd*8").lpf("1000:0 1000:10 1000:20 1000:30")
*
*/
['f', 'lpf'],
['f', 'cutoff', 'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the low-pass filter.'],
[['cutoff', 'resonance'], 'ctf', 'lpf', 'lp'],
/**
* Applies the cutoff frequency of the **h**igh-**p**ass **f**ilter.
*
* When using mininotation, you can also optionally add the 'hpq' parameter, separated by ':'.
*
* @name hpf
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms hcutoff
* @synonyms hp, hcutoff
* @example
* s("bd sd,hh*4").hpf("<4000 2000 1000 500 200 100>")
* @example
* s("bd sd,hh*4").hpf("<2000 2000:25>")
*
*/
['f', 'hpf', ''],
['f', 'hcutoff', ''],
// currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496
// ['hpf'],
[['hcutoff', 'hresonance'], 'hpf', 'hp'],
/**
* Controls the **h**igh-**p**ass **q**-value.
*
@@ -317,8 +309,7 @@ const generic_params = [
* s("bd sd,hh*4").hpf(2000).hpq("<0 10 20 30>")
*
*/
['f', 'hresonance', ''],
['f', 'hpq', ''],
['hresonance', 'hpq'],
/**
* Controls the **l**ow-**p**ass **q**-value.
*
@@ -329,8 +320,8 @@ const generic_params = [
* s("bd sd,hh*4").lpf(2000).lpq("<0 10 20 30>")
*
*/
['f', 'lpq'],
['f', 'resonance', ''],
// currently an alias of 'resonance' https://github.com/tidalcycles/strudel/issues/496
['resonance', 'lpq'],
/**
* DJ filter, below 0.5 is low pass filter, above is high pass filter.
*
@@ -340,40 +331,48 @@ const generic_params = [
* n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc()
*
*/
['f', 'djf', 'DJ filter, below 0.5 is low pass filter, above is high pass filter.'],
// ['f', 'cutoffegint', ''],
['djf'],
// ['cutoffegint'],
// TODO: does not seem to work
/**
* Sets the level of the delay signal.
*
* When using mininotation, you can also optionally add the 'delaytime' and 'delayfeedback' parameter,
* separated by ':'.
*
*
* @name delay
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd").delay("<0 .25 .5 1>")
* @example
* s("bd bd").delay("0.65:0.25:0.9 0.65:0.125:0.7")
*
*/
['f', 'delay', 'a pattern of numbers from 0 to 1. Sets the level of the delay signal.'],
[['delay', 'delaytime', 'delayfeedback']],
/**
* Sets the level of the signal that is fed back into the delay.
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>").slow(2)
*
*/
['f', 'delayfeedback', 'a pattern of numbers from 0 to 1. Sets the amount of delay feedback.'],
['delayfeedback', 'delayfb', 'dfb'],
/**
* Sets the time of the delay effect.
*
* @name delaytime
* @param {number | Pattern} seconds between 0 and Infinity
* @synonyms delayt, dt
* @example
* s("bd").delay(.25).delaytime("<.125 .25 .5 1>").slow(2)
*
*/
['f', 'delaytime', 'a pattern of numbers from 0 to 1. Sets the length of the delay.'],
['delaytime', 'delayt', 'dt'],
/* // TODO: test
* Specifies whether delaytime is calculated relative to cps.
*
@@ -383,22 +382,19 @@ const generic_params = [
* s("sd").delay().lock(1).osc()
*
*/
[
'f',
'lock',
'A pattern of numbers. Specifies whether delaytime is calculated relative to cps. When set to 1, delaytime is a direct multiple of a cycle.',
],
['lock'],
/**
* Set detune of oscillators. Works only with some synths, see <a target="_blank" href="https://tidalcycles.org/docs/patternlib/tutorials/synthesizers">tidal doc</a>
*
* @name detune
* @param {number | Pattern} amount between 0 and 1
* @synonyms det
* @superdirtOnly
* @example
* n("0 3 7").s('superzow').octave(3).detune("<0 .25 .5 1 2>").osc()
*
*/
['f', 'detune', ''],
['detune', 'det'],
/**
* Set dryness of reverb. See {@link room} and {@link size} for more information about reverb.
*
@@ -409,11 +405,7 @@ const generic_params = [
* @superdirtOnly
*
*/
[
'f',
'dry',
'when set to `1` will disable all reverb for this pattern. See `room` and `size` for more information about reverb.',
],
['dry'],
// TODO: does not seem to do anything
/*
* Used when using {@link begin}/{@link end} or {@link chop}/{@link striate} and friends, to change the fade out time of the 'grain' envelope.
@@ -424,17 +416,9 @@ const generic_params = [
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
*
*/
[
'f',
'fadeTime',
"Used when using begin/end or chop/striate and friends, to change the fade out time of the 'grain' envelope.",
],
['fadeTime', 'fadeOutTime'],
// TODO: see above
[
'f',
'fadeInTime',
'As with fadeTime, but controls the fade in time of the grain envelope. Not used if the grain begins at position 0 in the sample.',
],
['fadeInTime'],
/**
* Set frequency of sound.
*
@@ -446,15 +430,15 @@ const generic_params = [
* freq("110".mul.out(".5 1.5 .6 [2 3]")).s("superzow").osc()
*
*/
['f', 'freq', ''],
['freq'],
// TODO: https://tidalcycles.org/docs/configuration/MIDIOSC/control-voltage/#gate
['f', 'gate', ''],
// ['f', 'hatgrain', ''],
// ['f', 'lagogo', ''],
// ['f', 'lclap', ''],
// ['f', 'lclaves', ''],
// ['f', 'lclhat', ''],
// ['f', 'lcrash', ''],
['gate', 'gat'],
// ['hatgrain'],
// ['lagogo'],
// ['lclap'],
// ['lclaves'],
// ['lclhat'],
// ['lcrash'],
// TODO:
// https://tidalcycles.org/docs/reference/audio_effects/#leslie-1
// https://tidalcycles.org/docs/reference/audio_effects/#leslie
@@ -468,7 +452,7 @@ const generic_params = [
* @superdirtOnly
*
*/
['f', 'leslie', ''],
['leslie'],
/**
* Rate of modulation / rotation for leslie effect
*
@@ -480,7 +464,7 @@ const generic_params = [
*
*/
// TODO: the rate seems to "lag" (in the example, 1 will be fast)
['f', 'lrate', ''],
['lrate'],
/**
* Physical size of the cabinet in meters. Be careful, it might be slightly larger than your computer. Affects the Doppler amount (pitch warble)
*
@@ -491,31 +475,28 @@ const generic_params = [
* @superdirtOnly
*
*/
['f', 'lsize', ''],
// ['f', 'lfo', ''],
// ['f', 'lfocutoffint', ''],
// ['f', 'lfodelay', ''],
// ['f', 'lfoint', ''],
// ['f', 'lfopitchint', ''],
// ['f', 'lfoshape', ''],
// ['f', 'lfosync', ''],
// ['f', 'lhitom', ''],
// ['f', 'lkick', ''],
// ['f', 'llotom', ''],
// ['f', 'lophat', ''],
// ['f', 'lsnare', ''],
['f', 'degree', ''], // TODO: what is this? not found in tidal doc
['f', 'mtranspose', ''], // TODO: what is this? not found in tidal doc
['f', 'ctranspose', ''], // TODO: what is this? not found in tidal doc
['f', 'harmonic', ''], // TODO: what is this? not found in tidal doc
['f', 'stepsPerOctave', ''], // TODO: what is this? not found in tidal doc
['f', 'octaveR', ''], // TODO: what is this? not found in tidal doc
// TODO: why is this needed? what's the difference to late / early?
[
'f',
'nudge',
'Nudges events into the future by the specified number of seconds. Negative numbers work up to a point as well (due to internal latency)',
],
['lsize'],
// ['lfo'],
// ['lfocutoffint'],
// ['lfodelay'],
// ['lfoint'],
// ['lfopitchint'],
// ['lfoshape'],
// ['lfosync'],
// ['lhitom'],
// ['lkick'],
// ['llotom'],
// ['lophat'],
// ['lsnare'],
['degree'], // TODO: what is this? not found in tidal doc
['mtranspose'], // TODO: what is this? not found in tidal doc
['ctranspose'], // TODO: what is this? not found in tidal doc
['harmonic'], // TODO: what is this? not found in tidal doc
['stepsPerOctave'], // TODO: what is this? not found in tidal doc
['octaveR'], // TODO: what is this? not found in tidal doc
// TODO: why is this needed? what's the difference to late / early? Answer: it's in seconds, and delays the message at
// OSC time (so can't be negative, at least not beyond the latency value)
['nudge'],
// TODO: the following doc is just a guess, it's not documented in tidal doc.
/**
* Sets the default octave of a synth.
@@ -526,9 +507,9 @@ const generic_params = [
* n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc()
* @superDirtOnly
*/
['i', 'octave', ''],
['f', 'offset', ''], // TODO: what is this? not found in tidal doc
// ['f', 'ophatdecay', ''],
['octave'],
['offset'], // TODO: what is this? not found in tidal doc
// ['ophatdecay'],
// TODO: example
/**
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
@@ -541,13 +522,9 @@ const generic_params = [
* s("~ sd").delay(.5).delaytime(.125).orbit(2)
* )
*/
[
'i',
'orbit',
'a pattern of numbers. An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share hardware output bus offset and global effects, e.g. reverb and delay. The maximum number of orbits is specified in the superdirt startup, numbers higher than maximum will wrap around.',
],
['f', 'overgain', ''], // TODO: what is this? not found in tidal doc
['f', 'overshape', ''], // TODO: what is this? not found in tidal doc
['orbit'],
['overgain'], // TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that
['overshape'], // TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
/**
* Sets position in stereo.
*
@@ -557,11 +534,7 @@ const generic_params = [
* s("[bd hh]*2").pan("<.5 1 .5 0>")
*
*/
[
'f',
'pan',
'a pattern of numbers between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)',
],
['pan'],
// TODO: this has no effect (see example)
/*
* Controls how much multichannel output is fanned out
@@ -572,11 +545,7 @@ const generic_params = [
* s("[bd hh]*2").pan("<.5 1 .5 0>").panspan("<0 .5 1>").osc()
*
*/
[
'f',
'panspan',
'a pattern of numbers between -inf and inf, which controls how much multichannel output is fanned out (negative is backwards ordering)',
],
['panspan'],
// TODO: this has no effect (see example)
/*
* Controls how much multichannel output is spread
@@ -587,50 +556,42 @@ const generic_params = [
* s("[bd hh]*2").pan("<.5 1 .5 0>").pansplay("<0 .5 1>").osc()
*
*/
[
'f',
'pansplay',
'a pattern of numbers between 0.0 and 1.0, which controls the multichannel spread range (multichannel only)',
],
[
'f',
'panwidth',
'a pattern of numbers between 0.0 and inf, which controls how much each channel is distributed over neighbours (multichannel only)',
],
[
'f',
'panorient',
'a pattern of numbers between -1.0 and 1.0, which controls the relative position of the centre pan in a pair of adjacent speakers (multichannel only)',
],
// ['f', 'pitch1', ''],
// ['f', 'pitch2', ''],
// ['f', 'pitch3', ''],
// ['f', 'portamento', ''],
['pansplay'],
['panwidth'],
['panorient'],
// ['pitch1'],
// ['pitch2'],
// ['pitch3'],
// ['portamento'],
// TODO: LFO rate see https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
['f', 'rate', "used in SuperDirt softsynths as a control rate or 'speed'"],
['rate'],
// TODO: slide param for certain synths
['f', 'slide', ''],
['slide'],
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
['f', 'semitone', ''],
['semitone'],
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
// ['f', 'velocity', ''],
['f', 'voice', ''], // TODO: synth param
// ['velocity'],
['voice'], // TODO: synth param
/**
* Sets the level of reverb.
*
* When using mininotation, you can also optionally add the 'size' parameter, separated by ':'.
*
* @name room
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd sd").room("<0 .2 .4 .6 .8 1>")
* @example
* s("bd sd").room("<0.9:1 0.9:4>")
*
*/
['f', 'room', 'a pattern of numbers from 0 to 1. Sets the level of reverb.'],
[['room', 'size']],
/**
* Sets the room size of the reverb, see {@link room}.
*
* @name roomsize
* @synonyms size
* @param {number | Pattern} size between 0 and 10
* @synonyms size, sz
* @example
* s("bd sd").room(.8).roomsize("<0 1 2 4 8>")
*
@@ -638,20 +599,11 @@ const generic_params = [
// 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?
[
'f',
'size',
'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.',
],
[
'f',
'roomsize',
'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.',
],
// ['f', 'sagogo', ''],
// ['f', 'sclap', ''],
// ['f', 'sclaves', ''],
// ['f', 'scrash', ''],
['size', 'sz', 'roomsize'],
// ['sagogo'],
// ['sclap'],
// ['sclaves'],
// ['scrash'],
/**
* Wave shaping distortion. CAUTION: it might get loud
*
@@ -661,11 +613,7 @@ const generic_params = [
* s("bd sd,hh*4").shape("<0 .2 .4 .6 .8>")
*
*/
[
'f',
'shape',
'wave shaping distortion, a pattern of numbers from 0 for no distortion up to 1 for loads of distortion.',
],
['shape'],
/**
* Changes the speed of sample playback, i.e. a cheap way of changing pitch.
*
@@ -677,11 +625,7 @@ const generic_params = [
* speed("1 1.5*2 [2 1.1]").s("piano").clip(1)
*
*/
[
'f',
'speed',
'a pattern of numbers which changes the speed of sample playback, i.e. a cheap way of changing pitch. Negative values will play the sample backwards!',
],
['speed'],
/**
* Used in conjunction with {@link speed}, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`.
*
@@ -692,11 +636,7 @@ const generic_params = [
* @superdirtOnly
*
*/
[
's',
'unit',
'used in conjunction with `speed`, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`.',
],
['unit'],
/**
* Made by Calum Gunn. Reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter. The SuperCollider manual defines Squiz as:
*
@@ -709,14 +649,14 @@ const generic_params = [
* @superdirtOnly
*
*/
['f', 'squiz', ''],
['f', 'stutterdepth', ''], // TODO: what is this? not found in tidal doc
['f', 'stuttertime', ''], // TODO: what is this? not found in tidal doc
['f', 'timescale', ''], // TODO: what is this? not found in tidal doc
['f', 'timescalewin', ''], // TODO: what is this? not found in tidal doc
// ['f', 'tomdecay', ''],
// ['f', 'vcfegint', ''],
// ['f', 'vcoegint', ''],
['squiz'],
// ['stutterdepth'], // TODO: what is this? not found in tidal doc
// ['stuttertime'], // TODO: what is this? not found in tidal doc
// ['timescale'], // TODO: what is this? not found in tidal doc
// ['timescalewin'], // TODO: what is this? not found in tidal doc
// ['tomdecay'],
// ['vcfegint'],
// ['vcoegint'],
// TODO: Use a rest (~) to override the effect <- vowel
/**
*
@@ -729,11 +669,7 @@ const generic_params = [
* .vowel("<a e i <o u>>")
*
*/
[
's',
'vowel',
'formant filter to make things sound like vowels, a pattern of either `a`, `e`, `i`, `o` or `u`. Use a rest (`~`) for no effect.',
],
['vowel'],
/* // TODO: find out how it works
* Made by Calum Gunn. Divides an audio stream into tiny segments, using the signal's zero-crossings as segment boundaries, and discards a fraction of them. Takes a number between 1 and 100, denoted the percentage of segments to drop. The SuperCollider manual describes the Waveloss effect this way:
*
@@ -744,12 +680,12 @@ const generic_params = [
*
* @name waveloss
*/
['f', 'waveloss', ''],
['waveloss'],
// TODO: midi effects?
['f', 'dur', ''],
// ['f', 'modwheel', ''],
['f', 'expression', ''],
['f', 'sustainpedal', ''],
['dur'],
// ['modwheel'],
['expression'],
['sustainpedal'],
/* // TODO: doesn't seem to do anything
*
* Tremolo Audio DSP effect
@@ -760,59 +696,58 @@ const generic_params = [
* n("0,4,7").tremolodepth("<0 .3 .6 .9>").osc()
*
*/
// TODO: tremdp alias
['f', 'tremolodepth', "Tremolo Audio DSP effect | params are 'tremolorate' and 'tremolodepth'"],
['f', 'tremolorate', "Tremolo Audio DSP effect | params are 'tremolorate' and 'tremolodepth'"],
['tremolodepth', 'tremdp'],
['tremolorate', 'tremr'],
// TODO: doesn't seem to do anything
['f', 'phaserdepth', "Phaser Audio DSP effect | params are 'phaserrate' and 'phaserdepth'"],
['f', 'phaserrate', "Phaser Audio DSP effect | params are 'phaserrate' and 'phaserdepth'"],
['phaserdepth', 'phasdp'],
['phaserrate', 'phasr'],
['f', 'fshift', 'frequency shifter'],
['f', 'fshiftnote', 'frequency shifter'],
['f', 'fshiftphase', 'frequency shifter'],
['fshift'],
['fshiftnote'],
['fshiftphase'],
['f', 'triode', 'tube distortion'],
['f', 'krush', 'shape/bass enhancer'],
['f', 'kcutoff', ''],
['f', 'octer', 'octaver effect'],
['f', 'octersub', 'octaver effect'],
['f', 'octersubsub', 'octaver effect'],
['f', 'ring', 'ring modulation'],
['f', 'ringf', 'ring modulation'],
['f', 'ringdf', 'ring modulation'],
['f', 'distort', 'noisy fuzzy distortion'],
['f', 'freeze', 'Spectral freeze'],
['f', 'xsdelay', ''],
['f', 'tsdelay', ''],
['f', 'real', 'Spectral conform'],
['f', 'imag', ''],
['f', 'enhance', 'Spectral enhance'],
['f', 'partials', ''],
['f', 'comb', 'Spectral comb'],
['f', 'smear', 'Spectral smear'],
['f', 'scram', 'Spectral scramble'],
['f', 'binshift', 'Spectral binshift'],
['f', 'hbrick', 'High pass sort of spectral filter'],
['f', 'lbrick', 'Low pass sort of spectral filter'],
['f', 'midichan', ''],
['f', 'control', ''],
['f', 'ccn', ''],
['f', 'ccv', ''],
['f', 'polyTouch', ''],
['f', 'midibend', ''],
['f', 'miditouch', ''],
['f', 'ctlNum', ''],
['f', 'frameRate', ''],
['f', 'frames', ''],
['f', 'hours', ''],
['s', 'midicmd', ''],
['f', 'minutes', ''],
['f', 'progNum', ''],
['f', 'seconds', ''],
['f', 'songPtr', ''],
['f', 'uid', ''],
['f', 'val', ''],
['f', 'cps', ''],
['triode'],
['krush'],
['kcutoff'],
['octer'],
['octersub'],
['octersubsub'],
['ring'],
['ringf'],
['ringdf'],
['distort'],
['freeze'],
['xsdelay'],
['tsdelay'],
['real'],
['imag'],
['enhance'],
['partials'],
['comb'],
['smear'],
['scram'],
['binshift'],
['hbrick'],
['lbrick'],
['midichan'],
['control'],
['ccn'],
['ccv'],
['polyTouch'],
['midibend'],
['miditouch'],
['ctlNum'],
['frameRate'],
['frames'],
['hours'],
['midicmd'],
['minutes'],
['progNum'],
['seconds'],
['songPtr'],
['uid'],
['val'],
['cps'],
/**
* If set to 1, samples will be cut to the duration of their event.
* In tidal, this would be done with legato, which [is about to land in strudel too](https://github.com/tidalcycles/strudel/issues/111)
@@ -823,34 +758,67 @@ const generic_params = [
* note("c a f e ~").s("piano").clip(1)
*
*/
['f', 'clip', ''],
['clip'],
];
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
const makeControl = function (name) {
const func = (...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
controls.createParam = function (names) {
const name = Array.isArray(names) ? names[0] : names;
var withVal;
if (Array.isArray(names)) {
withVal = (xs) => {
if (Array.isArray(xs)) {
const result = {};
xs.forEach((x, i) => {
if (i < names.length) {
result[names[i]] = x;
}
});
return result;
} else {
return { [name]: xs };
}
};
} else {
withVal = (x) => ({ [name]: x });
}
const func = (...pats) => sequence(...pats).withValue(withVal);
const setter = function (...pats) {
if (!pats.length) {
return this.fmap((value) => ({ [name]: value }));
return this.fmap(withVal);
}
return this.set(func(...pats));
};
Pattern.prototype[name] = setter;
registerControl(name, func);
return func;
};
generic_params.forEach(([type, name, description]) => {
controls[name] = makeControl(name);
});
generic_params.forEach(([names, ...aliases]) => {
const name = Array.isArray(names) ? names[0] : names;
controls[name] = controls.createParam(names);
// create custom param
controls.createParam = (name) => {
return makeControl(name);
};
aliases.forEach((alias) => {
controls[alias] = controls[name];
Pattern.prototype[alias] = Pattern.prototype[name];
});
});
controls.createParams = (...names) =>
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
controls.adsr = register('adsr', (adsr, pat) => {
adsr = !Array.isArray(adsr) ? [adsr] : adsr;
const [attack, decay, sustain, release] = adsr;
return pat.set({ attack, decay, sustain, release });
});
controls.ds = register('ds', (ds, pat) => {
ds = !Array.isArray(ds) ? [ds] : ds;
const [decay, sustain] = ds;
return pat.set({ decay, sustain });
});
export default controls;
+23 -17
View File
@@ -10,44 +10,49 @@ import { logger } from './logger.mjs';
export class Cyclist {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
this.started = false;
this.cps = 1; // TODO
this.phase = 0;
this.getTime = getTime;
this.cps = 1;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.lastBegin = 0; // query begin of last tick
this.lastEnd = 0; // query end of last tick
this.getTime = getTime; // get absolute time
this.onToggle = onToggle;
this.latency = latency;
this.latency = latency; // fixed trigger time offset
const round = (x) => Math.round(x * 1000) / 1000;
this.clock = createClock(
getTime,
// called slightly before each cycle
(phase, duration, tick) => {
if (tick === 0) {
this.origin = phase;
}
const begin = round(phase - this.origin);
this.phase = begin - latency;
const end = round(begin + duration);
const time = getTime();
try {
const haps = this.pattern.queryArc(begin, end); // get Haps
const time = getTime();
const begin = this.lastEnd;
this.lastBegin = begin;
const end = round(begin + duration * this.cps);
this.lastEnd = end;
const haps = this.pattern.queryArc(begin, end);
const tickdeadline = phase - time; // time left till phase begins
this.lastTick = time + tickdeadline;
haps.forEach((hap) => {
if (hap.part.begin.equals(hap.whole.begin)) {
const deadline = hap.whole.begin + this.origin - time + latency;
const duration = hap.duration * 1;
onTrigger?.(hap, deadline, duration);
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
const duration = hap.duration / this.cps;
onTrigger?.(hap, deadline, duration, this.cps);
}
});
} catch (e) {
logger(`[cyclist] error: ${e.message}`);
onError?.(e);
}
}, // called slightly before each cycle
},
interval, // duration of each cycle
);
}
getPhase() {
return this.getTime() - this.origin - this.latency;
}
now() {
return this.getTime() - this.origin + this.clock.minLatency;
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
}
setStarted(v) {
this.started = v;
@@ -69,6 +74,7 @@ export class Cyclist {
stop() {
logger('[cyclist] stop');
this.clock.stop();
this.lastEnd = 0;
this.setStarted(false);
}
setPattern(pat, autostart = false) {
+96 -2
View File
@@ -1,6 +1,6 @@
/*
draw.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/draw.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -13,7 +13,7 @@ export const getDrawContext = (id = 'test-canvas') => {
canvas.id = id;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0;z-index:5';
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
document.body.prepend(canvas);
}
return canvas.getContext('2d');
@@ -65,3 +65,97 @@ Pattern.prototype.onPaint = function (onPaint) {
this.context = { onPaint };
return this;
};
// const round = (x) => Math.round(x * 1000) / 1000;
// encapsulates starting and stopping animation frames
export class Framer {
constructor(onFrame, onError) {
this.onFrame = onFrame;
this.onError = onError;
}
start() {
const self = this;
let frame = requestAnimationFrame(function updateHighlights(time) {
try {
self.onFrame(time);
} catch (err) {
self.onError(err);
}
frame = requestAnimationFrame(updateHighlights);
});
self.cancel = () => {
cancelAnimationFrame(frame);
};
}
stop() {
if (this.cancel) {
this.cancel();
}
}
}
// syncs animation frames to a cyclist scheduler
// see vite-vanilla-repl-cm6 for an example
export class Drawer {
constructor(onDraw, drawTime) {
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
lookbehind = Math.abs(lookbehind);
this.visibleHaps = [];
this.lastFrame = null;
this.drawTime = drawTime;
this.framer = new Framer(
() => {
if (!this.scheduler) {
console.warn('Drawer: no scheduler');
return;
}
// calculate current frame time (think right side of screen for pianoroll)
const phase = this.scheduler.now() + lookahead;
// first frame just captures the phase
if (this.lastFrame === null) {
this.lastFrame = phase;
return;
}
// query haps from last frame till now. take last 100ms max
const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase);
this.lastFrame = phase;
this.visibleHaps = (this.visibleHaps || [])
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
.filter((h) => h.whole.end >= phase - lookbehind - lookahead)
// add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead;
onDraw(this.visibleHaps, time, this);
},
(err) => {
console.warn('draw error', err);
},
);
}
invalidate(scheduler = this.scheduler) {
if (!scheduler) {
return;
}
this.scheduler = scheduler;
const t = scheduler.now();
let [_, lookahead] = this.drawTime;
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
// remove all future haps
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
// query future haps
const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query..
// append future haps
this.visibleHaps = this.visibleHaps.concat(futureHaps);
}
start(scheduler) {
this.scheduler = scheduler;
this.invalidate();
this.framer.start();
}
stop() {
if (this.framer) {
this.framer.stop();
}
}
}
+1 -7
View File
@@ -11,15 +11,9 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { Pattern, timeCat, register, silence } from './pattern.mjs';
import { rotate, flatten } from './util.mjs';
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
import Fraction from './fraction.mjs';
const splitAt = function (index, value) {
return [value.slice(0, index), value.slice(index)];
};
const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
const left = function (n, x) {
const [ons, offs] = n;
const [xs, ys] = x;
-8
View File
@@ -6,12 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { isPattern } from './index.mjs';
let scoped = false;
export const evalScope = async (...args) => {
if (scoped) {
console.warn('evalScope was called more than once.');
}
scoped = true;
const results = await Promise.allSettled(args);
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
results.forEach((result, i) => {
@@ -42,9 +37,6 @@ function safeEval(str, options = {}) {
}
export const evaluate = async (code, transpiler) => {
if (!scoped) {
await evalScope(); // at least scope Pattern.prototype.boostrap
}
if (transpiler) {
code = transpiler(code); // transform syntactically correct js code to semantically usable code
}
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,8 @@
# vite-vanilla-repl-cm6
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
```sh
npm i
npm run dev
```
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite Vanilla Strudel REPL</title>
</head>
<body>
<main>
<nav>
<button id="play">eval</button>
<button id="stop">stop</button>
</nav>
<div class="container">
<div id="editor"></div>
<div id="output"></div>
</div>
<canvas id="roll"></canvas>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
@@ -0,0 +1,39 @@
import { StrudelMirror } from '@strudel/codemirror';
import { funk42 } from './tunes';
import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
import './style.css';
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
// init canvas
const canvas = document.getElementById('roll');
canvas.width = canvas.width * 2;
canvas.height = canvas.height * 2;
const drawContext = canvas.getContext('2d');
const drawTime = [-2, 2]; // time window of drawn haps
const editor = new StrudelMirror({
defaultOutput: webaudioOutput,
getTime: () => getAudioContext().currentTime,
transpiler,
root: document.getElementById('editor'),
initialCode: funk42,
drawTime,
onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }),
prebake: async () => {
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
const loadModules = evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
},
});
document.getElementById('play').addEventListener('click', () => editor.evaluate());
document.getElementById('stop').addEventListener('click', () => editor.stop());
@@ -0,0 +1,23 @@
{
"name": "vite-vanilla-repl-cm6",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.3.2"
},
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
}
}
@@ -0,0 +1,31 @@
body,
html {
margin: 0;
height: 100%;
background: #282c34;
}
main {
height: 100%;
display: flex;
flex-direction: column;
}
.container {
flex-grow: 1;
max-height: 100%;
position: relative;
overflow: auto;
}
#editor {
overflow: auto;
}
.cm-editor {
height: 100%;
}
#roll {
height: 300px;
}
@@ -0,0 +1,112 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
// make it 22 edo
.fmap(v => Math.pow(2,v/22))
// mess with the base frequency
.mul("<300 [300@3 200]>/8").freq()
.layer(
// chords
x=>x.div(freq(2)).s("flute").euclidLegato("<3 2>",8)
.shape(.4).lpf(sine.range(800,4000).slow(8)),
// adlibs
x=>x.arp("{0 3 2 [1 3]}%1.5")
.s('xylo').mul(freq(2))
.delay(.5).delayfeedback(.4).juxBy(.5, rev)
.hpf(sine.range(200,3000).slow(8)),
// bass
x=>x.arp("[0 [2 1?]](5,8)").s('sawtooth').div(freq(4))
.lpf(sine.range(400,2000).slow(8)).lpq(8).shape(.4)
.off(1/8, x=>x.mul(freq(2)).degradeBy(.5)).gain(.3)
).clip(1).release(.2)
.stack(
// drums
s("bd sd:<2 1>, [~ hh]*2, [~ rim]").bank('RolandTR909')
.off(1/8, x=>x.speed(2).gain(.4)).sometimes(ply(2)).gain(.8)
.mask("<0@4 1@12>/4")
.reset("<x@15 [x(3,8) x*[4 8]]>")
// wait for it...
).fast(2/3)
//.crush(6) // remove "//" if you dare`;
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', {
m11: ['2M 3m 4P 7m'],
'^7#11': ['3M 4A 5P 7M'],
}, ['C4', 'C6'])
stack(
stack(
"<Bbm11 A^7#11>/2".voicings('hip').note()
.s("gm_epiano1:2")
.arp("[<[0 1 2 3] [3 2 1 0]> ~@5]/2")
.release(2).late(.25).lpf(2000),
"<Bb1 A1>/2".note().s('gm_acoustic_bass'),
n("<0 2 3>(3,8)".off(1/8, add(4)))
.scale("<Bb4:minor A4:lydian>/2")
.s('gm_electric_guitar_jazz')
.decay(sine.range(.05, .2).slow(32)).sustain(0)
.delay(.5).lpf(sine.range(100,5000).slow(64))
.gain(.7).room(.5).pan(sine.range(0,1).slow(11))
).add(perlin.range(0,.25).note()),
stack(
s("bd:1(3,8) rim").bank('RolandTR707').slow(2).room("<0 <.1 .6>>")
.when("<0@7 1>",x=>x.echoWith(3, .0625, (x,i) => x.speed(1+i*.24))),
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
)
)
.late("[0 .05]*2").late(12)
`;
export const funk42 = `// froos - how to funk in 42 lines of code
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5)
let drums = stack(
s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"),
s("hh*4").begin(.2).release(.02).end(.25).release(.02)
.gain(.3).bank('RolandTR707').late(.02).room(.5),
//s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02)
).fast(2)
let wurli = note(\`<
[[a2,g3,[b3 c4],e4] ~ [g3,c4,e4](3,8)@4 ~@2]!3
[[e2,e3,a3,b3,e4]@3 [e2,e3,ab3,b3,e4]@5]>\`)
.s("gm_epiano1:5").decay(.2).sustain("<[1 0@7]!3 1>")
.gain("<[.8@2 .4@14]!3 .7>").room(.3)
let organ = note("<[~@3 [a3,d4,f#4]@2 [[a3,c4,e4]@2 ~] ~@2]!3 ~>".add(12))
.s("gm_percussive_organ:2").gain(.6).lpf(1800).pan(.2).room(.3);
let clav = note(\`<
[~@3 a2 [g3,[b3 c4],e4]@2 ~ a2 [g3,b3,e4] ~@2 [g3,c4,e4] ~@4]!3
[~@3 e3 [[a3 b3],c3,e3]@2 ~ e2 [e3,a3]@3 [b3,e3] ~@2 [b3,e3]@2]>\`)
.s("gm_clavinet:1").decay("<.25!3 [.25 .4]>").sustain(0)
.gain(.7).pan(.8).room(.2);
let bass = note(\`<
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
[a1 [~ [g2 a2]] [e3 d3] [c3 [g3 a3]]]
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
[e2@6 e1@5 e1 [[d2 e3] g1]@4]
>\`).s("gm_electric_bass_pick:1").release(.1)
stack(
drums
,wurli
,organ
,clav
,bass
)`;
@@ -6,5 +6,3 @@ This folder demonstrates how to set up a strudel repl using vite and vanilla JS.
npm i
npm run dev
```
or view it [live on githack](https://rawcdn.githack.com/tidalcycles/strudel/5fb36acb046ead7cd6ad3cd10f532e7f585f536a/packages/core/examples/vite-vanilla-repl/dist/index.html)
@@ -10,7 +10,7 @@
"preview": "vite preview"
},
"devDependencies": {
"vite": "^3.2.0"
"vite": "^4.3.3"
},
"dependencies": {
"@strudel.cycles/core": "workspace:*",
+13
View File
@@ -126,6 +126,19 @@ export class Hap {
setContext(context) {
return new Hap(this.whole, this.part, this.value, context);
}
ensureObjectValue() {
/* if (isNote(hap.value)) {
// supports primitive hap values that look like notes
hap.value = { note: hap.value };
} */
if (typeof this.value !== 'object') {
throw new Error(
`expected hap.value to be an object, but got "${this.value}". Hint: append .note() or .s() to the end`,
'error',
);
}
}
}
export default Hap;
+1
View File
@@ -18,6 +18,7 @@ export * from './util.mjs';
export * from './speak.mjs';
export * from './evaluate.mjs';
export * from './repl.mjs';
export * from './cyclist.mjs';
export * from './logger.mjs';
export * from './time.mjs';
export * from './draw.mjs';
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.6.8",
"version": "0.8.1",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
@@ -35,7 +35,7 @@
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+192 -286
View File
@@ -21,141 +21,6 @@ let stringParser;
// intended to use with mini to automatically interpret all strings as mini notation
export const setStringParser = (parser) => (stringParser = parser);
const alignments = ['in', 'out', 'mix', 'squeeze', 'squeezeout', 'trig', 'trigzero'];
const methodRegistry = [];
const getterRegistry = [];
const controlRegistry = [];
const controlSubscribers = [];
const composifiedRegistry = [];
//////////////////////////////////////////////////////////////////////
// Magic for supporting higher order composition of method chains
// Dresses the given (unary) function with methods for composition chaining, so e.g.
// `fast(2).iter(4)` composes to pattern functions into a new one.
function composify(func) {
if (!func.__composified) {
for (const [name, method] of methodRegistry) {
func[name] = method;
}
for (const [name, getter] of getterRegistry) {
Object.defineProperty(func, name, getter);
}
func.__composified = true;
composifiedRegistry.push(func);
} else {
console.log('Warning: attempt at composifying a function more than once');
}
return func;
}
export function registerMethod(name, addAlignments = false, addControls = false) {
if (addAlignments || addControls) {
// This method needs to make its 'this' object available to chained alignments and/or
// control parameters, so it has to be implemented as a getter
const getter = {
get: function () {
const func = this;
const wrapped = function (...args) {
const composed = (pat) => func(pat)[name](...args);
return composify(composed);
};
if (addAlignments) {
for (const alignment of alignments) {
wrapped[alignment] = function (...args) {
const composed = (pat) => func(pat)[name][alignment](...args);
return composify(composed);
};
for (const [controlname, controlfunc] of controlRegistry) {
wrapped[alignment][controlname] = function (...args) {
const composed = (pat) => func(pat)[name][alignment](controlfunc(...args));
return composify(composed);
};
}
}
}
if (addControls) {
for (const [controlname, controlfunc] of controlRegistry) {
wrapped[controlname] = function (...args) {
const composed = (pat) => func(pat)[name](controlfunc(...args));
return composify(composed);
};
}
}
return wrapped;
},
};
getterRegistry.push([name, getter]);
// Add to functions already 'composified'
for (const composified of composifiedRegistry) {
Object.defineProperty(composified, name, getter);
}
} else {
// No chained alignments/controls needed, so we can just add as a plain method,
// probably more efficient this way?
const method = function (...args) {
const func = this;
const composed = (pat) => func(pat)[name](...args);
return composify(composed);
};
methodRegistry.push([name, method]);
// Add to functions already 'composified'
for (const composified of composifiedRegistry) {
composified[name] = method;
}
}
}
export function registerControl(controlname, controlfunc) {
registerMethod(controlname);
controlRegistry.push([controlname, controlfunc]);
for (const subscriber of controlSubscribers) {
subscriber(controlname, controlfunc);
}
}
export function withControls(func) {
for (const [controlname, controlfunc] of controlRegistry) {
func(controlname, controlfunc);
}
controlSubscribers.push(func);
}
export function addToPrototype(name, func) {
Pattern.prototype[name] = func;
registerMethod(name);
}
export function curryPattern(func, arity = func.length) {
const fn = function curried(...args) {
if (args.length >= arity) {
return func.apply(this, args);
}
const partial = function (...args2) {
return curried.apply(this, args.concat(args2));
};
if (args.length == arity - 1) {
return composify(partial);
}
return partial;
};
if (arity == 1) {
composify(fn);
}
return fn;
}
//////////////////////////////////////////////////////////////////////
// The core Pattern class
/** @class Class representing a pattern. */
export class Pattern {
/**
@@ -778,9 +643,7 @@ export class Pattern {
* @noAutocomplete
*/
get firstCycleValues() {
return this.sortHapsByPart()
.firstCycle()
.map((hap) => hap.value);
return this.firstCycle().map((hap) => hap.value);
}
/**
@@ -830,7 +693,7 @@ export class Pattern {
const otherPat = reify(other);
return this.fmap((a) => otherPat.fmap((b) => func(a)(b))).squeezeJoin();
}
_opSqueezeout(other, func) {
_opSqueezeOut(other, func) {
const thisPat = this;
const otherPat = reify(other);
return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a))).squeezeJoin();
@@ -977,7 +840,7 @@ export class Pattern {
}
//////////////////////////////////////////////////////////////////////
// functions relating to chords/patterns of lists
// functions relating to chords/patterns of lists/lists of patterns
// returns Array<Hap[]> where each list of haps satisfies eq
function groupHapsBy(eq, haps) {
@@ -997,11 +860,11 @@ function groupHapsBy(eq, haps) {
const congruent = (a, b) => a.spanEquals(b);
// Pattern<Hap<T>> -> Pattern<Hap<T[]>>
// returned pattern contains arrays of congruent haps
addToPrototype('collect', function () {
Pattern.prototype.collect = function () {
return this.withHaps((haps) =>
groupHapsBy(congruent, haps).map((_haps) => new Hap(_haps[0].whole, _haps[0].part, _haps, {})),
);
});
};
/**
* Selects indices in in stacked notes.
@@ -1009,12 +872,12 @@ addToPrototype('collect', function () {
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arpWith(haps => haps[2])
* */
addToPrototype('arpWith', function (func) {
Pattern.prototype.arpWith = function (func) {
return this.collect()
.fmap((v) => reify(func(v)))
.innerJoin()
.withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value)));
});
};
/**
* Selects indices in in stacked notes.
@@ -1022,10 +885,39 @@ addToPrototype('arpWith', function (func) {
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arp("0 [0,2] 1 [0,2]").slow(2)
* */
addToPrototype('arp', function (pat) {
Pattern.prototype.arp = function (pat) {
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
};
/*
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
* @name weave
* @memberof Pattern
* @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd"))
* @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd"))
addToPrototype('weave', function (t, ...pats) {
return this.weaveWith(t, ...pats.map((x) => set.out(x)));
});
*/
/*
* Like 'weave', but accepts functions rather than patterns, which are applied to the pattern.
* @name weaveWith
* @memberof Pattern
addToPrototype('weaveWith', function (t, ...funcs) {
const pat = this;
const l = funcs.length;
t = Fraction(t);
if (l == 0) {
return silence;
}
return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t);
});
*/
//////////////////////////////////////////////////////////////////////
// compose matrix functions
@@ -1122,15 +1014,15 @@ function _composeOp(a, b, func) {
func: [(a, b) => b(a)],
};
const hows = alignments.map((x) => x.charAt(0).toUpperCase() + x.slice(1));
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Trig', 'Trigzero'];
// generate methods to do what and how
for (const [what, [op, preprocess]] of Object.entries(composers)) {
// make plain version, e.g. pat._add(value) adds that plain value
// to all the values in pat
addToPrototype('_' + what, function (value) {
Pattern.prototype['_' + what] = function (value) {
return this.fmap((x) => op(x, value));
});
};
// make patternified monster version
Object.defineProperty(Pattern.prototype, what, {
@@ -1144,7 +1036,7 @@ function _composeOp(a, b, func) {
// add methods to that function for each behaviour
for (const how of hows) {
const howfunc = function (...other) {
wrapper[how.toLowerCase()] = function (...other) {
var howpat = pat;
other = sequence(other);
if (preprocess) {
@@ -1162,41 +1054,19 @@ function _composeOp(a, b, func) {
}
return result;
};
for (const [controlname, controlfunc] of controlRegistry) {
howfunc[controlname] = (...args) => howfunc(controlfunc(...args));
}
wrapper[how.toLowerCase()] = howfunc;
}
wrapper.squeezein = wrapper.squeeze;
for (const [controlname, controlfunc] of controlRegistry) {
wrapper[controlname] = (...args) => wrapper.in(controlfunc(...args));
}
return wrapper;
},
});
registerMethod(what, true, true);
}
// Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
for (const howLower of alignments) {
// Using a 'get'ted function so that all the controls are added
Object.defineProperty(Pattern.prototype, howLower, {
get: function () {
const pat = this;
const howfunc = function (...args) {
return pat.set[howLower](args);
};
for (const [controlname, controlfunc] of controlRegistry) {
howfunc[controlname] = (...args) => howfunc(controlfunc(...args));
}
return howfunc;
},
});
registerMethod(howLower, false, true);
// Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
for (const how of hows) {
Pattern.prototype[how.toLowerCase()] = function (...args) {
return this.set[how.toLowerCase()](args);
};
}
}
// binary composers
@@ -1208,36 +1078,36 @@ function _composeOp(a, b, func) {
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
* .slow(4)
*/
addToPrototype('struct', function (...args) {
Pattern.prototype.struct = function (...args) {
return this.keepif.out(...args);
});
addToPrototype('structAll', function (...args) {
};
Pattern.prototype.structAll = function (...args) {
return this.keep.out(...args);
});
};
/**
* Returns silence when mask is 0 or "~"
*
* @example
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>").slow(2)
*/
addToPrototype('mask', function (...args) {
Pattern.prototype.mask = function (...args) {
return this.keepif.in(...args);
});
addToPrototype('maskAll', function (...args) {
};
Pattern.prototype.maskAll = function (...args) {
return this.keep.in(...args);
});
};
/**
* Resets the pattern to the start of the cycle for each onset of the reset pattern.
*
* @example
* s("<bd lt> sd, hh*4").reset("<x@3 x(3,8)>")
*/
addToPrototype('reset', function (...args) {
Pattern.prototype.reset = function (...args) {
return this.keepif.trig(...args);
});
addToPrototype('resetAll', function (...args) {
};
Pattern.prototype.resetAll = function (...args) {
return this.keep.trig(...args);
});
};
/**
* Restarts the pattern for each onset of the restart pattern.
* While reset will only reset the current cycle, restart will start from cycle 0.
@@ -1245,18 +1115,20 @@ function _composeOp(a, b, func) {
* @example
* s("<bd lt> sd, hh*4").restart("<x@3 x(3,8)>")
*/
addToPrototype('restart', function (...args) {
Pattern.prototype.restart = function (...args) {
return this.keepif.trigzero(...args);
});
addToPrototype('restartAll', function (...args) {
};
Pattern.prototype.restartAll = function (...args) {
return this.keep.trigzero(...args);
});
};
})();
// aliases
export const polyrhythm = stack;
export const pr = stack;
export const pm = polymeter;
// methods that create patterns, which are added to patternified Pattern methods
// TODO: remove? this is only used in old transpiler (shapeshifter)
Pattern.prototype.factories = {
@@ -1414,6 +1286,20 @@ export function timeCat(...timepats) {
return stack(...pats);
}
/**
* Allows to arrange multiple patterns together over multiple cycles.
* Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use.
*
* @return {Pattern}
* @example
* arrange([4, "<c a f e>(3,8)"],[2, "<g a>(5,8)"]).note()
*/
export function arrange(...sections) {
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
return timeCat(...sections).slow(total);
}
export function fastcat(...pats) {
return slowcat(...pats)._fast(pats.length);
}
@@ -1490,73 +1376,36 @@ export function polymeter(...args) {
return polymeterSteps(0, ...args);
}
// alias
export function pm(...args) {
polymeter(...args);
}
export const mask = curryPattern((a, b) => reify(b).mask(a));
export const struct = curryPattern((a, b) => reify(b).struct(a));
export const superimpose = curryPattern((a, b) => reify(b).superimpose(...a));
const methodToFunction = function (name, addAlignments = false) {
const func = curryPattern((a, b) => reify(b)[name](a));
withControls((controlname, controlfunc) => {
func[controlname] = function (...pats) {
return func(controlfunc(...pats));
};
});
if (addAlignments) {
for (const alignment of alignments) {
func[alignment] = curryPattern((a, b) => reify(b)[name][alignment](a));
withControls((controlname, controlfunc) => {
func[alignment][controlname] = function (...pats) {
return func[alignment](controlfunc(...pats));
};
});
}
}
return func;
};
export const mask = curry((a, b) => reify(b).mask(a));
export const struct = curry((a, b) => reify(b).struct(a));
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
// operators
export const set = methodToFunction('set', true);
export const keep = methodToFunction('keep', true);
export const keepif = methodToFunction('keepif', true);
export const add = methodToFunction('add', true);
export const sub = methodToFunction('sub', true);
export const mul = methodToFunction('mul', true);
export const div = methodToFunction('div', true);
export const mod = methodToFunction('mod', true);
export const pow = methodToFunction('pow', true);
export const band = methodToFunction('band', true);
export const bor = methodToFunction('bor', true);
export const bxor = methodToFunction('bxor', true);
export const blshift = methodToFunction('blshift', true);
export const brshift = methodToFunction('brshift', true);
export const lt = methodToFunction('lt', true);
export const gt = methodToFunction('gt', true);
export const lte = methodToFunction('lte', true);
export const gte = methodToFunction('gte', true);
export const eq = methodToFunction('eq', true);
export const eqt = methodToFunction('eqt', true);
export const ne = methodToFunction('ne', true);
export const net = methodToFunction('net', true);
export const and = methodToFunction('and', true);
export const or = methodToFunction('or', true);
export const func = methodToFunction('func', true);
// alignments
// export const in = methodToFunction('in'); // reserved word :(
export const out = methodToFunction('out');
export const mix = methodToFunction('mix');
export const squeeze = methodToFunction('squeeze');
export const squeezeout = methodToFunction('squeezeout');
export const trig = methodToFunction('trig');
export const trigzero = methodToFunction('trigzero');
export const set = curry((a, b) => reify(b).set(a));
export const keep = curry((a, b) => reify(b).keep(a));
export const keepif = curry((a, b) => reify(b).keepif(a));
export const add = curry((a, b) => reify(b).add(a));
export const sub = curry((a, b) => reify(b).sub(a));
export const mul = curry((a, b) => reify(b).mul(a));
export const div = curry((a, b) => reify(b).div(a));
export const mod = curry((a, b) => reify(b).mod(a));
export const pow = curry((a, b) => reify(b).pow(a));
export const band = curry((a, b) => reify(b).band(a));
export const bor = curry((a, b) => reify(b).bor(a));
export const bxor = curry((a, b) => reify(b).bxor(a));
export const blshift = curry((a, b) => reify(b).blshift(a));
export const brshift = curry((a, b) => reify(b).brshift(a));
export const lt = curry((a, b) => reify(b).lt(a));
export const gt = curry((a, b) => reify(b).gt(a));
export const lte = curry((a, b) => reify(b).lte(a));
export const gte = curry((a, b) => reify(b).gte(a));
export const eq = curry((a, b) => reify(b).eq(a));
export const eqt = curry((a, b) => reify(b).eqt(a));
export const ne = curry((a, b) => reify(b).ne(a));
export const net = curry((a, b) => reify(b).net(a));
export const and = curry((a, b) => reify(b).and(a));
export const or = curry((a, b) => reify(b).or(a));
export const func = curry((a, b) => reify(b).func(a));
/**
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
@@ -1566,44 +1415,45 @@ export const trigzero = methodToFunction('trigzero');
* @noAutocomplete
*
*/
export function register(name, func) {
export function register(name, func, patternify = true) {
if (Array.isArray(name)) {
const result = {};
for (const name_item of name) {
result[name_item] = register(name_item, func);
result[name_item] = register(name_item, func, patternify);
}
return result;
}
const arity = func.length;
var pfunc; // the patternified function
registerMethod(name);
const pfunc = function (...args) {
args = args.map(reify);
const pat = args[args.length - 1];
if (arity === 1) {
return func(pat);
}
const [left, ...right] = args.slice(0, -1);
let mapFn = (...args) => {
// make sure to call func with the correct argument count
// args.length is expected to be <= arity-1
// so we set undefined args explicitly undefined
Array(arity - 1)
.fill()
.map((_, i) => args[i] ?? undefined);
return func(...args, pat);
if (patternify) {
pfunc = function (...args) {
args = args.map(reify);
const pat = args[args.length - 1];
if (arity === 1) {
return func(pat);
}
const [left, ...right] = args.slice(0, -1);
let mapFn = (...args) => {
// make sure to call func with the correct argument count
// args.length is expected to be <= arity-1
// so we set undefined args explicitly undefined
Array(arity - 1)
.fill()
.map((_, i) => args[i] ?? undefined);
return func(...args, pat);
};
mapFn = curry(mapFn, null, arity - 1);
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
};
mapFn = curryPattern(mapFn, arity - 1);
const app = (acc, p) => acc.appLeft(p);
const start = left.fmap(mapFn);
return right.reduce(app, start).innerJoin();
};
} else {
pfunc = function (...args) {
args = args.map(reify);
return func(...args);
};
}
Pattern.prototype[name] = function (...args) {
args = args.map(reify);
// For methods that take a single argument (plus 'this'), allow
// multiple arguments but sequence them
if (arity === 2 && args.length !== 1) {
@@ -1611,6 +1461,7 @@ export function register(name, func) {
} else if (arity !== args.length + 1) {
throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`);
}
args = args.map(reify);
return pfunc(...args, this);
};
@@ -1624,7 +1475,7 @@ export function register(name, func) {
// toplevel functions get curried as well as patternified
// because pfunc uses spread args, we need to state the arity explicitly!
return curryPattern(pfunc, arity);
return curry(pfunc, null, arity);
}
//////////////////////////////////////////////////////////////////////
@@ -2390,6 +2241,61 @@ 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.
* @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)
*/
const slice = register(
'slice',
function (npat, ipat, opat) {
return npat.innerBind((n) =>
ipat.outerBind((i) =>
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 });
}),
),
);
},
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)
*/
const splice = register(
'splice',
function (npat, ipat, opat) {
const sliced = slice(npat, ipat, opat);
return sliced.withHap(function (hap) {
return hap.withValue((v) => ({
...{
speed: (1 / v._slices / hap.whole.duration) * (v.speed || 1),
unit: 'c',
},
...v,
}));
});
},
false, // turns off auto-patternification
);
const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
return _loopAt(factor, pat, 1);
});
+37 -6
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 { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
const scale = (normalized, min, max) => normalized * (max - min) + min;
const getValue = (e) => {
@@ -18,7 +18,7 @@ const getValue = (e) => {
}
note = note ?? n;
if (typeof note === 'string') {
return toMidi(note);
return noteToMidi(note);
}
if (typeof note === 'number') {
return note;
@@ -50,6 +50,7 @@ Pattern.prototype.pianoroll = function ({
timeframe: timeframeProp,
fold = 0,
vertical = 0,
labels = 0,
} = {}) {
const ctx = getDrawContext();
const w = ctx.canvas.width;
@@ -87,7 +88,7 @@ Pattern.prototype.pianoroll = function ({
const isActive = event.whole.begin <= t && event.whole.end > t;
ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
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);
@@ -114,6 +115,14 @@ Pattern.prototype.pianoroll = function ({
];
}
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);
}
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
@@ -181,6 +190,7 @@ export function pianoroll({
timeframe: timeframeProp,
fold = 0,
vertical = 0,
labels = false,
ctx,
} = {}) {
const w = ctx.canvas.width;
@@ -240,7 +250,7 @@ export function pianoroll({
const color = event.value?.color || event.context?.color;
ctx.fillStyle = color || inactive;
ctx.strokeStyle = color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
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);
@@ -267,6 +277,14 @@ export function pianoroll({
];
}
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);
}
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
@@ -284,7 +302,7 @@ export function pianoroll({
return this;
}
function getOptions(drawTime, options = {}) {
export function getDrawOptions(drawTime, options = {}) {
let [lookbehind, lookahead] = drawTime;
lookbehind = Math.abs(lookbehind);
const cycles = lookahead + lookbehind;
@@ -293,5 +311,18 @@ function getOptions(drawTime, options = {}) {
}
Pattern.prototype.punchcard = function (options) {
return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }),
);
};
/* Pattern.prototype.pianoroll = function (options) {
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),
);
}; */
export function drawPianoroll(options) {
const { drawTime, ...rest } = options;
pianoroll({ ...getDrawOptions(drawTime), ...rest });
}
+30 -18
View File
@@ -2,6 +2,7 @@ import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
export function repl({
interval,
@@ -17,36 +18,26 @@ export function repl({
}) {
const scheduler = new Cyclist({
interval,
onTrigger: async (hap, deadline, duration) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration);
}
if (hap.context.onTrigger) {
const cps = 1;
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
},
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
onToggle,
});
const setPattern = (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
};
setTime(() => scheduler.now()); // TODO: refactor?
const evaluate = async (code, autostart = true) => {
if (!code) {
throw new Error('no code to evaluate');
}
try {
beforeEval?.({ code });
await beforeEval?.({ code });
let { pattern } = await _evaluate(code, transpiler);
logger(`[eval] code updated`);
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
setPattern(pattern, autostart);
afterEval?.({ code, pattern });
return pattern;
} catch (err) {
@@ -58,5 +49,26 @@ export function repl({
const stop = () => scheduler.stop();
const start = () => scheduler.start();
const pause = () => scheduler.pause();
return { scheduler, evaluate, start, stop, pause };
const setCps = (cps) => scheduler.setCps(cps);
evalScope({
setCps,
setcps: setCps,
});
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
}
export const getTrigger =
({ getTime, defaultOutput }) =>
async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
};
+28
View File
@@ -0,0 +1,28 @@
/*
controls.test.mjs - <short description TODO>
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/controls.test.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import controls from '../controls.mjs';
import { mini } from '../../mini/mini.mjs';
import { describe, it, expect } from 'vitest';
describe('controls', () => {
it('should support controls', () => {
expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
});
it('should support compound controls', () => {
expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
{ s: 'bd', n: 3 },
{ s: 'sd', n: 4, gain: 1.4 },
]);
});
it('should support ignore extra elements in compound controls', () => {
expect(controls.s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
{ s: 'bd', n: 3, gain: 0.4 },
{ s: 'sd', n: 4, gain: 0.5 },
]);
});
});
+36 -14
View File
@@ -45,8 +45,6 @@ import {
rev,
time,
run,
hitch,
set,
} from '../index.mjs';
import { steady } from '../signal.mjs';
@@ -206,7 +204,7 @@ describe('Pattern', () => {
),
);
});
it('can squeezeout() structure', () => {
it('can SqueezeOut() structure', () => {
sameFirst(
sequence(1, [2, 3]).add.squeezeout(10, 20, 30),
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
@@ -254,7 +252,7 @@ describe('Pattern', () => {
),
);
});
it('can squeezeout() structure', () => {
it('can SqueezeOut() structure', () => {
sameFirst(sequence(1, [2, 3]).keep.squeezeout(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
});
});
@@ -296,7 +294,7 @@ describe('Pattern', () => {
),
);
});
it('can squeezeout() structure', () => {
it('can SqueezeOut() structure', () => {
sameFirst(sequence(1, [2, 3]).keepif.squeezeout(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
});
});
@@ -931,14 +929,6 @@ describe('Pattern', () => {
});
});
describe('alignments', () => {
it('Can combine controls', () => {
sameFirst(s('bd').set.in.n(3), s('bd').n(3));
sameFirst(s('bd').set.squeeze.n(3, 4), sequence(s('bd').n(3), s('bd').n(4)));
});
it('Can combine functions with alignmed controls', () => {
sameFirst(s('bd').apply(fast(2).set(n(3))), s('bd').fast(2).set.in.n(3));
sameFirst(s('bd').apply(fast(2).set.in.n(3)), s('bd').fast(2).set.in.n(3));
});
it('Can squeeze arguments', () => {
expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle());
});
@@ -969,7 +959,7 @@ describe('Pattern', () => {
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
});
});
describe('composable functions', () => {
/*describe('composable functions', () => {
it('Can compose functions', () => {
sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
});
@@ -981,4 +971,36 @@ describe('Pattern', () => {
// sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2));
});
});
describe('weave', () => {
it('Can distribute patterns along a pattern', () => {
sameFirst(n(0, 1).weave(2, s('bd', silence), s(silence, 'sd')), sequence(s('bd').n(0), s('sd').n(1)));
});
});
*/
describe('slice', () => {
it('Can slice a sample', () => {
sameFirst(
s('break').slice(4, sequence(0, 1, 2, 3)),
sequence(
{ begin: 0, end: 0.25, s: 'break', _slices: 4 },
{ begin: 0.25, end: 0.5, s: 'break', _slices: 4 },
{ begin: 0.5, end: 0.75, s: 'break', _slices: 4 },
{ begin: 0.75, end: 1, s: 'break', _slices: 4 },
),
);
});
});
describe('splice', () => {
it('Can splice a sample', () => {
sameFirst(
s('break').splice(4, sequence(0, 1, 2, 3)),
sequence(
{ begin: 0, end: 0.25, s: 'break', _slices: 4, unit: 'c', speed: 1 },
{ begin: 0.25, end: 0.5, s: 'break', _slices: 4, unit: 'c', speed: 1 },
{ begin: 0.5, end: 0.75, s: 'break', _slices: 4, unit: 'c', speed: 1 },
{ begin: 0.75, end: 1, s: 'break', _slices: 4, unit: 'c', speed: 1 },
),
);
});
});
});
+17 -17
View File
@@ -8,8 +8,8 @@ import { pure } from '../pattern.mjs';
import {
isNote,
tokenizeNote,
toMidi,
fromMidi,
noteToMidi,
midiToFreq,
freqToMidi,
_mod,
compose,
@@ -75,27 +75,27 @@ describe('isNote', () => {
expect(tokenizeNote(123)).toStrictEqual([]);
});
});
describe('toMidi', () => {
describe('noteToMidi', () => {
it('should turn notes into midi', () => {
expect(toMidi('A4')).toEqual(69);
expect(toMidi('C4')).toEqual(60);
expect(toMidi('Db4')).toEqual(61);
expect(toMidi('C3')).toEqual(48);
expect(toMidi('Cb3')).toEqual(47);
expect(toMidi('Cbb3')).toEqual(46);
expect(toMidi('C#3')).toEqual(49);
expect(toMidi('C#3')).toEqual(49);
expect(toMidi('C##3')).toEqual(50);
expect(noteToMidi('A4')).toEqual(69);
expect(noteToMidi('C4')).toEqual(60);
expect(noteToMidi('Db4')).toEqual(61);
expect(noteToMidi('C3')).toEqual(48);
expect(noteToMidi('Cb3')).toEqual(47);
expect(noteToMidi('Cbb3')).toEqual(46);
expect(noteToMidi('C#3')).toEqual(49);
expect(noteToMidi('C#3')).toEqual(49);
expect(noteToMidi('C##3')).toEqual(50);
});
it('should throw an error when given a non-note', () => {
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
});
});
describe('fromMidi', () => {
describe('midiToFreq', () => {
it('should turn midi into frequency', () => {
expect(fromMidi(69)).toEqual(440);
expect(fromMidi(57)).toEqual(220);
expect(midiToFreq(69)).toEqual(440);
expect(midiToFreq(57)).toEqual(220);
});
});
describe('freqToMidi', () => {
+20 -11
View File
@@ -19,7 +19,7 @@ export const tokenizeNote = (note) => {
};
// turns the given note into its midi number representation
export const toMidi = (note) => {
export const noteToMidi = (note) => {
const [pc, acc, oct = 3] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
@@ -28,7 +28,7 @@ export const toMidi = (note) => {
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const fromMidi = (n) => {
export const midiToFreq = (n) => {
return Math.pow(2, (n - 69) / 12) * 440;
};
@@ -45,7 +45,7 @@ export const valueToMidi = (value, fallbackValue) => {
return freqToMidi(freq);
}
if (typeof note === 'string') {
return toMidi(note);
return noteToMidi(note);
}
if (typeof note === 'number') {
return note;
@@ -62,18 +62,19 @@ export const valueToMidi = (value, fallbackValue) => {
*/
export const getFreq = (noteOrMidi) => {
if (typeof noteOrMidi === 'number') {
return fromMidi(noteOrMidi);
return midiToFreq(noteOrMidi);
}
return fromMidi(toMidi(noteOrMidi));
return midiToFreq(noteToMidi(noteOrMidi));
};
const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
/**
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
* @noAutocomplete
*/
export const midi2note = (n) => {
const oct = Math.floor(n / 12) - 1;
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
const pc = pcs[n % 12];
return pc + oct;
};
@@ -91,7 +92,7 @@ export const getPlayableNoteValue = (hap) => {
}
// if value is number => interpret as midi number as long as its not marked as frequency
if (typeof note === 'number' && context.type !== 'frequency') {
note = fromMidi(hap.value);
note = midiToFreq(hap.value);
} else if (typeof note === 'number' && context.type === 'frequency') {
note = hap.value; // legacy workaround.. will be removed in the future
} else if (typeof note !== 'string' || !isNote(note)) {
@@ -110,9 +111,9 @@ export const getFrequency = (hap) => {
return getFreq(value.note || value.n || value.value);
}
if (typeof value === 'number' && context.type !== 'frequency') {
value = fromMidi(hap.value);
value = midiToFreq(hap.value);
} else if (typeof value === 'string' && isNote(value)) {
value = fromMidi(toMidi(hap.value));
value = midiToFreq(noteToMidi(hap.value));
} else if (typeof value !== 'number') {
throw new Error('not a note or frequency: ' + value);
}
@@ -139,7 +140,7 @@ export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
export const flatten = (arr) => [].concat(...arr);
export const id = (a) => a;
export const constant = curry((a, b) => a);
export const constant = (a, b) => a;
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
@@ -170,7 +171,7 @@ export function parseNumeral(numOrString) {
return asNumber;
}
if (isNote(numOrString)) {
return toMidi(numOrString);
return noteToMidi(numOrString);
}
throw new Error(`cannot parse as numeral: "${numOrString}"`);
}
@@ -206,3 +207,11 @@ export function parseFractional(numOrString) {
}
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
export const splitAt = function (index, value) {
return [value.slice(0, index), value.slice(index)];
};
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
+1 -1
View File
@@ -44,6 +44,6 @@ function createClock(
};
const getPhase = () => phase;
// setCallback
return { setDuration, start, stop, pause, duration, getPhase, minLatency };
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
}
export default createClock;
+1 -3
View File
@@ -28,9 +28,7 @@ export const csound = register('csound', (instrument, pat) => {
logger('[csound] not loaded yet', 'warning');
return;
}
if (typeof hap.value !== 'object') {
throw new Error('csound only support objects as hap values');
}
hap.ensureObjectValue();
let { gain = 0.8 } = hap.value;
gain *= 0.2;
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/csound",
"version": "0.6.2",
"version": "0.7.1",
"description": "csound bindings for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -32,11 +32,11 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@csound/browser": "6.18.5",
"@csound/browser": "6.18.7",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.6.2",
"version": "0.7.1",
"description": "Code evaluator for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -44,7 +44,7 @@
},
"devDependencies": {
"@strudel.cycles/mini": "workspace:*",
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+30 -15
View File
@@ -5,8 +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, isNote, getPlayableNoteValue, logger } from '@strudel.cycles/core';
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { noteToMidi } from '@strudel.cycles/core';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -63,7 +64,7 @@ function getDevice(output, outputs) {
}
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.midi = function (output, channel = 1) {
Pattern.prototype.midi = function (output) {
if (!supportsMidi()) {
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
}
@@ -90,11 +91,6 @@ Pattern.prototype.midi = function (output, channel = 1) {
);
}
return this.onTrigger((time, hap) => {
let note = getPlayableNoteValue(hap);
const velocity = hap.context?.velocity ?? 0.9;
if (!isNote(note)) {
throw new Error('not a note: ' + note);
}
if (!midiReady) {
return;
}
@@ -106,15 +102,34 @@ Pattern.prototype.midi = function (output, channel = 1) {
.join(' | ')}`,
);
}
// console.log('midi', value, output);
hap.ensureObjectValue();
// calculate time
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
time = time * 1000 + timingOffset;
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
// await enableWebMidi()
device.playNote(note, channel, {
time,
duration: hap.duration.valueOf() * 1000 - 5,
attack: velocity,
});
// destructure value
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
const duration = hap.duration.valueOf() * 1000 - 5;
if (note) {
const midiNumber = noteToMidi(note);
device.playNote(midiNumber, midichan, {
time,
duration,
attack: velocity,
});
}
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);
device.sendControlChange(ccn, scaled, midichan, { time });
}
});
};
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.6.0",
"version": "0.7.1",
"description": "Midi API for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -31,9 +31,9 @@
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"webmidi": "^3.0.21"
"webmidi": "^3.1.5"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
+171 -134
View File
@@ -1,4 +1,4 @@
// Generated by Peggy 2.0.1.
// Generated by Peggy 3.0.2.
//
// https://peggyjs.org/
@@ -43,16 +43,19 @@ peg$SyntaxError.prototype.format = function(sources) {
}
}
var s = this.location.start;
var loc = this.location.source + ":" + s.line + ":" + s.column;
var offset_s = (this.location.source && (typeof this.location.source.offset === "function"))
? this.location.source.offset(s)
: s;
var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
if (src) {
var e = this.location.end;
var filler = peg$padEnd("", s.line.toString().length, ' ');
var filler = peg$padEnd("", offset_s.line.toString().length, ' ');
var line = src[s.line - 1];
var last = s.line === e.line ? e.column : line.length + 1;
var hatLen = (last - s.column) || 1;
str += "\n --> " + loc + "\n"
+ filler + " |\n"
+ s.line + " | " + line + "\n"
+ offset_s.line + " | " + line + "\n"
+ filler + " | " + peg$padEnd("", s.column - 1, ' ')
+ peg$padEnd("", hatLen, "^");
} else {
@@ -182,21 +185,21 @@ function peg$parse(input, options) {
var peg$c8 = "#";
var peg$c9 = "^";
var peg$c10 = "_";
var peg$c11 = ":";
var peg$c12 = "[";
var peg$c13 = "]";
var peg$c14 = "{";
var peg$c15 = "}";
var peg$c16 = "%";
var peg$c17 = "<";
var peg$c18 = ">";
var peg$c19 = "@";
var peg$c20 = "!";
var peg$c21 = "(";
var peg$c22 = ")";
var peg$c23 = "/";
var peg$c24 = "*";
var peg$c25 = "?";
var peg$c11 = "[";
var peg$c12 = "]";
var peg$c13 = "{";
var peg$c14 = "}";
var peg$c15 = "%";
var peg$c16 = "<";
var peg$c17 = ">";
var peg$c18 = "@";
var peg$c19 = "!";
var peg$c20 = "(";
var peg$c21 = ")";
var peg$c22 = "/";
var peg$c23 = "*";
var peg$c24 = "?";
var peg$c25 = ":";
var peg$c26 = "struct";
var peg$c27 = "target";
var peg$c28 = "euclid";
@@ -237,21 +240,21 @@ function peg$parse(input, options) {
var peg$e15 = peg$literalExpectation("#", false);
var peg$e16 = peg$literalExpectation("^", false);
var peg$e17 = peg$literalExpectation("_", false);
var peg$e18 = peg$literalExpectation(":", false);
var peg$e19 = peg$literalExpectation("[", false);
var peg$e20 = peg$literalExpectation("]", false);
var peg$e21 = peg$literalExpectation("{", false);
var peg$e22 = peg$literalExpectation("}", false);
var peg$e23 = peg$literalExpectation("%", false);
var peg$e24 = peg$literalExpectation("<", false);
var peg$e25 = peg$literalExpectation(">", false);
var peg$e26 = peg$literalExpectation("@", false);
var peg$e27 = peg$literalExpectation("!", false);
var peg$e28 = peg$literalExpectation("(", false);
var peg$e29 = peg$literalExpectation(")", false);
var peg$e30 = peg$literalExpectation("/", false);
var peg$e31 = peg$literalExpectation("*", false);
var peg$e32 = peg$literalExpectation("?", false);
var peg$e18 = peg$literalExpectation("[", false);
var peg$e19 = peg$literalExpectation("]", false);
var peg$e20 = peg$literalExpectation("{", false);
var peg$e21 = peg$literalExpectation("}", false);
var peg$e22 = peg$literalExpectation("%", false);
var peg$e23 = peg$literalExpectation("<", false);
var peg$e24 = peg$literalExpectation(">", false);
var peg$e25 = peg$literalExpectation("@", false);
var peg$e26 = peg$literalExpectation("!", false);
var peg$e27 = peg$literalExpectation("(", false);
var peg$e28 = peg$literalExpectation(")", false);
var peg$e29 = peg$literalExpectation("/", false);
var peg$e30 = peg$literalExpectation("*", false);
var peg$e31 = peg$literalExpectation("?", false);
var peg$e32 = peg$literalExpectation(":", false);
var peg$e33 = peg$literalExpectation("struct", false);
var peg$e34 = peg$literalExpectation("target", false);
var peg$e35 = peg$literalExpectation("euclid", false);
@@ -279,36 +282,37 @@ function peg$parse(input, options) {
var peg$f8 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) };
var peg$f12 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
var peg$f12 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
var peg$f13 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
for (const op of ops) {
op(result);
}
return result;
};
var peg$f13 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f14 = function(tail) { return { alignment: 'stack', list: tail }; };
var peg$f15 = function(tail) { return { alignment: 'rand', list: tail }; };
var peg$f16 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } };
var peg$f17 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
var peg$f18 = function(sc) { return sc; };
var peg$f19 = function(s) { return { name: "struct", args: { mini:s }}};
var peg$f20 = function(s) { return { name: "target", args : { name:s}}};
var peg$f21 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
var peg$f22 = function(a) { return { name: "stretch", args :{ amount: a}}};
var peg$f23 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
var peg$f24 = function(a) { return { name: "shift", args :{ amount: a}}};
var peg$f25 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
var peg$f26 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
var peg$f27 = function(s, v) { return v};
var peg$f28 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
var peg$f29 = function(sg) {return sg};
var peg$f30 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
var peg$f31 = function(sc) { return sc };
var peg$f32 = function(c) { return c };
var peg$f33 = function(v) { return new CommandStub("setcps", { value: v})};
var peg$f34 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
var peg$f35 = function() { return new CommandStub("hush")};
var peg$f14 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f15 = function(tail) { return { alignment: 'stack', list: tail }; };
var peg$f16 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
var peg$f18 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
var peg$f19 = function(sc) { return sc; };
var peg$f20 = function(s) { return { name: "struct", args: { mini:s }}};
var peg$f21 = function(s) { return { name: "target", args : { name:s}}};
var peg$f22 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
var peg$f23 = function(a) { return { name: "stretch", args :{ amount: a}}};
var peg$f24 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
var peg$f25 = function(a) { return { name: "shift", args :{ amount: a}}};
var peg$f26 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
var peg$f27 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
var peg$f28 = function(s, v) { return v};
var peg$f29 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
var peg$f30 = function(sg) {return sg};
var peg$f31 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
var peg$f32 = function(sc) { return sc };
var peg$f33 = function(c) { return c };
var peg$f34 = function(v) { return new CommandStub("setcps", { value: v})};
var peg$f35 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
var peg$f36 = function() { return new CommandStub("hush")};
var peg$currPos = 0;
var peg$savedPos = 0;
var peg$posDetailsCache = [{ line: 1, column: 1 }];
@@ -421,11 +425,11 @@ function peg$parse(input, options) {
}
}
function peg$computeLocation(startPos, endPos) {
function peg$computeLocation(startPos, endPos, offset) {
var startPosDetails = peg$computePosDetails(startPos);
var endPosDetails = peg$computePosDetails(endPos);
return {
var res = {
source: peg$source,
start: {
offset: startPos,
@@ -438,6 +442,11 @@ function peg$parse(input, options) {
column: endPosDetails.column
}
};
if (offset && peg$source && (typeof peg$source.offset === "function")) {
res.start = peg$source.offset(res.start);
res.end = peg$source.offset(res.end);
}
return res;
}
function peg$fail(expected) {
@@ -848,15 +857,6 @@ function peg$parse(input, options) {
s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e17); }
}
if (s0 === peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 58) {
s0 = peg$c11;
peg$currPos++;
} else {
s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e18); }
}
}
}
}
}
@@ -899,11 +899,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 91) {
s2 = peg$c12;
s2 = peg$c11;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e19); }
if (peg$silentFails === 0) { peg$fail(peg$e18); }
}
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
@@ -911,11 +911,11 @@ function peg$parse(input, options) {
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 93) {
s6 = peg$c13;
s6 = peg$c12;
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e20); }
if (peg$silentFails === 0) { peg$fail(peg$e19); }
}
if (s6 !== peg$FAILED) {
s7 = peg$parsews();
@@ -943,11 +943,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 123) {
s2 = peg$c14;
s2 = peg$c13;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e21); }
if (peg$silentFails === 0) { peg$fail(peg$e20); }
}
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
@@ -955,11 +955,11 @@ function peg$parse(input, options) {
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 125) {
s6 = peg$c15;
s6 = peg$c14;
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e22); }
if (peg$silentFails === 0) { peg$fail(peg$e21); }
}
if (s6 !== peg$FAILED) {
s7 = peg$parsepolymeter_steps();
@@ -990,11 +990,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 37) {
s1 = peg$c16;
s1 = peg$c15;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e23); }
if (peg$silentFails === 0) { peg$fail(peg$e22); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseslice();
@@ -1019,11 +1019,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
s1 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 60) {
s2 = peg$c17;
s2 = peg$c16;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e24); }
if (peg$silentFails === 0) { peg$fail(peg$e23); }
}
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
@@ -1031,11 +1031,11 @@ function peg$parse(input, options) {
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 62) {
s6 = peg$c18;
s6 = peg$c17;
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e25); }
if (peg$silentFails === 0) { peg$fail(peg$e24); }
}
if (s6 !== peg$FAILED) {
s7 = peg$parsews();
@@ -1088,6 +1088,9 @@ function peg$parse(input, options) {
s0 = peg$parseop_replicate();
if (s0 === peg$FAILED) {
s0 = peg$parseop_degrade();
if (s0 === peg$FAILED) {
s0 = peg$parseop_tail();
}
}
}
}
@@ -1102,11 +1105,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 64) {
s1 = peg$c19;
s1 = peg$c18;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e26); }
if (peg$silentFails === 0) { peg$fail(peg$e25); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
@@ -1130,11 +1133,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 33) {
s1 = peg$c20;
s1 = peg$c19;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e27); }
if (peg$silentFails === 0) { peg$fail(peg$e26); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
@@ -1158,11 +1161,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 40) {
s1 = peg$c21;
s1 = peg$c20;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e28); }
if (peg$silentFails === 0) { peg$fail(peg$e27); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1186,11 +1189,11 @@ function peg$parse(input, options) {
}
s12 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 41) {
s13 = peg$c22;
s13 = peg$c21;
peg$currPos++;
} else {
s13 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e29); }
if (peg$silentFails === 0) { peg$fail(peg$e28); }
}
if (s13 !== peg$FAILED) {
peg$savedPos = s0;
@@ -1224,11 +1227,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 47) {
s1 = peg$c23;
s1 = peg$c22;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e30); }
if (peg$silentFails === 0) { peg$fail(peg$e29); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseslice();
@@ -1252,11 +1255,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 42) {
s1 = peg$c24;
s1 = peg$c23;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e31); }
if (peg$silentFails === 0) { peg$fail(peg$e30); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseslice();
@@ -1280,11 +1283,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 63) {
s1 = peg$c25;
s1 = peg$c24;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e32); }
if (peg$silentFails === 0) { peg$fail(peg$e31); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsenumber();
@@ -1301,6 +1304,34 @@ function peg$parse(input, options) {
return s0;
}
function peg$parseop_tail() {
var s0, s1, s2;
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 58) {
s1 = peg$c25;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e32); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f12(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parseslice_with_ops() {
var s0, s1, s2, s3;
@@ -1314,7 +1345,7 @@ function peg$parse(input, options) {
s3 = peg$parseslice_op();
}
peg$savedPos = s0;
s0 = peg$f12(s1, s2);
s0 = peg$f13(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1339,7 +1370,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f13(s1);
s1 = peg$f14(s1);
}
s0 = s1;
@@ -1388,7 +1419,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f14(s1);
s1 = peg$f15(s1);
}
s0 = s1;
@@ -1437,7 +1468,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f15(s1);
s1 = peg$f16(s1);
}
s0 = s1;
@@ -1458,7 +1489,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f16(s1, s2);
s0 = peg$f17(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1478,7 +1509,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f17(s1, s2);
s0 = peg$f18(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1488,18 +1519,20 @@ function peg$parse(input, options) {
}
function peg$parsemini() {
var s0, s1, s2, s3, s4;
var s0, s1, s2, s3, s4, s5, s6;
s0 = peg$currPos;
s1 = peg$parsews();
s2 = peg$parsequote();
if (s2 !== peg$FAILED) {
s3 = peg$parsestack_or_choose();
if (s3 !== peg$FAILED) {
s4 = peg$parsequote();
if (s4 !== peg$FAILED) {
s3 = peg$parsews();
s4 = peg$parsestack_or_choose();
if (s4 !== peg$FAILED) {
s5 = peg$parsews();
s6 = peg$parsequote();
if (s6 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f18(s3);
s0 = peg$f19(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1561,7 +1594,7 @@ function peg$parse(input, options) {
s3 = peg$parsemini_or_operator();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f19(s3);
s0 = peg$f20(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1594,7 +1627,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f20(s4);
s0 = peg$f21(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1639,7 +1672,7 @@ function peg$parse(input, options) {
s7 = null;
}
peg$savedPos = s0;
s0 = peg$f21(s3, s5, s7);
s0 = peg$f22(s3, s5, s7);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1672,7 +1705,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f22(s3);
s0 = peg$f23(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1701,7 +1734,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f23(s3);
s0 = peg$f24(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1730,7 +1763,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f24(s3);
s0 = peg$f25(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1759,7 +1792,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f25(s3);
s0 = peg$f26(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1801,7 +1834,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f26(s4);
s0 = peg$f27(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1876,11 +1909,11 @@ function peg$parse(input, options) {
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 91) {
s3 = peg$c12;
s3 = peg$c11;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e19); }
if (peg$silentFails === 0) { peg$fail(peg$e18); }
}
if (s3 !== peg$FAILED) {
s4 = peg$parsews();
@@ -1893,7 +1926,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) {
peg$savedPos = s7;
s7 = peg$f27(s5, s9);
s7 = peg$f28(s5, s9);
} else {
peg$currPos = s7;
s7 = peg$FAILED;
@@ -1910,7 +1943,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) {
peg$savedPos = s7;
s7 = peg$f27(s5, s9);
s7 = peg$f28(s5, s9);
} else {
peg$currPos = s7;
s7 = peg$FAILED;
@@ -1922,15 +1955,15 @@ function peg$parse(input, options) {
}
s7 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 93) {
s8 = peg$c13;
s8 = peg$c12;
peg$currPos++;
} else {
s8 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e20); }
if (peg$silentFails === 0) { peg$fail(peg$e19); }
}
if (s8 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f28(s5, s6);
s0 = peg$f29(s5, s6);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1976,7 +2009,7 @@ function peg$parse(input, options) {
s4 = peg$parsecomment();
}
peg$savedPos = s0;
s0 = peg$f29(s1);
s0 = peg$f30(s1);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1998,7 +2031,7 @@ function peg$parse(input, options) {
s5 = peg$parsemini_or_operator();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f30(s1, s5);
s0 = peg$f31(s1, s5);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2023,7 +2056,7 @@ function peg$parse(input, options) {
s1 = peg$parsemini_or_operator();
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f31(s1);
s1 = peg$f32(s1);
}
s0 = s1;
if (s0 === peg$FAILED) {
@@ -2056,7 +2089,7 @@ function peg$parse(input, options) {
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f32(s2);
s0 = peg$f33(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2081,7 +2114,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f33(s3);
s0 = peg$f34(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2110,7 +2143,7 @@ function peg$parse(input, options) {
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f34(s3);
s0 = peg$f35(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2136,7 +2169,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f35();
s1 = peg$f36();
}
s0 = s1;
@@ -2162,10 +2195,13 @@ function peg$parse(input, options) {
this.location_ = location();
}
var PatternStub = function(source, alignment)
var PatternStub = function(source, alignment, seed)
{
this.type_ = "pattern";
this.arguments_ = { alignment : alignment};
this.arguments_ = { alignment: alignment };
if (seed !== undefined) {
this.arguments_.seed = seed;
}
this.source_ = source;
}
@@ -2191,6 +2227,7 @@ function peg$parse(input, options) {
this.options_ = options;
}
var seed = 0;
peg$result = peg$startRuleFunction();
+15 -8
View File
@@ -19,10 +19,13 @@ This program is free software: you can redistribute it and/or modify it under th
this.location_ = location();
}
var PatternStub = function(source, alignment)
var PatternStub = function(source, alignment, seed)
{
this.type_ = "pattern";
this.arguments_ = { alignment : alignment};
this.arguments_ = { alignment: alignment };
if (seed !== undefined) {
this.arguments_.seed = seed;
}
this.source_ = source;
}
@@ -48,6 +51,7 @@ This program is free software: you can redistribute it and/or modify it under th
this.options_ = options;
}
var seed = 0;
}
start = statement
@@ -96,7 +100,7 @@ quote = '"' / "'"
// ------------------ steps and cycles ---------------------------
// single step definition (e.g bd)
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_"
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
// define a sub cycle e.g. [1 2, 3 [4]]
@@ -119,7 +123,7 @@ slice = step / sub_cycle / polymeter / slow_sequence
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
// at this point, we assume we can represent them as regular sequence operators
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail
op_weight = "@" a:number
{ return x => x.options_['weight'] = a }
@@ -137,7 +141,10 @@ op_fast = "*"a:slice
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
op_degrade = "?"a:number?
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) }
op_tail = ":" s:slice
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
slice_with_ops = s:slice ops:slice_op*
@@ -159,12 +166,12 @@ stack_tail = tail:(comma @sequence)+
// a choose is a series of pipe-separated sequence, one of which is
// chosen at random, each cycle
choose_tail = tail:(pipe @sequence)+
{ return { alignment: 'rand', list: tail }; }
{ return { alignment: 'rand', list: tail, seed: seed++ }; }
// if the stack contains only one element, we don't create a stack but return the
// underlying element
stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
polymeter_stack = head:sequence tail:stack_tail?
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
@@ -175,7 +182,7 @@ polymeter_stack = head:sequence tail:stack_tail?
// Experimental haskellish parser begins
// mini-notation = a quoted stack
mini = ws quote sc:stack_or_choose quote
mini = ws quote ws sc:stack_or_choose ws quote
{ return sc; }
// ------------------ operators ---------------------------
+16 -30
View File
@@ -7,17 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
import * as krill from './krill-parser.js';
import * as strudel from '@strudel.cycles/core';
/* var _seedState = 0;
const randOffset = 0.0002;
function _nextSeed() {
return _seedState++;
} */
const randOffset = 0.0003;
const applyOptions = (parent, code) => (pat, i) => {
const ast = parent.source_[i];
const options = ast.options_;
const ops = options?.ops;
if (ops) {
for (const op of ops) {
switch (op.type_) {
@@ -43,27 +39,14 @@ const applyOptions = (parent, code) => (pat, i) => {
break;
}
case 'degradeBy': {
// TODO: find out what is right here
// example:
/*
stack(
s("hh*8").degrade(),
s("[ht*8]?")
)
*/
// above example will only be in sync when _degradeBy is used...
// it also seems that the nextSeed will create undeterministic behaviour
// as it uses a global _seedState. This is probably the reason for
// https://github.com/tidalcycles/strudel/issues/245
// this is how it was:
/*
return strudel.reify(pat)._degradeByWith(
strudel.rand.early(randOffset * _nextSeed()).segment(1),
op.arguments_.amount ?? 0.5,
);
*/
pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
pat = strudel
.reify(pat)
._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5);
break;
}
case 'tail': {
const friend = patternifyAST(op.arguments_.element, code);
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
break;
}
default: {
@@ -108,9 +91,7 @@ export function patternifyAST(ast, code) {
return strudel.stack(...aligned);
}
if (alignment === 'rand') {
// https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422
// return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
return strudel.chooseCycles(...children);
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
}
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (!weightedChildren && alignment === 'slowcat') {
@@ -209,3 +190,8 @@ export function minify(thing) {
}
return strudel.reify(thing);
}
// calling this function will cause patterns to parse strings as mini notation by default
export function miniAllStrings() {
strudel.setStringParser(mini);
}
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.6.0",
"version": "0.8.1",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
@@ -35,8 +35,8 @@
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"peggy": "^2.0.1",
"vite": "^3.2.2",
"vitest": "^0.25.7"
"peggy": "^3.0.2",
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+20 -3
View File
@@ -140,7 +140,22 @@ describe('mini', () => {
expect(haps.length < 230).toBe(true);
// 'Had too many cycles remaining after degradeBy 0.8');
});
/*it('supports the random choice operator ("|") with nesting', () => {
it('supports multiple independent uses of the random choice operator ("|")', () => {
const numCycles = 1000;
const values = mini('[a|b] [a|b]')
.queryArc(0, numCycles)
.map((e) => e.value);
const observed = { aa: 0, ab: 0, ba: 0, bb: 0 };
for (let i = 0; i < values.length; i += 2) {
const chunk = values.slice(i, i + 2);
observed[chunk.join('')]++;
}
for (const count of Object.values(observed)) {
// Should fall within 99% confidence interval for binomial with p=0.25.
expect(215 <= count && count <= 286).toBe(true);
}
});
it('supports the random choice operator ("|") with nesting', () => {
const numCycles = 900;
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
@@ -165,6 +180,8 @@ describe('mini', () => {
// 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
// PRNG, this test should succeed
expect(chisq <= 15.086).toBe(true);
// assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
});*/
});
it('supports lists', () => {
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
});
});
+4 -3
View File
@@ -45,9 +45,10 @@ let startedAt = -1;
* @memberof Pattern
* @returns Pattern
*/
Pattern.prototype.osc = async function () {
const osc = await connect();
return this.onTrigger((time, hap, currentTime, cps = 1) => {
Pattern.prototype.osc = function () {
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
hap.ensureObjectValue();
const osc = await connect();
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
// time should be audio time of onset
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.6.0",
"version": "0.7.1",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"publishConfig": {
@@ -40,7 +40,7 @@
"osc-js": "^2.4.0"
},
"devDependencies": {
"pkg": "^5.7.0",
"vite": "^3.2.2"
"pkg": "^5.8.1",
"vite": "^4.3.3"
}
}
+33 -14
View File
@@ -13,24 +13,43 @@ npm i @strudel.cycles/react
Here is a minimal example of how to set up a MiniRepl:
```jsx
import { evalScope, controls } from '@strudel.cycles/core';
import * as React from 'react';
import '@strudel.cycles/react/dist/style.css';
import { MiniRepl } from '@strudel.cycles/react';
import { prebake } from '../repl/src/prebake.mjs';
import { evalScope, controls } from '@strudel.cycles/core';
import { samples, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
/* probably import other strudel packages */
);
async function prebake() {
await samples(
'https://strudel.tidalcycles.org/tidal-drum-machines.json',
'github:ritchse/tidal-drum-machines/main/machines/'
);
await samples(
'https://strudel.tidalcycles.org/EmuSP12.json',
'https://strudel.tidalcycles.org/EmuSP12/'
);
}
prebake();
async function init() {
await evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/tonal')
);
await prebake();
initAudioOnFirstClick();
}
export function Repl({ tune }) {
return <MiniRepl tune={tune} hideOutsideView={true} />;
if (typeof window !== 'undefined') {
init();
}
export default function App() {
return <MiniRepl tune={`s("bd sd,hh*4")`} />;
}
```
For a more sophisticated example, check out the [nano-repl](./examples/nano-repl/)!
- Open [example on stackblitz](https://stackblitz.com/edit/react-ts-saaair?file=tune.tsx,App.tsx)
- Also check out the [nano-repl](./examples/nano-repl/) for a more sophisticated example
@@ -0,0 +1,15 @@
# nano-repl
this is an example of how to create a repl with strudel and react.
## Usage
after cloning the strudel repo:
```sh
pnpm i
cd packages/react/examples/nano-repl
pnpm dev
```
you should now have a repl running at `http://localhost:5173/`
@@ -15,17 +15,18 @@
"@strudel.cycles/osc": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/react": "workspace:*"
},
"devDependencies": {
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.0.1",
"autoprefixer": "^10.4.8",
"postcss": "^8.4.16",
"tailwindcss": "^3.1.8",
"vite": "^3.0.7"
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"tailwindcss": "^3.3.2",
"vite": "^4.3.3"
}
}
+30 -18
View File
@@ -1,22 +1,34 @@
import { controls, evalScope } from '@strudel.cycles/core';
import { CodeMirror, useHighlighting, useKeydown, useStrudel, flash } from '@strudel.cycles/react';
import { getAudioContext, initAudioOnFirstClick, panic, webaudioOutput } from '@strudel.cycles/webaudio';
import {
getAudioContext,
initAudioOnFirstClick,
panic,
webaudioOutput,
registerSynthSounds,
} from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { useCallback, useState } from 'react';
import './style.css';
// import { prebake } from '../../../../../repl/src/prebake.mjs';
initAudioOnFirstClick();
// TODO: only import stuff when play is pressed?
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
);
async function init() {
// TODO: only import stuff when play is pressed?
const loadModules = evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/osc'),
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
}
init();
const defaultTune = `samples({
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
@@ -31,7 +43,7 @@ stack(
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
.add(perlin.range(0,.5)) // random pitch variation
.superimpose(add(.05)) // add second, slightly detuned voice
.n() // wrap in "n"
.note() // wrap in "note"
.decay(.15).sustain(0) // make each note of equal length
.s('sawtooth') // waveform
.gain(.4) // turn down
@@ -40,7 +52,7 @@ stack(
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.note() // wrap in "n"
.s('square') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
@@ -49,7 +61,7 @@ stack(
,"a4 c5 <e6 a6>".struct("x(5,8)")
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n"
.note() // wrap in "note"
.decay(.1).sustain(0) // make notes short
.s('triangle') // waveform
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
@@ -66,7 +78,7 @@ function App() {
const [code, setCode] = useState(defaultTune);
const [view, setView] = useState();
// const [code, setCode] = useState(`"c3".note().slow(2)`);
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern } = useStrudel({
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern, started } = useStrudel({
code,
defaultOutput: webaudioOutput,
getTime,
@@ -75,8 +87,8 @@ function App() {
useHighlighting({
view,
pattern,
active: !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.getPhase(),
active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.now(),
});
const error = evalError || schedulerError;
@@ -103,7 +115,7 @@ function App() {
}
}
},
[scheduler, evaluate, view],
[scheduler, evaluate, view, code],
),
);
return (
@@ -2,6 +2,18 @@
@tailwind components;
@tailwind utilities;
:root {
--background: #222;
--lineBackground: #22222250;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
--selectionMatch: #036dd626;
--lineHighlight: #00000050;
--gutterBackground: transparent;
--gutterForeground: #8a919966;
}
body {
background: #123;
}
@@ -6,9 +6,23 @@ This program is free software: you can redistribute it and/or modify it under th
module.exports = {
// TODO: find out if leaving out tutorial path works now
content: ['./src/**/*.{js,jsx,ts,tsx}'],
content: ['./src/**/*.{js,jsx,ts,tsx}', '../../src/**/*.{html,js,jsx,md,mdx,ts,tsx}'],
theme: {
extend: {},
extend: {
colors: {
// codemirror-theme settings
background: 'var(--background)',
lineBackground: 'var(--lineBackground)',
foreground: 'var(--foreground)',
caret: 'var(--caret)',
selection: 'var(--selection)',
selectionMatch: 'var(--selectionMatch)',
gutterBackground: 'var(--gutterBackground)',
gutterForeground: 'var(--gutterForeground)',
gutterBorder: 'var(--gutterBorder)',
lineHighlight: 'var(--lineHighlight)',
},
},
},
plugins: [],
};
+20 -18
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/react",
"version": "0.6.0",
"version": "0.7.1",
"description": "React components for strudel",
"main": "src/index.js",
"publishConfig": {
@@ -32,31 +32,33 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.4.0",
"@codemirror/lang-javascript": "^6.1.1",
"@codemirror/autocomplete": "^6.6.0",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.7.3",
"@lezer/highlight": "^1.1.3",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@replit/codemirror-emacs": "^6.0.1",
"@replit/codemirror-vim": "^6.0.14",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@uiw/codemirror-themes": "^4.12.4",
"@uiw/react-codemirror": "^4.12.4",
"@uiw/codemirror-themes": "^4.19.16",
"@uiw/react-codemirror": "^4.19.16",
"react-hook-inview": "^4.5.0"
},
"peerDependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2",
"@vitejs/plugin-react": "^2.2.0",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.18",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"tailwindcss": "^3.0.24",
"vite": "^3.2.2"
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.3.2",
"vite": "^4.3.3"
}
}
-2
View File
@@ -8,8 +8,6 @@ evalScope(
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
);
+29 -8
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import _CodeMirror from '@uiw/react-codemirror';
import { EditorView, Decoration } from '@codemirror/view';
import { StateField, StateEffect } from '@codemirror/state';
@@ -7,7 +7,9 @@ import strudelTheme from '../themes/strudel-theme';
import './style.css';
import { useCallback } from 'react';
import { autocompletion } from '@codemirror/autocomplete';
import { strudelAutocomplete } from './Autocomplete';
//import { strudelAutocomplete } from './Autocomplete';
import { vim } from '@replit/codemirror-vim';
import { emacs } from '@replit/codemirror-emacs';
export const setFlash = StateEffect.define();
const flashField = StateField.define({
@@ -56,15 +58,19 @@ const highlightField = StateField.define({
haps
.map((hap) =>
(hap.context.locations || []).map(({ start, end }) => {
const color = hap.context.color || e.value.color || '#FFCA28';
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
}
// const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
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);
}),
)
@@ -82,7 +88,7 @@ const highlightField = StateField.define({
provide: (f) => EditorView.decorations.from(f),
});
const extensions = [
const staticExtensions = [
javascript(),
highlightField,
flashField,
@@ -97,6 +103,10 @@ export default function CodeMirror({
onViewChanged,
onSelectionChange,
theme,
keybindings,
isLineNumbersDisplayed,
fontSize = 18,
fontFamily = 'monospace',
options,
editorDidMount,
}) {
@@ -120,8 +130,18 @@ export default function CodeMirror({
},
[onSelectionChange],
);
const extensions = useMemo(() => {
let bindings = {
vim,
emacs,
};
if (bindings[keybindings]) {
return [...staticExtensions, bindings[keybindings]()];
}
return staticExtensions;
}, [keybindings]);
return (
<>
<div style={{ fontSize, fontFamily }} className="w-full">
<_CodeMirror
value={value}
theme={theme || strudelTheme}
@@ -129,8 +149,9 @@ export default function CodeMirror({
onCreateEditor={handleOnCreateEditor}
onUpdate={handleOnUpdate}
extensions={extensions}
basicSetup={{ lineNumbers: isLineNumbersDisplayed }}
/>
</>
</div>
);
}
+63 -42
View File
@@ -7,7 +7,6 @@ import useHighlighting from '../hooks/useHighlighting.mjs';
import useStrudel from '../hooks/useStrudel.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
import { Icon } from './Icon';
import styles from './MiniRepl.module.css';
import './style.css';
import { logger } from '@strudel.cycles/core';
import useEvent from '../hooks/useEvent.mjs';
@@ -19,17 +18,24 @@ export function MiniRepl({
tune,
hideOutsideView = false,
enableKeyboard,
onTrigger,
drawTime,
punchcard,
punchcardLabels,
onPaint,
canvasHeight = 200,
fontSize = 18,
fontFamily,
hideHeader = false,
theme,
highlightColor,
keybindings,
isLineNumbersDisplayed,
}) {
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
const evalOnMount = !!drawTime;
const drawContext = useCallback(
!!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
[drawTime],
punchcard ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
[punchcard],
);
const {
code,
@@ -49,7 +55,18 @@ export function MiniRepl({
} = useStrudel({
initialCode: tune,
defaultOutput: webaudioOutput,
editPattern: (pat) => (punchcard ? pat.punchcard() : pat),
editPattern: (pat, id) => {
//pat = pat.withContext((ctx) => ({ ...ctx, id }));
if (onTrigger) {
pat = pat.onTrigger(onTrigger, false);
}
if (onPaint) {
pat = pat.onPaint(onPaint);
} else if (punchcard) {
pat = pat.punchcard({ labels: punchcardLabels });
}
return pat;
},
getTime,
evalOnMount,
drawContext,
@@ -72,7 +89,6 @@ export function MiniRepl({
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.now(),
color: highlightColor,
});
// keyboard shortcuts
@@ -85,7 +101,7 @@ export function MiniRepl({
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === 'Period') {
} else if (e.key === '.' || e.code === 'Period') {
stop();
e.preventDefault();
}
@@ -96,26 +112,6 @@ export function MiniRepl({
),
);
// set active pattern on ctrl+enter
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === 'Period') {
stop();
e.preventDefault();
}
}
};
window.addEventListener('keydown', handleKeyPress, true);
return () => window.removeEventListener('keydown', handleKeyPress, true);
}
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
const [log, setLog] = useState([]);
useLogger(
useCallback((e) => {
@@ -124,29 +120,54 @@ export function MiniRepl({
// const logId = data?.pattern?.meta?.id;
if (logId === replId) {
setLog((l) => {
return l.concat([e.detail]).slice(-10);
return l.concat([e.detail]).slice(-8);
});
}
}, []),
);
return (
<div className={styles.container} ref={ref}>
<div className={styles.header}>
<div className={styles.buttons}>
<button className={cx(styles.button, started ? 'animate-pulse' : '')} onClick={() => togglePlay()}>
<Icon type={started ? 'stop' : 'play'} />
</button>
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
<Icon type="refresh" />
</button>
<div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight" ref={ref}>
{!hideHeader && (
<div className="flex justify-between bg-lineHighlight">
<div className="flex">
<button
className={cx(
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
started ? 'animate-pulse' : '',
)}
onClick={() => togglePlay()}
>
<Icon type={started ? 'stop' : 'play'} />
</button>
<button
className={cx(
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
)}
onClick={() => activateCode()}
>
<Icon type="refresh" />
</button>
</div>
</div>
{error && <div className={styles.error}>{error.message}</div>}
)}
<div className="overflow-auto relative">
{show && (
<CodeMirror6
value={code}
onChange={setCode}
onViewChanged={setView}
theme={theme}
fontFamily={fontFamily}
fontSize={fontSize}
keybindings={keybindings}
isLineNumbersDisplayed={isLineNumbersDisplayed}
/>
)}
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
</div>
<div className={styles.body}>
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} theme={theme} />}
</div>
{drawTime && (
{punchcard && (
<canvas
id={canvasId}
className="w-full pointer-events-none"
@@ -1,27 +0,0 @@
.container {
@apply overflow-hidden;
}
.header {
@apply flex justify-between bg-lineHighlight border-t border-l border-r border-lineHighlight rounded-t-md overflow-hidden;
}
.buttons {
@apply flex;
}
.button {
@apply cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground hover:bg-background;
}
.buttonDisabled {
@apply w-16 flex items-center justify-center p-1 opacity-50 cursor-not-allowed border-r border-lineHighlight;
}
.error {
@apply text-right p-1 text-sm text-red-200;
}
.body {
@apply overflow-auto relative;
}
+13 -1
View File
@@ -1,12 +1,24 @@
:root {
--background: #222;
--lineBackground: #22222299;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
--selectionMatch: #036dd626;
--lineHighlight: #00000050;
--gutterBackground: transparent;
--gutterForeground: #8a919966;
}
.cm-editor {
background-color: transparent !important;
height: 100%;
z-index: 11;
font-size: 18px;
}
.cm-theme {
width: 100%;
height: 100%;
}
.cm-theme-light {
+5 -4
View File
@@ -1,7 +1,8 @@
import { useEffect, useRef } from 'react';
import { setHighlights } from '../components/CodeMirror6';
const round = (x) => Math.round(x * 1000) / 1000;
function useHighlighting({ view, pattern, active, getTime, color }) {
function useHighlighting({ view, pattern, active, getTime }) {
const highlights = useRef([]);
const lastEnd = useRef(0);
useEffect(() => {
@@ -14,12 +15,12 @@ function useHighlighting({ view, pattern, active, getTime, color }) {
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
// see https://github.com/tidalcycles/strudel/issues/108
const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy
const span = [begin, audioTime + 1 / 60];
const span = [round(begin), round(audioTime + 1 / 60)];
lastEnd.current = span[1];
highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
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, color }) }); // highlight all still active + new active haps
view.dispatch({ effects: setHighlights.of({ haps: highlights.current }) }); // highlight all still active + new active haps
} catch (err) {
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
}
@@ -33,7 +34,7 @@ function useHighlighting({ view, pattern, active, getTime, color }) {
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
}
}
}, [pattern, active, view, color]);
}, [pattern, active, view]);
}
export default useHighlighting;
+9 -10
View File
@@ -10,7 +10,6 @@ function useStrudel({
getTime,
evalOnMount = false,
initialCode = '',
autolink = false,
beforeEval,
afterEval,
editPattern,
@@ -30,10 +29,11 @@ function useStrudel({
const [pattern, setPattern] = useState();
const [started, setStarted] = useState(false);
const isDirty = code !== activeCode;
const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
//const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
const shouldPaint = useCallback((pat) => !!pat?.context?.onPaint, []);
// TODO: make sure this hook reruns when scheduler.started changes
const { scheduler, evaluate, start, stop, pause } = useMemo(
const { scheduler, evaluate, start, stop, pause, setCps } = useMemo(
() =>
repl({
interval,
@@ -47,19 +47,17 @@ function useStrudel({
drawContext,
transpiler,
editPattern,
beforeEval: ({ code }) => {
beforeEval: async ({ code }) => {
setCode(code);
beforeEval?.();
await beforeEval?.();
},
afterEval: ({ pattern: _pattern, code }) => {
afterEval: (res) => {
const { pattern: _pattern, code } = res;
setActiveCode(code);
setPattern(_pattern);
setEvalError();
setSchedulerError();
if (autolink) {
window.location.hash = '#' + encodeURIComponent(btoa(code));
}
afterEval?.();
afterEval?.(res);
},
onToggle: (v) => {
setStarted(v);
@@ -156,6 +154,7 @@ function useStrudel({
stop,
pause,
togglePlay,
setCps,
};
}
+2 -3
View File
@@ -1,10 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { createRoot } from 'react-dom/client';
ReactDOM.render(
createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root'),
);
+39
View File
@@ -0,0 +1,39 @@
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,
};
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' },
],
});
+37
View File
@@ -0,0 +1,37 @@
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' },
],
});
+40
View File
@@ -0,0 +1,40 @@
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' },
],
});
+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' },
],
});
+15 -1
View File
@@ -1,7 +1,21 @@
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
extend: {
colors: {
// codemirror-theme settings
background: 'var(--background)',
lineBackground: 'var(--lineBackground)',
foreground: 'var(--foreground)',
caret: 'var(--caret)',
selection: 'var(--selection)',
selectionMatch: 'var(--selectionMatch)',
gutterBackground: 'var(--gutterBackground)',
gutterForeground: 'var(--gutterForeground)',
gutterBorder: 'var(--gutterBorder)',
lineHighlight: 'var(--lineHighlight)',
},
},
},
plugins: [],
corePlugins: {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/serial",
"version": "0.6.0",
"version": "0.7.1",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"publishConfig": {
@@ -32,6 +32,6 @@
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
+46 -7
View File
@@ -1,4 +1,6 @@
import { toMidi } from '@strudel.cycles/core';
import { noteToMidi, freqToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound, getEnvelope } from '@strudel.cycles/webaudio';
import gm from './gm.mjs';
let loadCache = {};
async function loadFont(name) {
@@ -8,7 +10,6 @@ async function loadFont(name) {
const load = async () => {
// TODO: make soundfont source configurable
const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`;
console.log('load font', name, url);
const preset = await fetch(url).then((res) => res.text());
let [_, data] = preset.split('={');
return eval('{' + data);
@@ -17,15 +18,24 @@ async function loadFont(name) {
return loadCache[name];
}
export async function getFontBufferSource(name, pitch, ac) {
if (typeof pitch === 'string') {
pitch = toMidi(pitch);
export async function getFontBufferSource(name, value, ac) {
let { note = 'c3', freq } = value;
let midi;
if (freq) {
midi = freqToMidi(freq);
} else if (typeof note === 'string') {
midi = noteToMidi(note);
} else if (typeof note === 'number') {
midi = note;
} else {
throw new Error(`unexpected "note" type "${typeof note}"`);
}
const { buffer, zone } = await getFontPitch(name, pitch, ac);
const { buffer, zone } = await getFontPitch(name, midi, ac);
const src = ac.createBufferSource();
src.buffer = buffer;
const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune;
const playbackRate = 1.0 * Math.pow(2, (100.0 * pitch - baseDetune) / 1200.0);
const playbackRate = 1.0 * Math.pow(2, (100.0 * midi - baseDetune) / 1200.0);
// src detune?
src.playbackRate.value = playbackRate;
const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd;
@@ -114,3 +124,32 @@ async function getBuffer(zone, audioContext) {
}
}
}
export function registerSoundfonts() {
Object.entries(gm).forEach(([name, fonts]) => {
registerSound(
name,
async (time, value, onended) => {
const { n = 0 } = value;
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
const font = fonts[n % fonts.length];
const ctx = getAudioContext();
const bufferSource = await getFontBufferSource(font, value, ctx);
bufferSource.start(time);
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 0.3, time);
bufferSource.connect(envelope);
const stop = (releaseTime) => {
bufferSource.stop(releaseTime + release);
releaseEnvelope(releaseTime);
};
bufferSource.onended = () => {
bufferSource.disconnect();
envelope.disconnect();
onended();
};
return { node: envelope, stop };
},
{ type: 'soundfont', prebake: true, fonts },
);
});
}
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
import { getFontBufferSource } from './fontloader.mjs';
import { getFontBufferSource, registerSoundfonts } from './fontloader.mjs';
import * as soundfontList from './list.mjs';
import { startPresetNote } from 'sfumato';
import { loadSoundfont } from './sfumato.mjs';
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList };
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts };
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.6.0",
"version": "0.8.1",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -36,7 +36,7 @@
"soundfont2": "^0.4.0"
},
"devDependencies": {
"node-fetch": "^3.2.6",
"vite": "^3.2.2"
"node-fetch": "^3.3.1",
"vite": "^4.3.3"
}
}
+27 -3
View File
@@ -1,5 +1,5 @@
import { Pattern, getPlayableNoteValue, toMidi } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { Pattern, getPlayableNoteValue, noteToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound } from '@strudel.cycles/webaudio';
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
Pattern.prototype.soundfont = function (sf, n = 0) {
@@ -8,7 +8,7 @@ Pattern.prototype.soundfont = function (sf, n = 0) {
const note = getPlayableNoteValue(h);
const preset = sf.presets[n % sf.presets.length];
const deadline = ctx.currentTime + t - ct;
const args = [ctx, preset, toMidi(note), deadline];
const args = [ctx, preset, noteToMidi(note), deadline];
const stop = startPresetNote(...args);
stop(deadline + h.duration);
});
@@ -21,5 +21,29 @@ export function loadSoundfont(url) {
}
const sf = _loadSoundfont(url);
soundfontCache.set(url, sf);
/*sf.then((font) => {
font.presets.forEach((preset) => {
console.log('preset', preset.header.name);
registerSound(
preset.header.name.replaceAll(' ', '_'),
(time, value, onended) => {
const ctx = getAudioContext();
let { note } = value; // freq ?
const p = font.presets.find((p) => p.header.name === preset.header.name);
if (!p) {
throw new Error('preset not found');
}
const deadline = time; // - ctx.currentTime;
const args = [ctx, p, noteToMidi(note), deadline];
const stop = startPresetNote(...args);
return { node: undefined, stop };
},
{ type: 'soundfont' },
);
});
//console.log('f', f);
});*/
return sf;
}
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.6.0",
"version": "0.8.1",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -34,10 +34,10 @@
"@strudel.cycles/core": "workspace:*",
"@tonaljs/tonal": "^4.7.2",
"chord-voicings": "^0.0.1",
"webmidi": "^3.0.21"
"webmidi": "^3.1.5"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+15
View File
@@ -9,6 +9,7 @@ This program is free software: you can redistribute it and/or modify it under th
import '../tonal.mjs'; // need to import this to add prototypes
import { pure, controls, seq } from '@strudel.cycles/core';
import { describe, it, expect } from 'vitest';
import { mini } from '../../mini/mini.mjs';
const { n } = controls;
describe('tonal', () => {
@@ -30,4 +31,18 @@ describe('tonal', () => {
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with colon', () => {
expect(
n(0, 1, 2)
.scale('C:major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with mininotation colon', () => {
expect(
n(0, 1, 2)
.scale(mini('C:major'))
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
});
+14 -6
View File
@@ -123,29 +123,37 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
/**
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
*
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
*
* The root note defaults to octave 3, if no octave number is given.
* Note that you currently cannot pattern `scale` with the mini notation, because the scale name includes a space.
* This will be improved in the future. Until then, use a sequence function like `cat` or `seq`.
*
* @memberof Pattern
* @name scale
* @param {string} scale Name of scale
* @returns Pattern
* @example
* "0 2 4 6 4 2".scale('C2 major').note()
* "0 2 4 6 4 2".scale("C2:major").note()
* @example
* "0 2 4 6 4 2"
* .scale(seq('C2 major', 'C2 minor').slow(2))
* .scale("C2:<major minor>")
* .note()
* @example
* "0 1 2 3 4 5 6 7".rev().scale("C2:<major minor>").note()
* .s("folkharp")
*/
export const scale = register('scale', function (scale /* : string */, pat) {
export const scale = register('scale', function (scale, pat) {
// Supports ':' list syntax in mininotation
if (Array.isArray(scale)) {
scale = scale.join(' ');
}
return pat.withHap((hap) => {
const isObject = typeof hap.value === 'object';
let note = isObject ? hap.value.n : hap.value;
const asNumber = Number(note);
if (!isNaN(asNumber)) {
// TODO: worth keeping for supporting ':' in (non-mininotation) strings?
scale = scale.replaceAll(':', ' ');
let [tonic, scaleName] = Scale.tokenize(scale);
const { pc, oct = 3 } = Note.get(tonic);
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
+44 -1
View File
@@ -6,10 +6,53 @@ This program is free software: you can redistribute it and/or modify it under th
import { stack, register } from '@strudel.cycles/core';
import _voicings from 'chord-voicings';
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
const { dictionaryVoicing, minTopNoteDiff } = _voicings.default || _voicings; // parcel module resolution fuckup
const lefthand = {
m7: ['3m 5P 7m 9M', '7m 9M 10m 12P'],
7: ['3M 6M 7m 9M', '7m 9M 10M 13M'],
'^7': ['3M 5P 7M 9M', '7M 9M 10M 12P'],
69: ['3M 5P 6A 9M'],
m7b5: ['3m 5d 7m 8P', '7m 8P 10m 12d'],
'7b9': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
'7b13': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
o7: ['1P 3m 5d 6M', '5d 6M 8P 10m'],
'7#11': ['7m 9M 11A 13A'],
'7#9': ['3M 7m 9A'],
mM7: ['3m 5P 7M 9M', '7M 9M 10m 12P'],
m6: ['3m 5P 6M 9M', '6M 9M 10m 12P'],
};
const guidetones = {
m7: ['3m 7m', '7m 10m'],
m9: ['3m 7m', '7m 10m'],
7: ['3M 7m', '7m 10M'],
'^7': ['3M 7M', '7M 10M'],
'^9': ['3M 7M', '7M 10M'],
69: ['3M 6M'],
6: ['3M 6M', '6M 10M'],
m7b5: ['3m 7m', '7m 10m'],
'7b9': ['3M 7m', '7m 10M'],
'7b13': ['3M 7m', '7m 10M'],
o7: ['3m 6M', '6M 10m'],
'7#11': ['3M 7m', '7m 10M'],
'7#9': ['3M 7m', '7m 10M'],
mM7: ['3m 7M', '7M 10m'],
m6: ['3m 6M', '6M 10m'],
};
const triads = {
'': ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
M: ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
m: ['1P 3m 5P', '3m 5P 8P', '5P 8P 10m'],
o: ['1P 3m 5d', '3m 5d 8P', '5d 8P 10m'],
aug: ['1P 3m 5A', '3m 5A 8P', '5A 8P 10m'],
};
export const voicingRegistry = {
lefthand: { dictionary: lefthand, range: ['F3', 'A4'] },
triads: { dictionary: triads },
guidetones: { dictionary: guidetones },
};
export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.6.0",
"version": "0.7.1",
"description": "Tone.js API for strudel",
"main": "index.mjs",
"type": "module",
@@ -34,7 +34,7 @@
"tone": "^14.7.77"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/transpiler",
"version": "0.6.0",
"version": "0.8.1",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"publishConfig": {
@@ -36,7 +36,7 @@
"estree-walker": "^3.0.1"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.8"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+82
View File
@@ -0,0 +1,82 @@
# @strudel/web
This package provides an easy to use bundle of multiple strudel packages for the web.
## Usage
Save this code as a `.html` file and double click it:
```html
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
```
With the help of [skypack](https://www.skypack.dev/), you don't need a bundler nor a server.
As soon as you call `initStrudel()`, all strudel functions are made available.
In this case, we are using the `note` function to create a pattern.
To actually play the pattern, you have to append `.play()` to the end.
Note: Due to the [Autoplay policy](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy), you can only play audio in a browser after a click event.
### Via npm
If you're using a bundler, you can install the package via `npm i @strudel/web`, then just import it like:
```js
import { initStrudel } from '@strudel/web';
```
The rest of the code should be the same. Check out [vite](https://vitejs.dev/) for a good bundler / dev server.
### Loading samples
By default, no external samples are loaded, but you can add them like this:
```js
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
document.getElementById('play').addEventListener('click',
() => s("bd sd").play()
)
```
You can learn [more about the `samples` function here](https://strudel.tidalcycles.org/learn/samples#loading-custom-samples).
### Evaluating Code
Instead of creating patterns directly in JS, you might also want to take in user input and turn that into a pattern.
This is called evaluation: Taking a piece of code and executing it on the fly.
To do that, you can use the `evaluate` function:
```js
initStrudel();
document.getElementById('play').addEventListener('click',
() => evaluate('note("c a f e").jux(rev)')
);
document.getElementById('play').addEventListener('stop',
() => hush()
);
```
### Double vs Single Quotes
There is a tiny difference between the [Strudel REPL](https://strudel.tidalcycles.org/) and `@strudel/web`.
In the REPL you can use 'single quotes' for regular JS strings and "double quotes" for mini notation patterns.
In `@strudel/web`, it does not matter which types of quotes you're using.
There will probably be an escapte hatch for that in the future.
## More Examples
Check out the examples folder for more examples, both using plain html and vite!
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
document.getElementById('play').addEventListener('stop', () => hush());
</script>
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<button id="a">A</button>
<button id="b">B</button>
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
click('stop', () => hush());
</script>
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="https://strudel.tidalcycles.org/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@strudel/web REPL Example</title>
</head>
<body>
<div id="app"></div>
<button id="a">A</button>
<button id="b">B</button>
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from '@strudel/web';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
click('stop', () => hush());
</script>
</body>
</html>
@@ -0,0 +1,18 @@
{
"name": "repl-example",
"private": true,
"version": "0.0.0",
"type": "module",
"license": "AGPL-3.0-or-later",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.3.2"
},
"dependencies": {
"@strudel/web": "workspace:*"
}
}
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
document.getElementById('play').addEventListener('click', () => s('[bd sd](3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
+45
View File
@@ -0,0 +1,45 @@
{
"name": "@strudel/web",
"version": "0.8.2",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"main": "web.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": [
"Alex McLean <alex@slab.org>"
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*"
},
"devDependencies": {
"vite": "^4.3.3"
}
}
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'web.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+66
View File
@@ -0,0 +1,66 @@
export * from '@strudel.cycles/core';
export * from '@strudel.cycles/webaudio';
//export * from '@strudel.cycles/soundfonts';
export * from '@strudel.cycles/transpiler';
export * from '@strudel.cycles/mini';
export * from '@strudel.cycles/tonal';
export * from '@strudel.cycles/webaudio';
import { Pattern, evalScope, controls } from '@strudel.cycles/core';
import { initAudioOnFirstClick, registerSynthSounds, webaudioScheduler } from '@strudel.cycles/webaudio';
// import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { evaluate as _evaluate } from '@strudel.cycles/transpiler';
import { miniAllStrings } from '@strudel.cycles/mini';
// init logic
export async function defaultPrebake() {
const loadModules = evalScope(
evalScope,
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
{ hush, evaluate },
);
await Promise.all([loadModules, registerSynthSounds() /* , registerSoundfonts() */]);
}
// when this function finishes, everything is initialized
let initDone;
let scheduler;
export function initStrudel(options = {}) {
initAudioOnFirstClick();
miniAllStrings();
const { prebake, ...schedulerOptions } = options;
initDone = (async () => {
await defaultPrebake();
await prebake?.();
})();
scheduler = webaudioScheduler(schedulerOptions);
}
window.initStrudel = initStrudel;
// this method will play the pattern on the default scheduler
Pattern.prototype.play = function () {
if (!scheduler) {
throw new Error('.play: no scheduler found. Have you called init?');
}
initDone.then(() => {
scheduler.setPattern(this, true);
});
return this;
};
// stop playback
export function hush() {
scheduler.stop();
}
// evaluate and play the given code using the transpiler
export async function evaluate(code, autoplay = true) {
const { pattern } = await _evaluate(code);
autoplay && pattern.play();
}
+70
View File
@@ -0,0 +1,70 @@
import { getAudioContext } from './webaudio.mjs';
export function gainNode(value) {
const node = getAudioContext().createGain();
node.gain.value = value;
return node;
}
export const getOscillator = ({ s, freq, t }) => {
// make oscillator
const o = getAudioContext().createOscillator();
o.type = s || 'triangle';
o.frequency.value = Number(freq);
o.start(t);
//o.stop(t + duration + release);
const stop = (time) => o.stop(time);
return { node: o, stop };
};
// alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future
export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
// sustain end
return {
node: gainNode,
stop: (t) => {
//if (typeof gainNode.gain.cancelAndHoldAtTime === 'function') {
// gainNode.gain.cancelAndHoldAtTime(t); // this seems to release instantly....
// see https://discord.com/channels/779427371270275082/937365093082079272/1086053607360712735
//} else {
// firefox: this will glitch when the sustain has not been reached yet at the time of release
gainNode.gain.setValueAtTime(sustain * velocity, t);
//}
gainNode.gain.linearRampToValueAtTime(0, t + release);
},
};
};
export const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
// for some reason, using exponential ramping creates little cracklings
/* let t = begin;
gainNode.gain.setValueAtTime(0, t);
gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack));
const sustainGain = Math.max(sustain * velocity, 0.001);
gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay));
if (end - begin < attack + decay) {
gainNode.gain.cancelAndHoldAtTime(end);
} else {
gainNode.gain.setValueAtTime(sustainGain, end);
}
gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */
return gainNode;
};
export const getFilter = (type, frequency, Q) => {
const filter = getAudioContext().createBiquadFilter();
filter.type = type;
filter.frequency.value = frequency;
filter.Q.value = Q;
return filter;
};
+2
View File
@@ -6,3 +6,5 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './webaudio.mjs';
export * from './sampler.mjs';
export * from './helpers.mjs';
export * from './synth.mjs';
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.6.1",
"version": "0.8.1",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
@@ -34,9 +34,10 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*"
"@strudel.cycles/core": "workspace:*",
"nanostores": "^0.8.1"
},
"devDependencies": {
"vite": "^3.2.2"
"vite": "^4.3.3"
}
}
+129 -56
View File
@@ -1,5 +1,6 @@
import { logger, toMidi, valueToMidi } from '@strudel.cycles/core';
import { getAudioContext } from './index.mjs';
import { logger, noteToMidi, valueToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound } from './index.mjs';
import { getEnvelope } from './helpers.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer>
const loadCache = {}; // string: Promise<ArrayBuffer>
@@ -20,7 +21,7 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u];
}
export const getSampleBufferSource = async (s, n, note, speed, freq) => {
export const getSampleBufferSource = async (s, n, note, speed, freq, bank) => {
let transpose = 0;
if (freq !== undefined && note !== undefined) {
logger('[sampler] hap has note and freq. ignoring note', 'warning');
@@ -29,28 +30,11 @@ export const getSampleBufferSource = async (s, n, note, speed, freq) => {
transpose = midi - 36; // C3 is middle C
const ac = getAudioContext();
// is sample from loaded samples(..)
const samples = getLoadedSamples();
if (!samples) {
throw new Error('no samples loaded');
}
const bank = samples?.[s];
if (!bank) {
throw new Error(
`sample not found: "${s}"`,
// , try one of ${Object.keys(samples)
// .map((s) => `"${s}"`)
// .join(', ')}.
);
}
if (typeof bank !== 'object') {
throw new Error('wrong format for sample bank:', s);
}
let sampleUrl;
if (Array.isArray(bank)) {
sampleUrl = bank[n % bank.length];
} else {
const midiDiff = (noteA) => toMidi(noteA) - midi;
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
const closest = Object.keys(bank)
.filter((k) => !k.startsWith('_'))
@@ -107,8 +91,6 @@ export const getLoadedBuffer = (url) => {
return bufferCache[url];
};
let sampleCache = { current: undefined };
/**
* Loads a collection of samples to use with `s`
* @example
@@ -123,7 +105,7 @@ let sampleCache = { current: undefined };
*
*/
export const samples = async (sampleMap, baseUrl = sampleMap._base || '') => {
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
if (typeof sampleMap === 'string') {
if (sampleMap.startsWith('github:')) {
let [_, path] = sampleMap.split('github:');
@@ -141,43 +123,134 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '') => {
}
return fetch(sampleMap)
.then((res) => res.json())
.then((json) => samples(json, baseUrl || json._base || base))
.then((json) => samples(json, baseUrl || json._base || base, options))
.catch((error) => {
console.error(error);
throw new Error(`error loading "${sampleMap}"`);
});
}
sampleCache.current = {
...sampleCache.current,
...Object.fromEntries(
Object.entries(sampleMap).map(([key, value]) => {
if (typeof value === 'string') {
value = [value];
}
if (typeof value !== 'object') {
throw new Error('wrong sample map format for ' + key);
}
baseUrl = value._base || baseUrl;
const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/');
if (Array.isArray(value)) {
return [key, value.map(replaceUrl)];
}
// must be object
return [
key,
Object.fromEntries(
Object.entries(value).map(([note, samples]) => {
return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)];
}),
),
];
}),
),
const { prebake, tag } = options;
Object.entries(sampleMap).forEach(([key, value]) => {
if (typeof value === 'string') {
value = [value];
}
if (typeof value !== 'object') {
throw new Error('wrong sample map format for ' + key);
}
baseUrl = value._base || baseUrl;
const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/');
if (Array.isArray(value)) {
//return [key, value.map(replaceUrl)];
value = value.map(replaceUrl);
} else {
// must be object
value = Object.fromEntries(
Object.entries(value).map(([note, samples]) => {
return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)];
}),
);
}
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: value,
baseUrl,
prebake,
tag,
});
});
};
const cutGroups = [];
export async function onTriggerSample(t, value, onended, bank) {
const {
s,
freq,
unit,
nudge = 0, // TODO: is this in seconds?
cut,
loop,
clip = 0, // if 1, samples will be cut off when the hap ends
n = 0,
note,
speed = 1, // sample playback speed
begin = 0,
end = 1,
} = value;
// load sample
if (speed === 0) {
// no playback
return;
}
const ac = getAudioContext();
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
//const soundfont = getSoundfontKey(s);
const time = t + nudge;
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank);
// asny stuff above took too long?
if (ac.currentTime > t) {
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
// console.warn('sample still loading:', s, n);
return;
}
if (!bufferSource) {
logger(`[sampler] could not load "${s}:${n}"`, 'error');
return;
}
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
if (unit === 'c') {
// are there other units?
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * 1; //cps;
}
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferSource.buffer.duration;
bufferSource.start(time, offset);
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
/*if (loop) {
// TODO: idea for loopBegin / loopEnd
// if one of [loopBegin,loopEnd] is <= 1, interpret it as normlized
// if [loopBegin,loopEnd] is bigger >= 1, interpret it as sample number
// this will simplify perfectly looping things, while still keeping the normalized option
// the only drawback is that looping between samples 0 and 1 is not possible (which is not real use case)
bufferSource.loop = true;
bufferSource.loopStart = offset;
bufferSource.loopEnd = offset + duration;
duration = loop * duration;
}*/
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
bufferSource.connect(envelope);
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
envelope.connect(out);
bufferSource.onended = function () {
bufferSource.disconnect();
envelope.disconnect();
out.disconnect();
onended();
};
};
const stop = (endTime, playWholeBuffer = !clip) => {
let releaseTime = endTime;
if (playWholeBuffer) {
releaseTime = t + (end - begin) * bufferDuration;
}
bufferSource.stop(releaseTime + release);
releaseEnvelope(releaseTime);
};
const handle = { node: out, bufferSource, stop };
export const resetLoadedSamples = () => {
sampleCache.current = undefined;
};
// cut groups
if (cut !== undefined) {
const prev = cutGroups[cut];
if (prev) {
prev.node.gain.setValueAtTime(1, time);
prev.node.gain.linearRampToValueAtTime(0, time + 0.01);
}
cutGroups[cut] = handle;
}
export const getLoadedSamples = () => sampleCache.current;
return handle;
}
+44
View File
@@ -0,0 +1,44 @@
import { midiToFreq, noteToMidi } from '@strudel.cycles/core';
import { registerSound } from './webaudio.mjs';
import { getOscillator, gainNode, getEnvelope } from './helpers.mjs';
export function registerSynthSounds() {
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
registerSound(
wave,
(t, value, onended) => {
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;
let { n, note, freq } = value;
// with synths, n and note are the same thing
n = note || n || 36;
if (typeof n === 'string') {
n = noteToMidi(n); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof n === 'number') {
freq = midiToFreq(n); // + 48);
}
// maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default)
// make oscillator
const { node: o, stop } = getOscillator({ t, s: wave, freq });
const g = gainNode(0.3);
// envelope
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
};
return {
node: o.connect(g).connect(envelope),
stop: (releaseTime) => {
releaseEnvelope(releaseTime);
stop(releaseTime + release);
},
};
},
{ type: 'synth', prebake: true },
);
});
}
+74 -216
View File
@@ -5,15 +5,22 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as strudel from '@strudel.cycles/core';
import { fromMidi, logger, toMidi } from '@strudel.cycles/core';
import './feedbackdelay.mjs';
import './reverb.mjs';
import { getSampleBufferSource } from './sampler.mjs';
const { Pattern } = strudel;
const { Pattern, logger } = strudel;
import './vowel.mjs';
import workletsUrl from './worklets.mjs?url';
import { getFilter, gainNode } from './helpers.mjs';
import { map } from 'nanostores';
// export const getAudioContext = () => Tone.getContext().rawContext;
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
soundMap.setKey(key, { onTrigger, data });
}
export function getSound(s) {
return soundMap.get()[s];
}
export const resetLoadedSounds = () => soundMap.set({});
let audioContext;
export const getAudioContext = () => {
@@ -38,76 +45,6 @@ export const panic = () => {
destination = null;
};
const getFilter = (type, frequency, Q) => {
const filter = getAudioContext().createBiquadFilter();
filter.type = type;
filter.frequency.value = frequency;
filter.Q.value = Q;
return filter;
};
const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
// for some reason, using exponential ramping creates little cracklings
/* let t = begin;
gainNode.gain.setValueAtTime(0, t);
gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack));
const sustainGain = Math.max(sustain * velocity, 0.001);
gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay));
if (end - begin < attack + decay) {
gainNode.gain.cancelAndHoldAtTime(end);
} else {
gainNode.gain.setValueAtTime(sustainGain, end);
}
gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */
return gainNode;
};
const getOscillator = ({ s, freq, t, duration, release }) => {
// make oscillator
const o = getAudioContext().createOscillator();
o.type = s || 'triangle';
o.frequency.value = Number(freq);
o.start(t);
o.stop(t + duration + release);
return o;
};
const getSoundfontKey = (s) => {
if (!globalThis.soundfontList) {
// soundfont package not loaded
return false;
}
if (globalThis.soundfontList?.instruments?.includes(s)) {
return s;
}
// check if s is one of the soundfonts, which are loaded into globalThis, to avoid coupling both packages
const nameIndex = globalThis.soundfontList?.instrumentNames?.indexOf(s);
// convert number nameIndex (0-128) to 3 digit string (001-128)
const name = nameIndex < 10 ? `00${nameIndex}` : nameIndex < 100 ? `0${nameIndex}` : nameIndex;
if (nameIndex !== -1) {
// TODO: indices of instrumentNames do not seem to match instruments
return globalThis.soundfontList.instruments.find((instrument) => instrument.startsWith(name));
}
return;
};
const splitSN = (s, n) => {
if (!s.includes(':')) {
return [s, n];
}
let [s2, n2] = s.split(':');
if (isNaN(Number(n2))) {
return [s, n];
}
return [s2, n2];
};
let workletsLoading;
function loadWorklets() {
if (workletsLoading) {
@@ -147,15 +84,13 @@ export async function initAudioOnFirstClick() {
});
}
function gainNode(value) {
const node = getAudioContext().createGain();
node.gain.value = value;
return node;
}
const cutGroups = [];
let delays = {};
const maxfeedback = 0.98;
function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = strudel.clamp(delayfeedback, 0, 0.98);
if (!delays[orbit]) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
@@ -191,165 +126,74 @@ function effectSend(input, effect, wet) {
}
// export const webaudioOutput = async (t, hap, ct, cps) => {
export const webaudioOutput = async (hap, deadline, hapDuration) => {
export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
const ac = getAudioContext();
/* if (isNote(hap.value)) {
// supports primitive hap values that look like notes
hap.value = { note: hap.value };
} */
if (typeof hap.value !== 'object') {
logger(
`hap.value "${hap.value}" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
'error',
);
/* throw new Error(
`hap.value "${hap.value}"" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
); */
return;
}
// calculate correct time (tone.js workaround)
hap.ensureObjectValue();
// calculate absolute time
let t = ac.currentTime + deadline;
// destructure value
// destructure
let {
freq,
s,
s = 'triangle',
bank,
sf,
clip = 0, // if 1, samples will be cut off when the hap ends
n = 0,
note,
source,
gain = 0.8,
// low pass
lpf,
cutoff = lpf,
lpq = 1,
resonance = lpq,
cutoff,
resonance = 1,
// high pass
hpf,
hcutoff = hpf,
hpq = 1,
hresonance = hpq,
hcutoff,
hresonance = 1,
// band pass
bpf,
bandf = bpf,
bpq = 1,
bandq = bpq,
bandf,
bandq = 1,
//
coarse,
crush,
shape,
pan,
speed = 1, // sample playback speed
begin = 0,
end = 1,
vowel,
delay = 0,
delayfeedback = 0.5,
delaytime = 0.25,
unit,
nudge = 0, // TODO: is this in seconds?
cut,
loop,
orbit = 1,
room,
size = 2,
roomsize = size,
} = hap.value;
const { velocity = 1 } = hap.context;
gain *= velocity; // legacy fix for velocity
// the chain will hold all audio nodes that connect to each other
const chain = [];
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
const onended = () => {
toDisconnect.forEach((n) => n?.disconnect());
};
if (bank && s) {
s = `${bank}_${s}`;
}
if (typeof s === 'string') {
[s, n] = splitSN(s, n);
}
if (typeof note === 'string') {
[note, n] = splitSN(note, n);
}
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = hap.value;
// with synths, n and note are the same thing
n = note || n || 36;
if (typeof n === 'string') {
n = toMidi(n); // e.g. c3 => 48
// get source AudioNode
let sourceNode;
if (source) {
sourceNode = source(t, hap.value, hapDuration);
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
const soundHandle = await onTrigger(t, hap.value, onended);
if (soundHandle) {
sourceNode = soundHandle.node;
soundHandle.stop(t + hapDuration);
}
// get frequency
if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48);
}
// make oscillator
const o = getOscillator({ t, s, freq, duration: hapDuration, release });
chain.push(o);
// level down oscillators as they are really loud compared to samples i've tested
chain.push(gainNode(0.3));
// TODO: make adsr work with samples without pops
// envelope
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hapDuration);
chain.push(adsr);
} else {
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = hap.value;
// load sample
if (speed === 0) {
// no playback
return;
}
if (!s) {
console.warn('no sample specified');
return;
}
const soundfont = getSoundfontKey(s);
let bufferSource;
if (soundfont) {
// is soundfont
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac, freq);
} else {
// is sample from loaded samples(..)
bufferSource = await getSampleBufferSource(s, n, note, speed, freq);
}
// asny stuff above took too long?
if (ac.currentTime > t) {
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
// console.warn('sample still loading:', s, n);
return;
}
if (!bufferSource) {
console.warn('no buffer source');
return;
}
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
if (unit === 'c') {
// are there other units?
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration;
}
let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value;
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * duration * bufferSource.playbackRate.value;
duration = (end - begin) * duration;
if (loop) {
bufferSource.loop = true;
bufferSource.loopStart = offset;
bufferSource.loopEnd = offset + duration;
duration = loop * duration;
}
t += nudge;
bufferSource.start(t, offset);
if (cut !== undefined) {
cutGroups[cut]?.stop(t); // fade out?
cutGroups[cut] = bufferSource;
}
chain.push(bufferSource);
bufferSource.stop(t + duration + release);
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + duration);
chain.push(adsr);
throw new Error(`sound ${s} not found! Is it loaded?`);
}
if (!sourceNode) {
// if onTrigger does not return anything, we will just silently skip
// this can be used for things like speed(0) in the sampler
return;
}
if (ac.currentTime > t) {
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
return;
}
const chain = []; // audio nodes that will be connected to each other sequentially
chain.push(sourceNode);
// gain stage
chain.push(gainNode(gain));
@@ -385,21 +229,35 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
}
// reverb
let reverbSend;
if (room > 0 && roomsize > 0) {
const reverbNode = getReverb(orbit, roomsize);
if (room > 0 && size > 0) {
const reverbNode = getReverb(orbit, size);
reverbSend = effectSend(post, reverbNode, room);
}
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
// disconnect all nodes when source node has ended:
chain[0].onended = () => chain.concat([delaySend, reverbSend]).forEach((n) => n?.disconnect());
// toDisconnect = all the node that should be disconnected in onended callback
// this is crucial for performance
toDisconnect = chain.concat([delaySend, reverbSend]);
};
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps);
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps, cps);
Pattern.prototype.webaudio = function () {
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
return this.onTrigger(webaudioOutputTrigger);
};
export function webaudioScheduler(options = {}) {
options = {
getTime: () => getAudioContext().currentTime,
defaultOutput: webaudioOutput,
...options,
};
const { defaultOutput, getTime } = options;
return new strudel.Cyclist({
...options,
onTrigger: strudel.getTrigger({ defaultOutput, getTime }),
});
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.6.0",
"version": "0.7.1",
"description": "WebDirt integration for Strudel",
"main": "index.mjs",
"type": "module",
@@ -35,7 +35,7 @@
"WebDirt": "github:dktr0/WebDirt"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
"vite": "^4.3.3",
"vitest": "^0.28.0"
}
}
+4 -3
View File
@@ -1,7 +1,7 @@
import * as strudel from '@strudel.cycles/core';
const { Pattern } = strudel;
import * as WebDirt from 'WebDirt';
import { getLoadedSamples, loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio';
//import { loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio';
let webDirt;
@@ -62,8 +62,9 @@ export function loadWebDirt(config) {
* @noAutocomplete
*/
Pattern.prototype.webdirt = function () {
throw new Error('webdirt support has been dropped..');
// create a WebDirt object and initialize Web Audio context
return this.onTrigger(async (time, e, currentTime) => {
/* return this.onTrigger(async (time, e, currentTime) => {
if (!webDirt) {
throw new Error('WebDirt not initialized!');
}
@@ -92,5 +93,5 @@ Pattern.prototype.webdirt = function () {
webDirt.playSample(msg, deadline);
}
}
});
}); */
};

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