Compare commits

..

182 Commits

Author SHA1 Message Date
Felix Roos 50f04e8cc8 fix: echoWith / echoForEach in tests 2025-09-15 22:57:18 +02:00
Felix Roos 5e3218a67b breaking: tidal style echoWith + add echoForEach 2025-09-15 22:41:51 +02:00
froos 2e97fb7be2 Merge pull request 'supradough: fix delay + add some jsdoc types' (#1578) from supradough-fixes into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1578
2025-09-15 00:00:18 +02:00
Felix Roos df1934f87b add some typechecking + fix delay 2025-09-14 23:54:51 +02:00
froos d3472feb5c Merge pull request 'added plyWith/plyWithClassic functions' (#1571) from Dsm0/strudel:add_plyWith into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1571
2025-09-14 12:42:57 +02:00
Dsm0 addb6db1c7 updated names + snapshot 2025-09-14 02:21:25 -07:00
froos 566d590453 Merge pull request 'feat: sound alias' (#1494) from Antipathie/strudel:feature/592-sound-alias into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1494
2025-09-14 10:39:11 +02:00
froos e57bd98da0 Merge branch 'main' into feature/592-sound-alias 2025-09-14 10:34:47 +02:00
Dsm0 f5c4373f99 added plyWith/plyWithClassic functions 2025-09-13 17:21:19 -07:00
froos e762291bb7 Merge pull request 'Bug Fix: Allow penv values to be falsy' (#1559) from glossing/strudel:glossing/penv-with-zeros into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1559
2025-09-14 01:20:14 +02:00
Felix Roos 9da027d377 hotfix: format 2025-09-14 01:11:34 +02:00
froos 96dccdc38b Merge pull request 'Update website/src/pages/workshop/first-sounds.mdx' (#1566) from fesmith/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1566
2025-09-14 01:10:47 +02:00
Felix Roos d6254294b0 fix: bring back tests on external PRs (push doesn't cut it for forks) 2025-09-14 01:09:30 +02:00
froos 339c1bb573 Merge pull request 'fix: autocomplete-styles + html rendering' (#1570) from autocomplete-styles into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1570
2025-09-14 01:07:47 +02:00
Felix Roos 9782795761 fix: autocomplete container style 2025-09-14 01:04:48 +02:00
Felix Roos 2d2b238da9 fix: use template element for string to html 2025-09-14 01:03:14 +02:00
froos f0839e02eb Merge pull request 'Feature: Synonyms in autocomplete and reference' (#1535) from glossing/strudel:glossing/synonyms-in-autocomplete into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1535
2025-09-14 00:49:16 +02:00
Felix Roos a9f7d825c8 hotfix: format 2025-09-14 00:40:54 +02:00
Aria 709b654ed3 Add synonyms to autocomplete 2025-09-13 16:21:09 -05:00
froos d81a214c74 Merge pull request 'Docs: add xen package examples' (#1446) from dudymas/strudel:docs/xen/added-tune into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1446
2025-09-13 20:01:47 +02:00
froos e57f240e70 Merge branch 'main' into docs/xen/added-tune 2025-09-13 20:00:35 +02:00
Felix Roos e18296e760 hotfix: word 2025-09-13 19:52:07 +02:00
froos c145355ccc Merge pull request 'Update website/src/pages/learn/code.mdx' (#1391) from anecondev/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1391
2025-09-13 19:51:09 +02:00
fesmith 352a3c39d7 Update website/src/pages/workshop/first-sounds.mdx 2025-09-13 00:00:36 +02:00
Felix Roos a46617f3f0 hotfix: use ?audioworklet for supradough worklet import 2025-09-12 10:16:37 +02:00
Felix Roos d164b5e7f5 Revert "Revert "Merge pull request 'supradough poc' (#1362) from supradough into main""
This reverts commit c0bf86d11e.
2025-09-12 10:08:55 +02:00
Felix Roos c0bf86d11e Revert "Merge pull request 'supradough poc' (#1362) from supradough into main"
This reverts commit 6f06683d70, reversing
changes made to 637d714ab2.
2025-09-11 23:19:22 +02:00
Switch Angel AKA Jade Rose 6f06683d70 Merge pull request 'supradough poc' (#1362) from supradough into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1362
2025-09-11 22:52:29 +02:00
Felix Roos 637d714ab2 Publish
- @strudel/mondo@1.1.4
2025-09-11 19:14:00 +02:00
Felix Roos c42d00f048 hotfix: export mondo getLocations 2025-09-11 19:11:45 +02:00
Felix Roos 702e558d1a Publish
- @strudel/mondo@1.1.3
2025-09-11 17:30:38 +02:00
froos bcc2e8f759 Merge pull request 'fix: exclude mondough dependencies' (#1563) from fix-mondo-package into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1563
2025-09-11 17:29:34 +02:00
Felix Roos 27339715b7 fix: exclude mondough dependencies 2025-09-11 17:22:06 +02:00
Felix Roos 5e2e6411e5 Publish
- @strudel/codemirror@1.2.5
 - @strudel/repl@1.2.6
2025-09-11 08:26:20 +02:00
froos 0fac16e769 Merge pull request 'add basicSetup for keybindings' (#1462) from Dsm0/strudel:codemirror-keymap-fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1462
2025-09-11 00:27:20 +02:00
Felix Roos d0ce82e3cd Publish
- @strudel/codemirror@1.2.4
 - @strudel/core@1.2.4
 - @strudel/csound@1.2.5
 - @strudel/draw@1.2.4
 - @strudel/gamepad@1.2.4
 - @strudel/hydra@1.2.4
 - @strudel/midi@1.2.5
 - @strudel/mini@1.2.4
 - @strudel/mondo@1.1.2
 - @strudel/motion@1.2.4
 - @strudel/mqtt@1.2.4
 - @strudel/osc@1.2.4
 - @strudel/repl@1.2.5
 - @strudel/sampler@0.2.3
 - @strudel/serial@1.2.4
 - @strudel/soundfonts@1.2.5
 - superdough@1.2.5
 - @strudel/tonal@1.2.4
 - @strudel/transpiler@1.2.4
 - @strudel/web@1.2.5
 - @strudel/webaudio@1.2.5
 - @strudel/xen@1.2.4
2025-09-10 23:08:16 +02:00
Switch Angel AKA Jade Rose e82629de2e Merge pull request 'feat: add delete user samples button for convenience' (#1556) from delete_samples_button into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1556
2025-09-10 17:25:25 +02:00
Aria 38ce593c60 Allow penv values to be falsy 2025-09-09 20:47:09 -05:00
Switch Angel AKA Jade Rose 8a91549a7b Merge branch 'main' into delete_samples_button 2025-09-09 06:42:38 +02:00
froos cc27d482cd Merge pull request 'Fix formatting of REPL footnote' (#1547) from ddbeck/strudel:fix-footnote into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1547
2025-09-08 08:54:00 +02:00
Switch Angel AKA Jade Rose 37e640470b Merge pull request 'fix: OSC' (#1557) from fixosc into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1557
2025-09-08 05:38:07 +02:00
Jade (Rose) Rowland 5e5e7730c1 rm errorlogger console 2025-09-07 23:29:24 -04:00
Jade (Rose) Rowland 89e7d52308 fixed 2025-09-07 23:28:18 -04:00
Jade (Rose) Rowland 99bb227cf4 format 2025-09-07 22:48:04 -04:00
Jade (Rose) Rowland 70e776e799 cleanup div 2025-09-07 22:44:09 -04:00
Jade (Rose) Rowland 66aad0004a fix formatting 2025-09-07 22:43:13 -04:00
Jade (Rose) Rowland a968545458 change sound background to distinguish action buttons 2025-09-07 22:41:03 -04:00
Jade (Rose) Rowland 5414bbe85d only show button on user samples 2025-09-07 22:27:48 -04:00
Jade (Rose) Rowland e7e80bfd83 working 2025-09-07 22:21:03 -04:00
froos c57f5bb429 Merge pull request 'Add examples for ? and | operators to documentation' (#1532) from james-collapse/strudel:mininotation-randomness-docs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1532
2025-09-07 22:47:10 +02:00
froos f2608e712b Merge pull request 'Signal flow documentation' (#1504) from glossing/strudel:glossing/signal-flow-documentation into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1504
Reviewed-by: Switch Angel AKA Jade Rose <daslyfe@noreply.codeberg.org>
2025-09-07 21:48:18 +02:00
froos 34b41fc26f Merge pull request 'Allow flattening of dirs with sample server' (#1529) from glossing/strudel:glossing/sample-server-with-flat into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1529
2025-09-07 21:27:28 +02:00
Switch Angel AKA Jade Rose f6e171fed8 Merge pull request 'fix: prevent orbit clicking by defining max number of channels in an orbit' (#1552) from orbitfix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1552
2025-09-07 21:05:23 +02:00
Jade (Rose) Rowland 2511dcc09e working 2025-09-07 15:02:13 -04:00
Daniel D. Beck 977420e74d Fix formatting of REPL footnote 2025-09-06 15:33:08 +02:00
Felix Roos 2fbdc9fdd7 hotfix: formatting 2025-09-06 00:43:03 +02:00
Felix Roos 0a96fa5896 hotfix: comment out tauri stuff to fix osc 2025-09-06 00:42:36 +02:00
james-collapse 3e0e903d38 Merge branch 'main' into mininotation-randomness-docs 2025-09-03 20:19:54 +02:00
James Walker 591c3fe08f Add documentation for ?n in mini-notation 2025-09-03 19:16:47 +01:00
Antipathie 56b1ea3fa5 Add soundAlias function 2025-09-03 00:47:45 +02:00
Aria c199b51645 Update autocomplete name to be the label and update ref to use main name as a synonym 2025-09-02 16:29:01 -05:00
Aria 8d2a368da9 Add some more synonyms to controls docs 2025-09-02 16:14:50 -05:00
Aria 052d09e892 Add synonyms to reference 2025-09-02 16:14:38 -05:00
Aria 439a7dc5e6 Include synonyms in autocomplete 2025-09-02 16:05:40 -05:00
froos 06ebcbff8c Merge pull request 'Padding for tooltips, preserving linebreaks in descriptions, not closing autocompletes on click' (#1521) from glossing/strudel:glossing/tooltip-padding-and-paragraphs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1521
2025-09-02 08:42:41 +02:00
James Walker 142160d79a Add examples for ? and | operators 2025-09-01 21:08:06 +01:00
Switch Angel AKA Jade Rose ff2c59a2a6 Merge pull request 'Update "restore defaults" to not delete patterns' (#1519) from glossing/strudel:glossing/dont-delete-patterns-on-default into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1519
2025-09-01 17:50:08 +02:00
Aria 9b9176325b Update restore defaults to not delete patterns 2025-09-01 17:50:08 +02:00
Jade (Rose) Rowland b8e1b8d7ac Merge branch 'main' into supradough 2025-08-31 22:42:07 -04:00
Switch Angel AKA Jade Rose e84c39837f Merge pull request 'feat: add the ability to control the speed and start time of the reverb IR' (#1501) from irspeed into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1501
2025-08-31 19:10:02 +02:00
Jade (Rose) Rowland 6527c68cda Merge branch 'main' into irspeed 2025-08-31 12:51:28 -04:00
Aria 6a09f54b25 Add vibrato to list of continuous modulators 2025-08-31 01:03:09 -05:00
yaxu dc6b766ab7 Update README.md 2025-08-30 12:47:06 +02:00
yaxu 9eb3c9410d Add verify link to mastodon a/c 2025-08-30 12:46:00 +02:00
Aria 189e650a73 Add flat flag, automatic json saving; filter out non-audio at base dir 2025-08-29 12:18:28 -05:00
Aria a5886bb9d4 Use white-space instead; fix scrolling; don't close autocomplete tooltips on click 2025-08-26 14:14:00 -05:00
Aria 877bc95a58 Switch to more specific class and fix p->div 2025-08-26 12:54:39 -05:00
Aria 18b1739b89 Add a space after paragraphs in descriptions and add padding to hover tooltips 2025-08-26 12:53:38 -05:00
Switch Angel AKA Jade Rose b54139376e Merge pull request 'Bug Fix: Duck on Mobile / Safari' (#1503) from glossing/strudel:glossing/ducks-on-mobile into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1503
2025-08-25 02:31:21 +02:00
Aria d64a0ef0eb Lint requires no empty blocks: pass comment 2025-08-25 02:31:21 +02:00
Aria 659071a4ee Ensure zeroGain is cleaned up after use; move start/stop to after callback assigned for safety 2025-08-25 02:31:21 +02:00
Aria 64f7bc4442 Working version of duck on iOS safari 2025-08-25 02:31:21 +02:00
Alex McLean 8c97f2e2d0 Merge pull request 'fix benchmarks' (#1517) from fix-bench into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1517
2025-08-23 18:39:52 +02:00
alex b8c98736c5 fix benchmarks 2025-08-23 17:29:15 +01:00
Aria a0fc52b1ec Final tweaks 2025-08-21 14:42:52 -05:00
froos 0693a32736 Merge pull request 'add --json flag to @strudel/sampler to generate a strudel.json' (#1513) from strudeljson-generate into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1513
2025-08-21 20:57:31 +02:00
Felix Roos 42a903ecc0 add --yes flag to readme, making sure first run works 2025-08-21 20:36:42 +02:00
Felix Roos da283eb55a doc: generate strudel.json 2025-08-21 20:36:17 +02:00
Felix Roos b12707316a bump @strudel/sampler to 0.2.1 2025-08-21 20:30:32 +02:00
Felix Roos 66aa3ac1da add --json flag to strudel sampler 2025-08-21 20:30:02 +02:00
froos d81b1ab65c Merge pull request 'remove hs2js postinstall' (#1510) from remove-postinstall into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1510
2025-08-21 10:04:28 +02:00
Felix Roos 8465517c76 remove hs2js postinstall 2025-08-21 10:00:41 +02:00
Felix Roos 215ab87809 fix: prettier 2025-08-21 09:41:21 +02:00
froos e5fe998327 Merge pull request 'Docs: Small additions to Learn Docs and fixed refrences to function Documentation' (#1502) from fyynn/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1502
2025-08-20 15:15:31 +02:00
froos 6f75a0a62b Merge pull request 'fix: repl autocomplete not rendering correctly' (#1480) from robase/strudel:fix-repl-autocomplete-styles into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1480
2025-08-20 12:31:36 +02:00
froos 0464845262 Merge pull request 'BUG FIX: make 'midin' initialization work with multiple controllers' (#1469) from cbabraham/strudel:fix/midi-init-bug into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1469
2025-08-20 12:10:00 +02:00
Aria e490774294 Mention order of effects 2025-08-19 16:06:18 -05:00
Aria 34e8a57472 Typos grammar etc 2025-08-19 11:32:07 -05:00
Aria 59c8d70714 Code format 2025-08-19 11:27:56 -05:00
Aria ce7cff2c3b Clean up wording on orbit routing 2025-08-19 11:26:25 -05:00
Aria 32fe73aba2 Update effects documentation to include information on signal flow 2025-08-19 11:10:34 -05:00
fyynn d6ee10e05c fixed new scrub refrence 2025-08-19 00:44:46 +02:00
fyynn 36441e7b73 Fixing hush refrence to code documentation
Signed-off-by: fyynn <fyynn@noreply.codeberg.org>
2025-08-19 00:25:31 +02:00
fyynn 189daa3942 Added Refrence to arp() arpWidth() to Docs
Signed-off-by: fyynn <fyynn@noreply.codeberg.org>
2025-08-19 00:21:32 +02:00
fyynn d009b9592e Added Refrence to arp() arpWidth() and hush to Docs
Signed-off-by: fyynn <fyynn@noreply.codeberg.org>
2025-08-19 00:09:41 +02:00
fyynn f7f1bd63e8 Adding the Duck/Sidechain effect to Learn Docs (learn/effects.mdx)
Added the duck Effect to the section global effects.

Signed-off-by: fyynn <fyynn@noreply.codeberg.org>
2025-08-18 23:49:43 +02:00
fyynn 7d24e45692 Added scrub() to Learn Docs
Signed-off-by: fyynn <fyynn@noreply.codeberg.org>
2025-08-18 23:36:39 +02:00
Jade (Rose) Rowland cf55d4c8d1 rm var 2025-08-18 11:49:11 -04:00
Jade (Rose) Rowland e7e636886d add tests 2025-08-17 17:21:36 -04:00
Jade (Rose) Rowland e9aef9e4d4 working 2025-08-17 16:49:10 -04:00
robase 612efad3eb clean up comments 2025-08-07 09:25:01 +02:00
robase e7839a09a1 fix: tooltip should respect themes 2025-08-07 09:25:01 +02:00
robase 3292f88810 fix: repl autocomplete not rendering correctly 2025-08-07 09:25:01 +02:00
Chandler Abraham 2eda047108 Merge branch 'main' of https://codeberg.org/uzu/strudel into fix/midi-init-bug 2025-07-22 21:52:03 -07:00
Chandler Abraham c8cf1dc712 fix(midi): ensure midin initializes device state correctly
The midin function only initialized the refs object for a device on the initial MIDI setup. This caused an error if a second MIDI device was connected in the same session, as its refs object would not exist, leading to a 'cannot read properties of undefined' error when accessed.

This commit fixes the issue by ensuring that the refs object for a given MIDI input is initialized every time  is called, not just on the first call.
2025-07-22 21:49:40 -07:00
daslyfe a26559fd3d Merge branch 'main' into supradough 2025-07-22 06:35:59 +02:00
Dsm0 5448fc6077 removed duplication and unnecessary keymappings in codemirror config 2025-07-16 15:17:34 -07:00
Dsm0 06fafbd8eb Ensure no default extensions conflict with extensions in defaultSettings 2025-07-16 15:03:55 -07:00
Dsm0 10156f0067 add basicSetup for keybindings 2025-07-15 19:13:24 -07:00
Felix Roos 5632d83afb fix: supradough onTrigger after breaking change 2025-07-16 02:21:39 +02:00
Felix Roos a32473d570 Merge remote-tracking branch 'origin/main' into supradough 2025-07-15 22:26:46 +02:00
dudymas e87bb4376f Merge branch 'main' into docs/xen/added-tune 2025-07-05 20:28:09 +02:00
dudymas 0722cf7ded feat(xen/tunejs): support sending a custom tuning 2025-07-05 13:43:42 -04:00
dudymas 37fde8f3e8 docs(website/learn/xen): discussed strumming 2025-07-05 13:43:09 -04:00
dudymas 8655c5b73c Merge branch 'main' into docs/xen/added-tune 2025-07-05 00:50:57 +02:00
dudymas 31c1450edc docs(website/learn/xen): add tune() examples 2025-07-04 18:50:21 -04:00
dudymas cbb4207bab chore(docker): basic support added 2025-07-04 18:50:21 -04:00
anecondev 5efcd14f40 Update website/src/pages/learn/code.mdx
Correct "appended" to "prepended"
2025-06-18 17:07:13 +02:00
froos 8051e56d13 Merge branch 'main' into supradough 2025-06-14 00:35:52 +02:00
Felix Roos 239987af0b snapshot 2025-06-12 10:23:06 +02:00
Felix Roos 8363ba5a41 format 2025-06-12 10:22:35 +02:00
Felix Roos 30f7127a92 Merge remote-tracking branch 'origin/main' into supradough 2025-06-12 10:22:08 +02:00
Felix Roos 14193abe03 improve export: add stereo + measure performance + add bigger pattern for testing 2025-06-12 02:26:50 +02:00
Felix Roos 3caf08e199 up latency 2025-06-12 02:26:08 +02:00
Felix Roos 5cbd38a3e8 improve filter performance making it accept hz directly 2025-06-12 02:25:46 +02:00
Felix Roos bbd7ed0f27 lazy sample loading 2025-06-12 02:25:04 +02:00
Jade (Rose) Rowland 43504d16b6 add chorus 2025-06-10 16:03:42 +02:00
Jade (Rose) Rowland 7e8206dcbc pitched delay 2025-06-10 12:31:21 +02:00
Felix Roos 3963b807eb fix: calling doughsamples multiple times 2025-06-09 22:32:41 +02:00
Felix Roos e99adffc56 vibrato 2025-06-09 22:29:35 +02:00
Felix Roos c9f494d865 fix: node export 2025-06-09 22:10:09 +02:00
Felix Roos e8254735bb apply speed to synths and samples + add normalize flag (for fit) 2025-06-09 19:35:50 +02:00
Jade (Rose) Rowland 3d30007756 supersaw 2025-06-09 15:55:44 +02:00
Jade (Rose) Rowland aeaed50446 gaincurve 2025-06-09 15:18:47 +02:00
Felix Roos 3ac0fdf619 rudimentary sample map loading (doughsamples) 2025-06-09 11:07:57 +02:00
Felix Roos d1b6b6be85 fix: hpf 2025-06-09 09:54:24 +02:00
Felix Roos 1081fc69e9 refactor: stereo sample support + channelwise fx 2025-06-09 09:52:29 +02:00
Felix Roos 73ef43dd2c move things around in init
+ shorten things
2025-06-08 22:56:55 +02:00
Jade (Rose) Rowland 181e51ef0e adjustable envelope curves 2025-06-08 20:26:20 +02:00
Jade (Rose) Rowland 1167fdb910 Merge branch 'supradough' of https://github.com/tidalcycles/strudel into supradough 2025-06-08 19:25:47 +02:00
Jade (Rose) Rowland ec9109dd89 exponential lerp 2025-06-08 19:24:55 +02:00
Felix Roos 55aef18851 buffers can now be repitched (still with aliasing) 2025-06-08 14:43:23 +02:00
Felix Roos 356d4360ec fix: scheduling + sample playback 2025-06-08 09:05:14 +02:00
Felix Roos 0f2aa9569b half working samples 2025-06-08 02:04:49 +02:00
Felix Roos ac7ee376e5 pitch envelope 2025-06-08 00:43:58 +02:00
Felix Roos ab052ce1ac fix: polyblepped sawtooth + fm = death 2025-06-07 23:15:09 +02:00
Felix Roos 5988e3d3b4 fix: hot reloading for dough.mjs (see comment) 2025-06-07 22:42:48 +02:00
Felix Roos 2f2bf5bf29 fix: note2midi for notes without octave 2025-06-07 22:36:54 +02:00
Felix Roos b000f2297d fm 2025-06-07 20:31:47 +02:00
Felix Roos 77463a199a add square alias for pulse 2025-06-07 13:26:43 +02:00
Felix Roos e54449f5f7 white brown pink noise + crackle alias for dust 2025-06-07 13:24:06 +02:00
Felix Roos d743570546 set delay time and feedback from voice 2025-06-07 13:12:35 +02:00
Felix Roos 1dac69635d feedback delay (still hard coded feedback / time values) 2025-06-07 10:40:34 +02:00
Felix Roos 49f38aeddb remove log 2025-06-06 16:26:17 +02:00
Felix Roos 43c05cb504 move supradough to separate package 2025-06-06 16:17:05 +02:00
Felix Roos ccc90d547c ignore dough-export to fix checks 2025-06-06 15:47:19 +02:00
Felix Roos 998a010247 better filter envelopes 2025-06-06 11:21:01 +02:00
Felix Roos e4cd5dc552 rough filter envelope implementation 2025-06-06 10:55:38 +02:00
Felix Roos 6cf0b6f651 hpf + bpf 2025-06-06 10:12:49 +02:00
Felix Roos 794bf86904 distort 2025-06-06 09:56:41 +02:00
Felix Roos 9ed690ce45 pulse osc + velocity 2025-06-06 09:37:40 +02:00
Felix Roos 623bf93368 panning 2025-06-06 08:50:02 +02:00
Felix Roos 0cc18a6468 crush (might be wrong) 2025-06-06 08:27:47 +02:00
Felix Roos 87598e10d1 coarse 2025-06-06 08:17:20 +02:00
Felix Roos ccec7e725a poc: render pattern audio with node script 2025-06-05 11:04:55 +02:00
Felix Roos f0e6c5483c handle invalid spawns + remove logs 2025-06-05 10:30:53 +02:00
Felix Roos 2cbd35f6d2 refactor: make webaudio glue as minimal as possible 2025-06-05 10:26:14 +02:00
Felix Roos 930dabe4d6 dough as a singleton worklet with voice allocation 2025-06-05 09:54:44 +02:00
Jade (Rose) Rowland ff4b7ca171 fix volume 2025-06-04 01:08:36 +01:00
Jade (Rose) Rowland 5129fa6677 fix click 2025-06-04 00:48:32 +01:00
Felix Roos 5336569c25 proper lpf mapping + remove redundant worklet params, 2025-06-01 21:20:28 +02:00
Felix Roos 483843bd0a begin supradough 2025-06-01 18:06:36 +02:00
79 changed files with 2849 additions and 299 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
name: Strudel tests
on: [push]
on: [push, pull_request]
jobs:
build:
@@ -19,7 +19,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache: "pnpm"
- run: pnpm install
- run: pnpm run format-check
- run: pnpm run lint
+21
View File
@@ -0,0 +1,21 @@
FROM node:24
WORKDIR /app
RUN npm install pnpm --global
COPY pnpm-workspace.yaml ./
COPY package.json pnpm-lock.yaml ./
COPY packages/ ./packages/
COPY examples/ ./examples/
RUN mkdir -p website/public
COPY website/package.json ./website/
RUN pnpm install
COPY . .
EXPOSE 4321
CMD ["pnpm", "dev"]
+2 -2
View File
@@ -3,8 +3,6 @@
Live coding patterns on the web
https://strudel.cc/
Development is moving to https://codeberg.org/uzu/strudel
- Try it here: <https://strudel.cc>
- Docs: <https://strudel.cc/learn>
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
@@ -47,3 +45,5 @@ There is a #strudel channel on the TidalCycles discord: <https://discord.com/inv
You can also ask questions and find related discussions on the tidal club forum: <https://club.tidalcycles.org/>
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
We also have a mastodon account: <a rel="me" href="https://social.toplap.org/@strudel">social.toplap.org/@strudel</a>
+1
View File
@@ -42,6 +42,7 @@ export default [
'**/hydra.mjs',
'**/jsdoc-synonyms.js',
'packages/hs2js/src/hs2js.mjs',
'packages/supradough/dough-export.mjs',
'**/samples',
],
},
+107 -59
View File
@@ -1,68 +1,122 @@
import jsdoc from '../../doc.json';
// import { javascriptLanguage } from '@codemirror/lang-javascript';
import { autocompletion } from '@codemirror/autocomplete';
import { h } from './html';
function plaintext(str) {
const escapeHtml = (str) => {
const div = document.createElement('div');
div.innerText = str;
return div.innerHTML;
}
};
const getDocLabel = (doc) => doc.name || doc.longname;
const getInnerText = (html) => {
var div = document.createElement('div');
const stripHtml = (html) => {
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
};
export function Autocomplete({ doc, label }) {
return h`<div class="prose dark:prose-invert max-h-[400px] overflow-auto p-2">
<h1 class="pt-0 mt-0">${label || getDocLabel(doc)}</h1>
${doc.description}
<ul>
${doc.params?.map(
({ name, type, description }) =>
`<li>${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}</li>`,
)}
</ul>
<div>
${doc.examples?.map((example) => `<div><pre>${plaintext(example)}</pre></div>`)}
</div>
</div>`[0];
/*
<pre
className="cursor-pointer"
onMouseDown={(e) => {
console.log('ola!');
navigator.clipboard.writeText(example);
e.stopPropagation();
}}
>
{example}
</pre>
*/
}
const getDocLabel = (doc) => doc.name || doc.longname;
const jsdocCompletions = jsdoc.docs
.filter(
(doc) =>
getDocLabel(doc) &&
!getDocLabel(doc).startsWith('_') &&
!['package'].includes(doc.kind) &&
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
)
// https://codemirror.net/docs/ref/#autocomplete.Completion
.map((doc) /*: Completion */ => ({
label: getDocLabel(doc),
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
info: () => Autocomplete({ doc }),
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
}));
const buildParamsList = (params) =>
params?.length
? `
<div class="autocomplete-info-params-section">
<h4 class="autocomplete-info-section-title">Parameters</h4>
<ul class="autocomplete-info-params-list">
${params
.map(
({ name, type, description }) => `
<li class="autocomplete-info-param-item">
<span class="autocomplete-info-param-name">${name}</span>
<span class="autocomplete-info-param-type">${type.names?.join(' | ')}</span>
${description ? `<div class="autocomplete-info-param-desc">${stripHtml(description)}</div>` : ''}
</li>
`,
)
.join('')}
</ul>
</div>
`
: '';
const buildExamples = (examples) =>
examples?.length
? `
<div class="autocomplete-info-examples-section">
<h4 class="autocomplete-info-section-title">Examples</h4>
${examples
.map(
(example) => `
<pre class="autocomplete-info-example-code">${escapeHtml(example)}</pre>
`,
)
.join('')}
</div>
`
: '';
export const Autocomplete = (doc) =>
h`
<div class="autocomplete-info-container">
<div class="autocomplete-info-tooltip">
<h3 class="autocomplete-info-function-name">${getDocLabel(doc)}</h3>
${doc.synonyms_text ? `<div class="autocomplete-info-function-synonyms">Synonyms: ${doc.synonyms_text}</div>` : ''}
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
${buildParamsList(doc.params)}
${buildExamples(doc.examples)}
</div>
</div>
`[0];
const isValidDoc = (doc) => {
const label = getDocLabel(doc);
return label && !label.startsWith('_') && !['package'].includes(doc.kind);
};
const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
export const getSynonymDoc = (doc, synonym) => {
const synonyms = doc.synonyms || [];
const docLabel = getDocLabel(doc);
// Swap `doc.name` in for `s` in the list of synonyms
const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
return {
...doc,
name: synonym,
longname: synonym,
synonyms: synonymsWithDoc,
synonyms_text: synonymsWithDoc.join(', '),
};
};
const jsdocCompletions = (() => {
const seen = new Set(); // avoid repetition
const completions = [];
for (const doc of jsdoc.docs) {
if (!isValidDoc(doc) || hasExcludedTags(doc)) continue;
const docLabel = getDocLabel(doc);
// Remove duplicates
const synonyms = doc.synonyms || [];
let labels = [docLabel, ...synonyms];
for (const label of labels) {
// https://codemirror.net/docs/ref/#autocomplete.Completion
if (label && !seen.has(label)) {
seen.add(label);
completions.push({
label,
info: () => Autocomplete(getSynonymDoc(doc, label)),
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
});
}
}
}
return completions;
})();
export const strudelAutocomplete = (context) => {
const word = context.matchBefore(/\w*/);
if (word.from === word.to && !context.explicit) return null;
export const strudelAutocomplete = (context /* : CompletionContext */) => {
let word = context.matchBefore(/\w*/);
if (word.from == word.to && !context.explicit) return null;
return {
from: word.from,
options: jsdocCompletions,
@@ -74,11 +128,5 @@ export const strudelAutocomplete = (context /* : CompletionContext */) => {
};
};
export function isAutoCompletionEnabled(on) {
return on
? [
autocompletion({ override: [strudelAutocomplete] }),
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
]
: []; // autocompletion({ override: [] })
}
export const isAutoCompletionEnabled = (enabled) =>
enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : [];
+63
View File
@@ -0,0 +1,63 @@
import {
keymap,
highlightSpecialChars,
drawSelection,
highlightActiveLine,
dropCursor,
rectangularSelection,
crosshairCursor,
lineNumbers,
highlightActiveLineGutter,
} from '@codemirror/view';
import {
defaultHighlightStyle,
syntaxHighlighting,
bracketMatching,
foldGutter,
foldKeymap,
} from '@codemirror/language';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete';
// Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
export const basicSetup = (() => [
// lineNumbers(),
// highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
// foldGutter(),
// drawSelection(),
dropCursor(),
// EditorState.allowMultipleSelections.of(true),
// indentOnInput(),
// syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
// autocompletion(),
rectangularSelection(),
crosshairCursor(),
// highlightActiveLine(),
// highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
// ...searchKeymap,
...historyKeymap,
// ...foldKeymap,
// ...completionKeymap,
]),
])();
/// A minimal set of extensions to create a functional editor. Only
/// includes [the default keymap](#commands.defaultKeymap), [undo
/// history](#commands.history), [special character
/// highlighting](#view.highlightSpecialChars), [custom selection
/// drawing](#view.drawSelection), and [default highlight
/// style](#language.defaultHighlightStyle).
export const minimalSetup = (() => [
highlightSpecialChars(),
history(),
drawSelection(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
keymap.of([...defaultKeymap, ...historyKeymap]),
])();
+8 -3
View File
@@ -1,8 +1,8 @@
import { closeBrackets } from '@codemirror/autocomplete';
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
// import { search, highlightSelectionMatches } from '@codemirror/search';
import { history, indentWithTab } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { indentWithTab } from '@codemirror/commands';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
import {
@@ -24,6 +24,7 @@ import { initTheme, activateTheme, theme } from './themes.mjs';
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
import { widgetPlugin, updateWidgets } from './widget.mjs';
import { persistentAtom } from '@nanostores/persistent';
import { basicSetup } from './basicSetup.mjs';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
@@ -85,13 +86,17 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
/* search(),
highlightSelectionMatches(), */
...initialSettings,
basicSetup,
mondo ? [] : javascript(),
javascriptLanguage.data.of({
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
}),
sliderPlugin,
widgetPlugin,
// indentOnInput(), // works without. already brought with javascript extension?
// bracketMatching(), // does not do anything
syntaxHighlighting(defaultHighlightStyle),
history(),
EditorView.updateListener.of((v) => onChange(v)),
drawSelection({ cursorBlinkRate: 0 }),
Prec.highest(
+3 -2
View File
@@ -1,6 +1,7 @@
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
export let html = (string) => {
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
const template = document.createElement('template');
template.innerHTML = string.trim();
return template.content.childNodes;
};
let parseChunk = (chunk) => {
if (Array.isArray(chunk)) return chunk.flat().join('');
+3 -2
View File
@@ -3,8 +3,9 @@ import { keymap, ViewPlugin } from '@codemirror/view';
// import { searchKeymap } from '@codemirror/search';
import { emacs } from '@replit/codemirror-emacs';
import { vim } from '@replit/codemirror-vim';
// import { vim } from './vim_test.mjs';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
import { defaultKeymap } from '@codemirror/commands';
const vscodePlugin = ViewPlugin.fromClass(
class {
@@ -27,5 +28,5 @@ const keymaps = {
export function keybindings(name) {
const active = keymaps[name];
return [active ? active() : [], keymap.of(historyKeymap)];
return [active ? Prec.high(active()) : []];
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "1.2.3",
"version": "1.2.5",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
@@ -42,7 +42,7 @@
"@lezer/highlight": "^1.2.1",
"@nanostores/persistent": "^0.10.2",
"@replit/codemirror-emacs": "^6.1.0",
"@replit/codemirror-vim": "^6.2.1",
"@replit/codemirror-vim": "^6.3.0",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
+5 -4
View File
@@ -1,6 +1,6 @@
import { hoverTooltip } from '@codemirror/view';
import jsdoc from '../../doc.json';
import { Autocomplete } from './autocomplete.mjs';
import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
const getDocLabel = (doc) => doc.name || doc.longname;
@@ -52,10 +52,11 @@ export const strudelTooltip = hoverTooltip(
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
if (!entry) {
// Try for synonyms
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
if (!entry) {
const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
if (!doc) {
return null;
}
entry = getSynonymDoc(doc, word);
}
return {
@@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip(
create(view) {
let dom = document.createElement('div');
dom.className = 'strudel-tooltip';
const ac = Autocomplete({ doc: entry, label: word });
const ac = Autocomplete(entry);
dom.appendChild(ac);
return { dom };
},
+6 -6
View File
@@ -1,11 +1,11 @@
import { describe, bench } from 'vitest';
import { calculateTactus, sequence, stack } from '../index.mjs';
import { calculateSteps, sequence, stack } from '../index.mjs';
const pat64 = sequence(...Array(64).keys());
describe('steps', () => {
calculateTactus(true);
calculateSteps(true);
bench(
'+tactus',
() => {
@@ -14,7 +14,7 @@ describe('steps', () => {
{ time: 1000 },
);
calculateTactus(false);
calculateSteps(false);
bench(
'-tactus',
() => {
@@ -25,7 +25,7 @@ describe('steps', () => {
});
describe('stack', () => {
calculateTactus(true);
calculateSteps(true);
bench(
'+tactus',
() => {
@@ -34,7 +34,7 @@ describe('stack', () => {
{ time: 1000 },
);
calculateTactus(false);
calculateSteps(false);
bench(
'-tactus',
() => {
@@ -43,4 +43,4 @@ describe('stack', () => {
{ time: 1000 },
);
});
calculateTactus(true);
calculateSteps(true);
+57 -6
View File
@@ -91,6 +91,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
* Define a custom webaudio node to use as a sound source.
*
* @name source
* @synonyms src
* @param {function} getSource
* @synonyms src
*
@@ -309,6 +310,17 @@ export const { fmvelocity } = registerControl('fmvelocity');
*/
export const { bank } = registerControl('bank');
/**
* mix control for the chorus effect
*
* @name chorus
* @param {string | Pattern} chorus mix amount between 0 and 1
* @example
* note("d d a# a").s("sawtooth").chorus(.5)
*
*/
export const { chorus } = registerControl('chorus');
// analyser node send amount 0 - 1 (used by scope)
export const { analyze } = registerControl('analyze');
// fftSize of analyser
@@ -525,6 +537,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
* shape of amplitude modulation
*
* @name tremoloshape
* @synonyms tremshape
* @param {number | Pattern} shape tri | square | sine | saw | ramp
* @example
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
@@ -540,11 +553,13 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
*
*/
export const { drive } = registerControl('drive');
/**
* modulate the amplitude of an orbit to create a "sidechain" like effect
*
* @name duckorbit
* @synonyms duck
* @param {number | Pattern} orbit target orbit
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
@@ -569,6 +584,7 @@ export const { duckdepth } = registerControl('duckdepth');
* the attack time of the duck effect
*
* @name duckattack
* @synonyms duckatt
* @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))
@@ -576,8 +592,6 @@ export const { duckdepth } = registerControl('duckdepth');
*/
export const { duckattack } = registerControl('duckattack', 'duckatt');
export const { drive } = registerControl('drive');
/**
* Create byte beats with custom expressions
*
@@ -700,7 +714,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
* The amount the signal is affected by the phaser effect. Defaults to 0.75
*
* @name phaserdepth
* @synonyms phd
* @synonyms phd, phasdp
* @param {number | Pattern} depth number between 0 and 1
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
@@ -1097,14 +1111,27 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
*
*/
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
/**
* Sets the level of the signal that is fed back into the delay.
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>")
*
*/
export const { delayspeed } = registerControl('delayspeed');
/**
* Sets the time of the delay effect.
*
* @name delaytime
* @param {number | Pattern} seconds between 0 and Infinity
* @name delayspeed
* @param {number | Pattern} delayspeed controls the pitch of the delay feedback
* @synonyms delayt, dt
* @example
* s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
*
*/
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
@@ -1182,6 +1209,7 @@ export const { dry } = registerControl('dry');
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
*
* @name fadeTime
* @synonyms fadeOutTime
* @param {number | Pattern} time between 0 and 1
* @example
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
@@ -1516,6 +1544,29 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade');
*
*/
export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse');
/**
* Sets speed of the sample for the impulse response.
* @name irspeed
* @param {string | Pattern} speed
* @example
* samples('github:switchangel/pad')
* $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.2).irspeed("<2 1 .5>/2").irbegin(.5).roomsize(.5)
*
*/
export const { irspeed } = registerControl('irspeed');
/**
* Sets the beginning of the IR response sample
* @name irbegin
* @param {string | Pattern} begin between 0 and 1
* @synonyms ir
* @example
* samples('github:switchangel/pad')
* $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.65).irspeed("-2").irbegin("<0 .5 .75>/2").roomsize(.6)
*
*/
export const { irbegin } = registerControl('irbegin');
/**
* Sets the room size of the reverb, see `room`.
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "1.2.3",
"version": "1.2.4",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
+87 -12
View File
@@ -1246,7 +1246,8 @@ export const silence = gap(1);
/* Like silence, but with a 'steps' (relative duration) of 0 */
export const nothing = gap(0);
/** A discrete value that repeats once per cycle.
/**
* A discrete value that repeats once per cycle.
*
* @returns {Pattern}
* @example
@@ -1299,7 +1300,8 @@ export function sequenceP(pats) {
return result;
}
/** The given items are played at the same time at the same length.
/**
* The given items are played at the same time at the same length.
*
* @return {Pattern}
* @synonyms polyrhythm, pr
@@ -1382,11 +1384,11 @@ export function stackBy(by, ...pats) {
.setSteps(steps);
}
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
*
* synonyms: `cat`
/**
* Concatenation: combines a list of patterns, switching between them successively, one per cycle.
*
* @return {Pattern}
* @synonyms cat
* @example
* slowcat("e5", "b4", ["d5", "c5"])
*
@@ -2352,20 +2354,42 @@ export const jux = register('jux', function (func, pat) {
/**
* Superimpose and offset multiple times, applying the given function each time.
* @name echoWith
* @synonyms echowith, stutWith, stutwith
* @name echoForEach
* @param {number} times how many times to repeat
* @param {number} time cycle offset between iterations
* @param {function} func function to apply, given the pattern and the iteration index
* @example
* "<0 [2 4]>"
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
* .scale("C:minor").note()
* .n().scale("C:minor")
*/
export const { echoForEach } = register(['echoForEach'], function (times, time, func, pat) {
return stack(...listRange(0, times - 1).map((i) => func(pat.late(Fraction(time).mul(i)), i)));
});
/**
* Superimpose and offset multiple times, applying the given function each time.
* @name echoWith
* @synonyms echowith, stutWith, stutwith
* @param {number} times how many times to repeat
* @param {number} time cycle offset between iterations
* @param {function} func function to apply, given the pattern and the iteration index
* @example
* "<0 [2 4]>".echoWith(4, 1/8, add(2))
* .n().scale("C:minor")
*/
export const { echoWith, echowith, stutWith, stutwith } = register(
['echoWith', 'echowith', 'stutWith', 'stutwith'],
function (times, time, func, pat) {
return stack(...listRange(0, times - 1).map((i) => func(pat.late(Fraction(time).mul(i)), i)));
if (times < 1) {
return silence;
}
let pats = pat;
for (let i = 1; i < times; i++) {
pat = func(pat.late(time));
pats = pats.stack(pat);
}
return pats;
},
);
@@ -2381,7 +2405,7 @@ export const { echoWith, echowith, stutWith, stutwith } = register(
* s("bd sd").echo(3, 1/6, .8)
*/
export const echo = register('echo', function (times, time, feedback, pat) {
return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
return pat._echoForEach(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
});
/**
@@ -2394,7 +2418,58 @@ export const echo = register('echo', function (times, time, feedback, pat) {
* s("bd sd").stut(3, .8, 1/6)
*/
export const stut = register('stut', function (times, feedback, time, pat) {
return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
return pat._echoForEach(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
});
export const applyN = register('applyN', function (n, func, p) {
let result = p;
for (let i = 0; i < n; i++) {
result = func(result);
}
return result;
});
/**
* The plyWith function repeats each event the given number of times, applying the given function to each event.\n
* @name plyWith
* @synonyms plywith
* @param {number} factor how many times to repeat
* @param {function} func function to apply, given the pattern
* @example
* "<0 [2 4]>"
* .plyWith(4, (p) => p.add(2))
* .scale("C:minor").note()
*/
export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) {
const result = pat
.fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor))
.squeezeJoin();
if (__steps) {
result._steps = Fraction(factor).mulmaybe(pat._steps);
}
return result;
});
/**
* The plyForEach function repeats each event the given number of times, applying the given function to each event.
* This version of ply uses the iteration index as an argument to the function, similar to echoWith.
* @name plyForEach
* @synonyms plyforeach
* @param {number} factor how many times to repeat
* @param {function} func function to apply, given the pattern and the iteration index
* @example
* "<0 [2 4]>"
* .plyForEach(4, (p,n) => p.add(n*2))
* .scale("C:minor").note()
*/
export const plyForEach = register(['plyForEach', 'plyforeach'], function (factor, func, pat) {
const result = pat
.fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor))
.squeezeJoin();
if (__steps) {
result._steps = Fraction(factor).mulmaybe(pat._steps);
}
return result;
});
/**
@@ -3277,7 +3352,7 @@ export const slice = register(
* @memberof Pattern
* @returns Pattern
* @example
* s("bd!8").onTriggerTime((hap) => {console.info(hap)})
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
*/
Pattern.prototype.onTriggerTime = function (func) {
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "1.2.4",
"version": "1.2.5",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
"version": "1.2.3",
"version": "1.2.4",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/gamepad",
"version": "1.2.3",
"version": "1.2.4",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
"version": "1.2.3",
"version": "1.2.4",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
+3
View File
@@ -493,6 +493,9 @@ export async function midin(input) {
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`,
);
}
// ensure refs for this input are initialized
if (!refs[input]) {
refs[input] = {};
}
const cc = (cc) => ref(() => refs[input][cc] || 0);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "1.2.4",
"version": "1.2.5",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
+4 -4
View File
@@ -1,10 +1,10 @@
import { describe, bench } from 'vitest';
import { calculateTactus } from '../../core/index.mjs';
import { calculateSteps } from '../../core/index.mjs';
import { mini } from '../index.mjs';
describe('mini', () => {
calculateTactus(true);
calculateSteps(true);
bench(
'+tactus',
() => {
@@ -13,7 +13,7 @@ describe('mini', () => {
{ time: 1000 },
);
calculateTactus(false);
calculateSteps(false);
bench(
'-tactus',
() => {
@@ -21,5 +21,5 @@ describe('mini', () => {
},
{ time: 1000 },
);
calculateTactus(true);
calculateSteps(true);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
"version": "1.2.3",
"version": "1.2.4",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -108,7 +108,7 @@ export function mondo(code, offset = 0) {
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
}
let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
export let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
export const mondi = (str, offset) => {
const code = `[${str}]`;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mondo",
"version": "1.1.1",
"version": "1.1.4",
"description": "mondo notation for strudel",
"main": "mondough.mjs",
"type": "module",
+2 -2
View File
@@ -1,5 +1,5 @@
import { defineConfig } from 'vite';
//import { dependencies } from './package.json';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
@@ -12,7 +12,7 @@ export default defineConfig({
fileName: (ext) => ({ es: 'mondough.mjs' })[ext],
},
rollupOptions: {
// external: [...Object.keys(dependencies)],
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/motion",
"version": "1.2.3",
"version": "1.2.4",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mqtt",
"version": "1.2.3",
"version": "1.2.4",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
"version": "1.2.3",
"version": "1.2.4",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"type": "module",
+4 -4
View File
@@ -1,10 +1,10 @@
import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
import { isTauri } from '../desktopbridge/utils.mjs';
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
import { isTauri } from '../desktopbridge/utils.mjs'; */
import { oscTrigger } from './osc.mjs';
const trigger = isTauri() ? oscTriggerTauri : oscTrigger;
const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger;
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
const currentTime = performance.now() / 1000;
return trigger(null, hap, currentTime, cps, targetTime);
return trigger(hap, currentTime, cps, targetTime);
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
"version": "1.2.4",
"version": "1.2.6",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
+10
View File
@@ -20,3 +20,13 @@ samples('http://localhost:5432')
LOG=1 npx @strudel/sampler # adds logging
PORT=5555 npx @strudel/sampler # changes port
```
## static json
when running with `--json`, you will simply get the json logged back:
```sh
npx --yes @strudel/sampler --json > strudel.json
```
this is useful if you want to create a sample pack from the current folder.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/sampler",
"version": "0.2.0",
"version": "0.2.3",
"description": "",
"keywords": [
"tidalcycles",
+69 -23
View File
@@ -1,22 +1,20 @@
#!/usr/bin/env node
import cowsay from 'cowsay';
import { createReadStream, existsSync } from 'fs';
import { createReadStream, existsSync, writeFileSync } from 'fs';
import { readdir } from 'fs/promises';
import http from 'http';
import { join, sep } from 'path';
import { join, resolve, sep } from 'path';
import readline from 'readline';
import os from 'os';
// eslint-disable-next-line
const LOG = !!process.env.LOG || false;
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
console.log(
cowsay.say({
text: 'welcome to @strudel/sampler',
e: 'oO',
T: 'U ',
}),
);
const isAudioFile = (f) => {
const ext = f.split('.').slice(-1)[0].toLowerCase();
return VALID_AUDIO_EXTENSIONS.includes(ext);
};
async function getFilesInDirectory(directory) {
let files = [];
@@ -29,42 +27,90 @@ async function getFilesInDirectory(directory) {
continue;
}
try {
const subFiles = (await getFilesInDirectory(fullPath)).filter((f) =>
['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()),
);
const subFiles = (await getFilesInDirectory(fullPath)).filter(isAudioFile);
files = files.concat(subFiles);
LOG && console.log(`${dirent.name} (${subFiles.length})`);
} catch (err) {
LOG && console.warn(`skipped due to error: ${fullPath}`);
}
} else {
files.push(fullPath);
isAudioFile(fullPath) && files.push(fullPath);
}
}
return files;
}
async function getBanks(directory) {
async function getBanks(directory, flat = false) {
let files = await getFilesInDirectory(directory);
let banks = {};
directory = directory.split(sep).join('/');
files = files.map((path) => {
path = path.split(sep).join('/');
const [bank] = path.split('/').slice(-2);
const subDir = path.replace(directory, '');
const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore
const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension
let bank = flat ? subDirFlatStem : path.split('/').slice(-2)[0];
banks[bank] = banks[bank] || [];
const relativeUrl = path.replace(directory, '');
banks[bank].push(relativeUrl);
return relativeUrl;
banks[bank].push(subDir);
return subDir;
});
banks._base = `http://localhost:5432`;
return { banks, files };
}
// eslint-disable-next-line
const directory = process.cwd();
const args = process.argv.slice(2);
function getArgValue(flag) {
const i = args.indexOf(flag);
if (i !== -1) {
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
if (nextIsFlag) return true;
return args[i + 1];
}
}
function getInput(query) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) =>
rl.question(query, (response) => {
rl.close();
resolve(response);
}),
);
}
let directory = getArgValue('--dir') || process.cwd();
directory = resolve(directory);
if (args.includes('--json')) {
const { banks } = await getBanks(directory, getArgValue('--flat'));
const json = JSON.stringify(banks);
const outFile = resolve(directory, 'strudel.json');
if (existsSync(outFile)) {
const answer = await getInput(`Warning: File already exists at ${outFile}. Overwrite? (y/N): `);
if (answer.toLowerCase() !== 'y') {
console.log('Aborted.');
process.exit(0);
}
}
writeFileSync(outFile, json, 'utf8');
console.log(`Wrote json to ${outFile}`);
}
console.log(
cowsay.say({
text: 'welcome to @strudel/sampler',
e: 'oO',
T: 'U ',
}),
);
const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const { banks, files } = await getBanks(directory);
const { banks, files } = await getBanks(directory, getArgValue('--flat'));
if (req.url === '/') {
res.setHeader('Content-Type', 'application/json');
return res.end(JSON.stringify(banks));
@@ -72,7 +118,7 @@ const server = http.createServer(async (req, res) => {
let subpath = decodeURIComponent(req.url);
const filePath = join(directory, subpath.split('/').join(sep));
//console.log('GET:', filePath);
// console.log('GET:', filePath);
const isFound = existsSync(filePath);
if (!isFound) {
res.statusCode = 404;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
"version": "1.2.3",
"version": "1.2.4",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
"version": "1.2.4",
"version": "1.2.5",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
+21 -3
View File
@@ -174,7 +174,7 @@ let curves = ['linear', 'exponential'];
export function getPitchEnvelope(param, value, t, holdEnd) {
// envelope is active when any of these values is set
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
if (!hasEnvelope) {
if (hasEnvelope === undefined) {
return;
}
const penv = nanFallback(value.penv, 1, true);
@@ -211,11 +211,29 @@ export function getVibratoOscillator(param, value, t) {
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
const constantNode = new ConstantSourceNode(audioContext);
constantNode.start(startTime);
constantNode.stop(stopTime);
// Certain browsers requires audio nodes to be connected in order for their onended events
// to fire, so we _mute it_ and then connect it to the destination
const zeroGain = gainNode(0);
zeroGain.connect(audioContext.destination);
constantNode.connect(zeroGain);
// Schedule the `onComplete` callback to occur at `stopTime`
constantNode.onended = () => {
// Ensure garbage collection
try {
zeroGain.disconnect();
} catch {
// pass
}
try {
constantNode.disconnect();
} catch {
// pass
}
onComplete();
};
constantNode.start(startTime);
constantNode.stop(stopTime);
return constantNode;
}
const mod = (freq, range = 1, type = 'sine') => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "1.2.4",
"version": "1.2.5",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
+16 -6
View File
@@ -1,7 +1,9 @@
import reverbGen from './reverbGen.mjs';
import { clamp } from './util.mjs';
if (typeof AudioContext !== 'undefined') {
AudioContext.prototype.adjustLength = function (duration, buffer) {
AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) {
const sampleOffset = Math.floor(clamp(offsetAmount, 0, 1) * buffer.length);
const newLength = buffer.sampleRate * duration;
const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate);
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
@@ -9,22 +11,30 @@ if (typeof AudioContext !== 'undefined') {
let newData = newBuffer.getChannelData(channel);
for (let i = 0; i < newLength; i++) {
newData[i] = oldData[i] || 0;
// loop the buffer around to prevent
let position = (sampleOffset + i * Math.abs(speed)) % oldData.length;
if (speed < 1) {
position = position * -1;
}
newData[i] = oldData.at(position) || 0;
}
}
return newBuffer;
};
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir) {
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) {
const convolver = this.createConvolver();
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir) => {
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir, irspeed, irbegin) => {
convolver.duration = d;
convolver.fade = fade;
convolver.lp = lp;
convolver.dim = dim;
convolver.ir = ir;
convolver.irspeed = irspeed;
convolver.irbegin = irbegin;
if (ir) {
convolver.buffer = this.adjustLength(d, ir);
convolver.buffer = this.adjustLength(d, ir, irspeed, irbegin);
} else {
reverbGen.generateReverb(
{
@@ -41,7 +51,7 @@ if (typeof AudioContext !== 'undefined') {
);
}
};
convolver.generate(duration, fade, lp, dim, ir);
convolver.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
return convolver;
};
}
+48 -43
View File
@@ -196,6 +196,52 @@ function getSamplesPrefixHandler(url) {
return;
}
export async function fetchSampleMap(url) {
// check if custom prefix handler
const handler = getSamplesPrefixHandler(url);
if (handler) {
return handler(url);
}
url = resolveSpecialPaths(url);
if (url.startsWith('github:')) {
url = githubPath(url, 'strudel.json');
}
if (url.startsWith('local:')) {
url = `http://localhost:5432`;
}
if (url.startsWith('shabda:')) {
let [_, path] = url.split('shabda:');
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
}
if (url.startsWith('shabda/speech')) {
let [_, path] = url.split('shabda/speech');
path = path.startsWith('/') ? path.substring(1) : path;
let [params, words] = path.split(':');
let gender = 'f';
let language = 'en-GB';
if (params) {
[language, gender] = params.split('/');
}
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
const base = url.split('/').slice(0, -1).join('/');
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
const json = await fetch(url)
.then((res) => res.json())
.catch((error) => {
console.error(error);
throw new Error(`error loading "${url}"`);
});
return [json, json._base || base];
}
/**
* Loads a collection of samples to use with `s`
* @example
@@ -217,49 +263,8 @@ function getSamplesPrefixHandler(url) {
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
if (typeof sampleMap === 'string') {
// check if custom prefix handler
const handler = getSamplesPrefixHandler(sampleMap);
if (handler) {
return handler(sampleMap);
}
sampleMap = resolveSpecialPaths(sampleMap);
if (sampleMap.startsWith('github:')) {
sampleMap = githubPath(sampleMap, 'strudel.json');
}
if (sampleMap.startsWith('local:')) {
sampleMap = `http://localhost:5432`;
}
if (sampleMap.startsWith('shabda:')) {
let [_, path] = sampleMap.split('shabda:');
sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`;
}
if (sampleMap.startsWith('shabda/speech')) {
let [_, path] = sampleMap.split('shabda/speech');
path = path.startsWith('/') ? path.substring(1) : path;
let [params, words] = path.split(':');
let gender = 'f';
let language = 'en-GB';
if (params) {
[language, gender] = params.split('/');
}
sampleMap = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
const base = sampleMap.split('/').slice(0, -1).join('/');
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
return fetch(sampleMap)
.then((res) => res.json())
.then((json) => samples(json, baseUrl || json._base || base, options))
.catch((error) => {
console.error(error);
throw new Error(`error loading "${sampleMap}"`);
});
const [json, base] = await fetchSampleMap(sampleMap);
return samples(json, baseUrl || base, options);
}
const { prebake, tag } = options;
processSampleMap(
+33 -10
View File
@@ -113,6 +113,19 @@ export async function aliasBank(...args) {
}
}
/**
* Register an alias for a sound.
* @param {string} original - The original sound name
* @param {string} alias - The alias to use for the sound
*/
export function soundAlias(original, alias) {
if (getSound(original) == null) {
logger('soundAlias: original sound not found');
return;
}
soundMap.setKey(alias, getSound(original));
}
export function getSound(s) {
if (typeof s !== 'string') {
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
@@ -196,7 +209,7 @@ export const resetLoadedSounds = () => soundMap.set({});
let audioContext;
export const setDefaultAudioContext = () => {
audioContext = new AudioContext();
audioContext = new AudioContext({ latencyHint: 'playback' });
return audioContext;
};
@@ -212,11 +225,17 @@ export function getAudioContextCurrentTime() {
return getAudioContext().currentTime;
}
let externalWorklets = [];
export function registerWorklet(url) {
externalWorklets.push(url);
}
let workletsLoading;
function loadWorklets() {
if (!workletsLoading) {
const audioCtx = getAudioContext();
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
const allWorkletURLs = externalWorklets.concat([workletsUrl]);
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL)));
}
return workletsLoading;
@@ -282,7 +301,6 @@ export async function initAudioOnFirstClick(options) {
return audioReady;
}
let delays = {};
const maxfeedback = 0.98;
let channelMerger, destinationGain;
@@ -326,7 +344,7 @@ export const panic = () => {
channelMerger == null;
};
function getDelay(orbit, delaytime, delayfeedback, t, channels) {
function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
@@ -414,7 +432,7 @@ function connectToOrbit(node, orbit) {
function setOrbit(audioContext, orbit, channels) {
if (orbits[orbit] == null) {
orbits[orbit] = {
gain: new GainNode(audioContext, { gain: 1 }),
gain: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }),
};
connectToDestination(orbits[orbit].gain, channels);
}
@@ -442,19 +460,22 @@ function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1
}
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir) {
function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) {
// If no reverb has been created for a given orbit, create one
if (!orbits[orbit].reverbNode) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
const reverb = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
connectToOrbit(reverb, orbit);
orbits[orbit].reverbNode = reverb;
}
if (
hasChanged(duration, orbits[orbit].reverbNode.duration) ||
hasChanged(fade, orbits[orbit].reverbNode.fade) ||
hasChanged(lp, orbits[orbit].reverbNode.lp) ||
hasChanged(dim, orbits[orbit].reverbNode.dim) ||
hasChanged(irspeed, orbits[orbit].reverbNode.irspeed) ||
hasChanged(irbegin, orbits[orbit].reverbNode.irbegin) ||
orbits[orbit].reverbNode.ir !== ir
) {
// only regenerate when something has changed
@@ -462,7 +483,7 @@ function getReverb(orbit, duration, fade, lp, dim, ir) {
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir);
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
}
return orbits[orbit].reverbNode;
}
@@ -619,6 +640,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
roomdim,
roomsize,
ir,
irspeed,
irbegin,
i = getDefaultValue('i'),
velocity = getDefaultValue('velocity'),
analyze, // analyser wet
@@ -832,7 +855,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
const delayNode = getDelay(orbit, delaytime, delayfeedback, t);
delaySend = effectSend(post, delayNode, delay);
audioNodes.push(delaySend);
}
@@ -850,7 +873,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
roomIR = await loadBuffer(url, ac, ir, 0);
}
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels);
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend);
}
+1
View File
@@ -0,0 +1 @@
pattern.wav
+3
View File
@@ -0,0 +1,3 @@
# supradough
platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.
+123
View File
@@ -0,0 +1,123 @@
// this is a poc of how a pattern can be rendered as a wav file using node
// run via: node dough-export.mjs
import fs from 'node:fs';
import WavEncoder from 'wav-encoder';
import { evalScope } from '@strudel/core';
import { miniAllStrings } from '@strudel/mini';
import { Dough } from './dough.mjs';
await evalScope(
import('@strudel/core'),
import('@strudel/mini'),
import('@strudel/tonal'),
// import('@strudel/tonal'),
);
miniAllStrings(); // allows using single quotes for mini notation / skip transpilation
let sampleRate = 48000,
cps = 0.4;
/* await doughsamples('github:eddyflux/crate');
await doughsamples('github:eddyflux/wax'); */
let pat = note('c,eb,g,<bb c4 d4 eb4>')
.s('sine')
.press()
.add(note(24))
.fmi(3)
.fmh(5.01)
.dec(0.4)
.delay('.6:<.12 .22>:.8')
.jux(press)
.rarely(add(note('12')))
.lpf(400)
.lpq(0.2)
.lpd(0.4)
.lpenv(3)
.fmdecay(0.4)
.fmenv(1)
.postgain(0.6)
.stack(s('<pink white>*8').dec(0.07).rarely(ply('2')).delay(0.5).hpf(sine.range(200, 2000).slow(4)).hpq(0.2))
.stack(
s('[- white@3]*2')
.dec(0.4)
.hpf('<2000!3 <4000 8000>>*4')
.hpq(0.6)
.ply('<1 2>*4')
.postgain(0.5)
.delay(0.5)
.jux(rev)
.lpf(5000),
)
.stack(
note('<c2 - [- f1] ->*2')
.s('square')
.lpf(sine.range(100, 300).slow(4))
.lpe(1)
.segment(8)
.lpd(0.3)
.lpq(0.2)
.dec(0.2)
.speed('<1 2>')
.ply('<1 2>')
.postgain(1),
)
.stack(
chord('<Cm Cm7 Cm9 Cm11 Fm Fm7 Fm9 Fm11>')
.voicing()
.s('<sine>')
.clip(1)
.rel(0.4)
.vib('4:.2')
.gain(0.7)
.hpf(1200)
.fm(0.5)
.att(1)
.lpa(0.5)
.lpf(200)
.lpenv(4)
.chorus(0.8),
)
.slow(1 / cps);
let cycles = 30;
let seconds = cycles + 1; // 1s release tail
const haps = pat.queryArc(0, cycles);
const dough = new Dough(sampleRate);
console.log('spawn voices...');
haps.forEach((hap) => {
hap.value._begin = Number(hap.whole.begin);
hap.value._duration = hap.duration /* / cps */;
dough.scheduleSpawn(hap.value);
});
console.log(`render ${seconds}s long buffer, each dot is 1 second:`);
const buffers = [new Float32Array(seconds * sampleRate), new Float32Array(seconds * sampleRate)];
let t = performance.now();
while (dough.t <= buffers[0].length) {
dough.update();
buffers[0][dough.t] = dough.out[0];
buffers[1][dough.t] = dough.out[1];
if (dough.t % sampleRate === 0) {
process.stdout.write('.');
}
}
const took = (performance.now() - t) / 1000;
const load = (took / seconds) * 100;
const speed = (seconds / took).toFixed(2);
console.log('');
console.log(`done!
rendered ${seconds}s in ${took.toFixed(2)}s
speed: ${speed}x
load: ${load.toFixed(2)}%`);
const patternAudio = {
sampleRate,
channelData: buffers,
};
WavEncoder.encode(patternAudio).then((buffer) => {
fs.writeFileSync('pattern.wav', new Float32Array(buffer));
});
+39
View File
@@ -0,0 +1,39 @@
import { Dough } from './dough.mjs';
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
class DoughProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.dough = new Dough(sampleRate, currentTime);
this.port.onmessage = (event) => {
if (event.data.spawn) {
this.dough.scheduleSpawn(event.data.spawn);
} else if (event.data.sample) {
this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate);
} else if (event.data.samples) {
event.data.samples.forEach(([name, channels, sampleRate]) => {
this.dough.loadSample(name, channels, sampleRate);
});
} else {
console.log('unrecognized event type', event.data);
}
};
}
process(inputs, outputs, params) {
if (this.disconnected) {
return false;
}
const output = outputs[0];
for (let i = 0; i < output[0].length; i++) {
this.dough.update();
for (let c = 0; c < output.length; c++) {
//prevent speaker blowout via clipping if threshold exceeds
output[c][i] = clamp(this.dough.out[c], -1, 1);
}
}
return true; // keep the audio processing going
}
}
registerProcessor('dough-processor', DoughProcessor);
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
// import _workletUrl from './dough-worklet.mjs?url'; // only for dev (breaks for production build)
import _workletUrl from './dough-worklet.mjs?audioworklet'; // only for prod (breaks in development?!)
export * from './dough.mjs';
export const workletUrl = _workletUrl;
+37
View File
@@ -0,0 +1,37 @@
{
"name": "supradough",
"version": "1.2.3",
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"devDependencies": {
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*",
"wav-encoder": "^1.3.0"
},
"dependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
"version": "1.2.3",
"version": "1.2.4",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/transpiler",
"version": "1.2.3",
"version": "1.2.4",
"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.4",
"version": "1.2.5",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs",
"publishConfig": {
+1
View File
@@ -7,4 +7,5 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './webaudio.mjs';
export * from './scope.mjs';
export * from './spectrum.mjs';
export * from './supradough.mjs';
export * from 'superdough';
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
"version": "1.2.4",
"version": "1.2.5",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
@@ -35,7 +35,8 @@
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"superdough": "workspace:*"
"superdough": "workspace:*",
"supradough": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
+130
View File
@@ -0,0 +1,130 @@
import { Pattern } from '@strudel/core';
import { connectToDestination, getAudioContext, getWorklet } from 'superdough';
let doughWorklet;
function initDoughWorklet() {
const ac = getAudioContext();
doughWorklet = getWorklet(
ac,
'dough-processor',
{},
{
outputChannelCount: [2],
},
);
connectToDestination(doughWorklet); // channels?
}
const soundMap = new Map();
const loadedSounds = new Map();
Pattern.prototype.supradough = function () {
return this.onTrigger((hap, __, cps, begin) => {
hap.value._begin = begin;
hap.value._duration = hap.duration / cps;
!doughWorklet && initDoughWorklet();
const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s;
const n = hap.value.n ?? 0;
const soundKey = `${s}:${n}`;
if (soundMap.has(s)) {
hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers)
}
if (soundMap.has(s) && !loadedSounds.has(soundKey)) {
const urls = soundMap.get(s);
const url = urls[n % urls.length];
console.log(`load ${soundKey} from ${url}`);
const loadSample = fetchSample(url);
loadedSounds.set(soundKey, loadSample);
loadSample.then(({ channels, sampleRate }) =>
doughWorklet.port.postMessage({
sample: soundKey,
channels,
sampleRate,
}),
);
}
doughWorklet.port.postMessage({ spawn: hap.value });
}, 1);
};
function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl');
}
let [_, path] = base.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
if (path.split('/').length === 2) {
// assume main as default branch if none set
path += '/main';
}
return `https://raw.githubusercontent.com/${path}/${subpath}`;
}
export async function fetchSampleMap(url) {
if (url.startsWith('github:')) {
url = githubPath(url, 'strudel.json');
}
if (url.startsWith('local:')) {
url = `http://localhost:5432`;
}
if (url.startsWith('shabda:')) {
let [_, path] = url.split('shabda:');
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
}
if (url.startsWith('shabda/speech')) {
let [_, path] = url.split('shabda/speech');
path = path.startsWith('/') ? path.substring(1) : path;
let [params, words] = path.split(':');
let gender = 'f';
let language = 'en-GB';
if (params) {
[language, gender] = params.split('/');
}
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
const base = url.split('/').slice(0, -1).join('/');
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
const json = await fetch(url)
.then((res) => res.json())
.catch((error) => {
console.error(error);
throw new Error(`error loading "${url}"`);
});
return [json, json._base || base];
}
// for some reason, only piano and flute work.. is it because mp3??
async function fetchSample(url) {
const buffer = await fetch(url)
.then((res) => res.arrayBuffer())
.then((buf) => getAudioContext().decodeAudioData(buf));
let channels = [];
for (let i = 0; i < buffer.numberOfChannels; i++) {
channels.push(buffer.getChannelData(i));
}
return { channels, sampleRate: buffer.sampleRate };
}
export async function doughsamples(sampleMap, baseUrl) {
if (typeof sampleMap === 'string') {
const [json, base] = await fetchSampleMap(sampleMap);
// console.log('json', json, 'base', base);
return doughsamples(json, base);
}
Object.entries(sampleMap).map(async ([key, urls]) => {
if (key !== '_base') {
urls = urls.map((url) => baseUrl + url);
// console.log('set', key, urls);
soundMap.set(key, urls);
}
});
}
+6 -1
View File
@@ -5,7 +5,12 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as strudel from '@strudel/core';
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough';
import './supradough.mjs';
import { workletUrl } from 'supradough';
registerWorklet(workletUrl);
const { Pattern, logger, repl } = strudel;
setLogger(logger);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/xen",
"version": "1.2.3",
"version": "1.2.4",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"type": "module",
+9 -4
View File
@@ -139,10 +139,10 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) {
/* Load a new scale */
Tune.prototype.loadScale = function(name){
Tune.prototype.loadScale = function(scale){
/* load the scale */
var freqs = TuningList[name].frequencies
var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies
this.scale = []
for (var i=0;i<freqs.length-1;i++) {
this.scale.push(freqs[i]/freqs[0])
@@ -207,8 +207,13 @@ Tune.prototype.search = function(letters) {
return possible
}
Tune.prototype.isValidScale = function(name) {
return !!TuningList[name];
function isArrayOfNumbers(arg) {
return Array.isArray(arg) && arg.length > 0 && arg.every(item => typeof item === 'number' && !isNaN(item));
}
/* allow an array of values too */
Tune.prototype.isValidScale = function(scale) {
return !!TuningList[scale] || isArrayOfNumbers(scale) ;
}
/* Return a collection of notes as an array */
+33 -10
View File
@@ -201,8 +201,8 @@ importers:
specifier: ^6.1.0
version: 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@replit/codemirror-vim':
specifier: ^6.2.1
version: 6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
specifier: ^6.3.0
version: 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@replit/codemirror-vscode-keymap':
specifier: ^6.0.2
version: 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
@@ -517,6 +517,18 @@ importers:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
packages/supradough:
devDependencies:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
wav-encoder:
specifier: ^1.3.0
version: 1.3.0
packages/tidal:
dependencies:
'@strudel/core':
@@ -625,6 +637,9 @@ importers:
superdough:
specifier: workspace:*
version: link:../superdough
supradough:
specifier: workspace:*
version: link:../supradough
devDependencies:
vite:
specifier: ^6.0.11
@@ -2232,14 +2247,14 @@ packages:
'@codemirror/state': ^6.0.1
'@codemirror/view': ^6.3.0
'@replit/codemirror-vim@6.2.1':
resolution: {integrity: sha512-qDAcGSHBYU5RrdO//qCmD8K9t6vbP327iCj/iqrkVnjbrpFhrjOt92weGXGHmTNRh16cUtkUZ7Xq7rZf+8HVow==}
'@replit/codemirror-vim@6.3.0':
resolution: {integrity: sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==}
peerDependencies:
'@codemirror/commands': ^6.0.0
'@codemirror/language': ^6.1.0
'@codemirror/search': ^6.2.0
'@codemirror/state': ^6.0.1
'@codemirror/view': ^6.0.3
'@codemirror/commands': 6.x.x
'@codemirror/language': 6.x.x
'@codemirror/search': 6.x.x
'@codemirror/state': 6.x.x
'@codemirror/view': 6.x.x
'@replit/codemirror-vscode-keymap@6.0.2':
resolution: {integrity: sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==}
@@ -5714,6 +5729,7 @@ packages:
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
node-fetch-native@1.6.6:
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
@@ -6803,6 +6819,7 @@ packages:
source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
engines: {node: '>= 8'}
deprecated: The work that was done in this beta branch won't be included in future versions
sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
@@ -7535,6 +7552,9 @@ packages:
walk-up-path@3.0.1:
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
wav-encoder@1.3.0:
resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==}
wav@1.0.2:
resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==}
@@ -7657,6 +7677,7 @@ packages:
workbox-google-analytics@7.0.0:
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
workbox-navigation-preload@7.0.0:
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
@@ -9595,7 +9616,7 @@ snapshots:
'@codemirror/state': 6.5.1
'@codemirror/view': 6.36.2
'@replit/codemirror-vim@6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)':
'@replit/codemirror-vim@6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)':
dependencies:
'@codemirror/commands': 6.8.0
'@codemirror/language': 6.10.8
@@ -15955,6 +15976,8 @@ snapshots:
walk-up-path@3.0.1: {}
wav-encoder@1.3.0: {}
wav@1.0.2:
dependencies:
buffer-alloc: 1.2.0
+221 -21
View File
@@ -1847,6 +1847,27 @@ exports[`runs examples > example "chop" example index 0 1`] = `
]
`;
exports[`runs examples > example "chorus" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]",
"[ 1/4 → 1/2 | note:d s:sawtooth chorus:0.5 ]",
"[ 1/2 → 3/4 | note:a# s:sawtooth chorus:0.5 ]",
"[ 3/4 → 1/1 | note:a s:sawtooth chorus:0.5 ]",
"[ 1/1 → 5/4 | note:d s:sawtooth chorus:0.5 ]",
"[ 5/4 → 3/2 | note:d s:sawtooth chorus:0.5 ]",
"[ 3/2 → 7/4 | note:a# s:sawtooth chorus:0.5 ]",
"[ 7/4 → 2/1 | note:a s:sawtooth chorus:0.5 ]",
"[ 2/1 → 9/4 | note:d s:sawtooth chorus:0.5 ]",
"[ 9/4 → 5/2 | note:d s:sawtooth chorus:0.5 ]",
"[ 5/2 → 11/4 | note:a# s:sawtooth chorus:0.5 ]",
"[ 11/4 → 3/1 | note:a s:sawtooth chorus:0.5 ]",
"[ 3/1 → 13/4 | note:d s:sawtooth chorus:0.5 ]",
"[ 13/4 → 7/2 | note:d s:sawtooth chorus:0.5 ]",
"[ 7/2 → 15/4 | note:a# s:sawtooth chorus:0.5 ]",
"[ 15/4 → 4/1 | note:a s:sawtooth chorus:0.5 ]",
]
`;
exports[`runs examples > example "chunk" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:A4 ]",
@@ -2585,6 +2606,52 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = `
]
`;
exports[`runs examples > example "delayfeedback" example index 0 2`] = `
[
"[ 0/1 → 1/1 | s:bd delay:0.25 delayfeedback:0.25 ]",
"[ 1/1 → 2/1 | s:bd delay:0.25 delayfeedback:0.5 ]",
"[ 2/1 → 3/1 | s:bd delay:0.25 delayfeedback:0.75 ]",
"[ 3/1 → 4/1 | s:bd delay:0.25 delayfeedback:1 ]",
]
`;
exports[`runs examples > example "delayspeed" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
"[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
"[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
"[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
"[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
]
`;
exports[`runs examples > example "delaysync" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd delay:0.25 delaysync:0.125 ]",
@@ -2598,19 +2665,6 @@ exports[`runs examples > example "delaysync" example index 0 1`] = `
]
`;
exports[`runs examples > example "delaytime" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd delay:0.25 delaytime:0.125 ]",
"[ 1/2 → 1/1 | s:bd delay:0.25 delaytime:0.125 ]",
"[ 1/1 → 3/2 | s:bd delay:0.25 delaytime:0.25 ]",
"[ 3/2 → 2/1 | s:bd delay:0.25 delaytime:0.25 ]",
"[ 2/1 → 5/2 | s:bd delay:0.25 delaytime:0.5 ]",
"[ 5/2 → 3/1 | s:bd delay:0.25 delaytime:0.5 ]",
"[ 3/1 → 7/2 | s:bd delay:0.25 delaytime:1 ]",
"[ 7/2 → 4/1 | s:bd delay:0.25 delaytime:1 ]",
]
`;
exports[`runs examples > example "density" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
@@ -3247,20 +3301,28 @@ exports[`runs examples > example "echo" example index 0 1`] = `
]
`;
exports[`runs examples > example "echoForEach" example index 0 1`] = `[]`;
exports[`runs examples > example "echoWith" example index 0 1`] = `
[
"[ -3/8 ⇜ (0/1 → 1/8) | note:Bb3 ]",
"[ -1/4 ⇜ (0/1 → 1/4) | note:D4 ]",
"[ -1/8 ⇜ (0/1 → 3/8) | note:F4 ]",
"[ -1/4 ⇜ (0/1 → 1/8) ⇝ 1/4 | note:D4 ]",
"[ -1/8 ⇜ (0/1 → 1/8) ⇝ 3/8 | note:F4 ]",
"[ 0/1 → 1/1 | note:C3 ]",
"[ -1/4 ⇜ (1/8 → 1/4) | note:D4 ]",
"[ -1/8 ⇜ (1/8 → 1/4) ⇝ 3/8 | note:F4 ]",
"[ (1/8 → 1/1) ⇝ 9/8 | note:Eb3 ]",
"[ -1/8 ⇜ (1/4 → 3/8) | note:F4 ]",
"[ (1/4 → 1/1) ⇝ 5/4 | note:G3 ]",
"[ (3/8 → 1/1) ⇝ 11/8 | note:Bb3 ]",
"[ 1/8 ⇜ (1/1 → 9/8) | note:Eb3 ]",
"[ 1/4 ⇜ (1/1 → 5/4) | note:G3 ]",
"[ 3/8 ⇜ (1/1 → 11/8) | note:Bb3 ]",
"[ 1/4 ⇜ (1/1 → 9/8) ⇝ 5/4 | note:G3 ]",
"[ 3/8 ⇜ (1/1 → 9/8) ⇝ 11/8 | note:Bb3 ]",
"[ 1/1 → 3/2 | note:Eb3 ]",
"[ 1/4 ⇜ (9/8 → 5/4) | note:G3 ]",
"[ 3/8 ⇜ (9/8 → 5/4) ⇝ 11/8 | note:Bb3 ]",
"[ 9/8 → 13/8 | note:G3 ]",
"[ 3/8 ⇜ (5/4 → 11/8) | note:Bb3 ]",
"[ 5/4 → 7/4 | note:Bb3 ]",
"[ 11/8 → 15/8 | note:D4 ]",
"[ 3/2 → 2/1 | note:G3 ]",
@@ -3268,17 +3330,23 @@ exports[`runs examples > example "echoWith" example index 0 1`] = `
"[ (7/4 → 2/1) ⇝ 9/4 | note:D4 ]",
"[ (15/8 → 2/1) ⇝ 19/8 | note:F4 ]",
"[ 13/8 ⇜ (2/1 → 17/8) | note:Bb3 ]",
"[ 7/4 ⇜ (2/1 → 9/4) | note:D4 ]",
"[ 15/8 ⇜ (2/1 → 19/8) | note:F4 ]",
"[ 7/4 ⇜ (2/1 → 17/8) ⇝ 9/4 | note:D4 ]",
"[ 15/8 ⇜ (2/1 → 17/8) ⇝ 19/8 | note:F4 ]",
"[ 2/1 → 3/1 | note:C3 ]",
"[ 7/4 ⇜ (17/8 → 9/4) | note:D4 ]",
"[ 15/8 ⇜ (17/8 → 9/4) ⇝ 19/8 | note:F4 ]",
"[ (17/8 → 3/1) ⇝ 25/8 | note:Eb3 ]",
"[ 15/8 ⇜ (9/4 → 19/8) | note:F4 ]",
"[ (9/4 → 3/1) ⇝ 13/4 | note:G3 ]",
"[ (19/8 → 3/1) ⇝ 27/8 | note:Bb3 ]",
"[ 17/8 ⇜ (3/1 → 25/8) | note:Eb3 ]",
"[ 9/4 ⇜ (3/1 → 13/4) | note:G3 ]",
"[ 19/8 ⇜ (3/1 → 27/8) | note:Bb3 ]",
"[ 9/4 ⇜ (3/1 → 25/8) ⇝ 13/4 | note:G3 ]",
"[ 19/8 ⇜ (3/1 → 25/8) ⇝ 27/8 | note:Bb3 ]",
"[ 3/1 → 7/2 | note:Eb3 ]",
"[ 9/4 ⇜ (25/8 → 13/4) | note:G3 ]",
"[ 19/8 ⇜ (25/8 → 13/4) ⇝ 27/8 | note:Bb3 ]",
"[ 25/8 → 29/8 | note:G3 ]",
"[ 19/8 ⇜ (13/4 → 27/8) | note:Bb3 ]",
"[ 13/4 → 15/4 | note:Bb3 ]",
"[ 27/8 → 31/8 | note:D4 ]",
"[ 7/2 → 4/1 | note:G3 ]",
@@ -4875,6 +4943,43 @@ exports[`runs examples > example "irand" example index 0 1`] = `
]
`;
exports[`runs examples > example "irbegin" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
"[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
"[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
]
`;
exports[`runs examples > example "iresponse" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd room:0.8 ir:shaker_large i:0 ]",
@@ -4896,6 +5001,43 @@ exports[`runs examples > example "iresponse" example index 0 1`] = `
]
`;
exports[`runs examples > example "irspeed" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
"[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
"[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
]
`;
exports[`runs examples > example "isaw" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:c3 clip:1 ]",
@@ -7237,6 +7379,64 @@ exports[`runs examples > example "ply" example index 0 1`] = `
]
`;
exports[`runs examples > example "plyForEach" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:C3 ]",
"[ 1/4 → 1/2 | note:Eb3 ]",
"[ 1/2 → 3/4 | note:G3 ]",
"[ 3/4 → 1/1 | note:Bb3 ]",
"[ 1/1 → 9/8 | note:Eb3 ]",
"[ 9/8 → 5/4 | note:G3 ]",
"[ 5/4 → 11/8 | note:Bb3 ]",
"[ 11/8 → 3/2 | note:D4 ]",
"[ 3/2 → 13/8 | note:G3 ]",
"[ 13/8 → 7/4 | note:Bb3 ]",
"[ 7/4 → 15/8 | note:D4 ]",
"[ 15/8 → 2/1 | note:F4 ]",
"[ 2/1 → 9/4 | note:C3 ]",
"[ 9/4 → 5/2 | note:Eb3 ]",
"[ 5/2 → 11/4 | note:G3 ]",
"[ 11/4 → 3/1 | note:Bb3 ]",
"[ 3/1 → 25/8 | note:Eb3 ]",
"[ 25/8 → 13/4 | note:G3 ]",
"[ 13/4 → 27/8 | note:Bb3 ]",
"[ 27/8 → 7/2 | note:D4 ]",
"[ 7/2 → 29/8 | note:G3 ]",
"[ 29/8 → 15/4 | note:Bb3 ]",
"[ 15/4 → 31/8 | note:D4 ]",
"[ 31/8 → 4/1 | note:F4 ]",
]
`;
exports[`runs examples > example "plyWith" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:C3 ]",
"[ 1/4 → 1/2 | note:Eb3 ]",
"[ 1/2 → 3/4 | note:G3 ]",
"[ 3/4 → 1/1 | note:Bb3 ]",
"[ 1/1 → 9/8 | note:Eb3 ]",
"[ 9/8 → 5/4 | note:G3 ]",
"[ 5/4 → 11/8 | note:Bb3 ]",
"[ 11/8 → 3/2 | note:D4 ]",
"[ 3/2 → 13/8 | note:G3 ]",
"[ 13/8 → 7/4 | note:Bb3 ]",
"[ 7/4 → 15/8 | note:D4 ]",
"[ 15/8 → 2/1 | note:F4 ]",
"[ 2/1 → 9/4 | note:C3 ]",
"[ 9/4 → 5/2 | note:Eb3 ]",
"[ 5/2 → 11/4 | note:G3 ]",
"[ 11/4 → 3/1 | note:Bb3 ]",
"[ 3/1 → 25/8 | note:Eb3 ]",
"[ 25/8 → 13/4 | note:G3 ]",
"[ 13/4 → 27/8 | note:Bb3 ]",
"[ 27/8 → 7/2 | note:D4 ]",
"[ 7/2 → 29/8 | note:G3 ]",
"[ 29/8 → 15/4 | note:Bb3 ]",
"[ 15/4 → 31/8 | note:D4 ]",
"[ 31/8 → 4/1 | note:F4 ]",
]
`;
exports[`runs examples > example "polymeter" example index 0 1`] = `
[
"[ 0/1 → 1/6 | note:c ]",
+1 -2
View File
@@ -8,8 +8,7 @@
"start": "astro dev",
"build": "astro build",
"preview": "astro preview --port 3009 --host 0.0.0.0",
"astro": "astro",
"postinstall": "cp node_modules/hs2js/dist/tree-sitter.wasm public && cp node_modules/hs2js/dist/tree-sitter-haskell.wasm public"
"astro": "astro"
},
"dependencies": {
"@algolia/client-search": "^5.20.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+1
View File
@@ -103,6 +103,7 @@ export const SIDEBAR: Sidebar = {
Understand: [
{ text: 'Coding syntax', link: 'learn/code' },
{ text: 'Pitch', link: 'understand/pitch' },
{ text: 'Xen Harmonic Functions', link: 'learn/xen' },
{ text: 'Cycles', link: 'understand/cycles' },
{ text: 'Voicings', link: 'understand/voicings' },
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
+1 -1
View File
@@ -44,7 +44,7 @@ xxx("foo").yyy("bar")
Generally, `xxx` and `yyy` are called [_functions_](<https://en.wikipedia.org/wiki/Function_(computer_programming)>), while `foo` and `bar` are called function [_arguments_ or _parameters_](<https://en.wikipedia.org/wiki/Parameter_(computer_programming)>).
So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data.
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is appended with a dot (`.`).
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is preceded with a dot (`.`).
Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code.
You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects.
@@ -34,11 +34,11 @@ import { JsDoc } from '../../docs/JsDoc';
## arp
<JsDoc client:idle name="Pattern#arp" h={0} />
<JsDoc client:idle name="arp" h={0} />
## arpWith 🧪
<JsDoc client:idle name="Pattern#arpWith" h={0} />
<JsDoc client:idle name="arpWith" h={0} />
## struct
@@ -58,7 +58,7 @@ import { JsDoc } from '../../docs/JsDoc';
## hush
<JsDoc client:idle name="hush" h={0} />
<JsDoc client:idle name="Pattern#hush" h={0} />
## invert
+137
View File
@@ -11,6 +11,129 @@ import { JsDoc } from '../../docs/JsDoc';
Whether you're using a synth or a sample, you can apply any of the following built-in audio effects.
As you might suspect, the effects can be chained together, and they accept a pattern string as their argument.
# Signal chain
<img src="/img/strudel-signal-flow.png"></img>
The signal chain in Strudel is as follows:
- An sound-generating event is triggered by a pattern
- This has a start time and a duration, which is usually
controlled by the note length and ADSR parameters
- If we exceed the max polyphony, old sounds begin to die off
- Muted sounds (one whose `s` value is `-`, `~`, or `_`) are skipped
- A sound is produced (through, say, a sample or an oscillator)
- This is where detune-based effects (like `detune`, `penv`, etc. occur)
- The following will occur _in order_ and only if they've been called in the pattern. Note that all of these are
single use effects, meaning that multiple occurrences of them in a pattern will simply override the values
(e.g. you can't do `s("bd").lpf(100).distort(2).lpf(800)` to lowpass, distort, and then lowpass
again)
- Phase vocoder (`stretch`)
- Gain is applied (`gain`)
- This is where the main (volume) ADSR happens
- A lowpass filter (`lpf`)
- A highpass filter (`hpf`)
- A bandpass filter (`bandpass`)
- A vowel filter (`vowel`)
- Sample rate reduction (`coarse`)
- Bit crushing (`crush`)
- Waveshape distortion (`shape`)
- Normal distortion (`distort`)
- Tremolo (`tremolo`)
- Compressor (`compressor`)
- Panning (`pan`)
- Phaser (`phaser`)
- Postgain (`post`)
- The sound is then split into multiple destinations
- Dry output (amount controlled by `dry` parameter)
- The sends
- Analyzers
- These are used for tooling like `scope` and `spectrum` and their setup usually happens behind the scenes
- Delay (amount controlled by `delay` parameter)
- Reverb (amount controlled by `room` parameter)
- The dry output, delay, and reverb are joined into what is called the "orbit" of the pattern (see more in the section below)
- The `duck` effect affects the volume of all signals in the orbit
- The orbit is then sent to the mixer
## Orbits
Orbits are the way in which outputs are handled in Strudel. They also prescribe which delay and reverb to associate with the dry signal.
By default, all orbits are mixed down to channels `1` and `2` in stereo, however with the "Multi Channel Orbits" setting
(under Settings at the right) you can use them as individual 2 channel stereo outs (orbit `i` will be mapped to
to channels `2i` and `2i + 1`). You can then use routers like Blackhole 16 to retrieve and record all of the channels in a DAW for later processing.
The default orbit is `1` and it is set with `orbit`. You may send a sound to multiple orbits via mininotation
<MiniRepl client:visible tune={`s("white").orbit("2,3,4").gain(0.2)`} />
but please be careful as this will create three copies of the sound behind the scenes, meaning that if they are mixed
down to a single output, they will triple the volume. We've reduced the gain here to save your ears.
⚠️ There is only one delay and reverb per orbit, so please be aware that if you attempt to change the parameters on two
patterns pointing to the same orbit, it can lead to unpredictable results. Compare, for example, this pretty pluck
with a large reverb:
<MiniRepl
client:visible
tune={`
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
.room(1).roomsize(10)`}
/>
versus the same pluck with a muted kick drum coming in and overwriting the `roomsize` value:
<MiniRepl
client:visible
tune={`
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
.room(1).roomsize(10)
$: s("bd\*4").room(0.01).roomsize(0.01).postgain(0)`}
/>
This is due to them sharing the same orbit: the default of `1`. It can be corrected simply by updating the orbits to be
distinct:
<MiniRepl
client:visible
tune={`
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
.room(1).roomsize(10).orbit(2)
$: s("bd\*4").room(0.01).roomsize(0.01).postgain(0)`}
/>
## Continuous changes
As all of the above is triggered by a _sound occurring_, it is often the case that parameters may not be
modified continuously in time. For example,
<MiniRepl
client:visible
tune={`
s("supersaw").lpf(tri.range(100, 5000).slow(2))`}
/>
Will not produce a continually LFO'd low-pass filter due to the `tri` only being sampled every time the note hits
(in this case the default of once per cycle). You can fake it by introducing more sound-generating events, e.g.:
<MiniRepl
client:visible
tune={`
s("supersaw").seg(16).lpf(tri.range(100, 5000).slow(2))`}
/>
Some parameters _do_ induce continuous variations in time, though:
- The ADSR curve (governed by `attack`, `sustain`, `decay`, `release`)
- The pitch envelope curve (governed by `penv` and its associated ADSR)
- The FM curve (`fmenv`)
- The filter envelopes (`lpenv`, `hpenv`, `bpenv`)
- Tremolo (`tremolo`)
- Phaser (`phaser`)
- Vibrato (`vib`)
- Ducking (`duckorbit`)
# Filters
Filters are an essential building block of [subtractive synthesis](https://en.wikipedia.org/wiki/Subtractive_synthesis).
@@ -339,4 +462,18 @@ global effects use the same chain for all events of the same orbit:
<JsDoc client:idle name="phasersweep" h={0} />
## Duck
### duckorbit
<JsDoc client:idle name="duckorbit" h={0} />
### duckattack
<JsDoc client:idle name="duckattack" h={0} />
### duckdepth
<JsDoc client:idle name="duckdepth" h={0} />
Next, we'll look at input / output via [MIDI, OSC and other methods](/learn/input-output).
+16
View File
@@ -168,6 +168,20 @@ Using "!" we can repeat without speeding up:
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
## Randomness
Events with a "?" placed after them will have a 50% chance of being removed from the pattern:
<MiniRepl client:idle tune={`note("[g3,b3,e4]*8?")`} punchcard />
Adding a number between 0 and 1 after the "?" will affect the likelihood of the event being removed. For example, events with "?0.1" placed after them will have a 10% chance of being removed:
<MiniRepl client:idle tune={`note("[g3,b3,e4]*8?0.1")`} punchcard />
Events separated by a "|" will be chosen from at random:
<MiniRepl client:idle tune={`note("[g3,b3,e4] | [a3,c3,e4] | [b3,d3,f#4]")`} punchcard />
## Mini-notation review
To recap what we've learned so far, compare the following patterns:
@@ -179,6 +193,8 @@ To recap what we've learned so far, compare the following patterns:
<MiniRepl client:idle tune={`note("<[g3,b3,e4] _ [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]? [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3|b3|e4] [a3,c3,e4] [b3,d3,f#4]>*2")`} />
## Euclidian rhythms
+22
View File
@@ -59,6 +59,14 @@ Furthermore, strudel also loads instrument samples from [VCSL](https://github.co
To see which sample names are available, open the `sounds` tab in the [REPL](https://strudel.cc/).
You can also create custom aliases for existing sounds using the `soundAlias` function:
<MiniRepl
client:idle
tune={`soundAlias("RolandTR808_bd", "kick")
s("kick")`}
/>
Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples themselves are not loaded until they are actually played.
This behaviour of loading things only when they are needed is also called `lazy loading`.
While it saves resources, it can also lead to sounds not being audible the first time they are triggered, because the sound is still loading.
@@ -178,6 +186,16 @@ 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).
## Generating strudel.json
You can use [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler) to generate a strudel.json file for you, by running:
```sh
npx --yes @strudel/sampler --json > strudel.json
```
See other uses of strudel/sampler further below, under "From Disk via @strudel/sampler".
## Github Shortcut
Because loading samples from github is common, there is a shortcut:
@@ -361,6 +379,10 @@ Sampler effects are functions that can be used to change the behaviour of sample
<JsDoc client:idle name="splice" h={0} />
### scrub
<JsDoc client:idle name="Pattern.scrub" h={0} />{' '}
### speed
<JsDoc client:idle name="speed" h={0} />
+95
View File
@@ -0,0 +1,95 @@
---
title: Xen Harmonic Functions
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# Xen Harmonic Functions
These functions allow the use of scales other than your typical chromatic 12 based ones.
### tune(scale)
<JsDoc client:idle name="tune" h={0} />
Here's an example of how to configure a basic hexany scale:
<MiniRepl client:idle tune={`"0 1 2 3 4 5".tune("hexany15").mul("220").freq()`} />
Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3`
For a full list of available scales from tunejs, see http://abbernie.github.io/tune/scales.html
You can set your root to be a particular note with `getFreq`
<MiniRepl
client:idle
tune={`"4 8 9 10 - - 5 7 9 11 - -".tune("tranh3")
.mul(getFreq('c3'))
.freq().clip(.5).room(1)`}
/>
Some tunings become more pronounced with a longer reverb decay:
<MiniRepl
client:idle
tune={`"<[5 6 8 10] - [5 7 9 12] -> -".tune("gumbeng")
.mul(getFreq('c3'))
.freq().clip(.8).room("3:10").rdim(10000).rfade(5)`}
/>
Additionally, you can combo this with `fmap` so that the base note changes:
<MiniRepl
client:idle
tune={`"9 11 12 10 - - -".tune("gunkali")
.mul("<c3 c3 a3 d#3>".fmap(getFreq))
.freq().legato("2 .7").room("1:15").rdim(8500).rlp(14000).rfade(8)`}
/>
Combining this with various polyrhythm tricks can become very evocative:
<MiniRepl
client:idle
tune={`"<[0 3 1 -] [-1 4 2 8]> ~ ~,<-4 -5>"
.transpose(4)
.tune("iraq")
.mul("<c3 d3 c#3>".fmap(getFreq))
.freq().clip(.5).room(1).rfade(9)`}
/>
Another helpful trick when exploring new tunings is to strum them.
Many have a much more enchanting sound that was chosen over many generations of musicians for being strummed.
Take the `sanza` tuning:
<MiniRepl
client:idle
tune={`"4 5 6 7 8 9".tune("sanza")
.mul(getFreq('c3'))
.freq()`}
/>
Notes 7 and 9 will clash quite a bit if you arp them normally. Many tunings will have this sort of sound, and it can feel distracting on its own.
See how close they are on the pitch wheel?
<MiniRepl client:idle tune={`"[7 9]!3".tune("sanza").mul(getFreq('c3')).freq()._pitchwheel()`} />
This quality is often due to how the tunings were formed with instruments that were played differently than a piano.
As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical:
<MiniRepl
client:idle
tune={`"[0 1 2 3 4 5 6]@0.3 -"
.transpose("<2 5 8 1>")
.tune("sanza")
.mul(getFreq('c3')).freq()
.legato("3").room(1).rfade(5)`}
/>
Note the legato and reverb effects make sure the sound of the strumming gets to wash together. Alternating the direction of the strum can make the
tones sound even more alive, too.
The `tranh3` tuning has a similar set of notes, with two clashing. You might trying plugging that in above and see if you find a favorite strumming pattern.
+3 -1
View File
@@ -9,7 +9,9 @@ import { MiniRepl } from '../../docs/MiniRepl';
{/* The [REPL](https://strudel.cc/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. */}
While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL^[REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel.
While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL[^1], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel.
[^1]: REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.
Besides a UI for playback control and meta information, the main part of the REPL interface is the code editor powered by CodeMirror. In it, the user can edit and evaluate pattern code live, using one of the available synthesis outputs to create music and/or sound art. The control flow of the REPL follows 3 basic steps:
+5 -5
View File
@@ -262,14 +262,14 @@ It is quite common that there are many ways to express the same idea.
**selecting sample numbers separately**
Instead of using ":", we can also use the `n` function to select sample numbers:
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
This is shorter and more readable than:
Instead of selecting sample numbers one by one:
<MiniRepl client:visible tune={`sound("jazz:0 jazz:1 [jazz:4 jazz:2] jazz:3*2")`} punchcard />
We can also use the `n` function to make it shorter and more readable:
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
## Recap
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
+147
View File
@@ -69,3 +69,150 @@
text-decoration: underline 0.18rem;
text-underline-offset: 0.22rem;
}
/* Override default styles from the codemirror inline css for autocomplete info tooltip*/
.cm-tooltip.cm-completionInfo {
padding: 0 !important;
border: 1px solid var(--foreground) !important;
border-radius: 4px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
max-width: 500px !important;
min-width: 300px !important;
max-height: 400px !important;
background-color: var(--lineHighlight) !important;
overflow: auto;
background: var(--background) !important;
}
/* Main tooltip container */
.autocomplete-info-container {
padding: 12px !important;
border-radius: 4px !important;
color: var(--foreground);
font-family: var(--font-family, 'SF Mono', 'Monaco', monospace);
font-size: var(--font-size, 13px);
line-height: 1.4;
max-width: 600px;
height: 100%;
min-width: 400px;
white-space: normal !important;
overflow-y: auto !important;
}
.autocomplete-info-tooltip {
overflow-y: auto !important;
}
.autocomplete-info-function-description {
white-space: pre-wrap !important;
}
.autocomplete-info-function-name {
font-size: 15px;
font-weight: 600;
color: var(--foreground);
margin: 0 0 8px 0;
}
.autocomplete-info-function-synonyms {
margin: 0 0 12px 0;
color: var(--foreground);
line-height: 1.5;
opacity: 0.8;
}
.autocomplete-info-function-description {
margin: 0 0 12px 0;
color: var(--foreground);
line-height: 1.5;
opacity: 0.8;
}
.autocomplete-info-section-title {
font-size: 12px;
font-weight: 600;
color: var(--foreground);
margin: 16px 0 6px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.autocomplete-info-section-title:first-child {
margin-top: 0;
}
.autocomplete-info-params-section {
margin-top: 12px;
}
.autocomplete-info-params-list {
list-style: none;
margin: 0;
padding: 0;
}
.autocomplete-info-param-item {
margin-bottom: 8px;
padding: 8px;
background-color: var(--lineBackground);
border-radius: 3px;
border-left: 2px solid var(--foreground, #555);
}
.autocomplete-info-param-item:last-child {
margin-bottom: 0;
}
.autocomplete-info-param-name {
font-weight: 600;
color: var(--variable, var(--foreground));
margin-right: 8px;
}
.autocomplete-info-param-type {
color: var(--comment);
font-size: 12px;
background-color: var(--gutterForeground);
padding: 1px 4px;
border-radius: 2px;
}
.autocomplete-info-param-desc {
color: var(--foreground);
font-size: 10px;
margin-top: 4px;
line-height: 1.4;
opacity: 0.7;
}
.autocomplete-info-examples-section {
margin-top: 12px;
}
.autocomplete-info-example-code {
background: var(--lineBackground);
color: var(--foreground);
padding: 8px;
border-radius: 3px;
font-family: var(--font-family, 'SF Mono', 'Monaco', monospace);
font-size: 12px;
line-height: 1.5;
margin: 4px 0;
overflow-x: auto;
white-space: pre;
border: 1px solid var(--foreground, #3a3a3a);
}
.autocomplete-info-tooltip::-webkit-scrollbar {
width: 4px;
}
.autocomplete-info-tooltip::-webkit-scrollbar-track {
}
.autocomplete-info-tooltip::-webkit-scrollbar-thumb {
border-radius: 2px;
}
.autocomplete-info-tooltip::-webkit-scrollbar-thumb:hover {
}
@@ -0,0 +1,10 @@
import cx from '@src/cx.mjs';
export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) {
return (
<button className={cx('hover:opacity-50 text-nowrap w-fit', className)} title={label} {...buttonProps}>
{labelIsHidden !== true && label}
{children}
</button>
);
}
@@ -12,8 +12,8 @@ import { useMemo } from 'react';
import { getMetadata } from '../../../metadata_parser.js';
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
import { parseJSON, isUdels } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx';
import { settingsMap, useSettings } from '../../../settings.mjs';
import { useSettings } from '../../../settings.mjs';
import { ActionButton } from '../button/action-button.jsx';
import { Pagination } from '../pagination/Pagination.jsx';
import { useState } from 'react';
import { useDebounce } from '../usedebounce.jsx';
@@ -75,15 +75,6 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
);
}
function ActionButton({ children, onClick, label, labelIsHidden }) {
return (
<button className="hover:opacity-50 text-nowrap" onClick={onClick} title={label}>
{labelIsHidden !== true && label}
{children}
</button>
);
}
const updateCodeWindow = (context, patternData, reset = false) => {
context.handleUpdate(patternData, reset);
};
@@ -2,9 +2,32 @@ import { useMemo, useState } from 'react';
import jsdocJson from '../../../../../doc.json';
import { Textbox } from '../textbox/Textbox';
const availableFunctions = jsdocJson.docs
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
const availableFunctions = (() => {
const seen = new Set(); // avoid repetition
const functions = [];
for (const doc of jsdocJson.docs) {
if (!isValid(doc)) continue;
functions.push(doc);
const synonyms = doc.synonyms || [];
for (const s of synonyms) {
if (!s || seen.has(s)) continue;
seen.add(s);
// Swap `doc.name` in for `s` in the list of synonyms
const synonymsWithDoc = [doc.name, ...synonyms].filter((x) => x && x !== s);
functions.push({
...doc,
name: s, // update names for the synonym
longname: s,
synonyms: synonymsWithDoc,
synonyms_text: synonymsWithDoc.join(', '),
});
}
}
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
})();
const getInnerText = (html) => {
var div = document.createElement('div');
@@ -311,7 +311,8 @@ export function SettingsTab({ started }) {
onClick={() => {
confirmDialog('Sure?').then((r) => {
if (r) {
settingsMap.set(defaultSettings);
const { userPatterns } = settingsMap.get(); // keep current patterns
settingsMap.set({ ...defaultSettings, userPatterns });
}
});
}}
+34 -11
View File
@@ -2,16 +2,21 @@ import useEvent from '@src/useEvent.mjs';
import { useStore } from '@nanostores/react';
import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio';
import { useMemo, useRef, useState } from 'react';
import { settingsMap, useSettings } from '../../../settings.mjs';
import { settingsMap, soundFilterType, useSettings } from '../../../settings.mjs';
import { ButtonGroup } from './Forms.jsx';
import ImportSoundsButton from './ImportSoundsButton.jsx';
import { Textbox } from '../textbox/Textbox.jsx';
import { ActionButton } from '../button/action-button.jsx';
import { confirmDialog } from '@src/repl/util.mjs';
import { clearIDB, userSamplesDBConfig } from '@src/repl/idbutils.mjs';
import { prebake } from '@src/repl/prebake.mjs';
const getSamples = (samples) =>
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
export function SoundsTab() {
const sounds = useStore(soundMap);
const { soundsFilter } = useSettings();
const [search, setSearch] = useState('');
const { BASE_URL } = import.meta.env;
@@ -27,18 +32,19 @@ export function SoundsTab() {
.sort((a, b) => a[0].localeCompare(b[0]))
.filter(([name]) => name.toLowerCase().includes(search.toLowerCase()));
if (soundsFilter === 'user') {
if (soundsFilter === soundFilterType.USER) {
return filtered.filter(([_, { data }]) => !data.prebake);
}
if (soundsFilter === 'drums') {
if (soundsFilter === soundFilterType.DRUMS) {
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
}
if (soundsFilter === 'samples') {
if (soundsFilter === soundFilterType.SAMPLES) {
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines');
}
if (soundsFilter === 'synths') {
if (soundsFilter === soundFilterType.SYNTHS) {
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
}
//TODO: tidy this up, it does not need to be saved in settings
if (soundsFilter === 'importSounds') {
return [];
}
@@ -57,10 +63,10 @@ export function SoundsTab() {
});
});
return (
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
<div className="pb-2 flex shrink-0 flex-wrap">
<div className=" flex shrink-0 flex-wrap">
<ButtonGroup
value={soundsFilter}
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
@@ -74,7 +80,26 @@ export function SoundsTab() {
></ButtonGroup>
</div>
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
{soundsFilter === soundFilterType.USER && soundEntries.length > 0 && (
<ActionButton
className="pl-2"
label="delete-all"
onClick={async () => {
try {
const confirmed = await confirmDialog('Delete all imported user samples?');
if (confirmed) {
clearIDB(userSamplesDBConfig.dbName);
soundMap.set({});
await prebake();
}
} catch (e) {
console.error(e);
}
}}
/>
)}
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal bg-background p-2 rounded-md">
{soundEntries.map(([name, { data, onTrigger }]) => {
return (
<span
@@ -151,9 +176,7 @@ export function SoundsTab() {
) : (
''
)}
{!soundEntries.length && soundsFilter !== 'importSounds'
? 'No custom sounds loaded in this pattern (yet).'
: ''}
{!soundEntries.length && soundsFilter !== 'importSounds' ? 'No sounds loaded' : ''}
</div>
</div>
);
+6 -2
View File
@@ -12,17 +12,21 @@ export const userSamplesDBConfig = {
};
// deletes all of the databases, useful for debugging
function clearIDB() {
function clearAllIDB() {
window.indexedDB
.databases()
.then((r) => {
for (var i = 0; i < r.length; i++) window.indexedDB.deleteDatabase(r[i].name);
for (var i = 0; i < r.length; i++) clearIDB(r[i].name);
})
.then(() => {
alert('All data cleared.');
});
}
export function clearIDB(dbName) {
return window.indexedDB.deleteDatabase(dbName);
}
// queries the DB, and registers the sounds so they can be played
export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => {}) {
openDB(config, (objectStore) => {
+4 -4
View File
@@ -285,7 +285,7 @@ stack(
.mul(gain(sine.struct("x*8").add(3/5).mul(2/5).fast(8)))
.piano()`;
// iter, echo, echoWith
// iter, echo, echoForEach
export const undergroundPlumber = `// "Underground plumber"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
@@ -307,7 +307,7 @@ stack(
.cutoff(400).decay(.12).sustain(0)
,
"[g2,[c3 eb3]]".iter(4)
.echoWith(4, 1/8, (x,n)=>x.transpose(n*12).gain(Math.pow(.4,n)))
.echoForEach(4, 1/8, (x,n)=>x.transpose(n*12).gain(Math.pow(.4,n)))
.note().layer(h)
.clip(.1)
)
@@ -432,7 +432,7 @@ export const festivalOfFingers3 = `// "Festival of fingers 3"
setcps(1)
n("[-7*3],0,2,6,[8 7]")
.echoWith(
.echoForEach(
4, // echo 4 times
1/4, // 1/4s between echos
(x,i)=>x
@@ -485,7 +485,7 @@ stack(
.decay(.1).sustain(0) // make notes short
.s('triangle') // waveform
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
.echoWith(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes
.echoForEach(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes
//.hush()
)
.slow(3/2)`;
+9 -1
View File
@@ -8,6 +8,14 @@ export const audioEngineTargets = {
osc: 'osc',
};
export const soundFilterType = {
USER: 'user',
DRUMS: 'drums',
SAMPLES: 'samples',
SYNTHS: 'synths',
ALL: 'all',
};
export const defaultSettings = {
activeFooter: 'intro',
keybindings: 'codemirror',
@@ -28,7 +36,7 @@ export const defaultSettings = {
fontSize: 18,
latestCode: '',
isZen: false,
soundsFilter: 'all',
soundsFilter: soundFilterType.ALL,
patternFilter: 'community',
// panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro
panelPosition: 'right',