Compare commits

...

172 Commits

Author SHA1 Message Date
Jade (Rose) Rowland f17a4d045f Publish
- @strudel/codemirror@1.2.3
 - @strudel/core@1.2.3
 - @strudel/csound@1.2.4
 - @strudel/draw@1.2.3
 - @strudel/embed@1.1.1
 - @strudel/gamepad@1.2.3
 - @strudel/hydra@1.2.3
 - @strudel/midi@1.2.4
 - @strudel/mini@1.2.3
 - mondolang@1.1.1
 - @strudel/mondo@1.1.1
 - @strudel/motion@1.2.3
 - @strudel/mqtt@1.2.3
 - @strudel/osc@1.2.3
 - @strudel/reference@1.2.1
 - @strudel/repl@1.2.4
 - @strudel/serial@1.2.3
 - @strudel/soundfonts@1.2.4
 - superdough@1.2.4
 - @strudel/tonal@1.2.3
 - @strudel/transpiler@1.2.3
 - @strudel/web@1.2.4
 - @strudel/webaudio@1.2.4
 - @strudel/xen@1.2.3
2025-08-18 11:46:29 -04:00
Alex McLean 8e1e83f3cf Merge pull request 'euclidish' (#1482) from euclidish into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1482
2025-08-17 23:18:45 +02:00
alex 2d64b538e7 Merge branch 'main' into euclidish 2025-08-17 22:16:58 +01:00
alex b717615558 optimise euclidish, creating new morph function in the process 2025-08-17 22:13:26 +01:00
Switch Angel AKA Jade Rose 8c4f10464b Merge pull request 'feat: Create a duck (sidechain) orbit effect' (#1470) from pump into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1470
2025-08-17 06:37:22 +02:00
Jade (Rose) Rowland 018649c4f3 fix test 2025-08-16 23:42:14 -04:00
Jade (Rose) Rowland 5ff59a878d fix lookahead 2025-08-16 23:38:03 -04:00
Jade (Rose) Rowland a20468b85f minor changes 2025-08-16 23:19:34 -04:00
Jade (Rose) Rowland 9648e35de2 Merge branch 'main' into pump 2025-08-16 23:06:36 -04:00
Switch Angel AKA Jade Rose cd344cefe0 Merge pull request 'Bug Fix: Restore log functionality after onTrigger arg removal' (#1491) from glossing/strudel:glossing/fix-log into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1491
Reviewed-by: Switch Angel AKA Jade Rose <daslyfe@noreply.codeberg.org>
2025-08-17 04:40:14 +02:00
Switch Angel AKA Jade Rose 1f6a313f02 Merge pull request 'Bug Fix: Handle zero appearing first in FM's ADSR' (#1496) from glossing/strudel:glossing/fix-fm-with-adsr-zeros into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1496
Reviewed-by: Switch Angel AKA Jade Rose <daslyfe@noreply.codeberg.org>
2025-08-17 04:33:38 +02:00
Switch Angel AKA Jade Rose 5cbeb227d8 Merge pull request 'Bug Fix: FM (and vibrato) for Supersaws' (#1495) from glossing/strudel:glossing/fix-fm-for-supersaw into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1495
Reviewed-by: Switch Angel AKA Jade Rose <daslyfe@noreply.codeberg.org>
2025-08-17 04:27:18 +02:00
Aria 2ab2cc5fc8 Add example tests 2025-08-15 13:27:08 -05:00
Aria 3dd667e825 Fix for a zero appearing (first) in FM's ADSR 2025-08-10 17:11:59 -05:00
Alex McLean 482ebb695a Merge branch 'main' into euclidish 2025-08-10 20:49:27 +02:00
Aria 218952b8b6 Use the per-sample frequency and properly wrap polyBlep 2025-08-09 16:25:11 -05:00
Aria 2e8a8b50df Seemingly working version of supersaw FM and light cleanup 2025-08-08 22:27:05 -05:00
Aria f22f89347e Final docstring cleanup & adding hap tag to logValues for consistency 2025-08-05 22:40:11 -05:00
Aria de15d79edf Add doscstrings and move stringifyValues into its own function for reuse 2025-08-05 22:29:59 -05:00
Aria 23e5358ae4 Formatting 2025-08-05 20:46:00 -05:00
Aria 3b6777e259 Fix signature of functions in log and logValues and add tests 2025-08-05 19:05:17 -05:00
Alex McLean ed5bf3b3fc Merge pull request 'Fix incorrect stack Mini Notation' (#1484) from samyk/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1484
2025-08-04 07:51:29 +02:00
daslyfe b089328da3 Merge pull request 'fix: regression caused by incorrectly exported alias for transpose and scaleTranspose' (#1489) from fixtest into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1489
2025-08-03 18:57:45 +02:00
daslyfe 784c711870 Merge branch 'main' into pump 2025-08-03 18:57:19 +02:00
Jade (Rose) Rowland 34b10b6c84 codeformat 2025-08-03 12:56:16 -04:00
Jade (Rose) Rowland 450f9685f3 fixed export 2025-08-03 12:54:19 -04:00
daslyfe 03090e399c Merge pull request 'Add strans and scaleTrans as synonyms of scaleTranspose' (#1488) from TodePond/strudel:lu/strans into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1488
2025-08-03 18:34:12 +02:00
Lu Wilson ed58d26da6 Merge branch 'main' into lu/strans 2025-08-03 16:14:33 +02:00
daslyfe e3605590e5 Merge pull request 'Add trans alias for transpose' (#1487) from TodePond/strudel:lu/trans into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1487
2025-08-02 18:18:25 +02:00
Lu[ke] Wilson d55ef8e843 add strans too 2025-08-02 10:26:36 +01:00
Lu[ke] Wilson efc2333360 add synonyms for scaleTranspose 2025-08-02 10:25:28 +01:00
Lu Wilson 92626e7c20 Merge branch 'main' into lu/trans 2025-08-02 11:18:37 +02:00
Lu[ke] Wilson ad113b3888 document ability to specify an array of names 2025-08-02 10:17:48 +01:00
Lu[ke] Wilson 39c45bc2b2 pass an array of names instead 2025-08-02 10:17:33 +01:00
daslyfe 899af88bd9 Merge branch 'main' into pump 2025-08-02 05:32:13 +02:00
Jade (Rose) Rowland c2b8cc4b82 webtimeout 2025-08-01 23:29:50 -04:00
daslyfe 40ed7b5382 Merge pull request 'feat: Create Accurate 909 bass drum synthesizer' (#1478) from 909bd into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1478
2025-08-02 03:27:24 +02:00
daslyfe 5d829419ba Merge branch 'main' into 909bd 2025-08-02 03:24:29 +02:00
daslyfe e26da854c0 Merge pull request 'feat: Add fmwave control and ability to fm with noise' (#1472) from noisefm into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1472
2025-08-02 03:23:48 +02:00
daslyfe a28b7a2d5a Merge branch 'main' into noisefm 2025-08-02 02:49:15 +02:00
daslyfe 83c776d70f Merge branch 'main' into 909bd 2025-08-02 02:49:00 +02:00
Lu[ke] Wilson 051efdc13b flyby improvement: add dec synonym to docs 2025-08-01 09:39:33 +01:00
Lu[ke] Wilson ef3cd56b0d add synonym to docs 2025-08-01 09:39:21 +01:00
Lu[ke] Wilson 9017085fa3 add trans alias for transpose 2025-08-01 09:36:36 +01:00
daslyfe cdbbe079db Merge pull request 'hotfix: uzu kit JSON should be in the repo to avoid offline loading errors' (#1485) from samplemaphotfix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1485
2025-07-30 00:22:40 +02:00
Jade (Rose) Rowland d6045c943b working 2025-07-29 18:20:13 -04:00
samyk 6ee9dea2ff Fix incorrect stack Mini Notation 2025-07-28 20:26:34 +02:00
alex 03cbe4b6d4 delint 2025-07-28 15:48:09 +02:00
alex 784ee576da fixes 2025-07-28 15:33:18 +02:00
alex ee3e1217b4 add euclidish 2025-07-28 15:24:19 +02:00
Jade (Rose) Rowland 30c80119e9 works 2025-07-28 01:20:42 -04:00
Jade (Rose) Rowland 973ae4c7da change 909bd to sbd 2025-07-27 15:02:00 -04:00
Jade (Rose) Rowland 4d13b981f1 variable srate 2025-07-27 01:28:50 -04:00
Jade (Rose) Rowland 7691403fd0 prevent clicks 2025-07-27 01:22:59 -04:00
Jade (Rose) Rowland 3b6e3624be working 2025-07-27 01:07:45 -04:00
Jade (Rose) Rowland 51f0bab1f8 duckarray 2025-07-26 12:54:59 -04:00
Jade (Rose) Rowland dae1560c40 description 2025-07-25 00:00:23 -04:00
Jade (Rose) Rowland 82f0dbb750 add another example 2025-07-24 23:59:38 -04:00
Jade (Rose) Rowland fb7d76a2ab working 2025-07-24 23:50:27 -04:00
Jade (Rose) Rowland 26225e4ea7 Merge branch 'main' into pump 2025-07-23 23:40:48 -04:00
daslyfe ab12c6ae0a Merge pull request 'feat: make uzu-drumkit the default drumkit' (#1422) from jade/uzudrumkit into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1422
2025-07-24 05:28:23 +02:00
Jade (Rose) Rowland 4a894b0be2 frmt 2025-07-23 23:25:14 -04:00
Jade (Rose) Rowland 4ad305550e Merge branch 'jade/uzudrumkit' of ssh://codeberg.org/uzu/strudel into jade/uzudrumkit 2025-07-23 23:16:39 -04:00
Jade (Rose) Rowland cf5c502a05 rm unessecary json 2025-07-23 23:16:14 -04:00
Jade (Rose) Rowland 2415210be6 fix lint 2025-07-23 18:41:20 -04:00
Jade (Rose) Rowland fe18aa770d test 2025-07-23 18:34:34 -04:00
daslyfe e136e95375 Merge branch 'main' into jade/uzudrumkit 2025-07-23 03:06:08 +02:00
daslyfe 004c0baa59 Merge pull request 'FIX: PWM modulation' (#1467) from pwfix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1467
2025-07-21 07:16:17 +02:00
daslyfe 5f44ae9c4b Merge branch 'main' into pwfix 2025-07-21 07:13:55 +02:00
Jade (Rose) Rowland 66828e17d3 working 2025-07-21 01:11:04 -04:00
Jade (Rose) Rowland b2f90ecb3b fix def skew 2025-07-20 23:56:30 -04:00
Jade (Rose) Rowland ccf2a6de52 fixing.. 2025-07-20 23:38:16 -04:00
Jade (Rose) Rowland 9416f317d4 fixing... 2025-07-20 23:29:01 -04:00
Jade (Rose) Rowland 75b6f59212 fix 2025-07-20 23:26:17 -04:00
daslyfe 84238c8999 Merge pull request 'Tremolo modulation' (#1095) from daslyfe/gainmod into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1095
2025-07-19 17:16:21 +02:00
daslyfe 2d855cf5e8 Merge branch 'main' into daslyfe/gainmod 2025-07-19 04:06:31 +02:00
Jade (Rose) Rowland dd04d49c75 fix test 2025-07-18 22:04:18 -04:00
Jade (Rose) Rowland 34874862ab change params 2025-07-18 22:03:33 -04:00
froos 0361bc629f Merge pull request 'fix: can now use def in mondough' (#1461) from mondo-def-fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1461
2025-07-15 09:39:32 +02:00
Felix Roos 1fdd87a4f3 doc: def 2025-07-15 09:35:24 +02:00
Felix Roos 16e9aca222 fix: can now use def in mondough 2025-07-15 09:33:14 +02:00
froos 4a3d5ca44f Merge pull request 'fixed keybinding presedence issue' (#1456) from Dsm0/strudel:keyboard_binding_presedence_fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1456
2025-07-11 11:10:23 +02:00
froos 6c0e8a0833 Merge pull request 'added multicursor support on ctrl/cmd + click' (#1457) from Dsm0/strudel:multicursor-on-click into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1457
2025-07-11 11:09:04 +02:00
Jade (Rose) Rowland 7336ff0ded Merge branch 'daslyfe/gainmod' of ssh://codeberg.org/uzu/strudel into daslyfe/gainmod 2025-07-10 23:19:19 -04:00
Jade (Rose) Rowland 735f57e276 working 2025-07-10 23:18:57 -04:00
Dsm0 eb4f1f69f6 pnpm codeformat 2025-07-10 17:28:56 -07:00
Dsm0 e7e321fe03 added multicursor support on ctrl/cmd + click 2025-07-10 15:03:22 -07:00
Dsm0 bf3de368f0 added codemirror as an option + removed fallback on defaults 2025-07-10 12:11:10 -07:00
Dsm0 d645cc6fe5 fixed keybinding presedence issue 2025-07-09 13:28:22 -07:00
froos 6fe11be218 Merge pull request 'added tab-indent setting' (#1454) from Dsm0/strudel:tab-indentation into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1454
2025-07-09 09:39:45 +02:00
Felix Roos cf7fc23981 add new am examples to doc 2025-07-09 09:33:28 +02:00
Felix Roos bfdfab0a00 fix: @synonym -> @synonyms 2025-07-09 09:33:11 +02:00
Jade (Rose) Rowland edb347e85a format 2025-07-08 22:24:54 -04:00
Jade (Rose) Rowland 63fcce79e0 default cycle 2025-07-08 21:24:27 -04:00
Dsm0 8defdc424c added tab-indent setting 2025-07-08 17:57:24 -07:00
Felix Roos 16b2fc0632 Merge remote-tracking branch 'origin/main' into daslyfe/gainmod 2025-07-08 15:49:39 +02:00
froos d0a544d7a7 Merge pull request 'ontrigger-refactoring' (#1442) from ontrigger-refactoring into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1442
2025-07-08 15:48:40 +02:00
Jade (Rose) Rowland 9b3c6b9f94 c 2025-07-08 00:46:39 -04:00
Jade (Rose) Rowland 6327e17feb remove unessecary cyclist changes 2025-07-08 00:44:57 -04:00
Jade (Rose) Rowland 15fe2734e9 Merge branch 'daslyfe/gainmod' of ssh://codeberg.org/uzu/strudel into daslyfe/gainmod 2025-07-08 00:30:40 -04:00
Jade (Rose) Rowland 1dd1ce254f rm deadcode 2025-07-08 00:30:00 -04:00
daslyfe 3ea91adfd6 Merge branch 'main' into daslyfe/gainmod 2025-07-08 06:29:27 +02:00
Jade (Rose) Rowland ce5f425142 fully implemented 2025-07-08 00:26:15 -04:00
Felix Roos 671a22fc22 chore: add link to contributors 2025-07-07 23:28:41 +02:00
froos 976955dae3 Merge pull request 'fix: remove first gm_synth_bass_1, as it doesn't work in safari' (#1452) from gm_synth_bass_1 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1452
2025-07-07 23:14:24 +02:00
Felix Roos a6ad58a375 fix: remove first gm_synth_bass_1, as it doesn't work in safari 2025-07-07 23:12:15 +02:00
Felix Roos 681eed3d0e revert add logo 2025-07-07 22:23:40 +02:00
Felix Roos 81a9811803 add logo 2025-07-07 22:23:16 +02:00
Felix Roos 72b582c605 fix: remove github contributor avatars 2025-07-07 22:20:04 +02:00
froos 2f0891d4e5 Merge pull request 'fix: minor doc changes' (#1435) from Aurel300/strudel:fix/minor-docs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1435
2025-07-07 00:06:15 +02:00
Felix Roos a7a283a99e doc: add version tagging guide to contributing.md 2025-07-06 17:00:33 +02:00
Jade (Rose) Rowland d9e44dcda6 sync without cyclist changes 2025-07-06 00:59:39 -04:00
Alex McLean acd5661106 Merge pull request 'Fix randrun and deps including shuffle. Fixes #1441' (#1447) from fix-randrun into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1447
2025-07-05 12:53:44 +02:00
froos b0b2b6a120 Merge pull request 'add setDefault + resetDefaults to superdough' (#1433) from set-default-default into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1433
2025-07-05 12:15:51 +02:00
Felix Roos 664d76cf1b resetDefaults when editor is reset 2025-07-05 11:32:16 +02:00
alex a901e2e387 snapshot 2025-07-05 10:28:20 +01:00
Felix Roos 82b14b5292 fix: lint 2025-07-05 11:13:40 +02:00
alex 05bcd5e32f Fix randrun and deps including shuffle. Fixes #1441 2025-07-05 09:01:48 +01:00
Jade (Rose) Rowland 077556e7cb Merge remote-tracking branch 'origin/ontrigger-refactoring' into daslyfe/gainmod 2025-07-05 00:45:58 -04:00
Jade (Rose) Rowland 4cc2de9b7e wip 2025-07-05 00:39:23 -04:00
froos b0c0a393a1 Merge pull request 'disable fm for supersaw' (#1443) from supersaw-no-fm into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1443
2025-07-04 22:36:18 +02:00
Felix Roos 168269a839 disable fm for supersaw 2025-07-04 22:29:18 +02:00
Felix Roos 34771f03d4 some clarification comments 2025-07-04 22:17:23 +02:00
Aurel Bílý 504fe9ed40 document setcps in cycles reference 2025-07-01 13:45:36 +02:00
froos caf6686d01 Merge pull request 'corrected minor spelling mistake (#1425)' (#1434) from tj-mueller/strudel_1425:spelling_mistake_1425 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1434
2025-06-30 13:28:09 +02:00
tj-mueller c063fb6b1c correct rd to cr 2025-06-30 11:51:39 +02:00
Felix Roos e3680b96de refactor: remove first param of all onTrigger calls 2025-06-30 10:18:54 +02:00
Jade (Rose) Rowland 688e275e0c merge 2025-06-30 00:50:02 -04:00
Felix Roos 2e33569786 add setDefault + resetDefaults to superdough 2025-06-30 05:48:08 +02:00
froos 46fe6d3c4d Merge pull request 'Add browser cache explanation in samples.mdx' (#1369) from wyan/patch-1 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1369
2025-06-30 05:26:10 +02:00
froos 9220b95677 Merge branch 'main' into wyan/patch-1 2025-06-30 05:24:26 +02:00
froos 5b27b19972 Merge pull request 'add-release-notes' (#1432) from add-release-notes into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1432
2025-06-30 05:18:41 +02:00
Jade Rose db413881db Merge pull request 'feat: delaytime in cycles' (#1341) from daslyfe/jade/delaycycle into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1341
2025-06-28 20:30:48 +02:00
Jade (Rose) Rowland 77bab433e0 cf 2025-06-28 14:06:16 -04:00
daslyfe f8cc1c2022 Merge branch 'main' into daslyfe/jade/delaycycle 2025-06-28 20:01:57 +02:00
froos e0bf9ea4d6 Merge pull request 'Case insensitive search in the reference tab' (#1420) from kdiab/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1420
2025-06-28 13:44:47 +02:00
Alex McLean 374661eb23 Merge pull request 'add unjoin, into and chunkinto' (#1418) from add-unjoin into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1418
2025-06-28 00:04:12 +02:00
alex df06248c54 snapshot 2025-06-27 22:59:01 +01:00
alex 27073d6c17 format 2025-06-27 22:56:19 +01:00
alex f44caf9096 tests and tweaks, and add chunkBackInto 2025-06-27 22:51:47 +01:00
alex ff5b11f5ed test + bugfix for chunkinto 2025-06-27 22:44:15 +01:00
Alex McLean 719c70a598 Merge branch 'main' into add-unjoin 2025-06-27 22:40:40 +02:00
Jade (Rose) Rowland 7a53f6c021 add test 2025-06-27 15:56:37 -04:00
Jade (Rose) Rowland 09d05abd70 delaysync 2025-06-27 15:54:39 -04:00
Jade (Rose) Rowland 3acc364a03 Merge branch 'main' into daslyfe/jade/delaycycle 2025-06-27 15:53:48 -04:00
Jade (Rose) Rowland 2dd528134a fix json 2025-06-27 02:52:04 -04:00
Jade (Rose) Rowland fef50e654c working 2025-06-27 02:38:43 -04:00
Khalid e19fd24447 Case insensitive search in the reference tab 2025-06-25 08:44:12 -04:00
alex fe9a91d1e4 add unjoin, into and chunkinto 2025-06-24 23:22:42 +01:00
Alice Wyan 1b7fbecf50 Fix style issue 2025-06-10 13:20:21 +02:00
Alice Wyan 117cf7e18a Update samples.mdx 2025-06-09 19:45:21 +02:00
Alice Wyan ae15bb7274 Add browser cache explanation in samples.mdx
Several users (including me) have been bitten by this, so it's perhaps worth mentioning in the docs that the strudel.json file gets cached by the browser and doesn't reload properly unless forced.
2025-06-09 15:03:41 +02:00
Jade (Rose) Rowland 94257e81ee lint 2025-05-02 23:44:55 -04:00
Jade (Rose) Rowland 006ce9d1da delaytime 2025-05-02 23:33:58 -04:00
Jade (Rose) Rowland bee8c81254 simplify targettime 2024-05-13 23:14:30 -04:00
Jade (Rose) Rowland 8ad48e1633 remove unesseccary time call 2024-05-13 23:02:48 -04:00
Jade (Rose) Rowland 4a181d5b33 updated non synced clock to work more like synced clock 2024-05-13 23:02:36 -04:00
Jade (Rose) Rowland d5ab6b7211 fix tests 2024-05-13 22:38:20 -04:00
Jade (Rose) Rowland 4ba9d47bff fix test 2024-05-13 21:10:43 -04:00
Jade (Rose) Rowland c6c5a3035c fixed non synced clock 2024-05-13 21:06:24 -04:00
Jade (Rose) Rowland e024ae2142 almost figured it out 2024-05-13 20:42:59 -04:00
Jade (Rose) Rowland 19df19375d kinda fixed it, need to find cause of incorrect cps calculation 2024-05-13 10:33:18 -04:00
Jade (Rose) Rowland cba96049a3 fixed duplicate variable 2024-05-10 00:03:52 -04:00
Jade (Rose) Rowland 1509ff1793 Merge branch 'tidalcycles:main' into gainmod 2024-05-09 23:55:13 -04:00
Jade (Rose) Rowland 6a33032da7 formatting 2024-05-09 23:26:04 -04:00
Jade (Rose) Rowland 6c05a4eb7b fixed tri shape 2024-05-09 00:21:21 -04:00
Jade (Rose) Rowland 16472b59dd fix cyclist clock cycle 2024-05-08 00:05:05 -04:00
Jade (Rose) Rowland 7df663e87d memory leaking 2024-05-05 23:54:30 -04:00
Jade (Rose) Rowland 3159f66560 working 2024-05-05 22:52:18 -04:00
Jade (Rose) Rowland 0d900c0134 sine test 2024-05-05 12:02:51 -04:00
Jade (Rose) Rowland 66a27a609c implementing 2024-05-03 14:58:20 -04:00
Jade (Rose) Rowland 496d60dc1c testing 2024-05-03 14:56:16 -04:00
79 changed files with 2020 additions and 270 deletions
+76
View File
@@ -150,6 +150,7 @@ Important: Always publish with `pnpm`, as `npm` does not support overriding main
## useful commands
```sh
#regenerate the test snapshots (ex: when updating or creating new pattern functions)
pnpm snapshot
@@ -160,6 +161,81 @@ pnpm run osc
#build the standalone version
pnpm tauri build
```
## version tag patching
here's a little guide on how to patch patterns in the database to prevent breaking old patterns due to breaking changes in newer versions.
the general tactic is to use `// @version x.y` to tag a pattern with a specific strudel version. when a pattern is evaluated, this metadata will de-activate any breaking changes that came after the specified version.
for example, in version 1.1, the default value for `fanchor` was changed from `0.5` to `0`.
if play a pattern that was made before that change, sounds that use filter evenlopes can sound very different, so by adding `// @version 1.0` will make it sound like it used to.
before releasing a new version with breaking changes, we can edit all patterns in the database, inserting the version tag they were created under:
as an example, to release version 1.2, do the following:
1. get date range
```sh
# get date of last version:
git log -1 --format=%aI @strudel/core@1.1.0
# 2024-05-31T23:07:26+02:00
# get date of current version:
git log -1 --format=%aI @strudel/core@1.2.0
# 2025-05-01T12:39:24+02:00
# might also use todays timestamp if version is not yet released
```
now we know, all patterns between these 2 dates have to receive a version tag (unless they already have one).
2. get patterns in question
```sql
SELECT *
FROM code_v1
WHERE code NOT LIKE '%@version%'
AND created_at > '2024-05-31T23:07:26+02:00'
AND created_at < '2025-05-01T12:39:24+02:00'
ORDER BY created_at ASC;
```
this gives us all unversioned patterns that were saved between 1.1.0 and 1.2.0. in this case, it's 9373 patterns!
3. insert version tags
we are now ready to insert the version tag to these patterns.
before updating thousands of patterns, it's probably a good idea to test if a single one gets udpated:
```sql
UPDATE code_v1
SET code = code || E'\n// @version 1.1'
WHERE hash = 'Ns2sMB40yIw4';
```
after [verifying](https://strudel.cc/?Ns2sMB40yIw4) that the version tag has been added, let's insert it everywhere:
```sql
UPDATE code_v1
SET code = code || E'\n// @version 1.1'
WHERE code NOT LIKE '%@version%'
AND created_at > '2024-05-31T23:07:26+02:00'
AND created_at < '2025-05-01T12:39:24+02:00'
```
4. verify
we can verify that the edits worked by querying all patterns that contain the new version tag:
```sql
SELECT *
FROM code_v1
WHERE code LIKE '%@version 1.1%'
AND created_at > '2024-05-31T23:07:26+02:00'
AND created_at < '2025-05-01T12:39:24+02:00'
ORDER BY created_at ASC;
```
## Have Fun
Remember to have fun, and that this project is driven by the passion of volunteers!
+1 -7
View File
@@ -38,13 +38,7 @@ Licensing info for the default sound banks can be found over on the [dough-sampl
## Contributing
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
<a href="https://codeberg.org/uzu/strudel/activity/contributors">
<img src="https://contrib.rocks/image?repo=tidalcycles/strudel" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). You can find the full list of contributors [here](https://codeberg.org/uzu/strudel/activity/contributors).
## Community
+10
View File
@@ -83,4 +83,14 @@ export default [
],
},
},
{
// Properties provided by AudioWorkletGlobalScope
files: ['packages/superdough/worklets.mjs'],
languageOptions: {
globals: {
currentTime: 'readonly',
sampleRate: 'readonly',
},
},
},
];
+1 -1
View File
@@ -1,5 +1,5 @@
/*
jsdoc-synonyms.js - Add support for @synonym tag
jsdoc-synonyms.js - Add support for @synonyms tag
Copyright (C) 2023 Strudel contributors - see <https://codeberg.org/uzu/strudel/activity/contributors>
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/>.
*/
+11 -1
View File
@@ -1,7 +1,7 @@
import { closeBrackets } from '@codemirror/autocomplete';
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
// import { search, highlightSelectionMatches } from '@codemirror/search';
import { history } from '@codemirror/commands';
import { history, indentWithTab } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
@@ -37,6 +37,14 @@ const extensions = {
isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
isFlashEnabled,
keybindings,
isTabIndentationEnabled: (on) => (on ? keymap.of([indentWithTab]) : []),
isMultiCursorEnabled: (on) =>
on
? [
EditorState.allowMultipleSelections.of(true),
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
]
: [],
};
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
@@ -51,6 +59,8 @@ export const defaultSettings = {
isFlashEnabled: true,
isTooltipEnabled: false,
isLineWrappingEnabled: false,
isTabIndentationEnabled: false,
isMultiCursorEnabled: false,
theme: 'strudelTheme',
fontFamily: 'monospace',
fontSize: 18,
+2 -2
View File
@@ -21,11 +21,11 @@ const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
const keymaps = {
vim,
emacs,
codemirror: () => keymap.of(defaultKeymap),
vscode: vscodeExtension,
};
export function keybindings(name) {
const active = keymaps[name];
return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []];
// keymap.of(searchKeymap),
return [active ? active() : [], keymap.of(historyKeymap)];
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "1.2.2",
"version": "1.2.3",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
+140 -14
View File
@@ -252,6 +252,20 @@ export const { fmenv } = registerControl('fmenv');
*
*/
export const { fmattack } = registerControl('fmattack');
/**
* waveform of the fm modulator
*
* @name fmwave
* @param {number | Pattern} wave waveform
* @example
* n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("<sine square sawtooth crackle>").fm(4).fmh(2.01)
* @example
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6)
*
*/
export const { fmwave } = registerControl('fmwave');
/**
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
*
@@ -306,6 +320,7 @@ export const { fft } = registerControl('fft');
*
* @name decay
* @param {number | Pattern} time decay time in seconds
* @synonyms dec
* @example
* note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0)
*
@@ -444,6 +459,78 @@ export const { crush } = registerControl('crush');
*/
export const { coarse } = registerControl('coarse');
/**
* modulate the amplitude of a sound with a continuous waveform
*
* @name tremolo
* @synonyms trem
* @param {number | Pattern} speed modulation speed in HZ
* @example
* note("d d d# d".fast(4)).s("supersaw").tremolo("<3 2 100> ").tremoloskew("<.5>")
*
*/
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
/**
* modulate the amplitude of a sound with a continuous waveform
*
* @name tremolosync
* @synonyms tremsync
* @param {number | Pattern} cycles modulation speed in cycles
* @example
* note("d d d# d".fast(4)).s("supersaw").tremolosync("4").tremoloskew("<1 .5 0>")
*
*/
export const { tremolosync } = registerControl(
['tremolosync', 'tremolodepth', 'tremoloskew', 'tremolophase'],
'tremsync',
);
/**
* depth of amplitude modulation
*
* @name tremolodepth
* @synonyms tremdepth
* @param {number | Pattern} depth
* @example
* note("a1 a1 a#1 a1".fast(4)).s("pulse").tremsync(4).tremolodepth("<1 2 .7>")
*
*/
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
/**
* alter the shape of the modulation waveform
*
* @name tremoloskew
* @synonyms tremskew
* @param {number | Pattern} amount between 0 & 1, the shape of the waveform
* @example
* note("{f a c e}%16").s("sawtooth").tremsync(4).tremoloskew("<.5 0 1>")
*
*/
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
/**
* alter the phase of the modulation waveform
*
* @name tremolophase
* @synonyms tremphase
* @param {number | Pattern} offset the offset in cycles of the modulation
* @example
* note("{f a c e}%16").s("sawtooth").tremsync(4).tremolophase("<0 .25 .66>")
*
*/
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
/**
* shape of amplitude modulation
*
* @name tremoloshape
* @param {number | Pattern} shape tri | square | sine | saw | ramp
* @example
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
*
*/
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
/**
* filter overdrive for supported filter types
*
@@ -453,6 +540,42 @@ export const { coarse } = registerControl('coarse');
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
*
*/
/**
* modulate the amplitude of an orbit to create a "sidechain" like effect
*
* @name duckorbit
* @param {number | Pattern} orbit target orbit
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)
*
*/
export const { duck } = registerControl('duckorbit', 'duck');
/**
* the amount of ducking applied to target orbit
*
* @name duckdepth
* @param {number | Pattern} depth depth of modulation from 0 to 1
* @example
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>"))
*
*/
export const { duckdepth } = registerControl('duckdepth');
/**
* the attack time of the duck effect
*
* @name duckattack
* @param {number | Pattern} time
* @example
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1))
*
*/
export const { duckattack } = registerControl('duckattack', 'duckatt');
export const { drive } = registerControl('drive');
/**
@@ -985,15 +1108,31 @@ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback',
*
*/
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
/* // TODO: test
/**
* Sets the time of the delay effect in cycles.
*
* @name delaysync
* @param {number | Pattern} cycles delay length in cycles
* @synonyms delayt, dt
* @example
* s("bd bd").delay(.25).delaysync("<1 2 3 5>".div(8))
*
*/
export const { delaysync } = registerControl('delaysync');
/**
* Specifies whether delaytime is calculated relative to cps.
*
* @name lock
* @param {number | Pattern} enable When set to 1, delaytime is a direct multiple of a cycle.
* @superdirtOnly
* @example
* s("sd").delay().lock(1).osc()
*
*
*/
export const { lock } = registerControl('lock');
/**
* Set detune for stacked voices of supported oscillators
@@ -1545,18 +1684,6 @@ export const { density } = registerControl('density');
// ['modwheel'],
export const { expression } = registerControl('expression');
export const { sustainpedal } = registerControl('sustainpedal');
/* // TODO: doesn't seem to do anything
*
* Tremolo Audio DSP effect
*
* @name tremolodepth
* @param {number | Pattern} depth between 0 and 1
* @example
* n("0,4,7").tremolodepth("<0 .3 .6 .9>").osc()
*
*/
export const { tremolodepth, tremdp } = registerControl('tremolodepth', 'tremdp');
export const { tremolorate, tremr } = registerControl('tremolorate', 'tremr');
export const { fshift } = registerControl('fshift');
export const { fshiftnote } = registerControl('fshiftnote');
@@ -1633,7 +1760,6 @@ export const { zmod } = registerControl('zmod');
// like crush but scaled differently
export const { zcrush } = registerControl('zcrush');
export const { zdelay } = registerControl('zdelay');
export const { tremolo } = registerControl('tremolo');
export const { zzfx } = registerControl('zzfx');
/**
+3 -2
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import createClock from './zyklus.mjs';
import { logger } from './logger.mjs';
import { errorLogger, logger } from './logger.mjs';
export class Cyclist {
constructor({
@@ -67,6 +67,7 @@ export class Cyclist {
// the following line is dumb and only here for backwards compatibility
// see https://codeberg.org/uzu/strudel/pulls/1004
const deadline = targetTime - phase;
// this onTrigger has another signature
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
this.cps = hap.value.cps;
@@ -75,7 +76,7 @@ export class Cyclist {
}
});
} catch (e) {
logger(`[cyclist] error: ${e.message}`);
errorLogger(e);
onError?.(e);
}
},
+24 -1
View File
@@ -10,7 +10,7 @@ https://rohandrape.net/?t=hmt
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { timeCat, register, silence } from './pattern.mjs';
import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs';
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
import Fraction, { lcm } from './fraction.mjs';
@@ -196,3 +196,26 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
return _euclidLegato(pulses, steps, rotation, pat);
});
/**
* A 'euclid' variant with an additional parameter that morphs the resulting
* rhythm from 0 (no morphing) to 1 (completely 'even'). For example
* `sound("bd").euclidish(3,8,0)` would be the same as
* `sound("bd").euclid(3,8)`, and `sound("bd").euclidish(3,8,1)` would be the
* same as `sound("bd bd bd")`. `sound("bd").euclidish(3,8,0.5)` would have a
* groove somewhere between.
* Inspired by the work of Malcom Braff.
* @name euclidish
* @synonyms eish
* @memberof Pattern
* @param {number} pulses the number of onsets
* @param {number} steps the number of steps to fill
* @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse)
* @example
* sound("hh").euclidish(7,12,sine.slow(8))
* .pan(sine.slow(8))
*/
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc);
return pat.struct(morphed).setSteps(steps);
});
+2 -7
View File
@@ -4,6 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Fraction from './fraction.mjs';
import { stringifyValues } from './util.mjs';
export class Hap {
/*
@@ -148,13 +149,7 @@ export class Hap {
}
showWhole(compact = false) {
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
typeof this.value === 'object'
? compact
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
: JSON.stringify(this.value)
: this.value
}`;
return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`;
}
combineContext(b) {
+6
View File
@@ -4,6 +4,12 @@ let debounce = 1000,
lastMessage,
lastTime;
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
logger(`[${origin}] error: ${e.message}`);
}
export function logger(message, type, data = {}) {
let t = performance.now();
if (lastMessage === message && t - lastTime < debounce) {
-1
View File
@@ -11,7 +11,6 @@ export class NeoCyclist {
constructor({ onTrigger, onToggle, getTime }) {
this.started = false;
this.cps = 0.5;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.getTime = getTime; // get absolute time
this.time_at_last_tick_message = 0;
// the clock of the worker and the audio context clock can drift apart over time
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "1.2.2",
"version": "1.2.3",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
+146 -7
View File
@@ -21,6 +21,8 @@ import {
numeralArgs,
parseNumeral,
pairs,
zipWith,
stringifyValues,
} from './util.mjs';
import drawLine from './drawLine.mjs';
import { logger } from './logger.mjs';
@@ -852,14 +854,29 @@ export class Pattern {
);
}
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) {
/**
* Writes the content of the current event to the console (visible in the side menu).
* @name log
* @memberof Pattern
* @example
* s("bd sd").log()
*/
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
return this.onTrigger((...args) => {
logger(func(...args), undefined, getData(...args));
}, false);
}
logValues(func = id) {
return this.log((_, hap) => func(hap.value));
/**
* A simplified version of `log` which writes all "values" (various configurable parameters)
* within the event to the console (visible in the side menu).
* @name logValues
* @memberof Pattern
* @example
* s("bd sd").gain("0.25 0.5 1").n("2 1 0").logValues()
*/
logValues(func = (value) => `[hap] ${stringifyValues(value, true)}`) {
return this.log((hap) => func(hap.value));
}
//////////////////////////////////////////////////////////////////////
@@ -869,6 +886,31 @@ export class Pattern {
console.log(drawLine(this));
return this;
}
//////////////////////////////////////////////////////////////////////
// methods relating to breaking patterns into subcycles
// Breaks a pattern into a pattern of patterns, according to the structure of the given binary pattern.
unjoin(pieces, func = id) {
return pieces.withHap((hap) =>
hap.withValue((v) => (v ? func(this.ribbon(hap.whole.begin, hap.whole.duration)) : this)),
);
}
/**
* Breaks a pattern into pieces according to the structure of a given pattern.
* True values in the given pattern cause the corresponding subcycle of the
* source pattern to be looped, and for an (optional) given function to be
* applied. False values result in the corresponding part of the source pattern
* to be played unchanged.
* @name into
* @memberof Pattern
* @example
* sound("bd sd ht lt").into("1 0", hurry(2))
*/
into(pieces, func) {
return this.unjoin(pieces, func).innerJoin();
}
}
//////////////////////////////////////////////////////////////////////
@@ -1263,7 +1305,7 @@ export function sequenceP(pats) {
* @synonyms polyrhythm, pr
* @example
* stack("g3", "b3", ["e4", "d4"]).note()
* // "g3,b3,[e4,d4]".note()
* // "g3,b3,[e4 d4]".note()
*
* @example
* // As a chained function:
@@ -1544,7 +1586,7 @@ export const func = curry((a, b) => reify(b).func(a));
/**
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
*
* @param {string} name name of the function
* @param {string | string[]} name name of the function, or an array of names to be used as synonyms
* @param {function} func function with 1 or more params, where last is the current pattern
* @noAutocomplete
*
@@ -2494,6 +2536,37 @@ export const { fastchunk, fastChunk } = register(
true,
);
/**
* Like `chunk`, but the function is applied to a looped subcycle of the source pattern.
* @name chunkInto
* @synonyms chunkinto
* @memberof Pattern
* @example
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
* .bank("tr909")
*/
export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], function (n, func, pat) {
return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iterback(n), func);
});
/**
* Like `chunkInto`, but moves backwards through the chunks.
* @name chunkBackInto
* @synonyms chunkbackinto
* @memberof Pattern
* @example
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
* .bank("tr909")
*/
export const { chunkbackinto, chunkBackInto } = register(['chunkbackinto', 'chunkBackInto'], function (n, func, pat) {
return pat.into(
fastcat(true, ...Array(n - 1).fill(false))
._iter(n)
._early(1),
func,
);
});
// TODO - redefine elsewhere in terms of mask
export const bypass = register(
'bypass',
@@ -2509,7 +2582,7 @@ export const bypass = register(
* 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
* @synonyms rib
* @param {number} offset start point of loop in cycles
* @param {number} cycles loop length in cycles
* @example
@@ -3207,7 +3280,7 @@ export const slice = register(
* s("bd!8").onTriggerTime((hap) => {console.info(hap)})
*/
Pattern.prototype.onTriggerTime = function (func) {
return this.onTrigger((t_deprecate, hap, currentTime, cps = 1, targetTime) => {
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
const diff = targetTime - currentTime;
window.setTimeout(() => {
func(hap);
@@ -3344,3 +3417,69 @@ export const { beat } = register(
['beat'],
__beat((x) => x.innerJoin()),
);
export const _morph = (from, to, by) => {
by = Fraction(by);
const dur = Fraction(1).div(from.length);
const positions = (list) => {
const result = [];
for (const [pos, value] of list.entries()) {
if (value) {
result.push([Fraction(pos).div(list.length), value]);
}
}
return result;
};
const arcs = zipWith(
([posa, valuea], [posb, valueb]) => {
const b = by.mul(posb - posa).add(posa);
const e = b.add(dur);
return new TimeSpan(b, e);
},
positions(from),
positions(to),
);
function query(state) {
const cycle = state.span.begin.sam();
const cycleArc = state.span.cycleArc();
const result = [];
for (const whole of arcs) {
const part = whole.intersection(cycleArc);
if (part !== undefined) {
result.push(
new Hap(
whole.withTime((x) => x.add(cycle)),
part.withTime((x) => x.add(cycle)),
true,
),
);
}
}
return result;
}
return new Pattern(query).splitQueries();
};
/**
* Takes two binary rhythms represented as lists of 1s and 0s, and a number
* between 0 and 1 that morphs between them. The two lists should contain the same
* number of true values.
* @example
* sound("hh").struct(morph([1,0,1,0,1,0,1,0], // straight rhythm
* [1,1,0,1,0,1,0], // wonky rhythm
* 0.25 // creates a slightly wonky rhythm
* )
* )
* @example
* sound("hh").struct(morph("1:0:1:0:1:0:1:0", // straight rhythm
* "1:1:0:1:0:1:0", // wonky rhythm
* sine.slow(8) // slowly morph between the rhythms
* )
* )
*/
export const morph = (frompat, topat, bypat) => {
frompat = reify(frompat);
topat = reify(topat);
bypat = reify(bypat);
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
};
+4 -3
View File
@@ -1,7 +1,7 @@
import { NeoCyclist } from './neocyclist.mjs';
import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs';
import { errorLogger, logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
@@ -245,6 +245,7 @@ export function repl({
export const getTrigger =
({ getTime, defaultOutput }) =>
async (hap, deadline, duration, cps, t) => {
// ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
// TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
@@ -252,9 +253,9 @@ export const getTrigger =
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t);
await hap.context.onTrigger(hap, getTime(), cps, t);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
errorLogger(err, 'getTrigger');
}
};
+1 -1
View File
@@ -264,7 +264,7 @@ export const randrun = (n) => {
const rands = timeToRands(t.floor().add(0.5), n);
const nums = rands
.map((n, i) => [n, i])
.sort((a, b) => a[0] > b[0] - a[0] < b[0])
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
.map((x) => x[1]);
const i = t.cyclePos().mul(n).floor() % n;
return nums[i];
+1 -1
View File
@@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) {
}
export const speak = register('speak', function (lang, voice, pat) {
return pat.onTrigger((_, hap) => {
return pat.onTrigger((hap) => {
triggerSpeech(hap.value, lang, voice);
});
});
+74 -1
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import Fraction from 'fraction.js';
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import {
TimeSpan,
@@ -55,6 +55,8 @@ import {
expand,
} from '../index.mjs';
import { log, logValues } from '../pattern.mjs';
import { steady } from '../signal.mjs';
import { n, s } from '../controls.mjs';
@@ -1271,4 +1273,75 @@ describe('Pattern', () => {
);
});
});
describe('unjoin', () => {
it('destructures a pattern into subcycles', () => {
sameFirst(
fastcat('a', 'b', 'c', 'd')
.unjoin(fastcat(true, fastcat(true, true)))
.fmap(fast(2))
.join(),
fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd'),
);
});
});
describe('into', () => {
it('applies a function to subcycles of a pattern', () => {
sameFirst(
fastcat('a', 'b', 'c', 'd').into(fastcat(fastcat('true', 'true'), 'true'), fast(2)),
fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd'),
);
});
});
describe('chunkinto', () => {
it('chunks into subcycles', () => {
sameFirst(
fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3),
fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')),
);
});
});
describe('chunkbackinto', () => {
it('chunks into subcycles backwards', () => {
sameFirst(
fastcat('a', 'b', 'c').chunkBackInto(3, fast(2)).fast(3),
fastcat('a', 'b', fastcat('c', 'c'), 'a', fastcat('b', 'b'), 'c', fastcat('a', 'a'), 'b', 'c'),
);
});
});
describe('log', () => {
it('logs to console', () => {
const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
const pattern = pure('a').log();
const haps = pattern.queryArc(0, 1);
// Force a trigger
haps.forEach((hap) => {
hap.context?.onTrigger?.(hap);
});
expect(mockConsoleLog).toHaveBeenCalledWith(
'%c[hap] 0/1 → 1/1: a',
'background-color: black;color:white;border-radius:15px',
);
mockConsoleLog.mockRestore();
});
});
describe('logValues', () => {
it('logs values to console', () => {
const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
const pattern = pure('a').note('c#').logValues();
const haps = pattern.queryArc(0, 1);
// Force a trigger
haps.forEach((hap) => {
hap.context?.onTrigger?.(hap);
});
expect(mockConsoleLog).toHaveBeenCalledWith(
'%c[hap] value:a note:c#',
'background-color: black;color:white;border-radius:15px',
);
mockConsoleLog.mockRestore();
});
});
});
+1 -1
View File
@@ -72,7 +72,7 @@ export class TimeSpan {
}
intersection(other) {
// Intersection of two timespans, returns None if they don't intersect.
// Intersection of two timespans, returns undefined if they don't intersect.
const intersect_begin = this.begin.max(other.begin);
const intersect_end = this.end.min(other.end);
+10
View File
@@ -487,3 +487,13 @@ export function getCurrentKeyboardState() {
// }
// return lcm((x * y) / gcd(x, y), ...z);
// };
// Takes values -- typically derived from events, i.e. `hap`s -- and renders them
// into a readable format
export function stringifyValues(value, compact = false) {
return typeof value === 'object'
? compact
? JSON.stringify(value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
: JSON.stringify(value)
: value;
}
+3 -3
View File
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
instrument = instrument || 'triangle';
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
return pat.onTrigger((time_deprecate, hap, currentTime, _cps, targetTime) => {
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
if (!_csound) {
logger('[csound] not loaded yet', 'warning');
return;
@@ -142,7 +142,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
p1 = `"${instrument}"`;
}
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
return pat.onTrigger((tidal_time, hap) => {
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
if (!_csound) {
logger('[csound] not loaded yet', 'warning');
return;
@@ -151,7 +151,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
throw new Error('csound only support objects as hap values');
}
// Time in seconds counting from now.
const p2 = tidal_time - getAudioContext().currentTime;
const p2 = targetTime - currentTime;
const p3 = hap.duration.valueOf() + 0;
const frequency = getFrequency(hap);
let { gain = 1, velocity = 0.9 } = hap.value;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "1.2.3",
"version": "1.2.4",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -6,7 +6,7 @@ const OFF_MESSAGE = 0x80;
const CC_MESSAGE = 0xb0;
Pattern.prototype.midi = function (output) {
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
return this.onTrigger((hap, currentTime, cps, targetTime) => {
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
//magic number to get audio engine to line up, can probably be calculated somehow
const latencyMs = 34;
+1 -1
View File
@@ -4,7 +4,7 @@ import { Invoke } from './utils.mjs';
const collator = new ClockCollator({});
export async function oscTriggerTauri(t_deprecate, hap, currentTime, cps = 1, targetTime) {
export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
const controls = parseControlsFromHap(hap, cps);
const params = [];
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
"version": "1.2.2",
"version": "1.2.3",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/embed",
"version": "1.1.0",
"version": "1.1.1",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/gamepad",
"version": "1.2.2",
"version": "1.2.3",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
"version": "1.2.2",
"version": "1.2.3",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
+1 -1
View File
@@ -333,7 +333,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
return this.onTrigger((hap, currentTime, cps, targetTime) => {
if (!WebMidi.enabled) {
logger('Midi not enabled');
return;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "1.2.3",
"version": "1.2.4",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
"version": "1.2.2",
"version": "1.2.3",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mondolang",
"version": "1.1.0",
"version": "1.1.1",
"description": "a language for functional composition that translates to js",
"main": "mondo.mjs",
"type": "module",
+1
View File
@@ -42,6 +42,7 @@ lib['%'] = pace;
lib['?'] = degradeBy; // todo: default 0.5 not working..
lib[':'] = tail;
lib['..'] = range;
lib['def'] = () => silence;
lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]"
//lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mondo",
"version": "1.1.0",
"version": "1.1.1",
"description": "mondo notation for strudel",
"main": "mondough.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/motion",
"version": "1.2.2",
"version": "1.2.3",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mqtt",
"version": "1.2.2",
"version": "1.2.3",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
+1 -1
View File
@@ -60,7 +60,7 @@ export function parseControlsFromHap(hap, cps) {
const collator = new ClockCollator({});
export async function oscTrigger(t_deprecate, hap, currentTime, cps = 1, targetTime) {
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
const osc = await connect();
const controls = parseControlsFromHap(hap, cps);
const keyvals = Object.entries(controls).flat();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
"version": "1.2.2",
"version": "1.2.3",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/reference",
"version": "1.2.0",
"version": "1.2.1",
"description": "Headless reference of all strudel functions",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
"version": "1.2.3",
"version": "1.2.4",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -36,7 +36,7 @@ export async function prebake() {
samples(`${ds}/tidal-drum-machines.json`),
samples(`${ds}/piano.json`),
samples(`${ds}/Dirt-Samples.json`),
samples(`${ds}/EmuSP12.json`),
samples(`${ds}/uzu-drumkit.json`),
samples(`${ds}/vcsl.json`),
samples(`${ds}/mridangam.json`),
]);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
"version": "1.2.2",
"version": "1.2.3",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
+1 -1
View File
@@ -537,7 +537,7 @@ export default {
],
gm_synth_bass_1: [
// Synth Bass 1: Bass
'0380_Aspirin_sf2_file',
// '0380_Aspirin_sf2_file', // broken in safari https://codeberg.org/uzu/strudel/issues/1384
'0380_Chaos_sf2_file',
'0380_FluidR3_GM_sf2_file',
// 0380_GeneralUserGS_sf2_file // laut
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
"version": "1.2.3",
"version": "1.2.4",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -3,7 +3,7 @@ import { getAudioContext, registerSound } from '@strudel/webaudio';
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
Pattern.prototype.soundfont = function (sf, n = 0) {
return this.onTrigger((time_deprecate, h, ct, cps, targetTime) => {
return this.onTrigger((h, ct, cps, targetTime) => {
const ctx = getAudioContext();
const note = getPlayableNoteValue(h);
const preset = sf.presets[n % sf.presets.length];
+1 -1
View File
@@ -74,6 +74,6 @@ export const dough = async (code) => {
worklet.node.connect(ac.destination);
};
export function doughTrigger(time_deprecate, hap, currentTime, cps, targetTime) {
export function doughTrigger(hap, currentTime, cps, targetTime) {
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps });
}
+17 -5
View File
@@ -1,5 +1,8 @@
import { getAudioContext } from './superdough.mjs';
import { clamp, nanFallback } from './util.mjs';
import { getNoiseBuffer } from './noise.mjs';
export const noises = ['pink', 'white', 'brown', 'crackle'];
export function gainNode(value) {
const node = getAudioContext().createGain();
@@ -206,7 +209,8 @@ export function getVibratoOscillator(param, value, t) {
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
// a bit of a hack, but it works very well :)
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
const constantNode = audioContext.createConstantSource();
const constantNode = new ConstantSourceNode(audioContext);
constantNode.start(startTime);
constantNode.stop(stopTime);
constantNode.onended = () => {
@@ -216,9 +220,17 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
}
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = freq;
let osc;
if (noises.includes(type)) {
osc = ctx.createBufferSource();
osc.buffer = getNoiseBuffer(type, 2);
osc.loop = true;
} else {
osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = freq;
}
osc.start();
const g = new GainNode(ctx, { gain: range });
osc.connect(g); // -range, range
@@ -253,7 +265,7 @@ export function applyFM(param, value, begin) {
modulator = fmmod.node;
stop = fmmod.stop;
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) {
// no envelope by default
modulator.connect(param);
} else {
+6
View File
@@ -1,5 +1,11 @@
let log = (msg) => console.log(msg);
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
logger(`[${origin}] error: ${e.message}`);
}
export const logger = (...args) => log(...args);
export const setLogger = (fn) => {
+1 -1
View File
@@ -4,7 +4,7 @@ import { getAudioContext } from './superdough.mjs';
let noiseCache = {};
// lazy generates noise buffers and keeps them forever
function getNoiseBuffer(type, density) {
export function getNoiseBuffer(type, density) {
const ac = getAudioContext();
if (noiseCache[type]) {
return noiseCache[type];
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "1.2.3",
"version": "1.2.4",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
+172 -57
View File
@@ -7,11 +7,11 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { clamp, nanFallback, _mod } from './util.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs';
import { map } from 'nanostores';
import { logger } from './logger.mjs';
import { logger, errorLogger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
export const DEFAULT_MAX_POLYPHONY = 128;
@@ -28,6 +28,13 @@ export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
function getModulationShapeInput(val) {
if (typeof val === 'number') {
return val % 5;
}
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
}
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
@@ -126,7 +133,7 @@ export const getAudioDevices = async () => {
return devicesMap;
};
const defaultDefaultValues = {
let defaultDefaultValues = {
s: 'triangle',
gain: 0.8,
postgain: 1,
@@ -143,13 +150,24 @@ const defaultDefaultValues = {
delay: 0,
byteBeatExpression: '0',
delayfeedback: 0.5,
delaytime: 0.25,
delaysync: 3 / 16,
orbit: 1,
i: 1,
velocity: 1,
fft: 8,
};
const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues });
export function setDefault(control, value) {
// const main = getControlName(control); // we cant do this because superdough is independent of strudel/core
defaultDefaultValues[control] = value;
}
export function resetDefaults() {
defaultDefaultValues = { ...defaultDefaultDefaultValues };
}
let defaultControls = new Map(Object.entries(defaultDefaultValues));
export function setDefaultValue(key, value) {
@@ -313,30 +331,40 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = clamp(delayfeedback, 0, 0.98);
if (!delays[orbit]) {
if (!orbits[orbit].delayNode) {
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, channels);
delays[orbit] = dly;
connectToOrbit(dly, orbit);
orbits[orbit].delayNode = dly;
}
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t);
return delays[orbit];
orbits[orbit].delayNode.delayTime.value !== delaytime &&
orbits[orbit].delayNode.delayTime.setValueAtTime(delaytime, t);
orbits[orbit].delayNode.feedback.value !== delayfeedback &&
orbits[orbit].delayNode.feedback.setValueAtTime(delayfeedback, t);
return orbits[orbit].delayNode;
}
export function getLfo(audioContext, time, end, properties = {}) {
return getWorklet(audioContext, 'lfo-processor', {
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
const lfoprops = {
frequency: 1,
depth: 1,
skew: 0,
depth,
skew: 0.5,
phaseoffset: 0,
time,
time: begin,
begin,
end,
shape: 1,
dcoffset: -0.5,
...properties,
});
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
@@ -370,31 +398,73 @@ function getFilterType(ftype) {
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
}
let reverbs = {};
//type orbit {
// gain: number,
// reverbNode: reverbNode
// delayNode: delayNode
//}
let orbits = {};
function connectToOrbit(node, orbit) {
if (orbits[orbit] == null) {
errorLogger(new Error('target orbit does not exist'), 'superdough');
}
node.connect(orbits[orbit].gain);
}
function setOrbit(audioContext, orbit, channels) {
if (orbits[orbit] == null) {
orbits[orbit] = {
gain: new GainNode(audioContext, { gain: 1 }),
};
connectToDestination(orbits[orbit].gain, channels);
}
}
function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1) {
const targetArr = [targetOrbit].flat();
targetArr.forEach((target) => {
if (orbits[target] == null) {
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
return;
}
webAudioTimeout(
audioContext,
() => {
orbits[target].gain.gain.cancelScheduledValues(t);
const currVal = orbits[target].gain.gain.value;
orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t);
orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime));
},
0,
t - 0.01,
);
});
}
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir, channels) {
function getReverb(orbit, duration, fade, lp, dim, ir) {
// If no reverb has been created for a given orbit, create one
if (!reverbs[orbit]) {
if (!orbits[orbit].reverbNode) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
connectToDestination(reverb, channels);
reverbs[orbit] = reverb;
connectToOrbit(reverb, orbit);
orbits[orbit].reverbNode = reverb;
}
if (
hasChanged(duration, reverbs[orbit].duration) ||
hasChanged(fade, reverbs[orbit].fade) ||
hasChanged(lp, reverbs[orbit].lp) ||
hasChanged(dim, reverbs[orbit].dim) ||
reverbs[orbit].ir !== ir
hasChanged(duration, orbits[orbit].reverbNode.duration) ||
hasChanged(fade, orbits[orbit].reverbNode.fade) ||
hasChanged(lp, orbits[orbit].reverbNode.lp) ||
hasChanged(dim, orbits[orbit].reverbNode.dim) ||
orbits[orbit].reverbNode.ir !== ir
) {
// only regenerate when something has changed
// avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
reverbs[orbit].generate(duration, fade, lp, dim, ir);
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir);
}
return reverbs[orbit];
return orbits[orbit].reverbNode;
}
export let analysers = {},
@@ -437,8 +507,7 @@ function effectSend(input, effect, wet) {
}
export function resetGlobalEffects() {
delays = {};
reverbs = {};
orbits = {};
analysers = {};
analysersData = {};
}
@@ -450,9 +519,10 @@ function mapChannelNumbers(channels) {
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
}
export const superdough = async (value, t, hapDuration, cps) => {
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
// new: t is always expected to be the absolute target onset time
const ac = getAudioContext();
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
let { stretch } = value;
if (stretch != null) {
//account for phase vocoder latency
@@ -478,15 +548,25 @@ export const superdough = async (value, t, hapDuration, cps) => {
}
// destructure
let {
tremolo,
tremolosync,
tremolodepth = 1,
tremoloskew,
tremolophase = 0,
tremoloshape,
s = getDefaultValue('s'),
bank,
source,
gain = getDefaultValue('gain'),
postgain = getDefaultValue('postgain'),
density = getDefaultValue('density'),
duckorbit,
duckattack,
duckdepth,
// filters
fanchor = getDefaultValue('fanchor'),
drive = 0.69,
release = 0,
// low pass
cutoff,
lpenv,
@@ -519,7 +599,9 @@ export const superdough = async (value, t, hapDuration, cps) => {
phasercenter,
//
coarse,
crush,
dry,
shape,
shapevol = getDefaultValue('shapevol'),
distort,
@@ -528,7 +610,8 @@ export const superdough = async (value, t, hapDuration, cps) => {
vowel,
delay = getDefaultValue('delay'),
delayfeedback = getDefaultValue('delayfeedback'),
delaytime = getDefaultValue('delaytime'),
delaysync = getDefaultValue('delaysync'),
delaytime,
orbit = getDefaultValue('orbit'),
room,
roomfade,
@@ -547,10 +630,18 @@ export const superdough = async (value, t, hapDuration, cps) => {
compressorRelease,
} = value;
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
const orbitChannels = mapChannelNumbers(
multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'),
);
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
setOrbit(ac, orbit, channels, t, cycle, cps);
if (duckorbit != null) {
duckOrbit(ac, duckorbit, t, duckattack, duckdepth);
}
gain = applyGainCurve(nanFallback(gain, 1));
postgain = applyGainCurve(postgain);
@@ -558,8 +649,11 @@ export const superdough = async (value, t, hapDuration, cps) => {
distortvol = applyGainCurve(distortvol);
delay = applyGainCurve(delay);
velocity = applyGainCurve(velocity);
tremolodepth = applyGainCurve(tremolodepth);
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
const end = t + hapDuration;
const endWithRelease = end + release;
const chainID = Math.round(Math.random() * 1000000);
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
@@ -634,7 +728,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
lprelease,
lpenv,
t,
t + hapDuration,
end,
fanchor,
ftype,
drive,
@@ -658,7 +752,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
hprelease,
hpenv,
t,
t + hapDuration,
end,
fanchor,
);
chain.push(hp());
@@ -669,20 +763,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
if (bandf !== undefined) {
let bp = () =>
createFilter(
ac,
'bandpass',
bandf,
bandq,
bpattack,
bpdecay,
bpsustain,
bprelease,
bpenv,
t,
t + hapDuration,
fanchor,
);
createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor);
chain.push(bp());
if (ftype === '24db') {
chain.push(bp());
@@ -700,6 +781,33 @@ export const superdough = async (value, t, hapDuration, cps) => {
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
if (tremolosync != null) {
tremolo = cps * tremolosync;
}
if (tremolo !== undefined) {
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
const gain = Math.max(1 - tremolodepth, 0);
const amGain = new GainNode(ac, { gain });
const time = cycle / cps;
const lfo = getLfo(ac, t, endWithRelease, {
skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1),
frequency: tremolo,
depth: tremolodepth,
time,
dcoffset: 0,
shape: tremoloshape,
phaseoffset: tremolophase,
min: 0,
max: 1,
curve: 1.5,
});
lfo.connect(amGain.gain);
chain.push(amGain);
}
compressorThreshold !== undefined &&
chain.push(
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease),
@@ -713,20 +821,19 @@ export const superdough = async (value, t, hapDuration, cps) => {
}
// phaser
if (phaser !== undefined && phaserdepth > 0) {
const phaserFX = getPhaser(t, t + hapDuration, phaser, phaserdepth, phasercenter, phasersweep);
const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep);
chain.push(phaserFX);
}
// last gain
const post = new GainNode(ac, { gain: postgain });
chain.push(post);
connectToDestination(post, channels);
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delyNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
delaySend = effectSend(post, delyNode, delay);
const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
delaySend = effectSend(post, delayNode, delay);
audioNodes.push(delaySend);
}
// reverb
@@ -755,6 +862,14 @@ export const superdough = async (value, t, hapDuration, cps) => {
analyserSend = effectSend(post, analyserNode, 1);
audioNodes.push(analyserSend);
}
if (dry != null) {
dry = applyGainCurve(dry);
const dryGain = new GainNode(ac, { gain: dry });
chain.push(dryGain);
connectToOrbit(dryGain, orbit);
} else {
connectToOrbit(post, orbit);
}
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
+83 -3
View File
@@ -9,12 +9,13 @@ import {
getVibratoOscillator,
webAudioTimeout,
getWorklet,
noises,
} from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const getFrequencyFromValue = (value) => {
const getFrequencyFromValue = (value, defaultNote = 36) => {
let { note, freq } = value;
note = note || 36;
note = note || defaultNote;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
@@ -40,7 +41,17 @@ const waveformAliases = [
['saw', 'sawtooth'],
['sin', 'sine'],
];
const noises = ['pink', 'white', 'brown', 'crackle'];
function makeSaturationCurve(amount, n_samples) {
const k = typeof amount === 'number' ? amount : 50;
const curve = new Float32Array(n_samples);
for (let i = 0; i < n_samples; i++) {
const x = (i * 2) / n_samples - 1;
curve[i] = Math.tanh(x * k);
}
return curve;
}
export function registerSynthSounds() {
[...waveforms].forEach((s) => {
@@ -84,6 +95,75 @@ export function registerSynthSounds() {
{ type: 'synth', prebake: true },
);
});
registerSound(
'sbd',
(t, value, onended) => {
const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value;
const ctx = getAudioContext();
const attackhold = 0.02;
const noiselvl = 1.2;
const noisedecay = 0.025;
const mixGain = 1;
const o = ctx.createOscillator();
o.type = 'triangle';
o.frequency.value = getFrequencyFromValue(value, 29);
o.detune.setValueAtTime(penv * 100, 0);
o.detune.setValueAtTime(penv * 100, t);
o.detune.exponentialRampToValueAtTime(0.001, t + pdecay);
const g = gainNode(1);
g.gain.setValueAtTime(1, t + attackhold);
g.gain.exponentialRampToValueAtTime(0.001, t + attackhold + decay);
o.start(t);
const noise = getNoiseOscillator('brown', t, 2);
const noiseGain = gainNode(1);
noiseGain.gain.setValueAtTime(noiselvl, t);
noiseGain.gain.exponentialRampToValueAtTime(0.001, t + noisedecay);
const sat = new WaveShaperNode(ctx);
// tri to sine diode shaper emulation
sat.curve = makeSaturationCurve(2, ctx.sampleRate);
const mix = gainNode(mixGain);
o.onended = () => {
o.disconnect();
g.disconnect();
sat.disconnect();
noise.node.disconnect();
noiseGain.disconnect();
mix.disconnect();
onended();
};
const node = o.connect(sat).connect(g).connect(mix);
noise.node.connect(noiseGain).connect(mix);
const holdEnd = t + decay;
let end = holdEnd + 0.01;
if (clip != null) {
end = Math.min(t + clip * duration, end);
}
// prevent clicking
mix.gain.setValueAtTime(mixGain, end - 0.01);
mix.gain.linearRampToValueAtTime(0, end);
o.stop(end);
noise.stop(end);
return {
node,
stop: (endTime) => {
o.stop(endTime);
},
};
},
{ type: 'synth', prebake: true },
);
registerSound(
'supersaw',
(begin, value, onended) => {
+8
View File
@@ -68,3 +68,11 @@ export const _mod = (n, m) => ((n % m) + m) % m;
export const getSoundIndex = (n, numSounds) => {
return _mod(Math.round(nanFallback(n, 0)), numSounds);
};
export function cycleToSeconds(cycle, cps) {
return cycle / cps;
}
export function secondsToCycle(t, cps) {
return t * cps;
}
+39 -26
View File
@@ -8,18 +8,28 @@ import FFT from './fft.js';
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const _mod = (n, m) => ((n % m) + m) % m;
// Restrict phase to the range [0, maxPhase) via wrapping
function wrapPhase(phase, maxPhase = 1) {
if (phase >= maxPhase) {
phase -= maxPhase;
} else if (phase < 0) {
phase += maxPhase;
}
return phase;
}
const blockSize = 128;
// adjust waveshape to remove frequencies above nyquist to prevent aliasing
// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
function polyBlep(phase, dt) {
// 0 <= phase < 1
dt = Math.min(dt, 1 - dt);
// Start of cycle
if (phase < dt) {
phase /= dt;
// 2 * (phase - phase^2/2 - 0.5)
return phase + phase - phase * phase - 1;
}
// -1 < phase < 0
// End of cycle
else if (phase > 1 - dt) {
phase = (phase - 1) / dt;
// 2 * (phase^2/2 + phase + 0.5)
@@ -31,7 +41,7 @@ function polyBlep(phase, dt) {
return 0;
}
}
// The order is important for dough integration
const waveshapes = {
tri(phase, skew = 0.5) {
const x = 1 - skew;
@@ -81,10 +91,12 @@ function getParamValue(block, param) {
}
return param[0];
}
const waveShapeNames = Object.keys(waveshapes);
class LFOProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{ name: 'begin', defaultValue: 0 },
{ name: 'time', defaultValue: 0 },
{ name: 'end', defaultValue: 0 },
{ name: 'frequency', defaultValue: 0.5 },
@@ -92,7 +104,10 @@ class LFOProcessor extends AudioWorkletProcessor {
{ name: 'depth', defaultValue: 1 },
{ name: 'phaseoffset', defaultValue: 0 },
{ name: 'shape', defaultValue: 0 },
{ name: 'curve', defaultValue: 1 },
{ name: 'dcoffset', defaultValue: 0 },
{ name: 'min', defaultValue: 0 },
{ name: 'max', defaultValue: 1 },
];
}
@@ -109,10 +124,13 @@ class LFOProcessor extends AudioWorkletProcessor {
}
process(inputs, outputs, parameters) {
// eslint-disable-next-line no-undef
const begin = parameters['begin'][0];
if (currentTime >= parameters.end[0]) {
return false;
}
if (currentTime <= begin) {
return true;
}
const output = outputs[0];
const frequency = parameters['frequency'][0];
@@ -122,7 +140,11 @@ class LFOProcessor extends AudioWorkletProcessor {
const skew = parameters['skew'][0];
const phaseoffset = parameters['phaseoffset'][0];
const curve = parameters['curve'][0];
const dcoffset = parameters['dcoffset'][0];
const min = parameters['min'][0];
const max = parameters['max'][0];
const shape = waveShapeNames[parameters['shape'][0]];
const blockSize = output[0].length ?? 0;
@@ -130,12 +152,12 @@ class LFOProcessor extends AudioWorkletProcessor {
if (this.phase == null) {
this.phase = _mod(time * frequency + phaseoffset, 1);
}
// eslint-disable-next-line no-undef
const dt = frequency / sampleRate;
for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < output.length; i++) {
const modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
output[i][n] = modval;
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
modval = Math.pow(modval, curve);
output[i][n] = clamp(modval, min, max);
}
this.incrementPhase(dt);
}
@@ -291,7 +313,6 @@ class LadderProcessor extends AudioWorkletProcessor {
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
let cutoff = parameters.frequency[0];
// eslint-disable-next-line no-undef
cutoff = (cutoff * 2 * _PI) / sampleRate;
cutoff = cutoff > 1 ? 1 : cutoff;
@@ -424,18 +445,13 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
];
}
process(input, outputs, params) {
// eslint-disable-next-line no-undef
if (currentTime <= params.begin[0]) {
return true;
}
// eslint-disable-next-line no-undef
if (currentTime >= params.end[0]) {
// this.port.postMessage({ type: 'onended' });
return false;
}
let frequency = params.frequency[0];
//apply detune in cents
frequency = frequency * Math.pow(2, params.detune[0] / 1200);
const output = outputs[0];
const voices = params.voices[0];
@@ -446,9 +462,6 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1;
//applies unison "spread" detune in semitones
const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n));
let gainL = gain1;
let gainR = gain2;
// invert right and left gain
@@ -456,21 +469,21 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
gainL = gain2;
gainR = gain1;
}
// eslint-disable-next-line no-undef
const dt = freq / sampleRate;
for (let i = 0; i < output[0].length; i++) {
// Main detuning
let freq = applySemitoneDetuneToFrequency(params.frequency[i] ?? params.frequency[0], params.detune[0] / 100);
// Individual voice detuning
freq = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
// We must wrap this here because it is passed into sawblep below which
// has domain [0, 1]
const dt = _mod(freq / sampleRate, 1);
this.phase[n] = this.phase[n] ?? Math.random();
const v = waveshapes.sawblep(this.phase[n], dt);
output[0][i] = output[0][i] + v * gainL;
output[1][i] = output[1][i] + v * gainR;
this.phase[n] += dt;
if (this.phase[n] > 1.0) {
this.phase[n] = this.phase[n] - 1;
}
this.phase[n] = wrapPhase(this.phase[n] + dt);
}
}
return true;
@@ -479,7 +492,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
const BUFFERED_BLOCK_SIZE = 2048;
function genHannWindow(length) {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
"version": "1.2.2",
"version": "1.2.3",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
+22 -17
View File
@@ -88,13 +88,14 @@ function scaleOffset(scale, offset, note) {
* @returns Pattern
* @memberof Pattern
* @name transpose
* @synonyms trans
* @example
* "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note()
* @example
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()
*/
export const transpose = register('transpose', function (intervalOrSemitones, pat) {
export const { transpose, trans } = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) {
return pat.withHap((hap) => {
const note = hap.value.note ?? hap.value;
if (typeof note === 'number') {
@@ -142,6 +143,7 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa
* @name scaleTranspose
* @param {offset} offset number of steps inside the scale
* @returns Pattern
* @synonyms scaleTrans, strans
* @example
* "-8 [2,4,6]"
* .scale('C4 bebop major')
@@ -149,22 +151,25 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa
* .note()
*/
export const scaleTranspose = register('scaleTranspose', function (offset /* : number | string */, pat) {
return pat.withHap((hap) => {
if (!hap.context.scale) {
throw new Error('can only use scaleTranspose after .scale');
}
if (typeof hap.value === 'object')
return hap.withValue(() => ({
...hap.value,
note: scaleOffset(hap.context.scale, Number(offset), hap.value.note),
}));
if (typeof hap.value !== 'string') {
throw new Error('can only use scaleTranspose with notes');
}
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
});
});
export const { scaleTranspose, scaleTrans, strans } = register(
['scaleTranspose', 'scaleTrans', 'strans'],
function (offset /* : number | string */, pat) {
return pat.withHap((hap) => {
if (!hap.context.scale) {
throw new Error('can only use scaleTranspose after .scale');
}
if (typeof hap.value === 'object')
return hap.withValue(() => ({
...hap.value,
note: scaleOffset(hap.context.scale, Number(offset), hap.value.note),
}));
if (typeof hap.value !== 'string') {
throw new Error('can only use scaleTranspose with notes');
}
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
});
},
);
/**
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/transpiler",
"version": "1.2.2",
"version": "1.2.3",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
"version": "1.2.3",
"version": "1.2.4",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
"version": "1.2.3",
"version": "1.2.4",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
+4 -7
View File
@@ -15,13 +15,10 @@ const hap2value = (hap) => {
return hap.value;
};
export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps);
// uses more precise, absolute t if available, see https://codeberg.org/uzu/strudel/pulls/1004
export const webaudioOutput = (hap, deadline, hapDuration, cps, t) =>
superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration);
Pattern.prototype.webaudio = function () {
return this.onTrigger(webaudioOutputTrigger);
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
// TODO: refactor output callbacks to eliminate deadline
export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => {
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
};
export function webaudioRepl(options = {}) {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/xen",
"version": "1.2.2",
"version": "1.2.3",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"type": "module",
File diff suppressed because it is too large Load Diff
-22
View File
@@ -359,28 +359,6 @@ stack(
"[~ [0 ~]] 0 [~ [4 ~]] 4".sub(7).restart(scales).scale(scales).early(.25)
).note().piano().slow(2)`;
/*
export const customTrigger = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// by Felix Roos
stack(
freq("55 [110,165] 110 [220,275]".mul("<1 <3/4 2/3>>").struct("x(3,8)").layer(x=>x.mul("1.006,.995"))),
freq("440(5,8)".clip(.18).mul("<1 3/4 2 2/3>")).gain(perlin.range(.2,.8))
).s("<sawtooth square>/2")
.onTrigger((t,hap,ct)=>{
const ac = Tone.getContext().rawContext;
t = ac.currentTime + t - ct;
const { freq, s, gain = 1 } = hap.value;
const master = ac.createGain();
master.gain.value = 0.1 * gain;
master.connect(ac.destination);
const o = ac.createOscillator();
o.type = s || 'triangle';
o.frequency.value = Number(freq);
o.connect(master);
o.start(t);
o.stop(t + hap.duration);
}).stack(s("bd(3,8),hh*4,~ sd").webdirt())`; */
export const swimmingWithSoundfonts = `// Koji Kondo - Swimming (Super Mario World)
stack(
n(
-1
View File
@@ -647,7 +647,6 @@
"evaluate"
],
"/packages/webaudio/webaudio.mjs": [
"webaudioOutputTrigger",
"webaudioOutput",
"webaudioRepl"
],
-17
View File
@@ -1,17 +0,0 @@
{
"_base": "https://raw.githubusercontent.com/ritchse/tidal-drum-machines/main/machines/EmuSP12/",
"bd": ["emusp12-bd/Bassdrum-01.wav","emusp12-bd/Bassdrum-02.wav","emusp12-bd/Bassdrum-03.wav","emusp12-bd/Bassdrum-04.wav","emusp12-bd/Bassdrum-05.wav","emusp12-bd/Bassdrum-06.wav","emusp12-bd/Bassdrum-07.wav","emusp12-bd/Bassdrum-08.wav","emusp12-bd/Bassdrum-09.wav","emusp12-bd/Bassdrum-10.wav","emusp12-bd/Bassdrum-11.wav","emusp12-bd/Bassdrum-12.wav","emusp12-bd/Bassdrum-13.wav","emusp12-bd/Bassdrum-14.wav"],
"cb": ["emusp12-cb/Cowbell.wav"],
"cp": ["emusp12-cp/Clap.wav"],
"cr": ["emusp12-cr/Crash.wav"],
"hh": ["emusp12-hh/Hat Closed-01.wav","emusp12-hh/Hat Closed-02.wav"],
"ht": ["emusp12-ht/Tom H-01.wav","emusp12-ht/Tom H-02.wav","emusp12-ht/Tom H-03.wav","emusp12-ht/Tom H-04.wav","emusp12-ht/Tom H-05.wav","emusp12-ht/Tom H-06.wav"],
"lt": ["emusp12-lt/Tom L-01.wav","emusp12-lt/Tom L-02.wav","emusp12-lt/Tom L-03.wav","emusp12-lt/Tom L-04.wav","emusp12-lt/Tom L-05.wav","emusp12-lt/Tom L-06.wav"],
"misc": ["emusp12-misc/Metal-01.wav","emusp12-misc/Metal-02.wav","emusp12-misc/Metal-03.wav","emusp12-misc/Scratch.wav","emusp12-misc/Shot-01.wav","emusp12-misc/Shot-02.wav","emusp12-misc/Shot-03.wav"],
"mt": ["emusp12-mt/Tom M-01.wav","emusp12-mt/Tom M-02.wav","emusp12-mt/Tom M-03.wav","emusp12-mt/Tom M-05.wav"],
"oh": ["emusp12-oh/Hhopen1.wav"],
"perc": ["emusp12-perc/Blow1.wav"],
"rd": ["emusp12-rd/Ride.wav"],
"rim": ["emusp12-rim/zRim Shot-01.wav","emusp12-rim/zRim Shot-02.wav"],
"sd": ["emusp12-sd/Snaredrum-01.wav","emusp12-sd/Snaredrum-02.wav","emusp12-sd/Snaredrum-03.wav","emusp12-sd/Snaredrum-04.wav","emusp12-sd/Snaredrum-05.wav","emusp12-sd/Snaredrum-06.wav","emusp12-sd/Snaredrum-07.wav","emusp12-sd/Snaredrum-08.wav","emusp12-sd/Snaredrum-09.wav","emusp12-sd/Snaredrum-10.wav","emusp12-sd/Snaredrum-11.wav","emusp12-sd/Snaredrum-12.wav","emusp12-sd/Snaredrum-13.wav","emusp12-sd/Snaredrum-14.wav","emusp12-sd/Snaredrum-15.wav","emusp12-sd/Snaredrum-16.wav","emusp12-sd/Snaredrum-17.wav","emusp12-sd/Snaredrum-18.wav","emusp12-sd/Snaredrum-19.wav","emusp12-sd/Snaredrum-20.wav","emusp12-sd/Snaredrum-21.wav"]
}
+76
View File
@@ -0,0 +1,76 @@
{
"_base": "https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/",
"bd": [
"bd/10_bd_switchangel.wav",
"bd/11_bd_mot4i.wav",
"bd/12_bd_mot4i.wav",
"bd/13_bd_mot4i.wav",
"bd/14_bd_switchangel.wav",
"bd/15_bd_switchangel.wav",
"bd/16_bd_switchangel.wav",
"bd/17_bd_switchangel.wav"
],
"brk": [
"brk/10_break_amen_pprocessed.wav"
],
"cb": [
"cb/10_perc_switchangel.wav"
],
"cp": [
"cp/10_cp_switchangel.wav",
"cp/11_cp_mot4i.wav"
],
"cr": [
"cr/10_cr_switchangel.wav",
"cr/11_cr_mot4i.wav"
],
"hh": [
"hh/10_hh_switchangel.wav",
"hh/11_hh_mot4i.wav",
"hh/12_hh_switchangel.wav",
"hh/13_hh_switchangel.wav",
"hh/14_hh_mot4i.wav"
],
"ht": [
"ht/10_ht_mot4i.wav"
],
"lt": [
"lt/10_lt_mot4i.wav"
],
"misc": [
"misc/10_misc_switchangel_ludens.wav",
"misc/11_misc_switchangel_ludens.wav",
"misc/12_misc_switchangel_ludens.wav",
"misc/13_misc_switchangel_ludens.wav",
"misc/14_misc_switchangel_ludens.wav"
],
"mt": [
"mt/10_mt_mot4i.wav"
],
"oh": [
"oh/10_oh_switchangel.wav",
"oh/11_oh_switchangel.wav",
"oh/12_oh_switchangel.wav",
"oh/13_oh_switchangel.wav"
],
"rd": [
"rd/10_rd_switchangel.wav"
],
"rim": [
"rim/10_rim_switchangel.wav",
"rim/11_rim_switch_angel.wav"
],
"sd": [
"sd/10_sd_switchangel-bounce-2.wav",
"sd/11_sd_switchangel_3.wav",
"sd/12_sd_switchangel_2.wav",
"sd/13_sd_switchangel_2.wav",
"sd/14_sd.wav"
],
"sh": [
"sh/10_sh_switchangel.wav"
],
"tb": [
"tb/10_tb.wav"
]
}
@@ -95,7 +95,7 @@ Diese Kombinationen von Buchstaben stehen für verschiedene Teile eines Schlagze
- `mt` = **m**iddle tom
- `ht` = **h**igh tom
- `rd` = **r**i**d**e cymbal
- `rd` = **cr**ash cymbal
- `cr` = **cr**ash cymbal
Probier verschiedene Sounds aus!
+28
View File
@@ -57,6 +57,34 @@ Each filter has 2 parameters:
<JsDoc client:idle name="vowel" h={0} />
# Amplitude Modulation
Amplitude modulation changes the amplitude (gain) periodically over time.
## am
<JsDoc client:idle name="am" h={0} />
## tremolosync
<JsDoc client:idle name="tremolosync" h={0} />
## tremolodepth
<JsDoc client:idle name="tremolodepth" h={0} />
## tremoloskew
<JsDoc client:idle name="tremoloskew" h={0} />
## tremolophase
<JsDoc client:idle name="tremolophase" h={0} />
## tremoloshape
<JsDoc client:idle name="tremoloshape" h={0} />
# Amplitude Envelope
The amplitude [envelope](<https://en.wikipedia.org/wiki/Envelope_(music)>) controls the dynamic contour of a sound.
@@ -176,3 +176,16 @@ $ chord <Dm9!3 Db7> # voicing
/>
The `$` sign is an alias for `,` so it will create a stack behind the scenes.
## variables
using the `def` keyword, you can define variables:
<MiniRepl
client:idle
mondo
tune={`
$ def melody [0 1 2 3]
$ n melody # scale C:minor
`}
/>
+15
View File
@@ -163,6 +163,21 @@ The last section could be written as:
}
```
Please note that browsers will often cache `strudel.json` on first load, and keep using the cached
version even if the orginal has been updated. If this bites you (for example while developing a new
sample pack), you can force the browser to download a new copy by i.e. changing capitalization of one
character in the URL, or adding a URL attribute, such as:
```javascript
samples('https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/strudel.json?version=2');
```
that gets ignored by GitHub (but changes the URL, forcing the browser to reload every time we increase
the version number).
It is also possible, of course, to just remove it from cache (deleting cache in browser Privacy settings,
or from the dev console if you're technically minded, or by using a cache deleting extension).
## Github Shortcut
Because loading samples from github is common, there is a shortcut:
+2
View File
@@ -67,6 +67,8 @@ Or using 2 beats per cycle:
s("bd sd, hh*4")`}
/>
You can use the `setcps` method to set the global tempo in cycles per second. `setcpm(x)` is the same as `setcps(x / 60)`.
<Box>
To set a specific bpm, use `setcpm(bpm/bpc)`
@@ -125,7 +125,7 @@ function UserPatterns({ context }) {
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,application/json"
accept="text/plain,text/x-markdown,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
import
@@ -21,7 +21,11 @@ export function Reference() {
return true;
}
return entry.name.includes(search) || (entry.synonyms?.some((s) => s.includes(search)) ?? false);
const lowCaseSearch = search.toLowerCase();
return (
entry.name.toLowerCase().includes(lowCaseSearch) ||
(entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false)
);
});
}, [search]);
@@ -109,6 +109,8 @@ export function SettingsTab({ started }) {
togglePanelTrigger,
maxPolyphony,
multiChannelOrbits,
isTabIndentationEnabled,
isMultiCursorEnabled,
} = useSettings();
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
@@ -262,6 +264,16 @@ export function SettingsTab({ started }) {
onChange={(cbEvent) => settingsMap.setKey('isLineWrappingEnabled', cbEvent.target.checked)}
value={isLineWrappingEnabled}
/>
<Checkbox
label="Enable Tab indentation"
onChange={(cbEvent) => settingsMap.setKey('isTabIndentationEnabled', cbEvent.target.checked)}
value={isTabIndentationEnabled}
/>
<Checkbox
label="Enable Multi-Cursor (Cmd/Ctrl+Click)"
onChange={(cbEvent) => settingsMap.setKey('isMultiCursorEnabled', cbEvent.target.checked)}
value={isMultiCursorEnabled}
/>
<Checkbox
label="Enable flashing on evaluation"
onChange={(cbEvent) => settingsMap.setKey('isFlashEnabled', cbEvent.target.checked)}
+4 -1
View File
@@ -28,7 +28,10 @@ export async function prebake() {
prebake: true,
tag: 'drum-machines',
}),
samples(`${baseNoTrailing}/EmuSP12.json`, undefined, { prebake: true, tag: 'drum-machines' }),
samples(`${baseNoTrailing}/uzu-drumkit.json`, undefined, {
prebake: true,
tag: 'drum-machines',
}),
samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }),
samples(
{
+2
View File
@@ -13,6 +13,7 @@ import {
resetGlobalEffects,
resetLoadedSounds,
initAudioOnFirstClick,
resetDefaults,
} from '@strudel/webaudio';
import { setVersionDefaultsFrom } from './util.mjs';
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
@@ -181,6 +182,7 @@ export function useReplContext() {
const resetEditor = async () => {
(await getModule('@strudel/tonal'))?.resetVoicings();
resetDefaults();
resetGlobalEffects();
clearCanvas();
clearHydra();
+4
View File
@@ -21,6 +21,8 @@ export const defaultSettings = {
isSyncEnabled: false,
isLineWrappingEnabled: false,
isPatternHighlightingEnabled: true,
isTabIndentationEnabled: false,
isMultiCursorEnabled: false,
theme: 'strudelTheme',
fontFamily: 'monospace',
fontSize: 18,
@@ -77,6 +79,8 @@ export function useSettings() {
isLineWrappingEnabled: parseBoolean(state.isLineWrappingEnabled),
isFlashEnabled: parseBoolean(state.isFlashEnabled),
isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled),
isTabIndentationEnabled: parseBoolean(state.isTabIndentationEnabled),
isMultiCursorEnabled: parseBoolean(state.isMultiCursorEnabled),
fontSize: Number(state.fontSize),
panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is!
isPanelPinned: parseBoolean(state.isPanelPinned),
+1 -1
View File
@@ -197,7 +197,7 @@ export async function importPatterns(fileList) {
if (file.type === 'application/json') {
const userPatterns = userPattern.getAll();
setUserPatterns({ ...userPatterns, ...parseJSON(content) });
} else if (file.type === 'text/plain') {
} else if (['text/x-markdown', 'text/plain'].includes(file.type)) {
const id = file.name.replace(/\.[^/.]+$/, '');
userPattern.update(id, { code: content });
}