Compare commits

..

183 Commits

Author SHA1 Message Date
Felix Roos b312ff63a9 Publish
- @strudel/codemirror@1.2.2
 - @strudel/core@1.2.2
 - @strudel/csound@1.2.3
 - @strudel/draw@1.2.2
 - @strudel/gamepad@1.2.2
 - @strudel/hydra@1.2.2
 - @strudel/midi@1.2.3
 - @strudel/mini@1.2.2
 - @strudel/motion@1.2.2
 - @strudel/mqtt@1.2.2
 - @strudel/osc@1.2.2
 - @strudel/repl@1.2.3
 - @strudel/serial@1.2.2
 - @strudel/soundfonts@1.2.3
 - superdough@1.2.3
 - @strudel/tonal@1.2.2
 - @strudel/transpiler@1.2.2
 - @strudel/web@1.2.3
 - @strudel/webaudio@1.2.3
 - @strudel/xen@1.2.2
2025-05-29 23:58:35 +02:00
Jade (Rose) Rowland 93bc353b6b Merge pull request #1359 from tidalcycles/bytebeat_fixes_3
Fix bytebeat sample offset
2025-05-29 14:36:43 +02:00
Jade (Rose) Rowland 14a0dea826 fixtest 2025-05-29 14:35:37 +02:00
Jade (Rose) Rowland 009f5445be codeformat 2025-05-29 14:33:25 +02:00
Jade (Rose) Rowland 3d37c73103 fixed 2025-05-29 14:32:52 +02:00
Alex McLean 0b0f9fe13b preserve stepcount in chunks (#1358)
* preserve stepcount in chunks
* format
2025-05-29 12:14:18 +02:00
Jade (Rose) Rowland 1d189763b2 Merge pull request #1357 from tidalcycles/bbfixes_2
Byte Beat improvements -> 2
2025-05-29 02:34:30 +02:00
Jade (Rose) Rowland a4b55776d2 test 2025-05-29 02:33:06 +02:00
Jade (Rose) Rowland 0287be547a bbstart 2025-05-29 02:27:55 +02:00
Jade (Rose) Rowland 6a83ede33d Merge pull request #1356 from tidalcycles/bb_fixes
Bytebeat improvements
2025-05-28 16:40:32 +02:00
Jade (Rose) Rowland 1e3f546918 bb waveforms 2025-05-28 16:36:53 +02:00
Jade (Rose) Rowland faa60be211 Merge pull request #1354 from tidalcycles/jade/bytebeats
feat: Byte Beats!
2025-05-27 17:42:38 +02:00
Jade (Rose) Rowland dde2b9c6f8 working 2025-05-27 17:41:32 +02:00
Jade (Rose) Rowland 382dce65df working 2025-05-27 17:39:55 +02:00
Jade (Rose) Rowland 63f4f202f1 working 2025-05-27 17:35:35 +02:00
Jade (Rose) Rowland 366637026b bbs 2025-05-27 17:34:53 +02:00
Jade (Rose) Rowland 608dfd515e WIP oscillations 2025-05-27 14:37:55 +02:00
Felix Roos 9bd7b66228 Publish
- @strudel/codemirror@1.2.1
 - @strudel/core@1.2.1
 - @strudel/csound@1.2.2
 - @strudel/draw@1.2.1
 - @strudel/gamepad@1.2.1
 - @strudel/hydra@1.2.1
 - @strudel/midi@1.2.2
 - @strudel/mini@1.2.1
 - @strudel/motion@1.2.1
 - @strudel/mqtt@1.2.1
 - @strudel/osc@1.2.1
 - @strudel/repl@1.2.2
 - @strudel/serial@1.2.1
 - @strudel/soundfonts@1.2.2
 - superdough@1.2.2
 - @strudel/tonal@1.2.1
 - @strudel/transpiler@1.2.1
 - @strudel/web@1.2.2
 - @strudel/webaudio@1.2.2
 - @strudel/xen@1.2.1
2025-05-24 11:30:02 +02:00
Jade (Rose) Rowland 5e4eb7fc53 gain 2025-05-20 21:53:10 +01:00
Jade (Rose) Rowland 45bccf13ff add test 2025-05-20 21:47:43 +01:00
Jade (Rose) Rowland 65f1760fad format 2025-05-20 21:45:14 +01:00
Jade (Rose) Rowland b8d2d02466 bb 2025-05-20 21:41:40 +01:00
Jade (Rose) Rowland 058cb36640 structure 2025-05-20 20:28:52 +01:00
Nachito 5c3501caa4 fix: typo on docs causing problems with autocompletion. (#1350)
* fix: scramble and shuffle comments

* fix: comments on themes,docs, tests and more
2025-05-18 20:43:04 +01:00
Jade (Rose) Rowland 1341bd9c92 Merge pull request #1343 from daslyfe/pw
feat: Create pulsewidth (pw) and pulsewidth lfo parameters
2025-05-14 19:12:06 -04:00
Jade (Rose) Rowland 760d1abec0 remove n 2025-05-14 19:10:59 -04:00
Jade (Rose) Rowland 2b44fc48a4 Merge pull request #1344 from daslyfe/multichannelorbits
feat: Multi Channel Orbits
2025-05-14 12:25:16 -04:00
Henré Botha 99779171f5 Fix typo (#1346) 2025-05-13 16:45:29 +01:00
Jade (Rose) Rowland 64ebfb57c3 lint 2025-05-07 11:30:42 -04:00
Jade (Rose) Rowland 099d321dbd format 2025-05-07 11:25:34 -04:00
Jade (Rose) Rowland 59bdfd2cb8 format 2025-05-07 11:12:05 -04:00
Jade (Rose) Rowland 56d3436fda asconst 2025-05-07 01:08:00 -04:00
Jade (Rose) Rowland 440ce0cd3b format 2025-05-06 02:14:35 -04:00
Jade (Rose) Rowland 98346261ed lint 2025-05-06 01:13:50 -04:00
Jade (Rose) Rowland 38e4718826 lint 2025-05-06 01:09:20 -04:00
Jade (Rose) Rowland 38908baccd working 2025-05-06 01:06:37 -04:00
Felix Roos 8d02d4270a Publish
- @strudel/repl@1.2.1
 - @strudel/web@1.2.1
2025-05-02 00:03:18 +02:00
Felix Roos 9cf1cd2656 revert web + repl versions for next try 2025-05-02 00:02:50 +02:00
Felix Roos 4c079b6ebe Merge pull request #1339 from tidalcycles/fix-web-pkg-build
Fix web + repl package builds
2025-05-02 00:01:27 +02:00
Felix Roos e9e2e881e5 fix: repl package build 2025-05-01 23:58:49 +02:00
Felix Roos 685b4f172b fix: duplicate key 2025-05-01 23:57:09 +02:00
Felix Roos 53e50d81b0 also use worklet plugin in web package 2025-05-01 23:56:29 +02:00
Felix Roos 8f58d5788f Publish
- @strudel/csound@1.2.1
 - @strudel/midi@1.2.1
 - @strudel/repl@1.2.1
 - @strudel/soundfonts@1.2.1
 - superdough@1.2.1
 - vite-plugin-bundle-audioworklet@0.1.1
 - @strudel/web@1.2.1
 - @strudel/webaudio@1.2.1
2025-05-01 23:54:13 +02:00
Felix Roos 306a6db8dc hotfix: remove broken tauri workflow 2025-05-01 23:52:36 +02:00
Felix Roos 757d78252b Merge pull request #1338 from tidalcycles/fix-worklet-bundling
fix: superdough worklets bundling
2025-05-01 23:50:56 +02:00
Felix Roos 6f2e026e4a fix: import in vitest config 2025-05-01 23:49:47 +02:00
Felix Roos 22f114be59 update metadata 2025-05-01 23:35:20 +02:00
Felix Roos f993056fb1 fix: tests -> use vite plugin in tests as well 2025-05-01 23:30:14 +02:00
Felix Roos fdbbcd2dd2 add vite-plugin-bundle-audioworklet and use it both from website and superdough 2025-05-01 23:26:04 +02:00
Felix Roos 7bec92ca99 fix: superdough worklets bundling 2025-05-01 23:13:32 +02:00
Felix Roos 5702914661 Publish
- @strudel/reference@1.2.0
2025-05-01 12:52:12 +02:00
Felix Roos f2a9e2d4e6 hotfix: downgrade reference to be able to get 1.2.0 2025-05-01 12:51:54 +02:00
Felix Roos 6398b5fb38 hotfix: make broken hs2js + tidal packages private to opt out of release
- some issue with the build command
2025-05-01 12:48:43 +02:00
Felix Roos 9ceebd4679 hotfix: reference package vite config 2025-05-01 12:47:56 +02:00
Felix Roos 177f5c1431 Publish
- @strudel/codemirror@1.2.0
 - @strudel/core@1.2.0
 - @strudel/csound@1.2.0
 - @strudel/draw@1.2.0
 - @strudel/gamepad@1.2.0
 - hs2js@0.2.0
 - @strudel/hydra@1.2.0
 - @strudel/midi@1.2.0
 - @strudel/mini@1.2.0
 - @strudel/motion@1.2.0
 - @strudel/mqtt@1.2.0
 - @strudel/osc@1.2.0
 - @strudel/reference@1.2.0
 - @strudel/repl@1.2.0
 - @strudel/sampler@0.2.0
 - @strudel/serial@1.2.0
 - @strudel/soundfonts@1.2.0
 - superdough@1.2.0
 - @strudel/tidal@0.2.0
 - @strudel/tonal@1.2.0
 - @strudel/transpiler@1.2.0
 - @strudel/web@1.2.0
 - @strudel/webaudio@1.2.0
 - @strudel/xen@1.2.0
2025-05-01 12:39:24 +02:00
Jade (Rose) Rowland d6c2a88496 Merge pull request #1333 from daslyfe/jade/soundimportorder
FIX: sound import order
2025-04-27 16:25:53 -04:00
Jade (Rose) Rowland 3ee7d0cd4f lint 2025-04-27 16:21:14 -04:00
Jade (Rose) Rowland 53d43dc44c working 2025-04-27 16:19:58 -04:00
Jade (Rose) Rowland 637c619345 Merge pull request #1332 from hpunq/import-sounds-docs
update docs to reflect import sounds tab change
2025-04-27 12:39:49 -04:00
Jade (Rose) Rowland 47fb441feb Merge pull request #1318 from daslyfe/jade/gaincurve
feat: Improve gain curve
2025-04-27 12:38:26 -04:00
hpunq d02b1fc05c update docs to reflect import sounds tab change 2025-04-27 11:05:06 +01:00
Jade (Rose) Rowland 480b5ff58b fix vel 2025-04-27 02:38:10 -04:00
Jade (Rose) Rowland c088c0c9d2 def 2025-04-27 02:21:49 -04:00
Jade (Rose) Rowland f32abac06f reset gaincurve 2025-04-27 02:20:53 -04:00
Jade (Rose) Rowland 697f0a82e6 Merge branch 'main' into jade/gaincurve 2025-04-27 02:10:30 -04:00
Jade (Rose) Rowland 98970417c3 Merge pull request #1331 from tidalcycles/BRANCH_NAME
Add Icon to import sample button
2025-04-27 01:54:49 -04:00
Jade (Rose) Rowland b5d48b2f0b Merge pull request #1329 from hpunq/sample-ui
Add new "import-sounds" tab with explanation on folder import
2025-04-27 01:53:40 -04:00
Jade (Rose) Rowland c48aba50f0 button size and icon 2025-04-27 01:51:41 -04:00
hpunq f539930509 formatted code with prettier 2025-04-26 01:04:11 +01:00
hpunq 759cdb6276 add new import sounds tab with explanation and hyperlinks 2025-04-24 13:53:42 +01:00
hpunq 3dae2a80ab change import sounds label to import sounds folder 2025-04-24 13:51:44 +01:00
Jade (Rose) Rowland 7cc44a8829 Merge pull request #1326 from daslyfe/origin/jade/newthemes
feat: new themes + theme improvements
2025-04-21 18:04:37 -04:00
Jade (Rose) Rowland 90506f9e61 Merge pull request #1321 from daslyfe/jade/slit
feat: Create scrub function for scrubbing an audio file
2025-04-20 12:24:15 -04:00
Jade (Rose) Rowland c852b5fab0 fix lint 2025-04-19 16:21:11 -04:00
Jade (Rose) Rowland bd08b74ebc fixed strudel bool 2025-04-19 16:10:38 -04:00
Jade (Rose) Rowland 8f4dcb6bc6 Merge pull request #1325 from daslyfe/jade/udelsconfig
fix: udels header
2025-04-18 01:17:33 -04:00
Jade (Rose) Rowland 17e1ebeb93 udelsconfig 2025-04-18 01:02:13 -04:00
Jade (Rose) Rowland ad25ce7b21 working 2025-04-18 01:01:10 -04:00
Jade (Rose) Rowland 6e3aa693d5 format 2025-04-17 23:35:10 -04:00
Jade (Rose) Rowland 2968017300 disabled 2025-04-17 23:32:20 -04:00
Alex McLean ab76b4424a send delta in seconds, to match tidal/superdirt (#1323) 2025-04-11 08:18:57 +01:00
Jade (Rose) Rowland b64daf6284 Merge pull request #1322 from daslyfe/jade/fixmultichannelaudio
FIX: Multichannel Audio
2025-04-08 00:08:57 -04:00
Jade (Rose) Rowland 2b69004eb4 lint 2025-04-08 00:04:22 -04:00
Jade (Rose) Rowland a32bb58fb3 rm dependency 2025-04-08 00:01:34 -04:00
Jade (Rose) Rowland 9e233fe587 var name 2025-04-07 23:55:34 -04:00
Jade (Rose) Rowland b1090a1dd8 rm deadcode 2025-04-07 23:52:15 -04:00
Jade (Rose) Rowland 344d9ca1a8 working 2025-04-07 23:43:29 -04:00
Jade (Rose) Rowland b92a28242e Merge pull request #1317 from daslyfe/jade/voicemax
feat: add max polyphony feature for superdough
2025-04-07 22:18:03 -04:00
Jade (Rose) Rowland 695ac6a5d3 var 2025-04-06 16:34:14 -04:00
Jade (Rose) Rowland a468201822 add tests 2025-04-06 16:32:37 -04:00
Jade (Rose) Rowland 05ea9c86fb working 2025-04-06 16:08:42 -04:00
Jade (Rose) Rowland 6e29210bdc Merge pull request #1315 from daslyfe/jade/readableerror
enhancement: make error messages easier to read
2025-04-05 15:12:05 -04:00
Jade (Rose) Rowland e81345ca2f Merge pull request #1319 from daslyfe/jade/bug/replace_white_spaces_in_sounds
fix: replace empty spaces in registered sound keys
2025-04-04 00:26:57 -04:00
Jade (Rose) Rowland 6b8ee8ad11 lint 2025-04-03 01:05:26 -04:00
Jade (Rose) Rowland c2d4b529f6 working 2025-04-03 00:59:46 -04:00
Jade (Rose) Rowland 999c0b2349 lint 2025-03-31 00:21:01 -04:00
Jade (Rose) Rowland 5ed28d6168 gain curve func 2025-03-31 00:17:26 -04:00
Jade (Rose) Rowland bceb37e8ea rm comment 2025-03-30 20:32:19 -04:00
Jade (Rose) Rowland 2f4cba4e94 format 2025-03-30 20:26:44 -04:00
Jade (Rose) Rowland d8e4055c6e settings panel 2025-03-30 20:22:24 -04:00
Jade (Rose) Rowland 6f9bcc53dc ramp cut audio node 2025-03-30 16:33:47 -04:00
Jade (Rose) Rowland 8962e5a4f0 max poly 2025-03-30 01:35:25 -04:00
Jade (Rose) Rowland e55b7c6ce3 mostly working 2025-03-30 00:55:12 -04:00
Jade (Rose) Rowland 9b4574c7b0 Merge pull request #1314 from daslyfe/jade/alias
small feat: Add alias for segment and ribbon
2025-03-26 12:01:04 -04:00
Rene Nyffenegger 6d355a803a Fix typo pattnr (#1316) 2025-03-26 13:54:15 +00:00
Jade (Rose) Rowland dee75bc759 update tests 2025-03-24 01:32:48 -04:00
Jade (Rose) Rowland dc8e5d5394 lint 2025-03-24 01:28:43 -04:00
Jade (Rose) Rowland 282180ef1c style update 2025-03-24 01:28:31 -04:00
Jade (Rose) Rowland 3eea908636 fix doc 2025-03-24 00:33:00 -04:00
Jade (Rose) Rowland bfbd920b1d fix doc 2025-03-24 00:18:26 -04:00
Jade (Rose) Rowland 739fb8562f update test 2025-03-24 00:15:36 -04:00
Jade (Rose) Rowland cbd448e934 update docs 2025-03-24 00:09:46 -04:00
Felix Roos b192c6823c Merge pull request #1312 from daslyfe/jade/bug/as_single_param
bugfix: Allow single param to be used in the as function
2025-03-23 20:05:31 +01:00
Jade (Rose) Rowland dccfefacd7 test 2025-03-22 12:06:43 -04:00
Jade (Rose) Rowland 38876e135b fixed 2025-03-22 11:54:10 -04:00
Felix Roos f652c2ca86 Merge pull request #1244 from nkymut/add-program-change
Add MIDI Program Change, SysEx, NRPN, PitchBend and AfterTouch Output
2025-03-20 23:35:17 +01:00
Felix Roos 4f6c4bb95f Merge pull request #1040 from fnordomat/soundfont-url
WIP: make soundfont base url configurable
2025-03-19 17:32:30 +01:00
Alex McLean f5bb88a696 Add num samples from 0 up to 20 (#1310)
* re-edited numbers
* num samples up to 20
* add freesound license
2025-03-18 19:07:08 +00:00
Alex McLean b27c9dbf37 add num samples (edited numbers) (#1309) 2025-03-17 09:38:54 +00:00
nkymut 5fcb96f73c codeformat 2025-03-11 23:33:45 +08:00
nkymut 17df418935 Update Documentation 2025-03-11 23:30:21 +08:00
nkymut 2ccb95aec0 Refactor: Consolidate configuration variables into midiConfig object
- add options argument to .midi
- add midiConfig object with properties
2025-03-11 23:29:37 +08:00
nkymut 3c2692bdda remove debugging artifacts 2025-03-09 06:06:59 +08:00
nkymut 756a65016d Merge branch 'add-program-change' of https://github.com/nkymut/strudel into add-program-change 2025-03-09 05:37:44 +08:00
nkymut a18d485283 Merge branch 'main' of https://github.com/tidalcycles/strudel into add-program-change 2025-03-09 05:34:18 +08:00
Felix Roos fcf6450ff0 Merge pull request #1288 from tidalcycles/sampler-improvements
@strudel/sampler improvements
2025-03-08 22:17:46 +01:00
Felix Roos 09dd374722 Merge branch 'main' into add-program-change 2025-03-08 22:08:03 +01:00
Felix Roos 9c9ce50b03 Merge pull request #1223 from nkymut/gamepad-pr
Add Gamepad module
2025-03-08 21:56:50 +01:00
Felix Roos edc2903106 fix: relative imports 2025-03-04 21:35:32 +01:00
Felix Roos b016720696 sampler: support serving any file (like strudel.json) 2025-02-20 23:37:59 +01:00
Felix Roos c639e44ca7 fix: dont bail when ip cannot be determined (happens when no wifi) 2025-02-20 23:07:45 +01:00
nkymut 43523b73c4 codeformat 2025-02-09 15:12:14 +08:00
nkymut b98ebc696d remove midimap JSDoc 2025-02-09 12:19:30 +08:00
nkymut f802f18d68 refactor(midi): extract MIDI message handlers into dedicated functions
- Move MIDI API logic into separate functions following #1274 (sendCC, sendProgramChange, sendPitchBend, etc.)
2025-02-09 10:56:51 +08:00
nkymut 919c2854d5 Merge branch 'main' of https://github.com/tidalcycles/strudel into add-program-change 2025-02-09 10:53:03 +08:00
nkymut 451cdcc3a9 fix sendChannelAfterTouch -> sendChannelAftertouch 2025-02-07 04:39:24 +08:00
nkymut 5ff1d35272 'miditouch' change sendKeyAfterTouch to sendChannelAfterTouch 2025-02-07 00:08:31 +08:00
nkymut 54c5454306 edit README.md 2025-02-05 08:17:42 +08:00
nkymut 9b279ff671 update 'midicmd' in README.md 2025-02-05 08:11:39 +08:00
nkymut 20dcae68c1 add sysex handler to midicmd 2025-02-05 07:42:06 +08:00
nkymut 52d1443cf8 Add midicmd JSdoc 2025-02-05 07:41:24 +08:00
nkymut ed7dc4ef6e codeformat 2025-02-04 02:49:47 +08:00
nkymut 4636d824fa remove experimental code 2025-02-03 08:40:39 +08:00
Felix Roos 9d5d86bb09 docs: use mask instead of gain, so you see what's active 2025-01-31 14:13:47 +01:00
Felix Roos e2b7c3a62b chore: remove obsolete snapshots 2025-01-31 11:05:12 +01:00
Felix Roos 1fb68af5af chore: pnpm i 2025-01-31 11:01:40 +01:00
Felix Roos b611c9c2b4 chore: revert to main package lock 2025-01-31 11:00:07 +01:00
Felix Roos ceb466ab9c chore: update vite 2025-01-31 10:58:27 +01:00
Felix Roos d09809ed12 Merge branch 'main' into gamepad-pr 2025-01-31 10:55:12 +01:00
nkymut 23ed342635 prettier! 2025-01-26 02:11:13 +08:00
nkymut 443760c0d1 update documentation 2025-01-26 02:09:37 +08:00
nkymut bb56ed479f Refactor gamepad polling to use Strudel's signal system
- Replace requestAnimationFrame polling with signal
- individual signals for buttons and axes from baseSignal
- Remove window message-based gamepad state handling for toggleButtons
- Add module-level state storage to keep button state across strudel code update
2025-01-26 02:04:13 +08:00
nkymut c5d7f95441 add testsnapshot 2025-01-25 10:01:58 +08:00
nkymut 3ffe3957ba Prettier! 2025-01-25 08:00:49 +08:00
nkymut 3189b365c8 fix midibend and miditouch 2025-01-25 03:27:46 +08:00
nkymut e19d059c0f update documents 2025-01-25 02:29:44 +08:00
nkymut b8b999eab5 add midibend, miditouch 2025-01-25 02:28:56 +08:00
nkymut 393d17a41b fix test error
- add mock 'midin' and 'sysex'
- typo in sysex example
2025-01-25 01:12:38 +08:00
nkymut d06a75a2cd Add cc to midicmd, add API Reference for midi related controls 2025-01-24 05:37:11 +08:00
nkymut 57c48f0c45 Add 'sysex' control
- sysex(id, data) and both arguments are patternable
2025-01-23 08:30:06 +08:00
nkymut a4792e29f5 update ProgramChange from pc to progNum
- add progNum keyword handler
- update midicmd handler to handle 'progNum' case
2025-01-22 06:48:55 +08:00
nkymut f95eadab2f adjust midicmd heading level 2025-01-20 01:31:26 +08:00
nkymut 1349ecb4af add midicmd documentation 2
- MiniRepl for input-output.mdx
- addresses #789 #710
2025-01-20 01:26:41 +08:00
nkymut 268c66cd3d add midicmd documentation
- addresses #789 #710
2025-01-20 01:11:39 +08:00
nkymut a8803784e1 prettier! 2025-01-18 14:42:58 +08:00
nkymut bfb4eee122 Merge branch 'main' of https://github.com/tidalcycles/strudel into add-program-change 2025-01-18 14:42:30 +08:00
nkymut 3325a8dbe9 Add device specific setting folder 1
- add example sysex data for setting NSX-39(Pocket Miku) voice data.
2025-01-18 14:38:22 +08:00
nkymut 13a4512601 Get sysex working
split sysex message into sysexid and sysexdata
sysexid is a device identification number or array
sysexdata is an array of data to be sent to the device
2025-01-18 14:31:11 +08:00
nkymut e085819fe2 add documentation for pc and sysex 2025-01-15 22:43:03 +08:00
nkymut 155ef9e95f register pc and sysex as control keywords 2025-01-14 22:10:16 +08:00
nkymut c242f5f625 midi mapping to handle program change and sysex 2025-01-14 17:11:14 +08:00
nkymut 859f153ec6 Add program change(pc) and sysex to midi 2025-01-14 16:54:53 +08:00
nkymut 0be9f439bc Prettier💄 2024-12-17 19:39:38 +08:00
nkymut 36fe942e24 getting rid of AI-smell 2024-12-17 19:35:09 +08:00
Yuta Nakayama 06958a5281 Merge branch 'main' into gamepad-pr 2024-12-17 19:22:42 +08:00
nkymut 033b123ae0 Add DOCS for gamepad module 2024-12-17 19:22:03 +08:00
nkymut f01d51f76a fix errors in README.md 2024-11-27 01:16:06 +08:00
nkymut d741b4d17a Add GamePad Module 2024-11-27 01:03:09 +08:00
Matthew Kaney bd69ffb4b7 Add high-resolution CC option to midi 2024-11-16 14:35:18 -05:00
Raphaël Forment 23a4bf6641 Getting rid of second argument 2024-11-09 02:33:56 +01:00
Raphaël Forment ec470aa2c6 Feat: midi() command support external instrument parameter mapping
This commit adds a second argument to the midi() command: mapping.
This argument should be an object containing a key-value map of
MIDI controls used by an external synthesizer. If any control is
used that matches the mapping, a CC message is sent.
2024-11-09 01:41:59 +01:00
fnordomat b233c33f56 (squash me) save setting in variable, not store 2024-04-01 17:18:32 +02:00
fnordomat e3b6884b86 make soundfont base url configurable 2024-04-01 10:46:32 +02:00
100 changed files with 3108 additions and 661 deletions
-62
View File
@@ -1,62 +0,0 @@
name: Tauri Builder
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
release:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
rust_target: x86_64-unknown-linux-gnu
- os: macos-latest
rust_target: x86_64-apple-darwin
- os: macos-latest
rust_target: aarch64-apple-darwin
- os: windows-latest
rust_target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8.6.2
- name: Node.js setup
uses: actions/setup-node@v3
with:
node-version: latest
# node-version-file: '.nvmrc'
- name: Install Rust (Stable)
run:
curl https://sh.rustup.rs -sSf | sh -s -- -y
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf libasound2-dev
- name: Install app dependencies from lockfile and build web
run: pnpm install
- name: Build the app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# tauri-action replaces \_\_VERSION\_\_ with the app version
tagName: ${{ github.ref_name }}
releaseName: "Strudel v__VERSION__"
releaseBody: |
See the assets to download this version and install.
releaseDraft: true
prerelease: false
+2 -1
View File
@@ -71,6 +71,7 @@
"jsdoc-json": "^2.0.2",
"lerna": "^8.1.9",
"prettier": "^3.4.2",
"vitest": "^3.0.4"
"vitest": "^3.0.4",
"vite-plugin-bundle-audioworklet": "workspace:*"
}
}
+3 -1
View File
@@ -11,7 +11,9 @@ export const flashField = StateField.define({
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value && tr.newDoc.length > 0) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
const mark = Decoration.mark({
attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` },
});
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash = Decoration.set([]);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "1.1.0",
"version": "1.2.2",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
+24 -32
View File
@@ -5,16 +5,15 @@ import whitescreen, { settings as whitescreenSettings } from './themes/whitescre
import teletext, { settings as teletextSettings } from './themes/teletext.mjs';
import algoboy, { settings as algoboySettings } from './themes/algoboy.mjs';
import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs';
import terminal, { settings as terminalSettings } from './themes/terminal.mjs';
import abcdef, { settings as abcdefSettings } from './themes/abcdef.mjs';
import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs';
import redText, { settings as redTextSettings } from './themes/red-text.mjs';
import greenText, { settings as greenTextSettings } from './themes/green-text.mjs';
import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
import atomone, { settings as atomOneSettings } from './themes/atomone.mjs';
import aura, { settings as auraSettings } from './themes/aura.mjs';
import bespin, { settings as bespinSettings } from './themes/bespin.mjs';
import darcula, { settings as darculaSettings } from './themes/darcula.mjs';
import dracula, { settings as draculaSettings } from './themes/dracula.mjs';
import duotoneDark, { settings as duotoneDarkSettings } from './themes/duotoneDark.mjs';
import duotoneLight, { settings as duotoneLightSettings } from './themes/duotoneLight.mjs';
import eclipse, { settings as eclipseSettings } from './themes/eclipse.mjs';
import githubDark, { settings as githubDarkSettings } from './themes/githubDark.mjs';
import githubLight, { settings as githubLightSettings } from './themes/githubLight.mjs';
@@ -32,52 +31,47 @@ import tokyoNightStorm, { settings as tokyoNightStormSettings } from './themes/t
import tokyoNightDay, { settings as tokyoNightDaySettings } from './themes/tokyoNightDay.mjs';
import vscodeDark, { settings as vscodeDarkSettings } from './themes/vscodeDark.mjs';
import vscodeLight, { settings as vscodeLightSettings } from './themes/vscodeLight.mjs';
// import xcodeDark, { settings as xcodeDarkSettings } from './themes/xcodeDark.mjs';
import xcodeLight, { settings as xcodeLightSettings } from './themes/xcodeLight.mjs';
import bbedit, { settings as bbeditSettings } from './themes/bbedit.mjs';
import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLilac.mjs';
import { setTheme } from '@strudel/draw';
export const themes = {
strudelTheme,
bluescreen,
blackscreen,
whitescreen,
teletext,
algoboy,
androidstudio,
atomone,
aura,
bbedit,
blackscreen,
bluescreen,
CutiePi,
darcula,
dracula,
// todo: optimize
// bespin,
//abcdef,
androidstudio,
duotoneDark,
eclipse,
githubDark,
CutiePi,
githubLight,
greenText,
gruvboxDark,
gruvboxLight,
sonicPink,
materialDark,
nord,
materialLight,
monokai,
noctisLilac,
nord,
redText,
solarizedDark,
solarizedLight,
sublime,
teletext,
tokyoNight,
tokyoNightDay,
tokyoNightStorm,
vscodeDark,
//xcodeDark,
// LIGHT
bbedit,
//duotoneLight,
eclipse,
githubLight,
gruvboxLight,
materialLight,
vscodeLight,
noctisLilac,
solarizedLight,
tokyoNightDay,
whitescreen,
xcodeLight,
};
@@ -88,21 +82,19 @@ export const settings = {
whitescreen: whitescreenSettings,
teletext: teletextSettings,
algoboy: algoboySettings,
terminal: terminalSettings,
abcdef: abcdefSettings,
androidstudio: androidstudioSettings,
atomone: atomOneSettings,
aura: auraSettings,
bbedit: bbeditSettings,
bespin: bespinSettings,
darcula: darculaSettings,
dracula: draculaSettings,
duotoneLight: duotoneLightSettings,
duotoneDark: duotoneDarkSettings,
eclipse: eclipseSettings,
CutiePi: CutiePiSettings,
sonicPink: sonicPinkSettings,
githubLight: githubLightSettings,
githubDark: githubDarkSettings,
greenText: greenTextSettings,
gruvboxDark: gruvboxDarkSettings,
gruvboxLight: gruvboxLightSettings,
materialDark: materialDarkSettings,
@@ -110,6 +102,7 @@ export const settings = {
noctisLilac: noctisLilacSettings,
nord: nordSettings,
monokai: monokaiSettings,
redText: redTextSettings,
solarizedLight: solarizedLightSettings,
solarizedDark: solarizedDarkSettings,
sublime: sublimeSettings,
@@ -118,7 +111,6 @@ export const settings = {
vscodeDark: vscodeDarkSettings,
vscodeLight: vscodeLightSettings,
xcodeLight: xcodeLightSettings,
//xcodeDark: xcodeDarkSettings,
tokyoNightDay: tokyoNightDaySettings,
};
+2 -2
View File
@@ -1,5 +1,5 @@
/**
* @name Cutie Pi
/*
* Cutie Pi
* by Switch Angel
*/
import { tags as t } from '@lezer/highlight';
-55
View File
@@ -1,55 +0,0 @@
/**
* @name abcdef
* @author codemirror.net
* https://codemirror.net/5/theme/abcdef.css
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#0f0f0f',
lineBackground: '#0f0f0f99',
foreground: '#defdef',
caret: '#00FF00',
selection: '#515151',
selectionMatch: '#515151',
gutterBackground: '#555',
gutterForeground: '#FFFFFF',
lineHighlight: '#314151',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#0f0f0f',
foreground: '#defdef',
caret: '#00FF00',
selection: '#515151',
selectionMatch: '#515151',
// gutterBackground: '#555',
gutterBackground: 'transparent',
/* gutterForeground: '#FFFFFF', */
gutterForeground: '#7a7b7c',
lineHighlight: '#0a6bcb3d',
},
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'darkgoldenrod', fontWeight: 'bold' },
{ tag: t.atom, color: '#77F' },
{ tag: t.comment, color: '#7a7b7c', fontStyle: 'italic' },
{ tag: t.number, color: 'violet' },
{ tag: t.definition(t.variableName), color: '#fffabc' },
{ tag: t.variableName, color: '#abcdef' },
{ tag: t.function(t.variableName), color: '#fffabc' },
{ tag: t.typeName, color: '#FFDD44' },
{ tag: t.tagName, color: '#def' },
{ tag: t.string, color: '#2b4' },
{ tag: t.meta, color: '#C9F' },
// { tag: t.qualifier, color: '#FFF700' },
// { tag: t.builtin, color: '#30aabc' },
{ tag: t.bracket, color: '#8a8a8a' },
{ tag: t.attributeName, color: '#DDFF00' },
{ tag: t.heading, color: 'aquamarine', fontWeight: 'bold' },
{ tag: t.link, color: 'blueviolet', fontWeight: 'bold' },
],
});
+1
View File
@@ -54,6 +54,7 @@ export default createTheme({
tag: [t.keyword, t.tagName, t.arithmeticOperator],
color: palette[1],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: palette[0] },
{ tag: [t.function(t.variableName), t.propertyName], color: palette[0] },
{ tag: t.atom, color: palette[1] },
],
+2 -2
View File
@@ -1,5 +1,5 @@
/**
* @name androidstudio
/*
* androidstudio
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
+4 -2
View File
@@ -1,5 +1,5 @@
/**
* @name Atom One
/*
* Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
@@ -38,12 +38,14 @@ export default createTheme({
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: 'hsl(207, 82%, 66%)',
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: 'hsl( 29, 54%, 61%)' },
{ tag: [t.tagName, t.heading], color: '#e06c75' },
{ tag: t.comment, color: '#54636D' },
{ tag: [t.variableName, t.propertyName, t.labelName], color: 'hsl(220, 14%, 71%)' },
{ tag: [t.attributeName, t.number], color: 'hsl( 29, 54%, 61%)' },
{ tag: t.className, color: 'hsl( 39, 67%, 69%)' },
{ tag: t.keyword, color: 'hsl(286, 60%, 67%)' },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: '#98c379' },
],
});
-39
View File
@@ -1,39 +0,0 @@
// this is different from https://thememirror.net/bespin
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#28211c',
lineBackground: '#28211c99',
foreground: '#9d9b97',
caret: '#797977',
selection: '#36312e',
selectionMatch: '#4f382b',
gutterBackground: '#28211c',
gutterForeground: '#666666',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#28211c',
foreground: '#9d9b97',
caret: '#797977',
selection: '#4f382b',
selectionMatch: '#4f382b',
gutterBackground: '#28211c',
gutterForeground: '#666666',
lineHighlight: '#ffffff1a',
},
styles: [
{ tag: [t.atom, t.number, t.link, t.bool], color: '#9b859d' },
{ tag: t.comment, color: '#937121' },
{ tag: [t.keyword, t.tagName], color: '#cf6a4c' },
{ tag: t.string, color: '#f9ee98' },
{ tag: t.bracket, color: '#9d9b97' },
{ tag: [t.variableName], color: '#5ea6ea' },
{ tag: t.definition(t.variableName), color: '#cf7d34' },
{ tag: [t.function(t.variableName), t.className], color: '#cf7d34' },
{ tag: [t.propertyName, t.attributeName], color: '#54be0d' },
],
});
+2 -3
View File
@@ -1,6 +1,5 @@
/**
* @name darcula
* @author darcula
/*
* darcula
* Name: IntelliJ IDEA darcula theme
* From IntelliJ IDEA by JetBrains
*/
+1 -2
View File
@@ -1,6 +1,5 @@
/**
/*
* @name dracula
* @author dracula
* Michael Kaminsky (http://github.com/mkaminsky11)
* Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)
*/
+3 -3
View File
@@ -1,6 +1,6 @@
/**
* @name duotone
* @author Bram de Haan
/*
* duotone
* author Bram de Haan
* by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes)
*/
import { tags as t } from '@lezer/highlight';
+3 -3
View File
@@ -1,6 +1,6 @@
/**
* @name duotone
* @author Bram de Haan
/*
* duotone
* author Bram de Haan
* by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes)
*/
import { tags as t } from '@lezer/highlight';
-3
View File
@@ -1,6 +1,3 @@
/**
* @name github
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
-3
View File
@@ -1,6 +1,3 @@
/**
* @name github
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
+39
View File
@@ -0,0 +1,39 @@
/*
* Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = ['#000000', '#8ed675', '#56bd2a', '#54636D', '#171717'];
export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
selection: hex[4],
selectionMatch: hex[0],
gutterBackground: hex[0],
gutterForeground: hex[3],
gutterBorder: 'transparent',
lineHighlight: hex[0],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[2],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
{ tag: t.comment, color: hex[3] },
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
{ tag: [t.attributeName, t.number], color: hex[1] },
{ tag: t.keyword, color: hex[2] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
],
});
+3 -4
View File
@@ -1,7 +1,6 @@
/**
* @name gruvbox-dark
* @author morhetz
* Name: Gruvbox
/*
* gruvbox-dark
* author morhetz
* From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-dark.css
*/
import { tags as t } from '@lezer/highlight';
+3 -4
View File
@@ -1,7 +1,6 @@
/**
* @name gruvbox-light
* @author morhetz
* Name: Gruvbox
/*
* gruvbox-light
* author morhetz
* From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-light.css
*/
import { tags as t } from '@lezer/highlight';
+39
View File
@@ -0,0 +1,39 @@
/*
* Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = ['#000000', '#ff5356', '#bd312a', '#54636D', '#171717'];
export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
selection: hex[4],
selectionMatch: hex[0],
gutterBackground: hex[0],
gutterForeground: hex[3],
gutterBorder: 'transparent',
lineHighlight: hex[0],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[2],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
{ tag: t.comment, color: hex[3] },
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
{ tag: [t.attributeName, t.number], color: hex[1] },
{ tag: t.keyword, color: hex[2] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
],
});
+39
View File
@@ -0,0 +1,39 @@
/*
* Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = ['#1e1e1e', '#fbde2d', '#ff1493', '#4c83ff', '#ededed', '#cccccc', '#ffffff30', '#dc2f8c'];
export const settings = {
background: '#000000',
lineBackground: 'transparent',
foreground: hex[4],
selection: hex[6],
gutterBackground: hex[0],
gutterForeground: hex[5],
gutterBorder: 'transparent',
lineHighlight: hex[0],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[4],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[3] },
{ tag: t.comment, color: '#54636D' },
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[4] },
{ tag: [t.attributeName, t.number], color: hex[3] },
{ tag: t.keyword, color: hex[1] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] },
],
});
+1 -3
View File
@@ -5,14 +5,11 @@ export const settings = {
background: '#222',
lineBackground: '#22222299',
foreground: '#fff',
// foreground: '#75baff',
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
};
@@ -20,6 +17,7 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#89ddff' },
{ tag: t.labelName, color: '#89ddff' },
{ tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' },
-34
View File
@@ -1,34 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#292A30',
lineBackground: '#292A3099',
foreground: '#CECFD0',
caret: '#fff',
selection: '#727377',
selectionMatch: '#727377',
lineHighlight: '#2F3239',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#292A30',
foreground: '#CECFD0',
caret: '#fff',
selection: '#727377',
selectionMatch: '#727377',
lineHighlight: '#ffffff0f',
},
styles: [
{ tag: [t.comment, t.quote], color: '#7F8C98' },
{ tag: [t.keyword], color: '#FF7AB2', fontWeight: 'bold' },
{ tag: [t.string, t.meta], color: '#FF8170' },
{ tag: [t.typeName], color: '#DABAFF' },
{ tag: [t.definition(t.variableName)], color: '#6BDFFF' },
{ tag: [t.name], color: '#6BAA9F' },
{ tag: [t.variableName], color: '#ACF2E4' },
{ tag: [t.regexp, t.link], color: '#FF8170' },
],
});
+242 -15
View File
@@ -1,5 +1,5 @@
/*
controls.mjs - <short description TODO>
controls.mjs - Registers audio controls for pattern manipulation and effects.
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/controls.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/>.
*/
@@ -445,6 +445,32 @@ export const { coarse } = registerControl('coarse');
*/
export const { drive } = registerControl('drive');
/**
* Create byte beats with custom expressions
*
* @name byteBeatExpression
* @synonyms bbexpr
*
* @param {number | Pattern} byteBeatExpression bitwise expression for creating bytebeat
* @example
* s("bytebeat").bbexpr('t*(t>>15^t>>66)')
*
*/
export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpression', 'bbexpr');
/**
* Create byte beats with custom expressions
*
* @name byteBeatStartTime
* @synonyms bbst
*
* @param {number | Pattern} byteBeatStartTime in samples (t)
* @example
* note("c3!8".add("{0 0 12 0 7 5 3}%8")).s("bytebeat:5").bbst("<3 1>".mul(10000))._scope()
*
*/
export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime', 'bbst');
/**
* Allows you to set the output channels on the interface
*
@@ -458,6 +484,41 @@ export const { drive } = registerControl('drive');
*/
export const { channels, ch } = registerControl('channels', 'ch');
/**
* controls the pulsewidth of the pulse oscillator
*
* @name pw
* @param {number | Pattern} pulsewidth
* @example
* note("{f a c e}%16").s("pulse").pw(".8:1:.2")
* @example
* n(run(8)).scale("D:pentatonic").s("pulse").pw("0 .75 .5 1")
*/
export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
/**
* controls the lfo rate for the pulsewidth of the pulse oscillator
*
* @name pwrate
* @param {number | Pattern} rate
* @example
* n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>")
*
*/
export const { pwrate } = registerControl('pwrate');
/**
* controls the lfo sweep for the pulsewidth of the pulse oscillator
*
* @name pwsweep
* @param {number | Pattern} sweep
* @example
* n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>")
*
*/
export const { pwsweep } = registerControl('pwsweep');
/**
* Phaser audio effect that approximates popular guitar pedals.
*
@@ -1461,7 +1522,7 @@ export const { vowel } = registerControl('vowel');
* @name waveloss
*/
export const { waveloss } = registerControl('waveloss');
/*
/**
* Noise crackle density
*
* @name density
@@ -1513,22 +1574,11 @@ export const { scram } = registerControl('scram');
export const { binshift } = registerControl('binshift');
export const { hbrick } = registerControl('hbrick');
export const { lbrick } = registerControl('lbrick');
export const { midichan } = registerControl('midichan');
export const { midimap } = registerControl('midimap');
export const { midiport } = registerControl('midiport');
export const { control } = registerControl('control');
export const { ccn } = registerControl('ccn');
export const { ccv } = registerControl('ccv');
export const { polyTouch } = registerControl('polyTouch');
export const { midibend } = registerControl('midibend');
export const { miditouch } = registerControl('miditouch');
export const { ctlNum } = registerControl('ctlNum');
export const { frameRate } = registerControl('frameRate');
export const { frames } = registerControl('frames');
export const { hours } = registerControl('hours');
export const { midicmd } = registerControl('midicmd');
export const { minutes } = registerControl('minutes');
export const { progNum } = registerControl('progNum');
export const { seconds } = registerControl('seconds');
export const { songPtr } = registerControl('songPtr');
export const { uid } = registerControl('uid');
@@ -1621,6 +1671,151 @@ export const ar = register('ar', (t, pat) => {
return pat.set({ attack, release });
});
//MIDI
/**
* MIDI channel: Sets the MIDI channel for the event.
*
* @name midichan
* @param {number | Pattern} channel MIDI channel number (0-15)
* @example
* note("c4").midichan(1).midi()
*/
export const { midichan } = registerControl('midichan');
export const { midimap } = registerControl('midimap');
/**
* MIDI port: Sets the MIDI port for the event.
*
* @name midiport
* @param {number | Pattern} port MIDI port
* @example
* note("c a f e").midiport("<0 1 2 3>").midi()
*/
export const { midiport } = registerControl('midiport');
/**
* MIDI command: Sends a MIDI command message.
*
* @name midicmd
* @param {number | Pattern} command MIDI command
* @example
* midicmd("clock*48,<start stop>/2").midi()
*/
export const { midicmd } = registerControl('midicmd');
/**
* MIDI control: Sends a MIDI control change message.
*
* @name control
* @param {number | Pattern} MIDI control number (0-127)
* @param {number | Pattern} MIDI controller value (0-127)
*/
export const control = register('control', (args, pat) => {
if (!Array.isArray(args)) {
throw new Error('control expects an array of [ccn, ccv]');
}
const [_ccn, _ccv] = args;
return pat.ccn(_ccn).ccv(_ccv);
});
/**
* MIDI control number: Sends a MIDI control change message.
*
* @name ccn
* @param {number | Pattern} MIDI control number (0-127)
*/
export const { ccn } = registerControl('ccn');
/**
* MIDI control value: Sends a MIDI control change message.
*
* @name ccv
* @param {number | Pattern} MIDI control value (0-127)
*/
export const { ccv } = registerControl('ccv');
export const { ctlNum } = registerControl('ctlNum');
// TODO: ctlVal?
/**
* MIDI NRPN non-registered parameter number: Sends a MIDI NRPN non-registered parameter number message.
* @name nrpnn
* @param {number | Pattern} nrpnn MIDI NRPN non-registered parameter number (0-127)
* @example
* note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi()
*/
export const { nrpnn } = registerControl('nrpnn');
/**
* MIDI NRPN non-registered parameter value: Sends a MIDI NRPN non-registered parameter value message.
* @name nrpv
* @param {number | Pattern} nrpv MIDI NRPN non-registered parameter value (0-127)
* @example
* note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi()
*/
export const { nrpv } = registerControl('nrpv');
/**
* MIDI program number: Sends a MIDI program change message.
*
* @name progNum
* @param {number | Pattern} program MIDI program number (0-127)
* @example
* note("c4").progNum(10).midichan(1).midi()
*/
export const { progNum } = registerControl('progNum');
/**
* MIDI sysex: Sends a MIDI sysex message.
* @name sysex
* @param {number | Pattern} id Sysex ID
* @param {number | Pattern} data Sysex data
* @example
* note("c4").sysex(["0x77", "0x01:0x02:0x03:0x04"]).midichan(1).midi()
*/
export const sysex = register('sysex', (args, pat) => {
if (!Array.isArray(args)) {
throw new Error('sysex expects an array of [id, data]');
}
const [id, data] = args;
return pat.sysexid(id).sysexdata(data);
});
/**
* MIDI sysex ID: Sends a MIDI sysex identifier message.
* @name sysexid
* @param {number | Pattern} id Sysex ID
* @example
* note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi()
*/
export const { sysexid } = registerControl('sysexid');
/**
* MIDI sysex data: Sends a MIDI sysex message.
* @name sysexdata
* @param {number | Pattern} data Sysex data
* @example
* note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi()
*/
export const { sysexdata } = registerControl('sysexdata');
/**
* MIDI pitch bend: Sends a MIDI pitch bend message.
* @name midibend
* @param {number | Pattern} midibend MIDI pitch bend (-1 - 1)
* @example
* note("c4").midibend(sine.slow(4).range(-0.4,0.4)).midi()
*/
export const { midibend } = registerControl('midibend');
/**
* MIDI key after touch: Sends a MIDI key after touch message.
* @name miditouch
* @param {number | Pattern} miditouch MIDI key after touch (0-1)
* @example
* note("c4").miditouch(sine.slow(4).range(0,1)).midi()
*/
export const { miditouch } = registerControl('miditouch');
// TODO: what is this?
export const { polyTouch } = registerControl('polyTouch');
export const getControlName = (alias) => {
if (controlAlias.has(alias)) {
return controlAlias.get(alias);
@@ -1632,14 +1827,46 @@ export const getControlName = (alias) => {
* Sets properties in a batch.
*
* @name as
* @param {Array} mapping the control names that are set
* @param {String | Array} mapping the control names that are set
* @example
* "c:.5 a:1 f:.25 e:.8".as("note:clip")
* @example
* "{0@2 0.25 0 0.5 .3 .5}%8".as("begin").s("sax_vib").clip(1)
*/
export const as = register('as', (mapping, pat) => {
mapping = Array.isArray(mapping) ? mapping : [mapping];
return pat.fmap((v) => {
v = Array.isArray(v) ? v : [v];
v = Object.fromEntries(mapping.map((prop, i) => [getControlName(prop), v[i]]));
return v;
});
});
/**
* Allows you to scrub an audio file like a tape loop by passing values that represents the position in the audio file
* in the optional array syntax ex: "0.5:2", the second value controls the speed of playback
* @name scrub
* @memberof Pattern
* @returns Pattern
* @example
* samples('github:switchangel/pad')
* s("swpad:0").scrub("{0.1!2 .25@3 0.7!2 <0.8:1.5>}%8")
* @example
* samples('github:yaxu/clean-breaks/main');
* s("amen/4").fit().scrub("{0@3 0@2 4@3}%8".div(16))
*/
export const scrub = register(
'scrub',
(beginPat, pat) => {
return beginPat.outerBind((v) => {
if (!Array.isArray(v)) {
v = [v];
}
const [beginVal, speedMultiplier = 1] = v;
return pat.begin(beginVal).mul(speed(speedMultiplier)).clip(1);
});
},
false,
);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "1.1.0",
"version": "1.2.2",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
+39 -14
View File
@@ -2105,11 +2105,13 @@ export const linger = register(
/**
* Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one.
* @name segment
* @synonyms seg
* @param {number} segments number of segments per cycle
* @example
* note(saw.range(40,52).segment(24))
*/
export const segment = register('segment', function (rate, pat) {
export const { segment, seg } = register(['segment', 'seg'], function (rate, pat) {
return pat.struct(pure(true)._fast(rate)).setSteps(rate);
});
@@ -2439,9 +2441,14 @@ const _chunk = function (n, func, pat, back = false, fast = false) {
return pat.when(binary_pat, func);
};
export const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) {
return _chunk(n, func, pat, false, false);
});
export const { chunk, slowchunk, slowChunk } = register(
['chunk', 'slowchunk', 'slowChunk'],
function (n, func, pat) {
return _chunk(n, func, pat, false, false);
},
true,
true,
);
/**
* Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles
@@ -2453,9 +2460,14 @@ export const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', '
* "0 1 2 3".chunkBack(4, x=>x.add(7))
* .scale("A:minor").note()
*/
export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], function (n, func, pat) {
return _chunk(n, func, pat, true);
});
export const { chunkBack, chunkback } = register(
['chunkBack', 'chunkback'],
function (n, func, pat) {
return _chunk(n, func, pat, true);
},
true,
true,
);
/**
* Like `chunk`, but the cycles of the source pattern aren't repeated
@@ -2469,9 +2481,14 @@ export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], fun
* .fastChunk(4, x => x.color('red')).slow(2)
* .scale("C2:major").note()
*/
export const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], function (n, func, pat) {
return _chunk(n, func, pat, false, true);
});
export const { fastchunk, fastChunk } = register(
['fastchunk', 'fastChunk'],
function (n, func, pat) {
return _chunk(n, func, pat, false, true);
},
true,
true,
);
// TODO - redefine elsewhere in terms of mask
export const bypass = register(
@@ -2485,16 +2502,24 @@ export const bypass = register(
);
/**
* Loops the pattern inside at `offset` for `cycles`.
* Loops the pattern inside an `offset` for `cycles`.
* If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it.
* @name ribbon
* @synonym rib
* @param {number} offset start point of loop in cycles
* @param {number} cycles loop length in cycles
* @example
* note("<c d e f>").ribbon(1, 2).fast(2)
* note("<c d e f>").ribbon(1, 2)
* @example
* // Looping a portion of randomness
* note(irand(8).segment(4).scale('C3 minor')).ribbon(1337, 2)
* n(irand(8).segment(4)).scale("c:pentatonic").ribbon(1337, 2)
* @example
* // rhythm generator
* s("bd!16?").ribbon(29,.5)
*/
export const ribbon = register('ribbon', (offset, cycles, pat) => pat.early(offset).restart(pure(1).slow(cycles)));
export const { ribbon, rib } = register(['ribbon', 'rib'], (offset, cycles, pat) =>
pat.early(offset).restart(pure(1).slow(cycles)),
);
export const hsla = register('hsla', (h, s, l, a, pat) => {
return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`);
+1 -2
View File
@@ -156,8 +156,7 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin();
});
/**
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
/** Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @synonyms pickSqueeze
+5 -2
View File
@@ -279,9 +279,9 @@ const _rearrangeWith = (ipat, n, pat) => {
};
/**
* @name shuffle
* Slices a pattern into the given number of parts, then plays those parts in random order.
* Each part will be played exactly once per cycle.
* @name shuffle
* @example
* note("c d e f").sound("piano").shuffle(4)
* @example
@@ -292,9 +292,9 @@ export const shuffle = register('shuffle', (n, pat) => {
});
/**
* @name scramble
* Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`,
* but parts might be played more than once, or not at all, per cycle.
* @name scramble
* @example
* note("c d e f").sound("piano").scramble(4)
* @example
@@ -526,6 +526,9 @@ export const degradeByWith = register(
* s("hh*8").degradeBy(0.2)
* @example
* s("[hh?0.2]*8")
* @example
* //beat generator
* s("bd").segment(16).degradeBy(.5).ribbon(16,1)
*/
export const degradeBy = register(
'degradeBy',
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "1.1.0",
"version": "1.2.3",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
"version": "1.1.0",
"version": "1.2.2",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
+93
View File
@@ -0,0 +1,93 @@
# @strudel/gamepad
This package adds gamepad input functionality to strudel Patterns.
## Install
```sh
npm i @strudel/gamepad --save
```
## Usage
```javascript
import { gamepad } from '@strudel/gamepad';
// Initialize gamepad (optional index parameter, defaults to 0)
const pad = gamepad(0);
// Use gamepad inputs in patterns
const pattern = sequence([
// Button inputs
pad.a, // A button value (0-1)
pad.tglA, // A button toggle (0 or 1)
// Analog stick inputs
pad.x1, // Left stick X (0-1)
pad.x1_2, // Left stick X (-1 to 1)
]);
```
## Available Controls
### Buttons
- Face Buttons
- `a`, `b`, `x`, `y` (or uppercase `A`, `B`, `X`, `Y`)
- Toggle versions: `tglA`, `tglB`, `tglX`, `tglY`
- Shoulder Buttons
- `lb`, `rb`, `lt`, `rt` (or uppercase `LB`, `RB`, `LT`, `RT`)
- Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT`
- D-Pad
- `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase)
- Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight`(or `tglU`, `tglD`, `tglL`, `tglR`)
### Analog Sticks
- Left Stick
- `x1`, `y1` (0 to 1 range)
- `x1_2`, `y1_2` (-1 to 1 range)
- Right Stick
- `x2`, `y2` (0 to 1 range)
- `x2_2`, `y2_2` (-1 to 1 range)
## Examples
```javascript
// Use button values to control amplitude
$: sequence([
s("bd").gain(pad.X), // X button controls gain
s("[hh oh]").gain(pad.tglY), // Y button toggles gain
]);
// Use analog stick for continuous control
$: note("c4*4".add(pad.y1_2.range(-24,24))) // Left stick Y controls pitch shift
.pan(pad.x1_2); // Left stick X controls panning
// Use toggle buttons to switch patterns on/off
// Define button sequences
const HADOKEN = [
'd', // Down
'r', // Right
'a', // A
];
const KONAMI = 'uuddlrlrba' //Konami Code ↑↑↓↓←→←→BA
// Add these lines to enable buttons(but why?)
$:pad.D.segment(16).gain(0)
$:pad.R.segment(16).gain(0)
$:pad.A.segment(16).gain(0)
// Check button sequence (returns 1 when detected, 0 when not within last 1 second)
$: sound("hadoken").gain(pad.checkSequence(HADOKEN))
```
## Multiple Gamepads
You can connect multiple gamepads by specifying the gamepad index:
```javascript
const pad1 = gamepad(0); // First gamepad
const pad2 = gamepad(1); // Second gamepad
```
+117
View File
@@ -0,0 +1,117 @@
import { MiniRepl } from '../../../website/src/docs/MiniRepl';
# Gamepad
The Gamepad module allows you to integrate gamepad input functionality into your musical patterns. This can be particularly useful for live performances or interactive installations where you want to manipulate sounds using a game controller.
## Getting Started
Initialize a gamepad by calling the gamepad() function with an optional index parameter.
<MiniRepl
client:idle
tune={`// Initialize gamepad (optional index parameter, defaults to 0)
const gp = gamepad(0)
note("c a f e").mask(gp.a)`}
/>
## Available Controls
The gamepad module provides access to buttons and analog sticks as normalized signals (0-1) that can modulate your patterns.
### Buttons
| Type | Controls |
| ---------------- | ---------------------------------------------------------------------------------------------- |
| Face Buttons | `a`, `b`, `x`, `y` (or uppercase `A`, `B`, `X`, `Y`) |
| | Toggle versions: `tglA`, `tglB`, `tglX`, `tglY` |
| Shoulder Buttons | `lb`, `rb`, `lt`, `rt` (or uppercase `LB`, `RB`, `LT`, `RT`) |
| | Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT` |
| D-Pad | `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) |
| | Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight` (or `tglU`, `tglD`, `tglL`, `tglR`) |
### Analog Sticks
| Stick | Controls |
| ----------- | ------------------------------ |
| Left Stick | `x1`, `y1` (0 to 1 range) |
| | `x1_2`, `y1_2` (-1 to 1 range) |
| Right Stick | `x2`, `y2` (0 to 1 range) |
| | `x2_2`, `y2_2` (-1 to 1 range) |
### Button Sequence
| Stick | Controls |
| --------------- | --------------------------------------- |
| Button Sequence | `btnSequence()`, `btnSeq()`, `btnseq()` |
## Using Gamepad Inputs
Once initialized, you can use various gamepad inputs in your patterns. Here are some examples:
### Button Inputs
You can use button inputs to control different aspects of your music, such as gain or triggering events.
<MiniRepl
client:idle
tune={`const gp = gamepad(0)
// Use button values to control amplitude
$: stack(
s("[[hh hh] oh hh oh]/2").mask(gp.tglX).bank("RolandTR909"), // X btn for HH
s("cr*1").mask(gp.Y).bank("RolandTR909"), // LB btn for CR
s("bd").mask(gp.tglA).bank("RolandTR909"), // A btn for BD
s("[ht - - mt - - lt - ]/2").mask(gp.tglB).bank("RolandTR909"), // B btn for Toms
s("sd*4").mask(gp.RB).bank("RolandTR909"), // RB btn for SD
).cpm(120)
`}
/>
### Analog Stick Inputs
Analog sticks can be used for continuous control, such as pitch shifting or panning.
<MiniRepl
client:idle
tune={`const gp = gamepad(0)
// Use analog stick for continuous control
$: note("c4 d3 a3 e3").sound("sawtooth")
.lpf(gp.x1.range(100,4000))
.lpq(gp.y1.range(5,30))
.decay(gp.y2.range(0.1,2))
.lpenv(gp.x2.range(-5,5))
.cpm(120)
`}
/>
### Button Sequences
You can define button sequences to trigger specific actions, like playing a sound when a sequence is detected.
<MiniRepl client:idle tune={`const gp = gamepad(0)
// Define button sequences
const HADOUKEN = [
'd', // Down
'r', // Right
'a', // A
]
const KONAMI = 'uuddlrlrba' //Konami Code ↑↑↓↓←→←→BA
// Check butto-n sequence (returns 1 while detected, 0 when not within last 1 second)
$: s("free_hadouken -").slow(2)
.mask(gp.btnSequence(HADOUKEN)).room(1).cpm(120)
// hadouken.wav by Syna-Max
//https://freesound.org/people/Syna-Max/sounds/67674/
samples({free_hadouken: 'https://cdn.freesound.org/previews/67/67674_111920-lq.mp3'})
`} />
## Multiple Gamepads
Strudel supports multiple gamepads. You can specify the gamepad index to connect to different devices.
<MiniRepl
client:idle
tune={`const pad1 = gamepad(0); // First gamepad
const pad2 = gamepad(1); // Second gamepad`}
/>
+246
View File
@@ -0,0 +1,246 @@
// @strudel/gamepad/index.mjs
import { signal } from '@strudel/core';
// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)
export const buttonMap = {
a: 0,
b: 1,
x: 2,
y: 3,
lb: 4,
rb: 5,
lt: 6,
rt: 7,
back: 8,
start: 9,
u: 12,
up: 12,
d: 13,
down: 13,
l: 14,
left: 14,
r: 15,
right: 15,
};
class ButtonSequenceDetector {
constructor(timeWindow = 1000) {
this.sequence = [];
this.timeWindow = timeWindow;
this.lastInputTime = 0;
this.buttonStates = Array(16).fill(0); // Track previous state of each button
// Button mapping for character inputs
}
addInput(buttonIndex, buttonValue) {
const currentTime = Date.now();
// Only add input on button press (rising edge)
if (buttonValue === 1 && this.buttonStates[buttonIndex] === 0) {
// Clear sequence if too much time has passed
if (currentTime - this.lastInputTime > this.timeWindow) {
this.sequence = [];
}
// Store the button name instead of index
const buttonName = Object.keys(buttonMap).find((key) => buttonMap[key] === buttonIndex) || buttonIndex.toString();
this.sequence.push({
input: buttonName,
timestamp: currentTime,
});
this.lastInputTime = currentTime;
//console.log(this.sequence);
// Keep only inputs within the time window
this.sequence = this.sequence.filter((entry) => currentTime - entry.timestamp <= this.timeWindow);
}
// Update button state
this.buttonStates[buttonIndex] = buttonValue;
}
checkSequence(targetSequence) {
if (!Array.isArray(targetSequence) && typeof targetSequence !== 'string') {
console.error('ButtonSequenceDetector: targetSequence must be an array or string');
return 0;
}
if (this.sequence.length < targetSequence.length) return 0;
// Convert string input to array if needed
const sequence =
typeof targetSequence === 'string'
? targetSequence.toLowerCase().split('')
: targetSequence.map((s) => s.toString().toLowerCase());
//console.log(this.sequence);
// Get the last n inputs where n is the target sequence length
const lastInputs = this.sequence.slice(-targetSequence.length).map((entry) => entry.input);
// Compare sequences
return lastInputs.every((input, index) => {
const target = sequence[index];
// Check if either the input matches directly or they refer to the same button in the map
return (
input === target ||
buttonMap[input] === buttonMap[target] ||
// Also check if the numerical index matches
buttonMap[input] === parseInt(target)
);
})
? 1
: 0;
}
}
class GamepadHandler {
constructor(index = 0) {
// Add index parameter
this._gamepads = {};
this._activeGamepad = index; // Use provided index
this._axes = [0, 0, 0, 0];
this._buttons = Array(16).fill(0);
this.setupEventListeners();
}
setupEventListeners() {
window.addEventListener('gamepadconnected', (e) => {
this._gamepads[e.gamepad.index] = e.gamepad;
if (!this._activeGamepad) {
this._activeGamepad = e.gamepad.index;
}
});
window.addEventListener('gamepaddisconnected', (e) => {
delete this._gamepads[e.gamepad.index];
if (this._activeGamepad === e.gamepad.index) {
this._activeGamepad = Object.keys(this._gamepads)[0] || null;
}
});
}
poll() {
if (this._activeGamepad !== null) {
const gamepad = navigator.getGamepads()[this._activeGamepad];
if (gamepad) {
// Update axes (normalized to 0-1 range)
this._axes = gamepad.axes.map((axis) => (axis + 1) / 2);
// Update buttons
this._buttons = gamepad.buttons.map((button) => button.value);
}
}
}
getAxes() {
return this._axes;
}
getButtons() {
return this._buttons;
}
}
// Module-level state store for toggle states
const gamepadStates = new Map();
export const gamepad = (index = 0) => {
const handler = new GamepadHandler(index);
const sequenceDetector = new ButtonSequenceDetector(2000);
// Base signal that polls gamepad state and handles sequence detection
const baseSignal = signal((t) => {
handler.poll();
const axes = handler.getAxes();
const buttons = handler.getButtons();
// Add all button inputs to sequence detector
buttons.forEach((value, i) => {
sequenceDetector.addInput(i, value);
});
return { axes, buttons, t };
});
// Create axes patterns
const axes = {
x1: baseSignal.fmap((state) => state.axes[0]),
y1: baseSignal.fmap((state) => state.axes[1]),
x2: baseSignal.fmap((state) => state.axes[2]),
y2: baseSignal.fmap((state) => state.axes[3]),
};
// Add bipolar versions
axes.x1_2 = axes.x1.toBipolar();
axes.y1_2 = axes.y1.toBipolar();
axes.x2_2 = axes.x2.toBipolar();
axes.y2_2 = axes.y2.toBipolar();
// Create button patterns
const buttons = Array(16)
.fill(null)
.map((_, i) => {
// Create unique key for this gamepad+button combination
const stateKey = `gamepad${index}_btn${i}`;
// Initialize toggle state if it doesn't exist
if (!gamepadStates.has(stateKey)) {
gamepadStates.set(stateKey, {
lastButtonState: 0,
toggleState: 0,
});
}
// Direct button value pattern (no longer needs to call addInput)
const btn = baseSignal.fmap((state) => state.buttons[i]);
// Button toggle pattern with persistent state
const toggle = baseSignal.fmap((state) => {
const currentState = state.buttons[i];
const buttonState = gamepadStates.get(stateKey);
if (currentState === 1 && buttonState.lastButtonState === 0) {
// Toggle the state on rising edge
buttonState.toggleState = buttonState.toggleState === 0 ? 1 : 0;
}
buttonState.lastButtonState = currentState;
return buttonState.toggleState;
});
return { value: btn, toggle };
});
// Create sequence checker pattern
const btnSequence = (sequence) => {
return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence));
};
const checkSequence = btnSequence;
const btnSeq = btnSequence;
const btnseq = btnSeq;
// Return an object with all controls
return {
...axes,
buttons,
...Object.fromEntries(
Object.entries(buttonMap).flatMap(([key, index]) => [
[key.toLowerCase(), buttons[index].value],
[key.toUpperCase(), buttons[index].value],
[`tgl${key.toLowerCase()}`, buttons[index].toggle],
[`tgl${key.toUpperCase()}`, buttons[index].toggle],
]),
),
checkSequence,
btnSequence,
btnSeq,
btnseq,
raw: baseSignal,
};
};
// Optional: Export for debugging or state management
export const getGamepadStates = () => Object.fromEntries(gamepadStates);
export const clearGamepadStates = () => gamepadStates.clear();
+3
View File
@@ -0,0 +1,3 @@
import './gamepad.mjs';
export * from './gamepad.mjs';
+37
View File
@@ -0,0 +1,37 @@
{
"name": "@strudel/gamepad",
"version": "1.2.2",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Yuta Nakayama <nkymut@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
}
}
+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, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+2 -1
View File
@@ -1,6 +1,7 @@
{
"name": "hs2js",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"description": "Experimental Haskell in JavaScript interpreter",
"main": "src/index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
"version": "1.1.0",
"version": "1.2.2",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
+184
View File
@@ -7,3 +7,187 @@ This package adds midi functionality to strudel Patterns.
```sh
npm i @strudel/midi --save
```
## Available Controls
The following MIDI controls are available:
OUTPUT:
- `midi` - opens a midi output device.
- `note` - Sends MIDI note messages. Can accept note names (e.g. "c4") or MIDI note numbers (0-127)
- `midichan` - Sets the MIDI channel (1-16, defaults to 1)
- `velocity` - Sets note velocity (0-1, defaults to 0.9)
- `gain` - Modifies velocity by multiplying with it (0-1, defaults to 1)
- `control` - Sets MIDI control change messages
- `ccn` - Sets MIDI CC controller number (0-127)
- `ccv` - Sets MIDI CC value (0-1)
- `progNum` - Sends MIDI program change messages (0-127)
- `sysex` - Sends MIDI System Exclusive messages (id: number 0-127 or array of bytes 0-127, data: array of bytes 0-127)
- `sysexid` - Sets MIDI System Exclusive ID (number 0-127 or array of bytes 0-127)
- `sysexdata` - Sets MIDI System Exclusive data (array of bytes 0-127)
- `midibend` - Sets MIDI pitch bend (-1 - 1)
- `miditouch` - Sets MIDI key after touch (0-1)
- `midicmd` - Sends MIDI system real-time messages to control timing and transport on MIDI devices.
- `nrpnn` - Sets MIDI NRPN non-registered parameter number (array of bytes 0-127)
- `nrpv` - Sets MIDI NRPN non-registered parameter value (0-127)
INPUT:
- `midin` - Opens a MIDI input port to receive MIDI control change messages.
Additional controls can be mapped using the mapping object passed to `.midi()`:
## Examples
### midi(outputName?, options?)
Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages.
If no outputName is given, it uses the first midi output it finds.
```javascript
$: chord("<C^7 A7 Dm7 G7>").voicing().midi('IAC Driver')
```
In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".`
### Options
The `.midi()` function accepts an options object with the following properties:
```javascript
$: note("c a f e").midi('IAC Driver', { isController: true, midimap: 'default'})
```
<details>
<summary>Available Options</summary>
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| isController | boolean | false | When true, disables sending note messages. Useful for MIDI controllers |
| latencyMs | number | 34 | Latency in milliseconds to align MIDI with audio engine |
| noteOffsetMs | number | 10 | Offset in milliseconds for note-off messages to prevent glitching |
| midichannel | number | 1 | Default MIDI channel (1-16) |
| velocity | number | 0.9 | Default note velocity (0-1) |
| gain | number | 1 | Default gain multiplier for velocity (0-1) |
| midimap | string | 'default' | Name of MIDI mapping to use for control changes |
| midiport | string/number | - | MIDI device name or index |
</details>
### midiport(outputName)
Selects the MIDI output device to use, pattern can be used to switch between devices.
```javascript
$: midiport('IAC Driver')
$: note("c a f e").midiport("<0 1 2 3>").midi()
```
### midichan(number)
Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default.
### control, ccn && ccv
`control` sends MIDI control change messages to your MIDI device.
- `ccn` sets the cc number. Depends on your synths midi mapping
- `ccv` sets the cc value. normalized from 0 to 1.
```javascript
$: note("c a f e").control([74, sine.slow(4)]).midi()
$: note("c a f e").ccn(74).ccv(sine.slow(4)).midi()
```
In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern.
Having everything in one pattern, the `ccv` pattern will be aligned to the note pattern, because the structure comes from the left by default.
But you can also control cc messages separately like this:
```javascript
$: note("c a f e").midi()
$: ccv(sine.segment(16).slow(4)).ccn(74).midi()
```
### progNum (Program Change)
`progNum` control sends MIDI program change messages to switch between different presets/patches on your MIDI device.
Program change values should be numbers between 0 and 127.
```javascript
// Play notes while changing programs
note("c3 e3 g3").progNum("<0 1 2>").midi()
```
Program change messages are useful for switching between different instrument sounds or presets during a performance.
The exact sound that each program number maps to depends on your MIDI device's configuration.
## sysex, sysexid && sysexdata (System Exclusive Message)
`sysex`, `sysexid` and `sysexdata` control sends MIDI System Exclusive (SysEx) messages to your MIDI device.
sysEx messages are device-specific commands that allow deeper control over synthesizer parameters.
The value should be an array of numbers between 0-255 representing the SysEx data bytes.
```javascript
// Send a simple SysEx message
let id = 0x43; //Yamaha
//let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers
let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa"
$: note("c d e f e d c").sysex(id, data).midi();
$: note("c d e f e d c").sysexid(id).sysexdata(data).midi();
```
The exact format of SysEx messages depends on your MIDI device's specification.
Consult your device's MIDI implementation guide for details on supported SysEx messages.
### midibend && miditouch
`midibend` sets MIDI pitch bend (-1 - 1)
`miditouch` sets MIDI key after touch (0-1)
```javascript
$: note("c d e f e d c").midibend(sine.slow(4).range(-0.4,0.4)).midi();
$: note("c d e f e d c").miditouch(sine.slow(4).range(0,1)).midi();
```
### midicmd
`midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices.
It supports the following commands:
- `clock`/`midiClock` - Sends MIDI timing clock messages
- `start` - Sends MIDI start message
- `stop` - Sends MIDI stop message
- `continue` - Sends MIDI continue message
```javascript
// You can control the clock with a pattern and ensure it starts in sync when the repl begins.
// Note: It might act unexpectedly if MIDI isn't set up initially.
stack(
midicmd("clock*48,<start stop>/2").midi('IAC Driver')
)
```
`midicmd` also supports sending control change, program change and sysex messages.
- `cc` - sends MIDI control change messages.
- `progNum` - sends MIDI program change messages.
- `sysex` - sends MIDI system exclusive messages.
```javascript
stack(
// "cc:ccn:ccv"
midicmd("cc:74:1").midi('IAC Driver'),
// "progNum:progNum"
midicmd("progNum:1").midi('IAC Driver'),
// "sysex:[sysexid]:[sysexdata]"
midicmd("sysex:[0x43]:[0x79:0x09:0x11:0x0A:0x00:0x00]").midi('IAC Driver')
)
```
+209 -27
View File
@@ -8,6 +8,7 @@ import * as _WebMidi from 'webmidi';
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi, getControlName } from '@strudel/core';
import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -43,13 +44,16 @@ export function enableWebMidi(options = {}) {
resolve(WebMidi);
return;
}
WebMidi.enable((err) => {
if (err) {
reject(err);
}
onReady?.(WebMidi);
resolve(WebMidi);
});
WebMidi.enable(
(err) => {
if (err) {
reject(err);
}
onReady?.(WebMidi);
resolve(WebMidi);
},
{ sysex: true },
);
});
}
@@ -174,6 +178,7 @@ function normalize(value = 0, min = 0, max = 1, exp = 1) {
normalized = Math.min(1, Math.max(0, normalized));
return Math.pow(normalized, exp);
}
function mapCC(mapping, value) {
return Object.keys(value)
.filter((key) => !!mapping[getControlName(key)])
@@ -196,18 +201,127 @@ function sendCC(ccn, ccv, device, midichan, timeOffsetString) {
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
}
Pattern.prototype.midi = function (output) {
if (isPattern(output)) {
// sends a program change message to the given device on the given channel
function sendProgramChange(progNum, device, midichan, timeOffsetString) {
if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) {
throw new Error('expected progNum (program change) to be a number between 0 and 127');
}
device.sendProgramChange(progNum, midichan, { time: timeOffsetString });
}
// sends a sysex message to the given device on the given channel
function sendSysex(sysexid, sysexdata, device, timeOffsetString) {
if (Array.isArray(sysexid)) {
if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all sysexid bytes must be integers between 0 and 255');
}
} else if (!Number.isInteger(sysexid) || sysexid < 0 || sysexid > 255) {
throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers');
}
if (!Array.isArray(sysexdata)) {
throw new Error('expected sysex to be an array of numbers (0-255)');
}
if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all sysex bytes must be integers between 0 and 255');
}
device.sendSysex(sysexid, sysexdata, { time: timeOffsetString });
}
// sends a NRPN message to the given device on the given channel
function sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString) {
if (Array.isArray(nrpnn)) {
if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all nrpnn bytes must be integers between 0 and 255');
}
} else if (!Number.isInteger(nrpv) || nrpv < 0 || nrpv > 255) {
throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers');
}
device.sendNRPN(nrpnn, nrpv, midichan, { time: timeOffsetString });
}
// sends a pitch bend message to the given device on the given channel
function sendPitchBend(midibend, device, midichan, timeOffsetString) {
if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) {
throw new Error('expected midibend to be a number between -1 and 1');
}
device.sendPitchBend(midibend, midichan, { time: timeOffsetString });
}
// sends a channel aftertouch message to the given device on the given channel
function sendAftertouch(miditouch, device, midichan, timeOffsetString) {
if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) {
throw new Error('expected miditouch to be a number between 0 and 1');
}
device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString });
}
// sends a note message to the given device on the given channel
function sendNote(note, velocity, duration, device, midichan, timeOffsetString) {
if (note == null || note === '') {
throw new Error('note cannot be null or empty');
}
if (velocity != null && (typeof velocity !== 'number' || velocity < 0 || velocity > 1)) {
throw new Error('velocity must be a number between 0 and 1');
}
if (duration != null && (typeof duration !== 'number' || duration < 0)) {
throw new Error('duration must be a positive number');
}
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
const midiNote = new Note(midiNumber, { attack: velocity, duration });
device.playNote(midiNote, midichan, {
time: timeOffsetString,
});
}
/**
* MIDI output: Opens a MIDI output port.
* @param {string | number} midiport MIDI device name or index defaulting to 0
* @param {object} options Additional MIDI configuration options
* @example
* note("c4").midichan(1).midi('IAC Driver Bus 1')
* @example
* note("c4").midichan(1).midi('IAC Driver Bus 1', { controller: true, latency: 50 })
*/
Pattern.prototype.midi = function (midiport, options = {}) {
if (isPattern(midiport)) {
throw new Error(
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
`.midi does not accept Pattern input for midiport. Make sure to pass device name with single quotes. Example: .midi('${
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
}')`,
);
}
// For backward compatibility
if (typeof midiport === 'object') {
const { port, isController = false, ...configOptions } = midiport;
options = {
isController,
...configOptions,
...options, // Keep any options passed separately
};
midiport = port;
}
let midiConfig = {
// Default configuration values
isController: false, // Disable sending notes for midi controllers
latencyMs: 34, // Default latency to get audio engine to line up in ms
noteOffsetMs: 10, // Default note-off offset to prevent glitching in ms
midichannel: 1, // Default MIDI channel
velocity: 0.9, // Default velocity
gain: 1, // Default gain
midimap: 'default', // Default MIDI map
midiport: midiport, // Store the port in the config
...options, // Override defaults with provided options
};
enableWebMidi({
onEnabled: ({ outputs }) => {
const device = getDevice(output, outputs);
const device = getDevice(midiConfig.midiport, outputs);
const otherOutputs = outputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
@@ -221,26 +335,35 @@ Pattern.prototype.midi = function (output) {
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
if (!WebMidi.enabled) {
console.log('not enabled');
logger('Midi not enabled');
return;
}
hap.ensureObjectValue();
//magic number to get audio engine to line up, can probably be calculated somehow
const latencyMs = 34;
const latencyMs = midiConfig.latencyMs;
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
// destructure value
// midi event values from hap with configurable defaults
let {
note,
nrpnn,
nrpv,
ccn,
ccv,
midichan = 1,
midichan = midiConfig.midichannel,
midicmd,
gain = 1,
velocity = 0.9,
midimap = 'default',
midiport = output,
midibend,
miditouch,
polyTouch,
gain = midiConfig.gain,
velocity = midiConfig.velocity,
progNum,
sysexid,
sysexdata,
midimap = midiConfig.midimap,
midiport = midiConfig.midiport,
} = hap.value;
const device = getDevice(midiport, WebMidi.outputs);
@@ -252,24 +375,62 @@ Pattern.prototype.midi = function (output) {
}
velocity = gain * velocity;
// Handle midimap
// if midimap is set, send a cc messages from defined controls
if (midicontrolMap.has(midimap)) {
const ccs = mapCC(midicontrolMap.get(midimap), hap.value);
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
} else if (midimap !== 'default') {
// Add warning when a non-existent midimap is specified
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
}
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
const duration = (hap.duration.valueOf() / cps) * 1000 - 10;
if (note != null) {
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
const midiNote = new Note(midiNumber, { attack: velocity, duration });
device.playNote(midiNote, midichan, {
time: timeOffsetString,
});
// Handle note
if (note !== undefined && !midiConfig.isController) {
// note off messages will often a few ms arrive late,
// try to prevent glitching by subtracting noteOffsetMs from the duration length
const duration = (hap.duration.valueOf() / cps) * 1000 - midiConfig.noteOffsetMs;
sendNote(note, velocity, duration, device, midichan, timeOffsetString);
}
// Handle program change
if (progNum !== undefined) {
sendProgramChange(progNum, device, midichan, timeOffsetString);
}
// Handle sysex
// sysex data is consist of 2 arrays, first is sysexid, second is sysexdata
// sysexid is a manufacturer id it is either a number or an array of 3 numbers.
// list of manufacturer ids can be found here : https://midi.org/sysexidtable
// if sysexid is an array the first byte is 0x00
if (sysexid !== undefined && sysexdata !== undefined) {
sendSysex(sysexid, sysexdata, device, timeOffsetString);
}
// Handle control change
if (ccv !== undefined && ccn !== undefined) {
sendCC(ccn, ccv, device, midichan, timeOffsetString);
}
// Handle NRPN non-registered parameter number
if (nrpnn !== undefined && nrpv !== undefined) {
sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString);
}
// Handle midibend
if (midibend !== undefined) {
sendPitchBend(midibend, device, midichan, timeOffsetString);
}
// Handle miditouch
if (miditouch !== undefined) {
sendAftertouch(miditouch, device, midichan, timeOffsetString);
}
// Handle midicmd
if (hap.whole.begin + 0 === 0) {
// we need to start here because we have the timing info
device.sendStart({ time: timeOffsetString });
@@ -282,6 +443,19 @@ Pattern.prototype.midi = function (output) {
device.sendStop({ time: timeOffsetString });
} else if (['continue'].includes(midicmd)) {
device.sendContinue({ time: timeOffsetString });
} else if (Array.isArray(midicmd)) {
if (midicmd[0] === 'progNum') {
sendProgramChange(midicmd[1], device, midichan, timeOffsetString);
} else if (midicmd[0] === 'cc') {
if (midicmd.length === 2) {
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, timeOffsetString);
}
} else if (midicmd[0] === 'sysex') {
if (midicmd.length === 3) {
const [_, id, data] = midicmd;
sendSysex(id, data, device, timeOffsetString);
}
}
}
});
};
@@ -289,6 +463,14 @@ Pattern.prototype.midi = function (output) {
let listeners = {};
const refs = {};
/**
* MIDI input: Opens a MIDI input port to receive MIDI control change messages.
* @param {string | number} input MIDI device name or index defaulting to 0
* @returns {Function}
* @example
* let cc = await midin('IAC Driver Bus 1')
* note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth")
*/
export async function midin(input) {
if (isPattern(input)) {
throw new Error(
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "1.1.0",
"version": "1.2.3",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
"version": "1.1.0",
"version": "1.2.2",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
+34 -35
View File
@@ -1,38 +1,37 @@
{
"name": "@strudel/motion",
"version": "1.1.0",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Yuta Nakayama <nkymut@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},
"devDependencies": {
"name": "@strudel/motion",
"version": "1.2.2",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Yuta Nakayama <nkymut@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mqtt",
"version": "1.1.0",
"version": "1.2.2",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
+1 -1
View File
@@ -37,7 +37,7 @@ function connect() {
export function parseControlsFromHap(hap, cps) {
hap.ensureObjectValue();
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
const delta = hap.duration.valueOf() / cps;
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
// make sure n and note are numbers
controls.n && (controls.n = parseNumeral(controls.n));
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
"version": "1.1.0",
"version": "1.2.2",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"type": "module",
+1 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/reference",
"version": "1.1.0",
"version": "1.2.0",
"description": "Headless reference of all strudel functions",
"main": "index.mjs",
"type": "module",
@@ -31,8 +31,6 @@
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
},
"devDependencies": {
"vite": "^6.0.11"
}
-4
View File
@@ -1,5 +1,4 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
@@ -11,9 +10,6 @@ export default defineConfig({
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
"version": "1.1.0",
"version": "1.2.3",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
@@ -46,6 +46,7 @@
},
"devDependencies": {
"@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11"
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"
}
}
+2 -1
View File
@@ -1,11 +1,12 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
import replace from '@rollup/plugin-replace';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/
export default defineConfig({
base: './',
plugins: [],
plugins: [bundleAudioWorkletPlugin()],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/sampler",
"version": "0.1.0",
"version": "0.2.0",
"description": "",
"keywords": [
"tidalcycles",
+7 -10
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import cowsay from 'cowsay';
import { createReadStream } from 'fs';
import { createReadStream, existsSync } from 'fs';
import { readdir } from 'fs/promises';
import http from 'http';
import { join, sep } from 'path';
@@ -70,12 +70,15 @@ const server = http.createServer(async (req, res) => {
return res.end(JSON.stringify(banks));
}
let subpath = decodeURIComponent(req.url);
if (!files.includes(subpath)) {
const filePath = join(directory, subpath.split('/').join(sep));
//console.log('GET:', filePath);
const isFound = existsSync(filePath);
if (!isFound) {
res.statusCode = 404;
res.end('File not found');
return;
}
const filePath = join(directory, subpath.split('/').join(sep));
const readStream = createReadStream(filePath);
readStream.on('error', (err) => {
res.statusCode = 500;
@@ -99,12 +102,6 @@ Object.keys(networkInterfaces).forEach((key) => {
});
});
if (!IP) {
console.error("Unable to determine server's IP address.");
// eslint-disable-next-line
process.exit(1);
}
server.listen(PORT, IP_ADDRESS, () => {
console.log(`@strudel/sampler is now serving audio files from:
${directory}
@@ -113,6 +110,6 @@ To use them in the Strudel REPL, run:
samples('http://localhost:${PORT}')
Or on a machine in the same network:
samples('http://${IP}:${PORT}')
${IP ? `samples('http://${IP}:${PORT}')` : `Unable to determine server's IP address.`}
`);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
"version": "1.1.0",
"version": "1.2.2",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
+8 -1
View File
@@ -9,6 +9,13 @@ import {
} from '@strudel/webaudio';
import gm from './gm.mjs';
let defaultSoundfontUrl = 'https://felixroos.github.io/webaudiofontdata/sound';
let soundfontUrl = defaultSoundfontUrl;
export function setSoundfontUrl(value) {
soundfontUrl = value;
}
let loadCache = {};
async function loadFont(name) {
if (loadCache[name]) {
@@ -16,7 +23,7 @@ async function loadFont(name) {
}
const load = async () => {
// TODO: make soundfont source configurable
const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`;
const url = `${soundfontUrl}/${name}.js`;
const preset = await fetch(url).then((res) => res.text());
let [_, data] = preset.split('={');
return eval('{' + data);
+2 -2
View File
@@ -1,6 +1,6 @@
import { getFontBufferSource, registerSoundfonts } from './fontloader.mjs';
import { getFontBufferSource, registerSoundfonts, setSoundfontUrl } from './fontloader.mjs';
import * as soundfontList from './list.mjs';
import { startPresetNote } from 'sfumato';
import { loadSoundfont } from './sfumato.mjs';
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts };
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts, setSoundfontUrl };
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
"version": "1.1.0",
"version": "1.2.3",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
+1
View File
@@ -212,6 +212,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
constantNode.onended = () => {
onComplete();
};
return constantNode;
}
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "1.1.0",
"version": "1.2.3",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
@@ -32,7 +32,8 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"devDependencies": {
"vite": "^6.0.11"
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"dependencies": {
"nanostores": "^0.11.3"
+3 -1
View File
@@ -344,7 +344,9 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
};
let envEnd = holdEnd + release + 0.01;
bufferSource.stop(envEnd);
const stop = (endTime, playWholeBuffer) => {};
const stop = (endTime) => {
bufferSource.stop(endTime);
};
const handle = { node: out, bufferSource, stop };
// cut groups
+135 -40
View File
@@ -8,16 +8,41 @@ import './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { clamp, nanFallback, _mod } from './util.mjs';
import workletsUrl from './worklets.mjs?worker&url';
import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
import { map } from 'nanostores';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
export const DEFAULT_MAX_POLYPHONY = 128;
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
let maxPolyphony = DEFAULT_MAX_POLYPHONY;
export function setMaxPolyphony(polyphony) {
maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY;
}
let multiChannelOrbits = false;
export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
soundMap.setKey(key.toLowerCase(), { onTrigger, data });
key = key.toLowerCase().replace(/\s+/g, '_');
soundMap.setKey(key, { onTrigger, data });
}
let gainCurveFunc = (val) => val;
export function applyGainCurve(val) {
return gainCurveFunc(val);
}
export function setGainCurve(newGainCurveFunc) {
gainCurveFunc = newGainCurveFunc;
}
function aliasBankMap(aliasMap) {
@@ -85,6 +110,18 @@ export function getSound(s) {
return soundMap.get()[s.toLowerCase()];
}
export const getAudioDevices = async () => {
await navigator.mediaDevices.getUserMedia({ audio: true });
let mediaDevices = await navigator.mediaDevices.enumerateDevices();
mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default');
const devicesMap = new Map();
devicesMap.set(DEFAULT_AUDIO_DEVICE_NAME, '');
mediaDevices.forEach((device) => {
devicesMap.set(device.label, device.deviceId);
});
return devicesMap;
};
const defaultDefaultValues = {
s: 'triangle',
gain: 0.8,
@@ -100,6 +137,7 @@ const defaultDefaultValues = {
shapevol: 1,
distortvol: 1,
delay: 0,
byteBeatExpression: '0',
delayfeedback: 0.5,
delaytime: 0.25,
orbit: 1,
@@ -155,18 +193,47 @@ export function getAudioContextCurrentTime() {
let workletsLoading;
function loadWorklets() {
if (!workletsLoading) {
workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl);
const audioCtx = getAudioContext();
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
}
return workletsLoading;
}
// this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) {
const { disableWorklets = false } = options;
const {
disableWorklets = false,
maxPolyphony,
audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME,
multiChannelOrbits = false,
} = options;
setMaxPolyphony(maxPolyphony);
setMultiChannelOrbits(multiChannelOrbits);
if (typeof window === 'undefined') {
return;
}
await getAudioContext().resume();
const audioCtx = getAudioContext();
if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) {
try {
const devices = await getAudioDevices();
const id = devices.get(audioDeviceName);
const isValidID = (id ?? '').length > 0;
if (audioCtx.sinkId !== id && isValidID) {
await audioCtx.setSinkId(id);
}
logger(
`[superdough] Audio Device set to ${audioDeviceName}, it might take a few seconds before audio plays on all output channels`,
);
} catch {
logger('[superdough] failed to set audio interface', 'warning');
}
}
await audioCtx.resume();
if (disableWorklets) {
logger('[superdough]: AudioWorklets disabled with disableWorklets');
return;
@@ -224,7 +291,7 @@ export const connectToDestination = (input, channels = [0, 1]) => {
});
stereoMix.connect(splitter);
channels.forEach((ch, i) => {
splitter.connect(channelMerger, i % stereoMix.channelCount, clamp(ch, 0, ctx.destination.channelCount - 1));
splitter.connect(channelMerger, i % stereoMix.channelCount, ch % ctx.destination.channelCount);
});
};
@@ -237,7 +304,7 @@ export const panic = () => {
channelMerger == null;
};
function getDelay(orbit, delaytime, delayfeedback, t) {
function getDelay(orbit, delaytime, delayfeedback, t, channels) {
if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
@@ -246,7 +313,7 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
connectToDestination(dly, [0, 1]);
connectToDestination(dly, channels);
delays[orbit] = dly;
}
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
@@ -254,16 +321,9 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
return delays[orbit];
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
//gain
const ac = getAudioContext();
const lfoGain = ac.createGain();
lfoGain.gain.value = sweep * 2;
// centerFrequency = centerFrequency * 2;
// sweep = sweep * 1.5;
const lfo = getWorklet(ac, 'lfo-processor', {
frequency,
export function getLfo(audioContext, time, end, properties = {}) {
return getWorklet(audioContext, 'lfo-processor', {
frequency: 1,
depth: 1,
skew: 0,
phaseoffset: 0,
@@ -271,8 +331,13 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000
end,
shape: 1,
dcoffset: -0.5,
...properties,
});
lfo.connect(lfoGain);
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
const ac = getAudioContext();
const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
//filters
const numStages = 2; //num of filters in series
@@ -303,12 +368,12 @@ function getFilterType(ftype) {
let reverbs = {};
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir) {
function getReverb(orbit, duration, fade, lp, dim, ir, channels) {
// If no reverb has been created for a given orbit, create one
if (!reverbs[orbit]) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
connectToDestination(reverb, [0, 1]);
connectToDestination(reverb, channels);
reverbs[orbit] = reverb;
}
if (
@@ -374,7 +439,14 @@ export function resetGlobalEffects() {
analysersData = {};
}
export const superdough = async (value, t, hapDuration) => {
let activeSoundSources = new Map();
//music programs/audio gear usually increments inputs/outputs from 1, we need to subtract 1 from the input because the webaudio API channels start at 0
function mapChannelNumbers(channels) {
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
}
export const superdough = async (value, t, hapDuration, cps) => {
const ac = getAudioContext();
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
let { stretch } = value;
@@ -435,7 +507,7 @@ export const superdough = async (value, t, hapDuration) => {
bpsustain,
bprelease,
bandq = getDefaultValue('bandq'),
channels = getDefaultValue('channels'),
//phaser
phaserrate: phaser,
phaserdepth = getDefaultValue('phaserdepth'),
@@ -471,16 +543,34 @@ export const superdough = async (value, t, hapDuration) => {
compressorRelease,
} = value;
gain = nanFallback(gain, 1);
//music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
const orbitChannels = mapChannelNumbers(
multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'),
);
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
gain = applyGainCurve(nanFallback(gain, 1));
postgain = applyGainCurve(postgain);
shapevol = applyGainCurve(shapevol);
distortvol = applyGainCurve(distortvol);
delay = applyGainCurve(delay);
velocity = applyGainCurve(velocity);
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
const onended = () => {
toDisconnect.forEach((n) => n?.disconnect());
};
const chainID = Math.round(Math.random() * 1000000);
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) {
const ch = activeSoundSources.entries().next();
const source = ch.value[1];
const chainID = ch.value[0];
const endTime = t + 0.25;
source?.node?.gain?.linearRampToValueAtTime(0, endTime);
source?.stop?.(endTime);
activeSoundSources.delete(chainID);
}
let audioNodes = [];
if (bank && s) {
s = `${bank}_${s}`;
value.s = s;
@@ -489,13 +579,18 @@ export const superdough = async (value, t, hapDuration) => {
// get source AudioNode
let sourceNode;
if (source) {
sourceNode = source(t, value, hapDuration);
sourceNode = source(t, value, hapDuration, cps);
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
const soundHandle = await onTrigger(t, value, onended);
const onEnded = () => {
audioNodes.forEach((n) => n?.disconnect());
activeSoundSources.delete(chainID);
};
const soundHandle = await onTrigger(t, value, onEnded);
if (soundHandle) {
sourceNode = soundHandle.node;
soundHandle.stop(t + hapDuration);
activeSoundSources.set(chainID, soundHandle);
}
} else {
throw new Error(`sound ${s} not found! Is it loaded?`);
@@ -623,8 +718,9 @@ export const superdough = async (value, t, hapDuration) => {
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
const delyNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
delaySend = effectSend(post, delyNode, delay);
audioNodes.push(delaySend);
}
// reverb
let reverbSend;
@@ -640,8 +736,9 @@ export const superdough = async (value, t, hapDuration) => {
}
roomIR = await loadBuffer(url, ac, ir, 0);
}
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR);
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels);
reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend);
}
// analyser
@@ -649,14 +746,12 @@ export const superdough = async (value, t, hapDuration) => {
if (analyze) {
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
analyserSend = effectSend(post, analyserNode, 1);
audioNodes.push(analyserSend);
}
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
// toDisconnect = all the node that should be disconnected in onended callback
// this is crucial for performance
toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]);
audioNodes = audioNodes.concat(chain);
};
export const superdoughTrigger = (t, hap, ct, cps) => {
+122 -15
View File
@@ -1,5 +1,5 @@
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { registerSound, getAudioContext, getLfo } from './superdough.mjs';
import {
applyFM,
gainNode,
@@ -25,6 +25,13 @@ const getFrequencyFromValue = (value) => {
return Number(freq);
};
function destroyAudioWorkletNode(node) {
if (node == null) {
return;
}
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
}
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const noises = ['pink', 'white', 'brown', 'crackle'];
@@ -63,7 +70,9 @@ export function registerSynthSounds() {
stop(envEnd);
return {
node,
stop: (releaseTime) => {},
stop: (endTime) => {
stop(endTime);
},
};
},
{ type: 'synth', prebake: true },
@@ -110,10 +119,12 @@ export function registerSynthSounds() {
let envGain = gainNode(1);
envGain = o.connect(envGain);
webAudioTimeout(
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
let timeoutNode = webAudioTimeout(
ac,
() => {
o.disconnect();
destroyAudioWorkletNode(o);
envGain.disconnect();
onended();
fm?.stop();
@@ -123,11 +134,87 @@ export function registerSynthSounds() {
end,
);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
return {
node: envGain,
stop: (time) => {
timeoutNode.stop(time);
},
};
},
{ prebake: true, type: 'synth' },
);
registerSound(
'bytebeat',
(begin, value, onended) => {
const defaultBeats = [
'(t%255 >= t/255%255)*255',
'(t*(t*8%60 <= 300)|(-t)*(t*4%512 < 256))+t/400',
't',
't*(t >> 10^t)',
't&128',
't&t>>8',
'((t%255+t%128+t%64+t%32+t%16+t%127.8+t%64.8+t%32.8+t%16.8)/3)',
'((t%64+t%63.8+t%64.15+t%64.35+t%63.5)/1.25)',
'(t&(t>>7)-t)',
'(sin(t*PI/128)*127+127)',
'((t^t/2+t+64*(sin((t*PI/64)+(t*PI/32768))+64))%128*2)',
'((t^t/2+t+64*(cos >> 0))%127.85*2)',
'((t^t/2+t+64)%128*2)',
'(((t * .25)^(t * .25)/100+(t * .25))%128)*2',
'((t^t/2+t+64)%7 * 24)',
];
const { n = 0 } = value;
const frequency = getFrequencyFromValue(value);
const { byteBeatExpression = defaultBeats[n % defaultBeats.length], byteBeatStartTime } = value;
const ac = getAudioContext();
let { duration } = value;
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
[0.001, 0.05, 0.6, 0.01],
);
const holdend = begin + duration;
const end = holdend + release + 0.01;
let o = getWorklet(
ac,
'byte-beat-processor',
{
frequency,
begin,
end,
},
{
outputChannelCount: [2],
},
);
o.port.postMessage({ codeText: byteBeatExpression, byteBeatStartTime, frequency });
let envGain = gainNode(1);
envGain = o.connect(envGain);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
let timeoutNode = webAudioTimeout(
ac,
() => {
destroyAudioWorkletNode(o);
envGain.disconnect();
onended();
},
begin,
end,
);
return {
node: envGain,
stop: (time) => {},
stop: (time) => {
timeoutNode.stop(time);
},
};
},
{ prebake: true, type: 'synth' },
@@ -137,7 +224,20 @@ export function registerSynthSounds() {
'pulse',
(begin, value, onended) => {
const ac = getAudioContext();
let { duration, n: pulsewidth = 0.5 } = value;
let { pwrate, pwsweep } = value;
if (pwsweep == null) {
if (pwrate != null) {
pwsweep = 0.3;
} else {
pwsweep = 0;
}
}
if (pwrate == null && pwsweep != null) {
pwrate = 1;
}
let { duration, pw: pulsewidth = 0.5 } = value;
const frequency = getFrequencyFromValue(value);
const [attack, decay, sustain, release] = getADSRValues(
@@ -145,10 +245,8 @@ export function registerSynthSounds() {
'linear',
[0.001, 0.05, 0.6, 0.01],
);
const holdend = begin + duration;
const end = holdend + release + 0.01;
let o = getWorklet(
ac,
'pulse-oscillator',
@@ -169,10 +267,17 @@ export function registerSynthSounds() {
let envGain = gainNode(1);
envGain = o.connect(envGain);
webAudioTimeout(
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
let lfo;
if (pwsweep != 0) {
lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep });
lfo.connect(o.parameters.get('pulsewidth'));
}
let timeoutNode = webAudioTimeout(
ac,
() => {
o.disconnect();
destroyAudioWorkletNode(o);
destroyAudioWorkletNode(lfo);
envGain.disconnect();
onended();
fm?.stop();
@@ -182,11 +287,11 @@ export function registerSynthSounds() {
end,
);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
return {
node: envGain,
stop: (time) => {},
stop: (time) => {
timeoutNode.stop(time);
},
};
},
{ prebake: true, type: 'synth' },
@@ -229,7 +334,9 @@ export function registerSynthSounds() {
stop(envEnd);
return {
node,
stop: (releaseTime) => {},
stop: (endTime) => {
stop(endTime);
},
};
},
{ type: 'synth', prebake: true },
+2 -1
View File
@@ -1,10 +1,11 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
plugins: [bundleAudioWorkletPlugin()],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
+138 -1
View File
@@ -295,7 +295,7 @@ class LadderProcessor extends AudioWorkletProcessor {
cutoff = (cutoff * 2 * _PI) / sampleRate;
cutoff = cutoff > 1 ? 1 : cutoff;
const k = Math.min(8, resonance * 0.4);
const k = Math.min(8, resonance * 0.13);
// drive makeup * resonance volume loss makeup
let makeupgain = (1 / drive) * Math.min(1.75, 1 + k);
@@ -710,6 +710,9 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
}
process(inputs, outputs, params) {
if (this.disconnected) {
return false;
}
if (currentTime <= params.begin[0]) {
return true;
}
@@ -758,3 +761,137 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
}
registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
/** BYTE BEATS */
const chyx = {
/*bit*/ bitC: function (x, y, z) {
return x & y ? z : 0;
},
/*bit reverse*/ br: function (x, size = 8) {
if (size > 32) {
throw new Error('br() Size cannot be greater than 32');
} else {
let result = 0;
for (let idx = 0; idx < size - 0; idx++) {
result += chyx.bitC(x, 2 ** idx, 2 ** (size - (idx + 1)));
}
return result;
}
},
/*sin that loops every 128 "steps", instead of every pi steps*/ sinf: function (x) {
return Math.sin(x / (128 / Math.PI));
},
/*cos that loops every 128 "steps", instead of every pi steps*/ cosf: function (x) {
return Math.cos(x / (128 / Math.PI));
},
/*tan that loops every 128 "steps", instead of every pi steps*/ tanf: function (x) {
return Math.tan(x / (128 / Math.PI));
},
/*converts t into a string composed of it's bits, regex's that*/ regG: function (t, X) {
return X.test(t.toString(2));
},
};
// Create shortened Math functions
let mathParams, byteBeatHelperFuncs;
function getByteBeatFunc(codetext) {
if ((mathParams || byteBeatHelperFuncs) == null) {
mathParams = Object.getOwnPropertyNames(Math);
byteBeatHelperFuncs = mathParams.map((k) => Math[k]);
const chyxNames = Object.getOwnPropertyNames(chyx);
const chyxFuncs = chyxNames.map((k) => chyx[k]);
mathParams.push('int', 'window', ...chyxNames);
byteBeatHelperFuncs.push(Math.floor, globalThis, ...chyxFuncs);
}
return new Function(...mathParams, 't', `return 0,\n${codetext || 0};`).bind(globalThis, ...byteBeatHelperFuncs);
}
class ByteBeatProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.port.onmessage = (event) => {
let { codeText } = event.data;
const { byteBeatStartTime } = event.data;
if (byteBeatStartTime != null) {
this.t = 0;
this.initialOffset = Math.floor(byteBeatStartTime);
}
//Optimization pulled from dollchan.net: https://github.com/Chasyxx/EnBeat_NEW, it seemed important
//Optimize code like eval(unescape(escape`XXXX`.replace(/u(..)/g,"$1%")))
codeText = codeText
.trim()
.replace(
/^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/,
(match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%')),
);
this.func = getByteBeatFunc(codeText);
};
this.initialOffset = null;
this.t = null;
this.func = null;
}
static get parameterDescriptors() {
return [
{
name: 'begin',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
{
name: 'frequency',
defaultValue: 440,
min: Number.EPSILON,
},
{
name: 'detune',
defaultValue: 0,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
},
{
name: 'end',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
];
}
process(inputs, outputs, params) {
if (this.disconnected) {
return false;
}
if (currentTime <= params.begin[0]) {
return true;
}
if (currentTime >= params.end[0]) {
return false;
}
if (this.t == null) {
this.t = params.begin[0] * sampleRate;
}
const output = outputs[0];
for (let i = 0; i < output[0].length; i++) {
const detune = getParamValue(i, params.detune);
const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100);
let local_t = (this.t / (sampleRate / 256)) * freq + this.initialOffset;
const funcValue = this.func(local_t);
let signal = (funcValue & 255) / 127.5 - 1;
const out = signal * 0.2;
for (let c = 0; c < output.length; c++) {
//prevent speaker blowout via clipping if threshold exceeds
output[c][i] = clamp(out, -0.4, 0.4);
}
this.t = this.t + 1;
}
return true; // keep the audio processing going
}
}
registerProcessor('byte-beat-processor', ByteBeatProcessor);
+2 -1
View File
@@ -1,6 +1,7 @@
{
"name": "@strudel/tidal",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"description": "Experimental Tidal Code interpreter",
"module": "tidal.mjs",
"repository": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
"version": "1.1.0",
"version": "1.2.2",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/transpiler",
"version": "1.1.0",
"version": "1.2.2",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"type": "module",
@@ -0,0 +1,16 @@
{
"name": "vite-plugin-bundle-audioworklet",
"main": "./vite-plugin-bundle-audioworklet.js",
"version": "0.1.1",
"description": "",
"keywords": [
"vite",
"audioworklet"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "MIT",
"type": "module",
"devDependencies": {
"vite": "^6.0.11"
}
}
@@ -0,0 +1,48 @@
import { createLogger, build } from 'vite';
const end = '?audioworklet';
function bundleAudioWorkletPlugin() /* : PluginOption */ {
let viteConfig /* : UserConfig */;
return {
name: 'vite-plugin-bundle-audioworklet',
/* apply: 'build', */
enforce: 'post',
config(config) {
viteConfig = config;
},
async transform(_code, id) {
if (!id.endsWith(end)) {
return;
}
const entry = id.replace(end, '');
const quietLogger = createLogger();
quietLogger.info = () => undefined;
const output = await build({
configFile: false,
clearScreen: false,
customLogger: quietLogger,
build: {
lib: {
entry,
name: '_',
formats: ['iife'],
},
write: false,
},
});
if (!(output instanceof Array)) {
throw new Error('Expected output to be Array');
}
const iife = output[0].output[0].code;
const encoded = Buffer.from(iife, 'utf8').toString('base64');
return `export default "data:text/javascript;base64,${encoded}";`;
},
};
}
export default bundleAudioWorkletPlugin;
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
"version": "1.1.0",
"version": "1.2.3",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs",
"publishConfig": {
@@ -41,6 +41,7 @@
},
"devDependencies": {
"@rollup/plugin-replace": "^6.0.2",
"vite": "^6.0.11"
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"
}
}
+2 -1
View File
@@ -2,11 +2,12 @@ import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
import replace from '@rollup/plugin-replace';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/
export default defineConfig({
base: './',
plugins: [],
plugins: [bundleAudioWorkletPlugin()],
build: {
lib: {
entry: resolve(__dirname, 'web.mjs'),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
"version": "1.1.0",
"version": "1.2.3",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/xen",
"version": "1.1.0",
"version": "1.2.2",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"type": "module",
+36 -3
View File
@@ -41,7 +41,7 @@ importers:
version: 2.2.7
'@vitest/coverage-v8':
specifier: 3.0.4
version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))
version: 3.0.4(vitest@3.0.4)
'@vitest/ui':
specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4)
@@ -75,6 +75,9 @@ importers:
prettier:
specifier: ^3.4.2
version: 3.4.2
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:packages/vite-plugin-bundle-audioworklet
vitest:
specifier: ^3.0.4
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
@@ -270,6 +273,16 @@ importers:
packages/embed: {}
packages/gamepad:
dependencies:
'@strudel/core':
specifier: workspace:*
version: link:../core
devDependencies:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
packages/hs2js:
dependencies:
web-tree-sitter:
@@ -418,6 +431,9 @@ importers:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
packages/sampler:
dependencies:
@@ -466,6 +482,9 @@ importers:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
packages/tidal:
dependencies:
@@ -530,6 +549,12 @@ importers:
specifier: ^3.0.4
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
packages/vite-plugin-bundle-audioworklet:
devDependencies:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
packages/web:
dependencies:
'@strudel/core':
@@ -554,6 +579,9 @@ importers:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
packages/webaudio:
dependencies:
@@ -643,6 +671,9 @@ importers:
'@strudel/draw':
specifier: workspace:*
version: link:../packages/draw
'@strudel/gamepad':
specifier: workspace:*
version: link:../packages/gamepad
'@strudel/hydra':
specifier: workspace:*
version: link:../packages/hydra
@@ -767,6 +798,9 @@ importers:
sharp:
specifier: ^0.33.5
version: 0.33.5
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../packages/vite-plugin-bundle-audioworklet
workbox-window:
specifier: ^7.3.0
version: 7.3.0
@@ -7548,7 +7582,6 @@ packages:
workbox-google-analytics@7.0.0:
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
workbox-navigation-preload@7.0.0:
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
@@ -10235,7 +10268,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
'@vitest/coverage-v8@3.0.4(vitest@3.0.4)':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
+764 -32
View File
@@ -933,6 +933,39 @@ exports[`runs examples > example "as" example index 0 1`] = `
]
`;
exports[`runs examples > example "as" example index 1 1`] = `
[
"[ 0/1 → 1/4 | begin:0 s:sax_vib clip:1 ]",
"[ 1/4 → 3/8 | begin:0.25 s:sax_vib clip:1 ]",
"[ 3/8 → 1/2 | begin:0 s:sax_vib clip:1 ]",
"[ 1/2 → 5/8 | begin:0.5 s:sax_vib clip:1 ]",
"[ 5/8 → 3/4 | begin:0.3 s:sax_vib clip:1 ]",
"[ 3/4 → 7/8 | begin:0.5 s:sax_vib clip:1 ]",
"[ (7/8 → 1/1) ⇝ 9/8 | begin:0 s:sax_vib clip:1 ]",
"[ 7/8 ⇜ (1/1 → 9/8) | begin:0 s:sax_vib clip:1 ]",
"[ 9/8 → 5/4 | begin:0.25 s:sax_vib clip:1 ]",
"[ 5/4 → 11/8 | begin:0 s:sax_vib clip:1 ]",
"[ 11/8 → 3/2 | begin:0.5 s:sax_vib clip:1 ]",
"[ 3/2 → 13/8 | begin:0.3 s:sax_vib clip:1 ]",
"[ 13/8 → 7/4 | begin:0.5 s:sax_vib clip:1 ]",
"[ 7/4 → 2/1 | begin:0 s:sax_vib clip:1 ]",
"[ 2/1 → 17/8 | begin:0.25 s:sax_vib clip:1 ]",
"[ 17/8 → 9/4 | begin:0 s:sax_vib clip:1 ]",
"[ 9/4 → 19/8 | begin:0.5 s:sax_vib clip:1 ]",
"[ 19/8 → 5/2 | begin:0.3 s:sax_vib clip:1 ]",
"[ 5/2 → 21/8 | begin:0.5 s:sax_vib clip:1 ]",
"[ 21/8 → 23/8 | begin:0 s:sax_vib clip:1 ]",
"[ 23/8 → 3/1 | begin:0.25 s:sax_vib clip:1 ]",
"[ 3/1 → 25/8 | begin:0 s:sax_vib clip:1 ]",
"[ 25/8 → 13/4 | begin:0.5 s:sax_vib clip:1 ]",
"[ 13/4 → 27/8 | begin:0.3 s:sax_vib clip:1 ]",
"[ 27/8 → 7/2 | begin:0.5 s:sax_vib clip:1 ]",
"[ 7/2 → 15/4 | begin:0 s:sax_vib clip:1 ]",
"[ 15/4 → 31/8 | begin:0.25 s:sax_vib clip:1 ]",
"[ 31/8 → 4/1 | begin:0 s:sax_vib clip:1 ]",
]
`;
exports[`runs examples > example "attack" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c3 attack:0 ]",
@@ -1483,6 +1516,52 @@ exports[`runs examples > example "brandBy" example index 0 1`] = `
]
`;
exports[`runs examples > example "byteBeatExpression" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]",
"[ 1/1 → 2/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]",
"[ 2/1 → 3/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]",
"[ 3/1 → 4/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]",
]
`;
exports[`runs examples > example "byteBeatStartTime" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 1/8 → 1/4 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 1/4 → 3/8 | note:60 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 3/8 → 1/2 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 1/2 → 5/8 | note:55 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 5/8 → 3/4 | note:53 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 3/4 → 7/8 | note:51 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 7/8 → 1/1 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 1/1 → 9/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 9/8 → 5/4 | note:60 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 5/4 → 11/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 11/8 → 3/2 | note:55 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 3/2 → 13/8 | note:53 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 13/8 → 7/4 | note:51 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 7/4 → 15/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 15/8 → 2/1 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 2/1 → 17/8 | note:60 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 17/8 → 9/4 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 9/4 → 19/8 | note:55 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 19/8 → 5/2 | note:53 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 5/2 → 21/8 | note:51 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 21/8 → 11/4 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 11/4 → 23/8 | note:48 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 23/8 → 3/1 | note:60 s:bytebeat n:5 byteBeatStartTime:30000 ]",
"[ 3/1 → 25/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 25/8 → 13/4 | note:55 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 13/4 → 27/8 | note:53 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 27/8 → 7/2 | note:51 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 7/2 → 29/8 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 29/8 → 15/4 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 15/4 → 31/8 | note:60 s:bytebeat n:5 byteBeatStartTime:10000 ]",
"[ 31/8 → 4/1 | note:48 s:bytebeat n:5 byteBeatStartTime:10000 ]",
]
`;
exports[`runs examples > example "cat" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:e5 ]",
@@ -2289,6 +2368,39 @@ exports[`runs examples > example "degradeBy" example index 1 1`] = `
]
`;
exports[`runs examples > example "degradeBy" example index 2 1`] = `
[
"[ 1/8 → 3/16 | s:bd ]",
"[ 1/4 → 5/16 | s:bd ]",
"[ 5/16 → 3/8 | s:bd ]",
"[ 1/2 → 9/16 | s:bd ]",
"[ 9/16 → 5/8 | s:bd ]",
"[ 11/16 → 3/4 | s:bd ]",
"[ 15/16 → 1/1 | s:bd ]",
"[ 9/8 → 19/16 | s:bd ]",
"[ 5/4 → 21/16 | s:bd ]",
"[ 21/16 → 11/8 | s:bd ]",
"[ 3/2 → 25/16 | s:bd ]",
"[ 25/16 → 13/8 | s:bd ]",
"[ 27/16 → 7/4 | s:bd ]",
"[ 31/16 → 2/1 | s:bd ]",
"[ 17/8 → 35/16 | s:bd ]",
"[ 9/4 → 37/16 | s:bd ]",
"[ 37/16 → 19/8 | s:bd ]",
"[ 5/2 → 41/16 | s:bd ]",
"[ 41/16 → 21/8 | s:bd ]",
"[ 43/16 → 11/4 | s:bd ]",
"[ 47/16 → 3/1 | s:bd ]",
"[ 25/8 → 51/16 | s:bd ]",
"[ 13/4 → 53/16 | s:bd ]",
"[ 53/16 → 27/8 | s:bd ]",
"[ 7/2 → 57/16 | s:bd ]",
"[ 57/16 → 29/8 | s:bd ]",
"[ 59/16 → 15/4 | s:bd ]",
"[ 63/16 → 4/1 | s:bd ]",
]
`;
exports[`runs examples > example "delay" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd delay:0 ]",
@@ -2337,6 +2449,27 @@ exports[`runs examples > example "delaytime" example index 0 1`] = `
]
`;
exports[`runs examples > example "density" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
"[ 1/4 → 1/2 | s:crackle density:0.01 ]",
"[ 1/2 → 3/4 | s:crackle density:0.01 ]",
"[ 3/4 → 1/1 | s:crackle density:0.01 ]",
"[ 1/1 → 5/4 | s:crackle density:0.01 ]",
"[ 5/4 → 3/2 | s:crackle density:0.01 ]",
"[ 3/2 → 7/4 | s:crackle density:0.01 ]",
"[ 7/4 → 2/1 | s:crackle density:0.01 ]",
"[ 2/1 → 9/4 | s:crackle density:0.01 ]",
"[ 9/4 → 5/2 | s:crackle density:0.01 ]",
"[ 5/2 → 11/4 | s:crackle density:0.01 ]",
"[ 11/4 → 3/1 | s:crackle density:0.01 ]",
"[ 3/1 → 13/4 | s:crackle density:0.01 ]",
"[ 13/4 → 7/2 | s:crackle density:0.01 ]",
"[ 7/2 → 15/4 | s:crackle density:0.01 ]",
"[ 15/4 → 4/1 | s:crackle density:0.01 ]",
]
`;
exports[`runs examples > example "detune" example index 0 1`] = `
[
"[ 0/1 → 1/12 | note:d s:supersaw detune:0.1 ]",
@@ -5206,6 +5339,292 @@ exports[`runs examples > example "mask" example index 0 1`] = `
]
`;
exports[`runs examples > example "midi" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 midichan:1 ]",
"[ 1/1 → 2/1 | note:c4 midichan:1 ]",
"[ 2/1 → 3/1 | note:c4 midichan:1 ]",
"[ 3/1 → 4/1 | note:c4 midichan:1 ]",
]
`;
exports[`runs examples > example "midi" example index 1 1`] = `
[
"[ 0/1 → 1/1 | note:c4 midichan:1 ]",
"[ 1/1 → 2/1 | note:c4 midichan:1 ]",
"[ 2/1 → 3/1 | note:c4 midichan:1 ]",
"[ 3/1 → 4/1 | note:c4 midichan:1 ]",
]
`;
exports[`runs examples > example "midibend" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 midibend:0 ]",
"[ 1/1 → 2/1 | note:c4 midibend:0.4 ]",
"[ 2/1 → 3/1 | note:c4 midibend:1.1102230246251565e-16 ]",
"[ 3/1 → 4/1 | note:c4 midibend:-0.4 ]",
]
`;
exports[`runs examples > example "midichan" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 midichan:1 ]",
"[ 1/1 → 2/1 | note:c4 midichan:1 ]",
"[ 2/1 → 3/1 | note:c4 midichan:1 ]",
"[ 3/1 → 4/1 | note:c4 midichan:1 ]",
]
`;
exports[`runs examples > example "midicmd" example index 0 1`] = `
[
"[ 0/1 → 1/48 | midicmd:clock ]",
"[ 0/1 → 2/1 | midicmd:start ]",
"[ 1/48 → 1/24 | midicmd:clock ]",
"[ 1/24 → 1/16 | midicmd:clock ]",
"[ 1/16 → 1/12 | midicmd:clock ]",
"[ 1/12 → 5/48 | midicmd:clock ]",
"[ 5/48 → 1/8 | midicmd:clock ]",
"[ 1/8 → 7/48 | midicmd:clock ]",
"[ 7/48 → 1/6 | midicmd:clock ]",
"[ 1/6 → 3/16 | midicmd:clock ]",
"[ 3/16 → 5/24 | midicmd:clock ]",
"[ 5/24 → 11/48 | midicmd:clock ]",
"[ 11/48 → 1/4 | midicmd:clock ]",
"[ 1/4 → 13/48 | midicmd:clock ]",
"[ 13/48 → 7/24 | midicmd:clock ]",
"[ 7/24 → 5/16 | midicmd:clock ]",
"[ 5/16 → 1/3 | midicmd:clock ]",
"[ 1/3 → 17/48 | midicmd:clock ]",
"[ 17/48 → 3/8 | midicmd:clock ]",
"[ 3/8 → 19/48 | midicmd:clock ]",
"[ 19/48 → 5/12 | midicmd:clock ]",
"[ 5/12 → 7/16 | midicmd:clock ]",
"[ 7/16 → 11/24 | midicmd:clock ]",
"[ 11/24 → 23/48 | midicmd:clock ]",
"[ 23/48 → 1/2 | midicmd:clock ]",
"[ 1/2 → 25/48 | midicmd:clock ]",
"[ 25/48 → 13/24 | midicmd:clock ]",
"[ 13/24 → 9/16 | midicmd:clock ]",
"[ 9/16 → 7/12 | midicmd:clock ]",
"[ 7/12 → 29/48 | midicmd:clock ]",
"[ 29/48 → 5/8 | midicmd:clock ]",
"[ 5/8 → 31/48 | midicmd:clock ]",
"[ 31/48 → 2/3 | midicmd:clock ]",
"[ 2/3 → 11/16 | midicmd:clock ]",
"[ 11/16 → 17/24 | midicmd:clock ]",
"[ 17/24 → 35/48 | midicmd:clock ]",
"[ 35/48 → 3/4 | midicmd:clock ]",
"[ 3/4 → 37/48 | midicmd:clock ]",
"[ 37/48 → 19/24 | midicmd:clock ]",
"[ 19/24 → 13/16 | midicmd:clock ]",
"[ 13/16 → 5/6 | midicmd:clock ]",
"[ 5/6 → 41/48 | midicmd:clock ]",
"[ 41/48 → 7/8 | midicmd:clock ]",
"[ 7/8 → 43/48 | midicmd:clock ]",
"[ 43/48 → 11/12 | midicmd:clock ]",
"[ 11/12 → 15/16 | midicmd:clock ]",
"[ 15/16 → 23/24 | midicmd:clock ]",
"[ 23/24 → 47/48 | midicmd:clock ]",
"[ 47/48 → 1/1 | midicmd:clock ]",
"[ 1/1 → 49/48 | midicmd:clock ]",
"[ 49/48 → 25/24 | midicmd:clock ]",
"[ 25/24 → 17/16 | midicmd:clock ]",
"[ 17/16 → 13/12 | midicmd:clock ]",
"[ 13/12 → 53/48 | midicmd:clock ]",
"[ 53/48 → 9/8 | midicmd:clock ]",
"[ 9/8 → 55/48 | midicmd:clock ]",
"[ 55/48 → 7/6 | midicmd:clock ]",
"[ 7/6 → 19/16 | midicmd:clock ]",
"[ 19/16 → 29/24 | midicmd:clock ]",
"[ 29/24 → 59/48 | midicmd:clock ]",
"[ 59/48 → 5/4 | midicmd:clock ]",
"[ 5/4 → 61/48 | midicmd:clock ]",
"[ 61/48 → 31/24 | midicmd:clock ]",
"[ 31/24 → 21/16 | midicmd:clock ]",
"[ 21/16 → 4/3 | midicmd:clock ]",
"[ 4/3 → 65/48 | midicmd:clock ]",
"[ 65/48 → 11/8 | midicmd:clock ]",
"[ 11/8 → 67/48 | midicmd:clock ]",
"[ 67/48 → 17/12 | midicmd:clock ]",
"[ 17/12 → 23/16 | midicmd:clock ]",
"[ 23/16 → 35/24 | midicmd:clock ]",
"[ 35/24 → 71/48 | midicmd:clock ]",
"[ 71/48 → 3/2 | midicmd:clock ]",
"[ 3/2 → 73/48 | midicmd:clock ]",
"[ 73/48 → 37/24 | midicmd:clock ]",
"[ 37/24 → 25/16 | midicmd:clock ]",
"[ 25/16 → 19/12 | midicmd:clock ]",
"[ 19/12 → 77/48 | midicmd:clock ]",
"[ 77/48 → 13/8 | midicmd:clock ]",
"[ 13/8 → 79/48 | midicmd:clock ]",
"[ 79/48 → 5/3 | midicmd:clock ]",
"[ 5/3 → 27/16 | midicmd:clock ]",
"[ 27/16 → 41/24 | midicmd:clock ]",
"[ 41/24 → 83/48 | midicmd:clock ]",
"[ 83/48 → 7/4 | midicmd:clock ]",
"[ 7/4 → 85/48 | midicmd:clock ]",
"[ 85/48 → 43/24 | midicmd:clock ]",
"[ 43/24 → 29/16 | midicmd:clock ]",
"[ 29/16 → 11/6 | midicmd:clock ]",
"[ 11/6 → 89/48 | midicmd:clock ]",
"[ 89/48 → 15/8 | midicmd:clock ]",
"[ 15/8 → 91/48 | midicmd:clock ]",
"[ 91/48 → 23/12 | midicmd:clock ]",
"[ 23/12 → 31/16 | midicmd:clock ]",
"[ 31/16 → 47/24 | midicmd:clock ]",
"[ 47/24 → 95/48 | midicmd:clock ]",
"[ 95/48 → 2/1 | midicmd:clock ]",
"[ 2/1 → 97/48 | midicmd:clock ]",
"[ 2/1 → 4/1 | midicmd:stop ]",
"[ 97/48 → 49/24 | midicmd:clock ]",
"[ 49/24 → 33/16 | midicmd:clock ]",
"[ 33/16 → 25/12 | midicmd:clock ]",
"[ 25/12 → 101/48 | midicmd:clock ]",
"[ 101/48 → 17/8 | midicmd:clock ]",
"[ 17/8 → 103/48 | midicmd:clock ]",
"[ 103/48 → 13/6 | midicmd:clock ]",
"[ 13/6 → 35/16 | midicmd:clock ]",
"[ 35/16 → 53/24 | midicmd:clock ]",
"[ 53/24 → 107/48 | midicmd:clock ]",
"[ 107/48 → 9/4 | midicmd:clock ]",
"[ 9/4 → 109/48 | midicmd:clock ]",
"[ 109/48 → 55/24 | midicmd:clock ]",
"[ 55/24 → 37/16 | midicmd:clock ]",
"[ 37/16 → 7/3 | midicmd:clock ]",
"[ 7/3 → 113/48 | midicmd:clock ]",
"[ 113/48 → 19/8 | midicmd:clock ]",
"[ 19/8 → 115/48 | midicmd:clock ]",
"[ 115/48 → 29/12 | midicmd:clock ]",
"[ 29/12 → 39/16 | midicmd:clock ]",
"[ 39/16 → 59/24 | midicmd:clock ]",
"[ 59/24 → 119/48 | midicmd:clock ]",
"[ 119/48 → 5/2 | midicmd:clock ]",
"[ 5/2 → 121/48 | midicmd:clock ]",
"[ 121/48 → 61/24 | midicmd:clock ]",
"[ 61/24 → 41/16 | midicmd:clock ]",
"[ 41/16 → 31/12 | midicmd:clock ]",
"[ 31/12 → 125/48 | midicmd:clock ]",
"[ 125/48 → 21/8 | midicmd:clock ]",
"[ 21/8 → 127/48 | midicmd:clock ]",
"[ 127/48 → 8/3 | midicmd:clock ]",
"[ 8/3 → 43/16 | midicmd:clock ]",
"[ 43/16 → 65/24 | midicmd:clock ]",
"[ 65/24 → 131/48 | midicmd:clock ]",
"[ 131/48 → 11/4 | midicmd:clock ]",
"[ 11/4 → 133/48 | midicmd:clock ]",
"[ 133/48 → 67/24 | midicmd:clock ]",
"[ 67/24 → 45/16 | midicmd:clock ]",
"[ 45/16 → 17/6 | midicmd:clock ]",
"[ 17/6 → 137/48 | midicmd:clock ]",
"[ 137/48 → 23/8 | midicmd:clock ]",
"[ 23/8 → 139/48 | midicmd:clock ]",
"[ 139/48 → 35/12 | midicmd:clock ]",
"[ 35/12 → 47/16 | midicmd:clock ]",
"[ 47/16 → 71/24 | midicmd:clock ]",
"[ 71/24 → 143/48 | midicmd:clock ]",
"[ 143/48 → 3/1 | midicmd:clock ]",
"[ 3/1 → 145/48 | midicmd:clock ]",
"[ 145/48 → 73/24 | midicmd:clock ]",
"[ 73/24 → 49/16 | midicmd:clock ]",
"[ 49/16 → 37/12 | midicmd:clock ]",
"[ 37/12 → 149/48 | midicmd:clock ]",
"[ 149/48 → 25/8 | midicmd:clock ]",
"[ 25/8 → 151/48 | midicmd:clock ]",
"[ 151/48 → 19/6 | midicmd:clock ]",
"[ 19/6 → 51/16 | midicmd:clock ]",
"[ 51/16 → 77/24 | midicmd:clock ]",
"[ 77/24 → 155/48 | midicmd:clock ]",
"[ 155/48 → 13/4 | midicmd:clock ]",
"[ 13/4 → 157/48 | midicmd:clock ]",
"[ 157/48 → 79/24 | midicmd:clock ]",
"[ 79/24 → 53/16 | midicmd:clock ]",
"[ 53/16 → 10/3 | midicmd:clock ]",
"[ 10/3 → 161/48 | midicmd:clock ]",
"[ 161/48 → 27/8 | midicmd:clock ]",
"[ 27/8 → 163/48 | midicmd:clock ]",
"[ 163/48 → 41/12 | midicmd:clock ]",
"[ 41/12 → 55/16 | midicmd:clock ]",
"[ 55/16 → 83/24 | midicmd:clock ]",
"[ 83/24 → 167/48 | midicmd:clock ]",
"[ 167/48 → 7/2 | midicmd:clock ]",
"[ 7/2 → 169/48 | midicmd:clock ]",
"[ 169/48 → 85/24 | midicmd:clock ]",
"[ 85/24 → 57/16 | midicmd:clock ]",
"[ 57/16 → 43/12 | midicmd:clock ]",
"[ 43/12 → 173/48 | midicmd:clock ]",
"[ 173/48 → 29/8 | midicmd:clock ]",
"[ 29/8 → 175/48 | midicmd:clock ]",
"[ 175/48 → 11/3 | midicmd:clock ]",
"[ 11/3 → 59/16 | midicmd:clock ]",
"[ 59/16 → 89/24 | midicmd:clock ]",
"[ 89/24 → 179/48 | midicmd:clock ]",
"[ 179/48 → 15/4 | midicmd:clock ]",
"[ 15/4 → 181/48 | midicmd:clock ]",
"[ 181/48 → 91/24 | midicmd:clock ]",
"[ 91/24 → 61/16 | midicmd:clock ]",
"[ 61/16 → 23/6 | midicmd:clock ]",
"[ 23/6 → 185/48 | midicmd:clock ]",
"[ 185/48 → 31/8 | midicmd:clock ]",
"[ 31/8 → 187/48 | midicmd:clock ]",
"[ 187/48 → 47/12 | midicmd:clock ]",
"[ 47/12 → 63/16 | midicmd:clock ]",
"[ 63/16 → 95/24 | midicmd:clock ]",
"[ 95/24 → 191/48 | midicmd:clock ]",
"[ 191/48 → 4/1 | midicmd:clock ]",
]
`;
exports[`runs examples > example "midin" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
"[ 1/4 → 1/2 | note:a cutoff:0 resonance:0 s:sawtooth ]",
"[ 1/2 → 3/4 | note:f cutoff:0 resonance:0 s:sawtooth ]",
"[ 3/4 → 1/1 | note:e cutoff:0 resonance:0 s:sawtooth ]",
"[ 1/1 → 5/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
"[ 5/4 → 3/2 | note:a cutoff:0 resonance:0 s:sawtooth ]",
"[ 3/2 → 7/4 | note:f cutoff:0 resonance:0 s:sawtooth ]",
"[ 7/4 → 2/1 | note:e cutoff:0 resonance:0 s:sawtooth ]",
"[ 2/1 → 9/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
"[ 9/4 → 5/2 | note:a cutoff:0 resonance:0 s:sawtooth ]",
"[ 5/2 → 11/4 | note:f cutoff:0 resonance:0 s:sawtooth ]",
"[ 11/4 → 3/1 | note:e cutoff:0 resonance:0 s:sawtooth ]",
"[ 3/1 → 13/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
"[ 13/4 → 7/2 | note:a cutoff:0 resonance:0 s:sawtooth ]",
"[ 7/2 → 15/4 | note:f cutoff:0 resonance:0 s:sawtooth ]",
"[ 15/4 → 4/1 | note:e cutoff:0 resonance:0 s:sawtooth ]",
]
`;
exports[`runs examples > example "midiport" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c midiport:0 ]",
"[ 1/4 → 1/2 | note:a midiport:0 ]",
"[ 1/2 → 3/4 | note:f midiport:0 ]",
"[ 3/4 → 1/1 | note:e midiport:0 ]",
"[ 1/1 → 5/4 | note:c midiport:1 ]",
"[ 5/4 → 3/2 | note:a midiport:1 ]",
"[ 3/2 → 7/4 | note:f midiport:1 ]",
"[ 7/4 → 2/1 | note:e midiport:1 ]",
"[ 2/1 → 9/4 | note:c midiport:2 ]",
"[ 9/4 → 5/2 | note:a midiport:2 ]",
"[ 5/2 → 11/4 | note:f midiport:2 ]",
"[ 11/4 → 3/1 | note:e midiport:2 ]",
"[ 3/1 → 13/4 | note:c midiport:3 ]",
"[ 13/4 → 7/2 | note:a midiport:3 ]",
"[ 7/2 → 15/4 | note:f midiport:3 ]",
"[ 15/4 → 4/1 | note:e midiport:3 ]",
]
`;
exports[`runs examples > example "miditouch" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 miditouch:0.5 ]",
"[ 1/1 → 2/1 | note:c4 miditouch:1 ]",
"[ 2/1 → 3/1 | note:c4 miditouch:0.5000000000000001 ]",
"[ 3/1 → 4/1 | note:c4 miditouch:0 ]",
]
`;
exports[`runs examples > example "mousex" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:C3 ]",
@@ -5426,6 +5845,24 @@ exports[`runs examples > example "note" example index 2 1`] = `
]
`;
exports[`runs examples > example "nrpnn" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
"[ 1/1 → 2/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
"[ 2/1 → 3/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
"[ 3/1 → 4/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
]
`;
exports[`runs examples > example "nrpv" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
"[ 1/1 → 2/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
"[ 2/1 → 3/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
"[ 3/1 → 4/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
]
`;
exports[`runs examples > example "octave" example index 0 1`] = `
[
"[ 0/1 → 1/1 | n:0 s:supersquare octave:3 ]",
@@ -6333,6 +6770,15 @@ exports[`runs examples > example "pressBy" example index 0 1`] = `
]
`;
exports[`runs examples > example "progNum" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 progNum:10 midichan:1 ]",
"[ 1/1 → 2/1 | note:c4 progNum:10 midichan:1 ]",
"[ 2/1 → 3/1 | note:c4 progNum:10 midichan:1 ]",
"[ 3/1 → 4/1 | note:c4 progNum:10 midichan:1 ]",
]
`;
exports[`runs examples > example "pure" example index 0 1`] = `
[
"[ 0/1 → 1/1 | e4 ]",
@@ -6342,6 +6788,186 @@ exports[`runs examples > example "pure" example index 0 1`] = `
]
`;
exports[`runs examples > example "pw" example index 0 1`] = `
[
"[ 0/1 → 1/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 1/16 → 1/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 1/8 → 3/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 3/16 → 1/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 1/4 → 5/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 5/16 → 3/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 3/8 → 7/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 7/16 → 1/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 1/2 → 9/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 9/16 → 5/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 5/8 → 11/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 11/16 → 3/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 3/4 → 13/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 13/16 → 7/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 7/8 → 15/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 15/16 → 1/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 1/1 → 17/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 17/16 → 9/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 9/8 → 19/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 19/16 → 5/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 5/4 → 21/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 21/16 → 11/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 11/8 → 23/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 23/16 → 3/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 3/2 → 25/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 25/16 → 13/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 13/8 → 27/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 27/16 → 7/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 7/4 → 29/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 29/16 → 15/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 15/8 → 31/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 31/16 → 2/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 2/1 → 33/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 33/16 → 17/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 17/8 → 35/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 35/16 → 9/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 9/4 → 37/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 37/16 → 19/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 19/8 → 39/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 39/16 → 5/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 5/2 → 41/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 41/16 → 21/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 21/8 → 43/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 43/16 → 11/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 11/4 → 45/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 45/16 → 23/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 23/8 → 47/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 47/16 → 3/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 3/1 → 49/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 49/16 → 25/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 25/8 → 51/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 51/16 → 13/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 13/4 → 53/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 53/16 → 27/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 27/8 → 55/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 55/16 → 7/2 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 7/2 → 57/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 57/16 → 29/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 29/8 → 59/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 59/16 → 15/4 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 15/4 → 61/16 | note:f s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 61/16 → 31/8 | note:a s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 31/8 → 63/16 | note:c s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
"[ 63/16 → 4/1 | note:e s:pulse pw:0.8 pwrate:1 pwsweep:0.2 ]",
]
`;
exports[`runs examples > example "pw" example index 1 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:pulse pw:0 ]",
"[ 1/8 → 1/4 | note:E3 s:pulse pw:0 ]",
"[ 1/4 → 3/8 | note:F#3 s:pulse pw:0.75 ]",
"[ 3/8 → 1/2 | note:A3 s:pulse pw:0.75 ]",
"[ 1/2 → 5/8 | note:B3 s:pulse pw:0.5 ]",
"[ 5/8 → 3/4 | note:D4 s:pulse pw:0.5 ]",
"[ 3/4 → 7/8 | note:E4 s:pulse pw:1 ]",
"[ 7/8 → 1/1 | note:F#4 s:pulse pw:1 ]",
"[ 1/1 → 9/8 | note:D3 s:pulse pw:0 ]",
"[ 9/8 → 5/4 | note:E3 s:pulse pw:0 ]",
"[ 5/4 → 11/8 | note:F#3 s:pulse pw:0.75 ]",
"[ 11/8 → 3/2 | note:A3 s:pulse pw:0.75 ]",
"[ 3/2 → 13/8 | note:B3 s:pulse pw:0.5 ]",
"[ 13/8 → 7/4 | note:D4 s:pulse pw:0.5 ]",
"[ 7/4 → 15/8 | note:E4 s:pulse pw:1 ]",
"[ 15/8 → 2/1 | note:F#4 s:pulse pw:1 ]",
"[ 2/1 → 17/8 | note:D3 s:pulse pw:0 ]",
"[ 17/8 → 9/4 | note:E3 s:pulse pw:0 ]",
"[ 9/4 → 19/8 | note:F#3 s:pulse pw:0.75 ]",
"[ 19/8 → 5/2 | note:A3 s:pulse pw:0.75 ]",
"[ 5/2 → 21/8 | note:B3 s:pulse pw:0.5 ]",
"[ 21/8 → 11/4 | note:D4 s:pulse pw:0.5 ]",
"[ 11/4 → 23/8 | note:E4 s:pulse pw:1 ]",
"[ 23/8 → 3/1 | note:F#4 s:pulse pw:1 ]",
"[ 3/1 → 25/8 | note:D3 s:pulse pw:0 ]",
"[ 25/8 → 13/4 | note:E3 s:pulse pw:0 ]",
"[ 13/4 → 27/8 | note:F#3 s:pulse pw:0.75 ]",
"[ 27/8 → 7/2 | note:A3 s:pulse pw:0.75 ]",
"[ 7/2 → 29/8 | note:B3 s:pulse pw:0.5 ]",
"[ 29/8 → 15/4 | note:D4 s:pulse pw:0.5 ]",
"[ 15/4 → 31/8 | note:E4 s:pulse pw:1 ]",
"[ 31/8 → 4/1 | note:F#4 s:pulse pw:1 ]",
]
`;
exports[`runs examples > example "pwrate" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/8 → 1/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/4 → 3/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 3/8 → 1/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/2 → 5/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 5/8 → 3/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 3/4 → 7/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 7/8 → 1/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/1 → 9/8 | note:D3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 9/8 → 5/4 | note:E3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 5/4 → 11/8 | note:F#3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 11/8 → 3/2 | note:A3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 3/2 → 13/8 | note:B3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 13/8 → 7/4 | note:D4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 7/4 → 15/8 | note:E4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 15/8 → 2/1 | note:F#4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 2/1 → 17/8 | note:D3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 17/8 → 9/4 | note:E3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 9/4 → 19/8 | note:F#3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 19/8 → 5/2 | note:A3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 5/2 → 21/8 | note:B3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 21/8 → 11/4 | note:D4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 11/4 → 23/8 | note:E4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 23/8 → 3/1 | note:F#4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 3/1 → 25/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 25/8 → 13/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 13/4 → 27/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 27/8 → 7/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 7/2 → 29/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 29/8 → 15/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 15/4 → 31/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 31/8 → 4/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
]
`;
exports[`runs examples > example "pwsweep" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/8 → 1/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/4 → 3/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 3/8 → 1/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/2 → 5/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 5/8 → 3/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 3/4 → 7/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 7/8 → 1/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.3 ]",
"[ 1/1 → 9/8 | note:D3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 9/8 → 5/4 | note:E3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 5/4 → 11/8 | note:F#3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 11/8 → 3/2 | note:A3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 3/2 → 13/8 | note:B3 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 13/8 → 7/4 | note:D4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 7/4 → 15/8 | note:E4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 15/8 → 2/1 | note:F#4 s:pulse pw:0.5 pwrate:0.1 pwsweep:0.8 ]",
"[ 2/1 → 17/8 | note:D3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 17/8 → 9/4 | note:E3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 9/4 → 19/8 | note:F#3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 19/8 → 5/2 | note:A3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 5/2 → 21/8 | note:B3 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 21/8 → 11/4 | note:D4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 11/4 → 23/8 | note:E4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 23/8 → 3/1 | note:F#4 s:pulse pw:0.5 pwrate:25 pwsweep:0.3 ]",
"[ 3/1 → 25/8 | note:D3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 25/8 → 13/4 | note:E3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 13/4 → 27/8 | note:F#3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 27/8 → 7/2 | note:A3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 7/2 → 29/8 | note:B3 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 29/8 → 15/4 | note:D4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 15/4 → 31/8 | note:E4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
"[ 31/8 → 4/1 | note:F#4 s:pulse pw:0.5 pwrate:5 pwsweep:0.8 ]",
]
`;
exports[`runs examples > example "queryArc" example index 0 1`] = `[]`;
exports[`runs examples > example "rand" example index 0 1`] = `
@@ -6777,35 +7403,76 @@ exports[`runs examples > example "rev" example index 0 1`] = `
exports[`runs examples > example "ribbon" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:d ]",
"[ 1/21/1 | note:e ]",
"[ 1/1 → 3/2 | note:d ]",
"[ 3/22/1 | note:e ]",
"[ 2/1 → 5/2 | note:d ]",
"[ 5/2 → 3/1 | note:e ]",
"[ 3/1 → 7/2 | note:d ]",
"[ 7/2 → 4/1 | note:e ]",
"[ 0/1 → 1/1 | note:d ]",
"[ 1/12/1 | note:e ]",
"[ 2/1 → 3/1 | note:d ]",
"[ 3/14/1 | note:e ]",
]
`;
exports[`runs examples > example "ribbon" example index 1 1`] = `
[
"[ 0/1 → 1/4 | note:G3 ]",
"[ 0/1 → 1/4 | note:A3 ]",
"[ 1/4 → 1/2 | note:C3 ]",
"[ 1/2 → 3/4 | note:D3 ]",
"[ 3/4 → 1/1 | note:F3 ]",
"[ 1/1 → 5/4 | note:Eb3 ]",
"[ 5/4 → 3/2 | note:Ab3 ]",
"[ 3/2 → 7/4 | note:G3 ]",
"[ 7/4 → 2/1 | note:C4 ]",
"[ 2/1 → 9/4 | note:G3 ]",
"[ 3/4 → 1/1 | note:G3 ]",
"[ 1/1 → 5/4 | note:E3 ]",
"[ 5/4 → 3/2 | note:C4 ]",
"[ 3/2 → 7/4 | note:A3 ]",
"[ 7/4 → 2/1 | note:E4 ]",
"[ 2/1 → 9/4 | note:A3 ]",
"[ 9/4 → 5/2 | note:C3 ]",
"[ 5/2 → 11/4 | note:D3 ]",
"[ 11/4 → 3/1 | note:F3 ]",
"[ 3/1 → 13/4 | note:Eb3 ]",
"[ 13/4 → 7/2 | note:Ab3 ]",
"[ 7/2 → 15/4 | note:G3 ]",
"[ 15/4 → 4/1 | note:C4 ]",
"[ 11/4 → 3/1 | note:G3 ]",
"[ 3/1 → 13/4 | note:E3 ]",
"[ 13/4 → 7/2 | note:C4 ]",
"[ 7/2 → 15/4 | note:A3 ]",
"[ 15/4 → 4/1 | note:E4 ]",
]
`;
exports[`runs examples > example "ribbon" example index 2 1`] = `
[
"[ 1/16 → 1/8 | s:bd ]",
"[ 1/8 → 3/16 | s:bd ]",
"[ 3/16 → 1/4 | s:bd ]",
"[ 1/4 → 5/16 | s:bd ]",
"[ 3/8 → 7/16 | s:bd ]",
"[ 9/16 → 5/8 | s:bd ]",
"[ 5/8 → 11/16 | s:bd ]",
"[ 11/16 → 3/4 | s:bd ]",
"[ 3/4 → 13/16 | s:bd ]",
"[ 7/8 → 15/16 | s:bd ]",
"[ 17/16 → 9/8 | s:bd ]",
"[ 9/8 → 19/16 | s:bd ]",
"[ 19/16 → 5/4 | s:bd ]",
"[ 5/4 → 21/16 | s:bd ]",
"[ 11/8 → 23/16 | s:bd ]",
"[ 25/16 → 13/8 | s:bd ]",
"[ 13/8 → 27/16 | s:bd ]",
"[ 27/16 → 7/4 | s:bd ]",
"[ 7/4 → 29/16 | s:bd ]",
"[ 15/8 → 31/16 | s:bd ]",
"[ 33/16 → 17/8 | s:bd ]",
"[ 17/8 → 35/16 | s:bd ]",
"[ 35/16 → 9/4 | s:bd ]",
"[ 9/4 → 37/16 | s:bd ]",
"[ 19/8 → 39/16 | s:bd ]",
"[ 41/16 → 21/8 | s:bd ]",
"[ 21/8 → 43/16 | s:bd ]",
"[ 43/16 → 11/4 | s:bd ]",
"[ 11/4 → 45/16 | s:bd ]",
"[ 23/8 → 47/16 | s:bd ]",
"[ 49/16 → 25/8 | s:bd ]",
"[ 25/8 → 51/16 | s:bd ]",
"[ 51/16 → 13/4 | s:bd ]",
"[ 13/4 → 53/16 | s:bd ]",
"[ 27/8 → 55/16 | s:bd ]",
"[ 57/16 → 29/8 | s:bd ]",
"[ 29/8 → 59/16 | s:bd ]",
"[ 59/16 → 15/4 | s:bd ]",
"[ 15/4 → 61/16 | s:bd ]",
"[ 31/8 → 63/16 | s:bd ]",
]
`;
@@ -7401,9 +8068,7 @@ exports[`runs examples > example "scope" example index 0 1`] = `
]
`;
exports[`runs examples > example "scramble
Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`,
but parts might be played more than once, or not at all, per cycle." example index 0 1`] = `
exports[`runs examples > example "scramble" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c s:piano ]",
"[ 1/4 → 1/2 | note:d s:piano ]",
@@ -7424,9 +8089,7 @@ but parts might be played more than once, or not at all, per cycle." example ind
]
`;
exports[`runs examples > example "scramble
Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`,
but parts might be played more than once, or not at all, per cycle." example index 1 1`] = `
exports[`runs examples > example "scramble" example index 1 1`] = `
[
"[ 0/1 → 1/8 | note:c s:piano ]",
"[ 1/8 → 1/4 | note:d s:piano ]",
@@ -7451,6 +8114,52 @@ but parts might be played more than once, or not at all, per cycle." example ind
]
`;
exports[`runs examples > example "scrub" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
"[ 1/8 → 1/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
"[ 1/4 → 5/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]",
"[ 5/8 → 3/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
"[ 3/4 → 7/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
"[ 7/8 → 1/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]",
"[ 1/1 → 9/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
"[ 9/8 → 5/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
"[ 5/4 → 13/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]",
"[ 13/8 → 7/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
"[ 7/4 → 15/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
"[ 15/8 → 2/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]",
"[ 2/1 → 17/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
"[ 17/8 → 9/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
"[ 9/4 → 21/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]",
"[ 21/8 → 11/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
"[ 11/4 → 23/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
"[ 23/8 → 3/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]",
"[ 3/1 → 25/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
"[ 25/8 → 13/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]",
"[ 13/4 → 29/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]",
"[ 29/8 → 15/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
"[ 15/4 → 31/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]",
"[ 31/8 → 4/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]",
]
`;
exports[`runs examples > example "scrub" example index 1 1`] = `
[
"[ 0/1 → 3/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
"[ 3/8 → 5/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
"[ 5/8 → 1/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]",
"[ 1/1 → 11/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
"[ 11/8 → 13/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
"[ 13/8 → 2/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]",
"[ 2/1 → 19/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
"[ 19/8 → 21/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
"[ 21/8 → 3/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]",
"[ 3/1 → 27/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
"[ 27/8 → 29/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]",
"[ 29/8 → 4/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]",
]
`;
exports[`runs examples > example "segment" example index 0 1`] = `
[
"[ 0/1 → 1/24 | note:40 ]",
@@ -7835,9 +8544,7 @@ exports[`runs examples > example "shrink" example index 3 1`] = `
]
`;
exports[`runs examples > example "shuffle
Slices a pattern into the given number of parts, then plays those parts in random order.
Each part will be played exactly once per cycle." example index 0 1`] = `
exports[`runs examples > example "shuffle" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c s:piano ]",
"[ 1/4 → 1/2 | note:d s:piano ]",
@@ -7858,9 +8565,7 @@ Each part will be played exactly once per cycle." example index 0 1`] = `
]
`;
exports[`runs examples > example "shuffle
Slices a pattern into the given number of parts, then plays those parts in random order.
Each part will be played exactly once per cycle." example index 1 1`] = `
exports[`runs examples > example "shuffle" example index 1 1`] = `
[
"[ 0/1 → 1/8 | note:c s:piano ]",
"[ 1/8 → 1/4 | note:d s:piano ]",
@@ -8957,6 +9662,33 @@ exports[`runs examples > example "swingBy" example index 0 1`] = `
]
`;
exports[`runs examples > example "sysex" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
"[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
"[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
"[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
]
`;
exports[`runs examples > example "sysexdata" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
"[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
"[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
"[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
]
`;
exports[`runs examples > example "sysexid" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
"[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
"[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
"[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
]
`;
exports[`runs examples > example "take" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd ]",
+13 -1
View File
@@ -11,7 +11,7 @@ import * as webaudio from '@strudel/webaudio';
import { mini, m } from '@strudel/mini/mini.mjs';
// import * as voicingHelpers from '@strudel/tonal/voicings.mjs';
// import euclid from '@strudel/core/euclid.mjs';
// import '@strudel/midi/midi.mjs';
//import '@strudel/midi/midi.mjs';
import * as tonalHelpers from '@strudel/tonal';
import '@strudel/xen/xen.mjs';
// import '@strudel/xen/tune.mjs';
@@ -21,6 +21,9 @@ import '@strudel/xen/xen.mjs';
// import '@strudel/webaudio/webaudio.mjs';
// import '@strudel/serial/serial.mjs';
import '../website/src/repl/piano';
//import * as motionHelpers from '../packages/motion/index.mjs';
//import * as geolocationHelpers from '../packages/geolocation/index.mjs';
import * as gamepadHelpers from '../packages/gamepad/index.mjs';
class MockedNode {
chain() {
@@ -123,6 +126,12 @@ const loadCsound = () => {};
const loadCSound = () => {};
const loadcsound = () => {};
const midin = () => {
return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0
};
const sysex = ([id, data]) => {};
// TODO: refactor to evalScope
evalScope(
// Tone,
@@ -131,6 +140,7 @@ evalScope(
uiHelpersMocked,
webaudio,
tonalHelpers,
gamepadHelpers,
/*
toneHelpers,
voicingHelpers,
@@ -138,6 +148,8 @@ evalScope(
uiHelpers,
*/
{
midin,
sysex,
// gist,
// euclid,
csound: id,
+3 -1
View File
@@ -1,7 +1,9 @@
import { configDefaults, defineConfig } from 'vitest/config';
import { defineConfig } from 'vitest/config';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
/// <reference types="vitest" />
export default defineConfig({
plugins: [bundleAudioWorkletPlugin()],
test: {
reporters: 'verbose',
isolate: false,
+2
View File
@@ -5,6 +5,7 @@ import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeUrls from 'rehype-urls';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
import tailwind from '@astrojs/tailwind';
import AstroPWA from '@vite-pwa/astro';
@@ -134,6 +135,7 @@ export default defineConfig({
site,
base,
vite: {
plugins: [bundleAudioWorkletPlugin()],
ssr: {
// Example: Force a broken package to skip SSR processing, if needed
// external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51
+3 -1
View File
@@ -29,6 +29,7 @@
"@strudel/csound": "workspace:*",
"@strudel/desktopbridge": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/gamepad": "workspace:*",
"@strudel/hydra": "workspace:*",
"@strudel/midi": "workspace:*",
"@strudel/mini": "workspace:*",
@@ -72,6 +73,7 @@
"@vite-pwa/astro": "^0.5.0",
"html-escaper": "^3.0.3",
"sharp": "^0.33.5",
"workbox-window": "^7.3.0"
"workbox-window": "^7.3.0",
"vite-plugin-bundle-audioworklet": "workspace:*"
}
}
+1
View File
@@ -84,6 +84,7 @@ export const SIDEBAR: Sidebar = {
{ text: 'Music metadata', link: 'learn/metadata' },
{ text: 'CSound', link: 'learn/csound' },
{ text: 'Hydra', link: 'learn/hydra' },
{ text: 'Input Devices', link: 'learn/input-devices' },
{ text: 'Device Motion', link: 'learn/devicemotion' },
],
'Pattern Functions': [
+1 -1
View File
@@ -23,7 +23,7 @@ is equivalent to this Pattern without Mini Notation:
Similarly, there is an equivalent function for every aspect of the mini notation.
Which representation to use is a matter of context. As a rule of thumb, functions
are better suited in a larger context, while mini notation is more practical for individiual rhythms.
are better suited in a larger context, while mini notation is more practical for individual rhythms.
## Limits of Mini Notation
+1 -1
View File
@@ -5,6 +5,6 @@ layout: ../../layouts/MainLayout.astro
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
import DeviceMotion from '../../../../packages/motion/docs/devicemotion.mdx';
import DeviceMotion from '@strudel/motion/docs/devicemotion.mdx';
<DeviceMotion />
+15
View File
@@ -0,0 +1,15 @@
---
title: Input Devices
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
import Gamepad from '@strudel/gamepad/docs/gamepad.mdx';
# Input Devices
Strudel supports various input devices like Gamepads and MIDI controllers to manipulate patterns in real-time.
<Gamepad />
+135 -12
View File
@@ -16,24 +16,97 @@ It is also possible to pattern other things with Strudel, such as software and h
Strudel supports MIDI without any additional software (thanks to [webmidi](https://npmjs.com/package/webmidi)), just by adding methods to your pattern:
## midi(outputName?)
## midiin(inputName?)
<JsDoc client:idle name="midin" h={0} />
## midi(outputName?,options?)
Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages.
If no outputName is given, it uses the first midi output it finds.
<MiniRepl client:idle tune={`chord("<C^7 A7 Dm7 G7>").voicing().midi()`} />
<MiniRepl
client:idle
tune={`
$: chord("<C^7 A7 Dm7 G7>").voicing().midi('IAC Driver')
`}
/>
In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".`
In the console, you will see a log of the available MIDI devices as soon as you run the code,
e.g.
```
`Midi connected! Using "Midi Through Port-0".`
```
The `.midi()` function accepts an options object with the following properties:
<MiniRepl
client:idle
tune={`$: note("d e c a f").midi('IAC Driver', { isController: true, midimap: 'default'})
`}
/>
<details>
<summary>Available Options</summary>
| Option | Type | Default | Description |
| ------------ | ------------- | --------- | ---------------------------------------------------------------------- |
| isController | boolean | false | When true, disables sending note messages. Useful for MIDI controllers |
| latencyMs | number | 34 | Latency in milliseconds to align MIDI with audio engine |
| noteOffsetMs | number | 10 | Offset in milliseconds for note-off messages to prevent glitching |
| midichannel | number | 1 | Default MIDI channel (1-16) |
| velocity | number | 0.9 | Default note velocity (0-1) |
| gain | number | 1 | Default gain multiplier for velocity (0-1) |
| midimap | string | 'default' | Name of MIDI mapping to use for control changes |
| midiport | string/number | - | MIDI device name or index |
</details>
### midiport(outputName)
Selects the MIDI output device to use, pattern can be used to switch between devices.
```javascript
$: midiport('IAC Driver');
$: note('c a f e').midiport('<0 1 2 3>').midi();
```
<JsDoc client:idle name="midiport" h={0} />
## midichan(number)
Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default.
## ccn && ccv
## midicmd(command)
`midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices.
It supports the following commands:
- `clock`/`midiClock` - Sends MIDI timing clock messages
- `start` - Sends MIDI start message
- `stop` - Sends MIDI stop message
- `continue` - Sends MIDI continue message
// You can control the clock with a pattern and ensure it starts in sync when the repl begins.
// Note: It might act unexpectedly if MIDI isn't set up initially.
<MiniRepl
client:idle
tune={`$:stack(
midicmd("clock*48,<start stop>/2").midi('IAC Driver')
)`}
/>
## control, ccn && ccv
- `control` sends MIDI control change messages to your MIDI device.
- `ccn` sets the cc number. Depends on your synths midi mapping
- `ccv` sets the cc value. normalized from 0 to 1.
<MiniRepl client:idle tune={`note("c a f e").control([74, sine.slow(4)]).midi()`} />
<MiniRepl client:idle tune={`note("c a f e").ccn(74).ccv(sine.slow(4)).midi()`} />
In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern.
@@ -56,6 +129,48 @@ Instead of setting `ccn` and `ccv` directly, you can also create mappings with `
<JsDoc client:idle name="defaultmidimap" h={0} />
## progNum (Program Change)
`progNum` sends MIDI program change messages to switch between different presets/patches on your MIDI device.
Program change values should be numbers between 0 and 127.
<MiniRepl client:idle tune={`// Switch between programs 0 and 1 every cycle
progNum("<0 1>").midi()
// Play notes while changing programs
note("c3 e3 g3").progNum("<0 1 2>").midi()`} />
Program change messages are useful for switching between different instrument sounds or presets during a performance.
The exact sound that each program number maps to depends on your MIDI device's configuration.
## sysex, sysexid && sysexdata (System Exclusive Message)
`sysex` sends MIDI System Exclusive (SysEx) messages to your MIDI device.
ysEx messages are device-specific commands that allow deeper control over synthesizer parameters.
The value should be an array of numbers between 0-255 representing the SysEx data bytes.
<MiniRepl
client:idle
tune={`// Send a simple SysEx message
let id = 0x43; //Yamaha
//let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers
let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa"
$: note("c a f e").sysex(id, data).midi();
$: note("c a f e").sysexid(id).sysexdata(data).midi();`}
/>
The exact format of SysEx messages depends on your MIDI device's specification.
Consult your device's MIDI implementation guide for details on supported SysEx messages.
## midibend && miditouch
`midibend` sets MIDI pitch bend (-1 - 1)
`miditouch` sets MIDI key after touch (0-1)
<MiniRepl client:idle tune={`note("c a f e").midibend(sine.slow(4).range(-0.4,0.4)).midi()`} />
<MiniRepl client:idle tune={`note("c a f e").miditouch(sine.slow(4).range(0,1)).midi()`} />
# OSC/SuperDirt/StrudelDirt
In TidalCycles, sound is usually generated using [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software.
@@ -118,8 +233,8 @@ The following example shows how to send a pattern to an MQTT broker:
client:only="react"
tune={`"hello world"
.mqtt(undefined, // username (undefined for open/public servers)
undefined, // password
'/strudel-pattern', // mqtt 'topic'
undefined, // password
'/strudel-pattern', // mqtt 'topic'
'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address
'mystrudel', // MQTT client id - randomly generated if not supplied
0 // latency / delay before sending messages (0 = no delay)
@@ -130,12 +245,14 @@ The following example shows how to send a pattern to an MQTT broker:
Other software can then receive the messages. For example using the [mosquitto](https://mosquitto.org/) commandline client tools:
```
> mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/strudel-pattern"
hello
world
hello
world
...
> mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/strudel-pattern"
> hello
> world
> hello
> world
> ...
```
Control patterns will be encoded as JSON, for example:
@@ -155,11 +272,17 @@ Control patterns will be encoded as JSON, for example:
Will send messages like the following:
```
{"s":"sax","speed":2}
{"s":"sax","speed":2}
{"s":"sax","speed":3}
{"s":"sax","speed":2}
...
```
Libraries for receiving MQTT are available for many programming languages.
```
```
+2 -17
View File
@@ -8,7 +8,7 @@ import { JsDoc } from '../../docs/JsDoc';
# Mini-notation
Just like [Tidal Cycles](https://tidalcycles.org/docs/reference/mini_notation/), Strudel uses a so called "Mini-Notation", which is a custom language that is designed for writing rhythmic patterns using little amounts of text.
Just like [Tidal Cycles](https://tidalcycles.org/), Strudel uses a so called "Mini-Notation", which is a custom language that is designed for writing rhythmic patterns using little amounts of text.
## Note
@@ -156,11 +156,7 @@ But to play multiple chords in a sequence, we have to wrap them in brackets:
## Elongation
With the "\_" symbol, we can manually extend the duration of an event:
<MiniRepl client:idle tune={`note("<[g3,b3,e4] _ _ [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
With the "@" symbol, we can do the same thing more conveniently by specifying the temporal "weight" of a sequence child:
With the "@" symbol, we can specify temporal "weight" of a sequence child:
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
@@ -172,16 +168,6 @@ Using "!" we can repeat without speeding up:
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
## Degradation
Using "?" we can randomly drop events from a pattern while preserving their time slot:
<MiniRepl client:idle tune={`note("<[g3,b3,e4]? [a3,c3,e4]? [b3,d3,f#4]>*2")`} punchcard />
We can include a number after the "?" to represent the probability of being dropped, otherwise defaulting to 0.5:
<MiniRepl client:idle tune={`note("<[g3,b3,e4]? [a3,c3,e4]?0.9 [b3,d3,f#4]>*2")`} punchcard />
## Mini-notation review
To recap what we've learned so far, compare the following patterns:
@@ -193,7 +179,6 @@ To recap what we've learned so far, compare the following patterns:
<MiniRepl client:idle tune={`note("<[g3,b3,e4] _ [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]?0.9 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
## Euclidian rhythms
+8 -6
View File
@@ -66,7 +66,7 @@ While it saves resources, it can also lead to sounds not being audible the first
# Sound Banks
If we open the `sounds` tab and then `drum machines`, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc..
If we open the `sounds` tab and then `drum-machines`, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc..
We _could_ use them like this:
@@ -180,11 +180,12 @@ It assumes a `strudel.json` file to be present at the root of the repository:
https://raw.githubusercontent.com/<user>/<repo>/<branch>/strudel.json
```
## From Disk via "Import Sounds"
## From Disk via "Import Sounds Folder"
If you don't want to upload your samples to the internet, you can also load them from your local disk.
Go to the `sounds` tab in the REPL and press "import sounds".
Then you can select a folder that contains audio files. The folder you select can also contain subfolders with audio files.
Go to the `sounds` tab in the REPL and open the `import-sounds` tab below the search bar.
Press the "import sounds folder" button and select a folder that contains audio files.
The folder you select can also contain subfolders with audio files.
Example:
```
@@ -200,12 +201,13 @@ Example:
```
In the above example the folder `samples` contains 2 subfolders `swoop` and `smash`, which contain audio files.
If you select that `samples` folder, the `user` tab (next to the import sounds button) will then contain 2 new sounds: `swoop(3) smash(3)`
If you select that `samples` folder, the `user` tab (next to the `import-sounds` tab) will then contain 2 new sounds: `swoop(3) smash(3)`
The individual samples can the be played normally like `s("swoop:0 swoop:1 smash:2")`.
The samples within each sound use zero-based indexing in alphabetical order.
## From Disk via @strudel/sampler
Instead of loading your samples into your browser with the "import sounds" button, you can also serve the samples from a local file server.
Instead of loading your samples into your browser with the "import sounds folder" button, you can also serve the samples from a local file server.
The easiest way to do this is using [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler):
```sh
+1 -1
View File
@@ -24,7 +24,7 @@ With the new stepwise `stepcat` function, the steps of the two patterns will be
By default, steps are counted according to the 'top level' in mini-notation. For example `"a [b c] d e"` has five events in it per cycle, but is counted as four steps, where `[b c]` is counted as a single step.
However, you can mark a different metrical level to count steps relative to, using a `^` at the start of a sub-pattern. If we do this to the subpattern in our example: `"a [^b c] d e"`, then the pattern is now counted as having _eight_ steps. This is because 'b' and 'c' are each counted as single steps, and the events in the pattenr are twice as long, and so counted as two steps each.
However, you can mark a different metrical level to count steps relative to, using a `^` at the start of a sub-pattern. If we do this to the subpattern in our example: `"a [^b c] d e"`, then the pattern is now counted as having _eight_ steps. This is because 'b' and 'c' are each counted as single steps, and the events in the pattern are twice as long, and so counted as two steps each.
## Pacing the steps
+10 -8
View File
@@ -1,12 +1,14 @@
---
import HeadCommon from '@components/HeadCommon.astro';
import { Udels } from '../../components/Udels/Udels.jsx';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
---
<body class="m-0">
<Udels client:only="react" />
</body>
<html lang="en" class="m-0">
<head>
<HeadCommon />
<title>Strudel UDELS</title>
</head>
<body class="m-0">
<Udels client:only="react" />
</body>
</html>
@@ -4,5 +4,9 @@ export default function UserFacingErrorMessage(Props) {
if (error == null) {
return;
}
return <div className="text-red-500 p-4 bg-lineHighlight animate-pulse">{error.message || 'Unknown Error :-/'}</div>;
return (
<div className="text-background px-2 py-1 bg-foreground w-full ml-auto">
Error: {error.message || 'Unknown Error :-/'}
</div>
);
}
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { getAudioDevices, setAudioDevice } from '../../util.mjs';
import { SelectInput } from './SelectInput';
import { getAudioDevices } from '@strudel/webaudio';
const initdevices = new Map();
@@ -21,9 +22,7 @@ export function AudioDeviceSelector({ audioDeviceName, onChange, isDisabled }) {
if (!devicesInitialized) {
return;
}
const deviceID = devices.get(deviceName);
onChange(deviceName);
setAudioDevice(deviceID);
};
const options = new Map();
Array.from(devices.keys()).forEach((deviceName) => {
@@ -20,25 +20,42 @@ export default function ImportSoundsButton({ onComplete }) {
});
return (
<label
style={{ alignItems: 'center' }}
className="flex bg-background ml-2 pl-2 pr-2 max-w-[300px] rounded-md hover:opacity-50 whitespace-nowrap cursor-pointer"
>
<input
disabled={isUploading}
ref={fileUploadRef}
id="audio_file"
style={{ display: 'none' }}
type="file"
directory=""
webkitdirectory=""
multiple
accept="audio/*, .wav, .mp3, .m4a, .flac, .aac, .ogg"
onChange={() => {
onChange();
}}
/>
{isUploading ? 'importing...' : 'import sounds'}
</label>
<div>
<label
style={{ alignItems: 'center', borderColor: 'red', border: 1 }}
className="flex bg-background p-4 w-fit rounded-xl hover:opacity-50 whitespace-nowrap cursor-pointer"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6 mr-2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7.5 7.5h-.75A2.25 2.25 0 0 0 4.5 9.75v7.5a2.25 2.25 0 0 0 2.25 2.25h7.5a2.25 2.25 0 0 0 2.25-2.25v-7.5a2.25 2.25 0 0 0-2.25-2.25h-.75m0-3-3-3m0 0-3 3m3-3v11.25m6-2.25h.75a2.25 2.25 0 0 1 2.25 2.25v7.5a2.25 2.25 0 0 1-2.25 2.25h-7.5a2.25 2.25 0 0 1-2.25-2.25v-.75"
/>
</svg>
<input
disabled={isUploading}
ref={fileUploadRef}
id="audio_file"
style={{ display: 'none' }}
type="file"
directory=""
webkitdirectory=""
multiple
accept="audio/*, .wav, .mp3, .m4a, .flac, .aac, .ogg"
onChange={() => {
onChange();
}}
/>
{isUploading ? 'importing...' : 'import sounds folder'}
</label>
</div>
);
}
@@ -5,7 +5,7 @@ export function SelectInput({ value, options, onChange, onClick, isDisabled }) {
<select
disabled={isDisabled}
onClick={onClick}
className="p-2 bg-background rounded-md text-foreground"
className="p-2 bg-background rounded-md text-foreground border-foreground"
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
>
@@ -1,10 +1,12 @@
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
import { themes } from '@strudel/codemirror';
import { Textbox } from '../textbox/Textbox.jsx';
import { isUdels } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx';
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
import { confirmDialog } from '../../util.mjs';
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
function Checkbox({ label, value, onChange, disabled = false }) {
return (
@@ -18,7 +20,7 @@ function Checkbox({ label, value, onChange, disabled = false }) {
function SelectInput({ value, options, onChange }) {
return (
<select
className="p-2 bg-background rounded-md text-foreground"
className="p-2 bg-background rounded-md text-foreground border-foreground"
value={value}
onChange={(e) => onChange(e.target.value)}
>
@@ -53,7 +55,7 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) {
);
}
function FormItem({ label, children }) {
function FormItem({ label, children, sublabel }) {
return (
<div className="grid gap-2">
<label>{label}</label>
@@ -105,6 +107,8 @@ export function SettingsTab({ started }) {
audioDeviceName,
audioEngineTarget,
togglePanelTrigger,
maxPolyphony,
multiChannelOrbits,
} = useSettings();
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
@@ -139,6 +143,42 @@ export function SettingsTab({ started }) {
}}
/>
</FormItem>
<FormItem label="Maximum Polyphony">
<Textbox
min={1}
max={Infinity}
onBlur={(e) => {
let v = parseInt(e.target.value);
v = isNaN(v) ? DEFAULT_MAX_POLYPHONY : v;
setMaxPolyphony(v);
settingsMap.setKey('maxPolyphony', v);
}}
onChange={(v) => {
v = Math.max(1, parseInt(v));
settingsMap.setKey('maxPolyphony', isNaN(v) ? undefined : v);
}}
type="number"
placeholder=""
value={maxPolyphony ?? ''}
/>
</FormItem>
<FormItem>
<Checkbox
label="Multi Channel Orbits"
onChange={(cbEvent) => {
const val = cbEvent.target.checked;
confirmDialog(RELOAD_MSG).then((r) => {
if (r == true) {
settingsMap.setKey('multiChannelOrbits', val);
setMultiChannelOrbits(val);
return window.location.reload();
}
});
}}
value={multiChannelOrbits}
/>
</FormItem>
<FormItem label="Theme">
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
</FormItem>
@@ -179,25 +219,7 @@ export function SettingsTab({ started }) {
value={togglePanelTrigger}
onChange={(value) => settingsMap.setKey('togglePanelTrigger', value)}
items={{ click: 'Click', hover: 'Hover' }}
></ButtonGroup>
{/* <Checkbox
label="Click"
onChange={(cbEvent) => {
if (cbEvent.target.checked) {
settingsMap.setKey('togglePanelTrigger', 'click');
}
}}
value={togglePanelTrigger != 'hover'}
/>
<Checkbox
label="Hover"
onChange={(cbEvent) => {
if (cbEvent.target.checked) {
settingsMap.setKey('togglePanelTrigger', 'hover');
}
}}
value={togglePanelTrigger == 'hover'}
/> */}
</FormItem>
<FormItem label="More Settings">
<Checkbox
@@ -14,6 +14,8 @@ export function SoundsTab() {
const sounds = useStore(soundMap);
const { soundsFilter } = useSettings();
const [search, setSearch] = useState('');
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
const soundEntries = useMemo(() => {
if (!sounds) {
@@ -37,6 +39,9 @@ export function SoundsTab() {
if (soundsFilter === 'synths') {
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
}
if (soundsFilter === 'importSounds') {
return [];
}
return filtered;
}, [sounds, soundsFilter, search]);
@@ -51,7 +56,6 @@ export function SoundsTab() {
ref?.stop(getAudioContext().currentTime + 0.01);
});
});
return (
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
@@ -65,9 +69,9 @@ export function SoundsTab() {
drums: 'drum-machines',
synths: 'Synths',
user: 'User',
importSounds: 'import-sounds',
}}
></ButtonGroup>
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
</div>
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
@@ -101,7 +105,55 @@ export function SoundsTab() {
</span>
);
})}
{!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''}
{!soundEntries.length && soundsFilter === 'importSounds' ? (
<div className="prose dark:prose-invert min-w-full pt-2 pb-8 px-4">
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
<p>
To import sounds into strudel, they must be contained{' '}
<a href={`${baseNoTrailing}/learn/samples/#from-disk-via-import-sounds-folder`} target="_blank">
within a folder or subfolder
</a>
. The best way to do this is to upload a samples folder containing subfolders of individual sounds or
soundbanks (see diagram below).{' '}
</p>
<pre className="bg-background" key={'sample-diagram'}>
{`└─ samples <-- import this folder
├─ swoop
│ ├─ swoopshort.wav
│ ├─ swooplong.wav
│ └─ swooptight.wav
└─ smash
├─ smashhigh.wav
├─ smashlow.wav
└─ smashmiddle.wav`}
</pre>
<p>
The name of a subfolder corresponds to the sound name under the user tab. Multiple samples within a
subfolder are all labelled with the same name, but can be accessed using .n( ) - remember sounds are
zero-indexed and in alphabetical order!
</p>
<p>
For more information, and other ways to use your own sounds in strudel,{' '}
<a href={`${baseNoTrailing}/learn/samples/#from-disk-via-import-sounds-folder`} target="_blank">
check out the docs
</a>
!
</p>
<h3>Preview Sounds</h3>
<pre className="bg-background" key={'sample-preview'}>
n("0 1 2 3 4 5").s("sample-name")
</pre>
<p>
Paste the line above into the main editor to hear the uploaded folder. Remember to use the name of your
sample as it appears under the "user" tab.
</p>
</div>
) : (
''
)}
{!soundEntries.length && soundsFilter !== 'importSounds'
? 'No custom sounds loaded in this pattern (yet).'
: ''}
</div>
</div>
);
@@ -3,7 +3,10 @@ import cx from '@src/cx.mjs';
export function Textbox({ onChange, className, ...inputProps }) {
return (
<input
className={cx('p-1 bg-background rounded-md my-2 border-foreground', className)}
className={cx(
'p-2 bg-background rounded-md border-foreground text-foreground placeholder-foreground',
className,
)}
onChange={(e) => onChange(e.target.value)}
{...inputProps}
/>
+11 -13
View File
@@ -54,25 +54,23 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
const blob = soundFile.blob;
// Files used to be uploaded as base64 strings, After Jan 1 2025 this check can be safely deleted
if (typeof blob === 'string') {
const soundPaths = sounds.get(parentDirectory) ?? new Set();
soundPaths.add(blob);
sounds.set(parentDirectory, soundPaths);
return;
}
return blobToDataUrl(blob).then((soundPath) => {
const soundPaths = sounds.get(parentDirectory) ?? new Set();
soundPaths.add(soundPath);
sounds.set(parentDirectory, soundPaths);
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
titlePathMap.set(title, soundPath);
sounds.set(parentDirectory, titlePathMap);
return;
});
}),
)
.then(() => {
sounds.forEach((soundPaths, key) => {
const value = Array.from(soundPaths);
sounds.forEach((titlePathMap, key) => {
const value = Array.from(titlePathMap.keys())
.sort((a, b) => {
return a.localeCompare(b);
})
.map((title) => titlePathMap.get(title));
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
+23
View File
@@ -115,6 +115,29 @@ export async function prebake() {
'numbers/7.wav',
'numbers/8.wav',
],
num: [
'num/00.wav',
'num/01.wav',
'num/02.wav',
'num/03.wav',
'num/04.wav',
'num/05.wav',
'num/06.wav',
'num/07.wav',
'num/08.wav',
'num/09.wav',
'num/10.wav',
'num/11.wav',
'num/12.wav',
'num/13.wav',
'num/14.wav',
'num/15.wav',
'num/16.wav',
'num/17.wav',
'num/18.wav',
'num/19.wav',
'num/20.wav',
],
},
'github:tidalcycles/dirt-samples',
{
+9 -19
View File
@@ -14,11 +14,11 @@ import {
resetLoadedSounds,
initAudioOnFirstClick,
} from '@strudel/webaudio';
import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs';
import { setVersionDefaultsFrom } from './util.mjs';
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
import { clearHydra } from '@strudel/hydra';
import { useCallback, useEffect, useRef, useState } from 'react';
import { settingsMap, useSettings } from '../settings.mjs';
import { parseBoolean, settingsMap, useSettings } from '../settings.mjs';
import {
setActivePattern,
setLatestCode,
@@ -28,7 +28,7 @@ import {
setViewingPatternData,
} from '../user_pattern_utils.mjs';
import { superdirtOutput } from '@strudel/osc/superdirtoutput';
import { audioEngineTargets, defaultAudioDeviceName } from '../settings.mjs';
import { audioEngineTargets } from '../settings.mjs';
import { useStore } from '@nanostores/react';
import { prebake } from './prebake.mjs';
import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs';
@@ -36,11 +36,15 @@ import './Repl.css';
import { setInterval, clearInterval } from 'worker-timers';
import { getMetadata } from '../metadata_parser';
const { latestCode } = settingsMap.get();
const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get();
let modulesLoading, presets, drawContext, clearCanvas, audioReady;
if (typeof window !== 'undefined') {
audioReady = initAudioOnFirstClick();
audioReady = initAudioOnFirstClick({
maxPolyphony,
audioDeviceName,
multiChannelOrbits: parseBoolean(multiChannelOrbits),
});
modulesLoading = loadModules();
presets = prebake();
drawContext = getDrawContext();
@@ -159,20 +163,6 @@ export function useReplContext() {
editorRef.current?.updateSettings(editorSettings);
}, [_settings]);
// on first load, set stored audio device if possible
useEffect(() => {
const { audioDeviceName } = _settings;
if (audioDeviceName !== defaultAudioDeviceName) {
getAudioDevices().then((devices) => {
const deviceID = devices.get(audioDeviceName);
if (deviceID == null) {
return;
}
setAudioDevice(deviceID);
});
}
}, []);
//
// UI Actions
//
+3 -34
View File
@@ -1,6 +1,6 @@
import { evalScope, hash2code, logger } from '@strudel/core';
import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs';
import { getAudioContext, initializeAudioOutput, setDefaultAudioContext, setVersionDefaults } from '@strudel/webaudio';
import { settingPatterns } from '../settings.mjs';
import { setVersionDefaults } from '@strudel/webaudio';
import { getMetadata } from '../metadata_parser';
import { isTauri } from '../tauri.mjs';
import './Repl.css';
@@ -81,6 +81,7 @@ export function loadModules() {
import('@strudel/soundfonts'),
import('@strudel/csound'),
import('@strudel/tidal'),
import('@strudel/gamepad'),
import('@strudel/motion'),
import('@strudel/mqtt'),
];
@@ -158,38 +159,6 @@ export const isUdels = () => {
return window.top?.location?.pathname.includes('udels');
};
export const getAudioDevices = async () => {
await navigator.mediaDevices.getUserMedia({ audio: true });
let mediaDevices = await navigator.mediaDevices.enumerateDevices();
mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default');
const devicesMap = new Map();
devicesMap.set(defaultAudioDeviceName, '');
mediaDevices.forEach((device) => {
devicesMap.set(device.label, device.deviceId);
});
return devicesMap;
};
export const setAudioDevice = async (id) => {
let audioCtx = getAudioContext();
if (audioCtx.sinkId === id) {
return;
}
await audioCtx.suspend();
await audioCtx.close();
audioCtx = setDefaultAudioContext();
await audioCtx.resume();
const isValidID = (id ?? '').length > 0;
if (isValidID) {
try {
await audioCtx.setSinkId(id);
} catch {
logger('failed to set audio interface', 'warning');
}
}
initializeAudioOutput();
};
export function setVersionDefaultsFrom(code) {
try {
const metadata = getMetadata(code);
+4 -4
View File
@@ -3,8 +3,6 @@ import { useStore } from '@nanostores/react';
import { register } from '@strudel/core';
import { isUdels } from './repl/util.mjs';
export const defaultAudioDeviceName = 'System Standard';
export const audioEngineTargets = {
webaudio: 'webaudio',
osc: 'osc',
@@ -36,10 +34,11 @@ export const defaultSettings = {
isPanelOpen: true,
togglePanelTrigger: 'click', //click | hover
userPatterns: '{}',
audioDeviceName: defaultAudioDeviceName,
audioEngineTarget: audioEngineTargets.webaudio,
isButtonRowHidden: false,
isCSSAnimationDisabled: false,
maxPolyphony: 128,
multiChannelOrbits: false,
};
let search = null;
@@ -52,7 +51,7 @@ const settings_key = `strudel-settings${instance > 0 ? instance : ''}`;
export const settingsMap = persistentMap(settings_key, defaultSettings);
const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false);
export const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false);
export function useSettings() {
const state = useStore(settingsMap);
@@ -83,6 +82,7 @@ export function useSettings() {
isPanelPinned: parseBoolean(state.isPanelPinned),
isPanelOpen: parseBoolean(state.isPanelOpen),
userPatterns: userPatterns,
multiChannelOrbits: parseBoolean(state.multiChannelOrbits),
};
}